在前面章节的博客中,博主介绍了ESP8266WiFi库 Tcp client的用法,并模拟了Http请求。但是,可以看出通过WiFiClient模拟Http请求,我们需要自己拼装Http请求协议,稍微不小心就很容易拼接错误。
那么有没有针对Http请求操作的库呢?答案肯定是有的,这就是博主本篇需要跟大家讲述的知识——ESP8266HTTPClient库。
请注意,ESP8266HTTPClient库不属于ESP8266WiFi库的一部分,所以需要引入
#include <ESP8266HTTPClient.h> 复制代码
博主说过,Http是基于Tcp协议之上的,所以你在ESP8266HTTPClient源码中会看到TcpClient的踪迹。
#ifndef ESP8266HTTPClient_H_ #define ESP8266HTTPClient_H_ #include <memory> #include <Arduino.h> #include <WiFiClient.h> //这里就是我们熟悉的TCP #ifdef DEBUG_ESP_HTTP_CLIENT #ifdef DEBUG_ESP_PORT #define DEBUG_HTTPCLIENT(...) DEBUG_ESP_PORT.printf( __VA_ARGS__ ) #endif #endif 复制代码
在讲述ESP8266HTTPClient库之前,为了更好的讲解它的使用,博主给大家略讲一下Http协议,更加深入的了解请自行查阅资料。
HTTP协议是Hyper Text Transfer Protocol的缩写,简称超文本传输协议,用于从WWW服务器传输文本到本地浏览器的传送协议。
HTTP是一个基于TCP/IP通信协议来传递数据,浏览器作为HTTP客户端通过URL向HTTP服务端即WEB服务器发送所有请求。WEB服务器根据接收到的请求后,向客户端发送响应信息。
HTTP协议作为TCP/IP模型中应用层的协议,承载于TCP协议之上,有时也承载于TLS或者SSL协议层之上,这个时候就是我们时常说的HTTPS。
[外链图片转存失败(img-VhNm8NA9-1562303373406)( www.arduino.cn/data/attach… )]
HTTP是一个应用层协议,由请求和响应构成,是一个标准的客户端服务器模型。HTTP默认的端口号是80,HTTPS的端口号是443。
浏览网页是HTTP主要应用,但不代表只用于网页浏览。HTTP只是一种协议,只要通信双方遵守这个协议,HTTP就能用。
一次HTTP操作称为一个事务,工作流程可分为4步:
以上四步骤,只要其中一步出现错误,那么就会产生错误信息返回给客户端。
客户端发送一个HTTP请求到服务器,请求信息包括以下格式:
请求行以一个方法符号开头,以空格分开,后面跟着请求的URI和协议的版本。
请求例子,使用Charles抓取的request:
GET /562f25980001b1b106000338.jpg HTTP/1.1 Host img.mukewang.com User-Agent Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36 Accept image/webp,image/*,*/*;q=0.8 Referer http://www.imooc.com/ Accept-Encoding gzip, deflate, sdch Accept-Language zh-CN,zh;q=0.8 复制代码
请求例子,使用Charles抓取的request:
POST / HTTP1.1 Host:www.wrox.com User-Agent:Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022) Content-Type:application/x-www-form-urlencoded Content-Length:40 Connection: Keep-Alive name=Professional%20Ajax&publisher=Wiley 复制代码
一般情况下,服务端接收并处理客户端发过来的请求会返回一个HTTP的响应信息。HTTP响应也由四个部分组成,分别是:
状态代码有三位数字组成,第一个数字定义了响应的类别,共分五种类别:
常见状态码:
在1.2中初略讲述了Http协议,相信读者应该有初步认识,那么接下来我们就可以开始进入ESP8266HTTPClient库了。
老规矩,先上一个博主总结的百度脑图: [外链图片转存失败(img-zfQwne3H-1562303373407)( raw.githubusercontent.com/tingyouwu/b… ].png)
总体上,根据功能可以把方法分为两大类:
有兴趣看源码的读者请看 ESP8266HTTPClient.cpp
http请求方法又可以有更好的细分。
函数说明:
/** * 解析url以获得所有参数,默认port是80端口 * @param url String */ bool begin(String url); /** * 设置host port 以及uri * @param host String(192.168.1.12,不需要带上http://前缀) * @param port uint16_t * @param uri String */ bool begin(String host, uint16_t port, String uri = "/"); 复制代码
注意点:
1. http://192.168.1.12/test.html 2. http://user:password@192.168.1.12/test.html 3. http://user:password@192.168.1.12:8888/test.html 复制代码
三者共同点:都需要http://开头 有host(192.168.1.12) 有uri(/test.html)。
函数说明:
/** * try to reuse the connection to the server * keep-alive 请求头 * @param reuse bool */ void setReuse(bool reuse); // keep-alive 复制代码
函数说明:
/** * set User Agent * User Agent请求头:使得服务器能够识别客户使用的操作系统及版本、CPU 类型、浏览器及版本、浏览器渲染引擎、浏览器语言、浏览器插件等。 * @param userAgent const char * */ void setUserAgent(const String& userAgent); 复制代码
函数说明:
/** * set the Authorizatio for the http request(访问权限认证请求头信息)* Authorization 是采用 basic auth 授权方式验证客户端请求,Authorization 请求头对应的值是 (basic base64编码) 忽略括号, * 其中 base64编码是将 用户名:密码 这种格式进行处理生成**的,并且自动在 header 中添加 Authorization。 * @param user const char * * @param password const char * */ void setAuthorization(const char * user, const char * password); /** * set the Authorizatio for the http request(访问权限认证请求头信息) * @param auth const char * base64 */ void setAuthorization(const char * auth); 复制代码
当然,Http的标准请求头还不止这些,请自行查阅资料。
函数说明:
/** * adds Header to the request * @param name 自定义请求头的名字 * @param value 自定义请求头的参数值 * @param first 是否要把当前请求头放在请求头的最前面 * @param replace 是否需要替换之前已经存在该请求头的参数值,默认就是覆盖旧值 */ void addHeader(const String& name, const String& value, bool first = false, bool replace = true); 复制代码
注意点:
/** * adds Header to the request * @param name * @param value * @param first */ void HTTPClient::addHeader(const String& name, const String& value, bool first, bool replace) { // 过滤请求头 if(!name.equalsIgnoreCase(F("Connection")) && !name.equalsIgnoreCase(F("User-Agent")) && !name.equalsIgnoreCase(F("Host")) && !(name.equalsIgnoreCase(F("Authorization")) && _base64Authorization.length())){ String headerLine = name; headerLine += ": "; if (replace) { int headerStart = _headers.indexOf(headerLine); if (headerStart != -1) { int headerEnd = _headers.indexOf('/n', headerStart); _headers = _headers.substring(0, headerStart) + _headers.substring(headerEnd + 1); } } headerLine += value; headerLine += "/r/n"; if(first) { _headers = headerLine + _headers; } else { _headers += headerLine; } } } 复制代码
函数说明:
/** * 发送一个get请求 * @return http 状态码 */ int GET(); 复制代码
函数说明:
/** * 发送一个post请求 * @param payload uint8_t * 需要提交的数据 * @param size size_t 提交的数据的字节数 * @return http 状态码 */ int POST(uint8_t * payload, size_t size); /** * 发送一个post请求 * @param payload String 需要提交的数据 * @return http 状态码 */ int POST(String payload); 复制代码
函数说明:
/** * 发送一个PUT请求(博主也没有用过PUT) * @param payload uint8_t * 需要提交的数据 * @param size size_t 提交的数据的字节数 * @return http 状态码 */ int PUT(uint8_t * payload, size_t size); /** * 发送一个PUT请求(博主也没有用过PUT) * @param payload String 需要提交的数据 * @return http 状态码 */ int PUT(String payload); 复制代码
函数说明:
/** * 发送一个PATCH请求(博主也没有用过PATCH) * @param payload uint8_t * 需要提交的数据 * @param size size_t 提交的数据的字节数 * @return http 状态码 */ int PATCH(uint8_t * payload, size_t size); /** * 发送一个PATCH请求(博主也没有用过PATCH) * @param payload String 需要提交的数据 * @return http 状态码 */ int PATCH(String payload); 复制代码
GET、POST、PUT、PATCH最终都会调用sendRequest方法。 函数说明:
/** * GET、POST、PUT、PATCH最终都会调用sendRequest方法 * sendRequest * @param type const char * 请求类型 "GET", "POST", .... * @param payload String 请求携带的数据 data for the message body * @return */ int sendRequest(const char * type, String payload); /** * sendRequest * @param type const char * 请求类型 "GET", "POST", .... * @param payload uint8_t * 请求携带的数据 data for the message body if null not send * @param size size_t 请求携带的数据字节数 size for the message body if 0 not send * @return -1 if no info or > 0 when Content-Length is set by server */ int sendRequest(const char * type, uint8_t * payload = NULL, size_t size = 0); /** * sendRequest * @param type const char * 请求类型 "GET", "POST", .... * @param stream Stream * 请求携带的数据流 data stream for the message body * @param size size_t 数据流大小 size for the message body if 0 not Content-Length is send * @return -1 if no info or > 0 when Content-Length is set by server */ int sendRequest(const char * type, Stream * stream, size_t size = 0); 复制代码
我们来看看sendRequest底层源码:
int HTTPClient::sendRequest(const char * type, uint8_t * payload, size_t size) { // connect to server if(!connect()) { //如果没有连接到服务器就提示Http拒绝连接 return returnError(HTTPC_ERROR_CONNECTION_REFUSED); } //判断是否有需要提交的内容 if(payload && size > 0) { //添加请求头 Content-Length addHeader(F("Content-Length"), String(size)); } // 拼装并发送Http请求头 if(!sendHeader(type)) { return returnError(HTTPC_ERROR_SEND_HEADER_FAILED); } // 根据需要,发送Http请求内容body if(payload && size > 0) { //这里就是我们熟悉的TCP发送 if(_tcp->write(&payload[0], size) != size) { return returnError(HTTPC_ERROR_SEND_PAYLOAD_FAILED); } } // 处理服务响应数据 (Header) return returnError(handleHeaderResponse()); } /** * 拼装并发送Http请求头 * @param type (GET, POST, ...) * @return status */ bool HTTPClient::sendHeader(const char * type) { if(!connected()) { return false; } //Http协议版本 String header = String(type) + " " + (_uri.length() ? _uri : F("/")) + F(" HTTP/1."); if(_useHTTP10) { header += "0"; } else { header += "1"; } //Http Host主机 header += String(F("/r/nHost: ")) + _host; if (_port != 80 && _port != 443) { header += ':'; header += String(_port); } header += String(F("/r/nUser-Agent: ")) + _userAgent + F("/r/nConnection: "); if(_reuse) { header += F("keep-alive"); } else { header += F("close"); } header += "/r/n"; if(!_useHTTP10) { header += F("Accept-Encoding: identity;q=1,chunked;q=0.1,*;q=0/r/n"); } if(_base64Authorization.length()) { _base64Authorization.replace("/n", ""); header += F("Authorization: Basic "); header += _base64Authorization; header += "/r/n"; } //自定义请求头 header += _headers + "/r/n"; DEBUG_HTTPCLIENT("[HTTP-Client] sending request header/n-----/n%s-----/n", header.c_str()); return (_tcp->write((const uint8_t *) header.c_str(), header.length()) == header.length()); } 复制代码
函数说明:
/** * 请求超时时间配置 ms为单位 * @param timeout unsigned int */ void setTimeout(uint16_t timeout); 复制代码
注意点:
函数说明:
/** * http协议版本 * @param usehttp10 true表示用http1.0,默认是false,用http1.1 */ void useHTTP10(bool usehttp10 = true); 复制代码
函数说明:
/** * 结束请求 * called after the payload is handled */ void end(void); 复制代码
看看源码:
void HTTPClient::end(void) { if(connected()) { if(_tcp->available() > 0) { //清除接收缓冲区 DEBUG_HTTPCLIENT("[HTTP-Client][end] still data in buffer (%d), clean up./n", _tcp->available()); while(_tcp->available() > 0) { _tcp->read(); } } if(_reuse && _canReuse) { //keep-alive的话,保持连接 DEBUG_HTTPCLIENT("[HTTP-Client][end] tcp keep open for reuse/n"); } else { //情况情况就结束连接 DEBUG_HTTPCLIENT("[HTTP-Client][end] tcp stop/n"); _tcp->stop(); } } else { DEBUG_HTTPCLIENT("[HTTP-Client][end] tcp is closed/n"); } } 复制代码
注意点:
函数说明:
/** * 设置需要收集的响应头(1-n个) * @param headerKeys[] const char * 响应头的名字 * @param headerKeysCount const size_t 响应头的个数 * 注意点:headerKeys数组元素个数需要大于等于 headerKeysCount */ void collectHeaders(const char* headerKeys[], const size_t headerKeysCount); 复制代码
源码说明:
void HTTPClient::collectHeaders(const char* headerKeys[], const size_t headerKeysCount) { //设置需要收集响应头的个数 _headerKeysCount = headerKeysCount; if(_currentHeaders) { //释放旧内存空间 delete[] _currentHeaders; } //申请新空间 _currentHeaders = new RequestArgument[_headerKeysCount]; for(size_t i = 0; i < _headerKeysCount; i++) { //设置响应头的key _currentHeaders[i].key = headerKeys[i]; } } 复制代码
RequestArgument定义如下:
struct RequestArgument { String key;//键值对里面的key String value;//键值对里面的value }; 复制代码
注意点:
函数说明:
/** * 获取响应头参数值 * @param name const char * 响应头的名字 * @return value of headerkey(name) */ String header(const char* name); 复制代码
源码说明:
String HTTPClient::header(const char* name) { for(size_t i = 0; i < _headerKeysCount; ++i) { if(_currentHeaders[i].key == name) { //_currentHeaders由collectHeaders方法生成 return _currentHeaders[i].value; } } return String(); } 复制代码
注意点:
函数说明:
/** * 获取第i个响应头参数值 * @param i size_t 响应头索引值 * @return value of header index */ String header(size_t i); 复制代码
源码说明:
String HTTPClient::header(size_t i) { //index不能超过收集响应头的个数 if(i < _headerKeysCount) { return _currentHeaders[i].value; } return String(); } 复制代码
注意点:
函数说明:
/** * 获取第i个响应头名字 * @param i size_t 响应头索引值 * @return name of header index */ String headerName(size_t i); 复制代码
源码说明:
String HTTPClient::headerName(size_t i) { //index不能超过收集响应头的个数 if(i < _headerKeysCount) { return _currentHeaders[i].key; } return String(); } 复制代码
注意点:
函数说明:
/** * 获取收集响应头个数 * @return count int */ int headers(); // get header count 复制代码
源码说明:
int HTTPClient::headers() { return _headerKeysCount; } 复制代码
注意点:
函数说明:
/** * 判断是否存在某一个响应头 * @param name const char* 响应头名字 * @return bool */ bool hasHeader(const char* name); // check if header exists 复制代码
源码说明:
bool HTTPClient::hasHeader(const char* name) { for(size_t i = 0; i < _headerKeysCount; ++i) { if((_currentHeaders[i].key == name) && (_currentHeaders[i].value.length() > 0)) { return true; } } return false; } 复制代码
注意点:
函数说明:
/** * 读取从服务器返回的响应头数据 * @return int http状态码 */ int handleHeaderResponse() 复制代码
函数源码:
/** * reads the response from the server * @return int http code */ int HTTPClient::handleHeaderResponse() { //判断tcp是否连接上 if(!connected()) { //没有和服务器建立连接 return HTTPC_ERROR_NOT_CONNECTED; } //传输编码 String transferEncoding; _returnCode = -1; _size = -1; _transferEncoding = HTTPC_TE_IDENTITY; unsigned long lastDataTime = millis(); while(connected()) { //判断接收缓冲区是否有数据返回 size_t len = _tcp->available(); if(len > 0) { //读取响应数据的第一行 String headerLine = _tcp->readStringUntil('/n'); headerLine.trim(); // remove /r lastDataTime = millis(); DEBUG_HTTPCLIENT("[HTTP-Client][handleHeaderResponse] RX: '%s'/n", headerLine.c_str()); //判断http协议版本 if(headerLine.startsWith("HTTP/1.")) { //获取状态码 _returnCode = headerLine.substring(9, headerLine.indexOf(' ', 9)).toInt(); } else if(headerLine.indexOf(':')) { //处理响应头key和value String headerName = headerLine.substring(0, headerLine.indexOf(':')); String headerValue = headerLine.substring(headerLine.indexOf(':') + 1); headerValue.trim(); //返回响应数据长度 if(headerName.equalsIgnoreCase("Content-Length")) { _size = headerValue.toInt(); } //Connection连接状态 if(headerName.equalsIgnoreCase("Connection")) { _canReuse = headerValue.equalsIgnoreCase("keep-alive"); } //获取传输编码 if(headerName.equalsIgnoreCase("Transfer-Encoding")) { transferEncoding = headerValue; } //这里处理我们需要收集响应头的信息,还记得我们前面有一个 collectHeaders 方法 for(size_t i = 0; i < _headerKeysCount; i++) { if(_currentHeaders[i].key.equalsIgnoreCase(headerName)) { _currentHeaders[i].value = headerValue; break; } } } if(headerLine == "") { DEBUG_HTTPCLIENT("[HTTP-Client][handleHeaderResponse] code: %d/n", _returnCode); if(_size > 0) { DEBUG_HTTPCLIENT("[HTTP-Client][handleHeaderResponse] size: %d/n", _size); } if(transferEncoding.length() > 0) { DEBUG_HTTPCLIENT("[HTTP-Client][handleHeaderResponse] Transfer-Encoding: %s/n", transferEncoding.c_str()); //传输编码是chunked分块编码 if(transferEncoding.equalsIgnoreCase("chunked")) { _transferEncoding = HTTPC_TE_CHUNKED; } else { //不支持其他传输编码,目前http1.1只支持chunked分块编码 return HTTPC_ERROR_ENCODING; } } else { _transferEncoding = HTTPC_TE_IDENTITY; } if(_returnCode) { //返回状态码 return _returnCode; } else { DEBUG_HTTPCLIENT("[HTTP-Client][handleHeaderResponse] Remote host is not an HTTP Server!"); return HTTPC_ERROR_NO_HTTP_SERVER; } } } else { //判断请求是否超时 if((millis() - lastDataTime) > _tcpTimeout) { //read超时错误 return HTTPC_ERROR_READ_TIMEOUT; } delay(0); } } return HTTPC_ERROR_CONNECTION_LOST; } 复制代码
函数说明:
/** * 把响应数据转成字符串 (可能需要很大内存空间) * @return String 响应数据转成字符串 */ String getString(void); 复制代码
注意点:
class StreamString: public Stream, public String { public: size_t write(const uint8_t *buffer, size_t size) override; size_t write(uint8_t data) override; int available() override; int read() override; int peek() override; void flush() override; }; #endif 复制代码
可以看出,StreamString继承了Stream和String,所以拥有了两者的方法。
函数说明:
/** * 获取响应数据的流 * @return WiFiClient& tcp响应数据的流 */ WiFiClient& getStream(void); 复制代码
函数说明:
/** * 获取响应数据的流 * @return WiFiClient& tcp响应数据的流 */ WiFiClient* getStreamPtr(void); 复制代码
在讲解该函数之前,博主先给读者简单介绍一下 分块编码(Transfer-Encoding: chunked):
HTTP/1.1 200 OK Content-Type: text/plain Transfer-Encoding: chunked 25/r/n This is the data in the first chunk/r/n 1C/r/n and this is the second one/r/n 3/r/n con/r/n 8/r/n sequence/r/n 0/r/n /r/n 复制代码
接下来开始讲解该函数,函数说明:
/** * 把响应数据的流写到其他流对象 * @param Stream* 其他流对象 * @return int 写成功的字节数 */ int writeToStream(Stream* stream); 复制代码
这个方法可以说是解析响应数据最重要的方法,所以博主在这里需要重点讲解一下,源码如下:
/** * write all message body / payload to Stream * @param stream Stream * * @return bytes written ( negative values are error codes ) */ int HTTPClient::writeToStream(Stream * stream) { if(!stream) { //本地流对象错误 return returnError(HTTPC_ERROR_NO_STREAM); } if(!connected()) { //当前无连接 return returnError(HTTPC_ERROR_NOT_CONNECTED); } // get length of document (is -1 when Server sends no Content-Length header) int len = _size; int ret = 0; if(_transferEncoding == HTTPC_TE_IDENTITY) { //没有分块编码 ret = writeToStreamDataBlock(stream, len); // have we an error? if(ret < 0) { return returnError(ret); } } else if(_transferEncoding == HTTPC_TE_CHUNKED) { //分块编码 int size = 0; while(1) { if(!connected()) { return returnError(HTTPC_ERROR_CONNECTION_LOST); } //读取每一个分块的头信息 String chunkHeader = _tcp->readStringUntil('/n'); if(chunkHeader.length() <= 0) { return returnError(HTTPC_ERROR_READ_TIMEOUT); } chunkHeader.trim(); // remove /r // read size of chunk 获取分块的大小 len = (uint32_t) strtol((const char *) chunkHeader.c_str(), NULL, 16); size += len; DEBUG_HTTPCLIENT("[HTTP-Client] read chunk len: %d/n", len); // data left? if(len > 0) { //读取分块数据 int r = writeToStreamDataBlock(stream, len); if(r < 0) { // error in writeToStreamDataBlock return returnError(r); } ret += r; } else { // if no length Header use global chunk size if(_size <= 0) { _size = size; } // check if we have write all data out if(ret != _size) { return returnError(HTTPC_ERROR_STREAM_WRITE); } break; } // 读取分块数据的结束字符串 char buf[2]; auto trailing_seq_len = _tcp->readBytes((uint8_t*)buf, 2); if (trailing_seq_len != 2 || buf[0] != '/r' || buf[1] != '/n') { return returnError(HTTPC_ERROR_READ_TIMEOUT); } delay(0); } } else { return returnError(HTTPC_ERROR_ENCODING); } end(); return ret; } 复制代码
再来看看 writeToStreamDataBlock 函数:
/** * write one Data Block to Stream * @param stream Stream * 流对象 * @param size int 读取字节数 * @return < 0 = error >= 0 = size written 读取成功个数 */ int HTTPClient::writeToStreamDataBlock(Stream * stream, int size) { //读一次的缓冲区大小,最大为1460 int buff_size = HTTP_TCP_BUFFER_SIZE; int len = size; int bytesWritten = 0; // 如果len小于buff_size,设置len大小的缓冲区,优化内存使用 if((len > 0) && (len < HTTP_TCP_BUFFER_SIZE)) { buff_size = len; } // 申请读内存空间 uint8_t * buff = (uint8_t *) malloc(buff_size); if(buff) { // read all data from server while(connected() && (len > 0 || len == -1)) { // 获取接收缓冲区的数据字节数 size_t sizeAvailable = _tcp->available(); if(sizeAvailable) { int readBytes = sizeAvailable; // 判断读取字节数 if(len > 0 && readBytes > len) { readBytes = len; } // not read more the buffer can handle if(readBytes > buff_size) { //readBytes的最大值是申请缓冲区的大小 readBytes = buff_size; } // 读取数据 int bytesRead = _tcp->readBytes(buff, readBytes); // 把数据写到其他流对象,比如数组 字符串 int bytesWrite = stream->write(buff, bytesRead); bytesWritten += bytesWrite; // are all Bytes a writen to stream ? if(bytesWrite != bytesRead) { //数据没有被成功一次性写入流对象 DEBUG_HTTPCLIENT("[HTTP-Client][writeToStream] short write asked for %d but got %d retry.../n", bytesRead, bytesWrite); // 检测写错误 if(stream->getWriteError()) { DEBUG_HTTPCLIENT("[HTTP-Client][writeToStreamDataBlock] stream write error %d/n", stream->getWriteError()); //reset write error for retry stream->clearWriteError(); } // some time for the stream delay(1); int leftBytes = (readBytes - bytesWrite); // 尝试重新写入剩余的字节数 bytesWrite = stream->write((buff + bytesWrite), leftBytes); bytesWritten += bytesWrite; if(bytesWrite != leftBytes) { // 重新尝试之后还是失败了 DEBUG_HTTPCLIENT("[HTTP-Client][writeToStream] short write asked for %d but got %d failed./n", leftBytes, bytesWrite); //释放内存空间,返回错误 free(buff); return HTTPC_ERROR_STREAM_WRITE; } } // check for write error if(stream->getWriteError()) { DEBUG_HTTPCLIENT("[HTTP-Client][writeToStreamDataBlock] stream write error %d/n", stream->getWriteError()); free(buff); return HTTPC_ERROR_STREAM_WRITE; } // count bytes to read left if(len > 0) { //计算剩余需要读取的字节数,每读一次重新计算一次 len -= readBytes; } delay(0); } else { delay(1); } } //读取完数据后释放空间 free(buff); DEBUG_HTTPCLIENT("[HTTP-Client][writeToStreamDataBlock] connection closed or file end (written: %d)./n", bytesWritten); //判断是否读取够了leg长度的字节数 if((size > 0) && (size != bytesWritten)) { DEBUG_HTTPCLIENT("[HTTP-Client][writeToStreamDataBlock] bytesWritten %d and size %d mismatch!./n", bytesWritten, size); return HTTPC_ERROR_STREAM_WRITE; } } else { DEBUG_HTTPCLIENT("[HTTP-Client][writeToStreamDataBlock] too less ram! need %d/n", HTTP_TCP_BUFFER_SIZE); return HTTPC_ERROR_TOO_LESS_RAM; } return bytesWritten; } 复制代码
通过上面的讲解,博主相信大家应该有个初步了解了,希望仔细研读。
函数说明:
/** * 获取响应数据字节数 * @return int 响应数据字节数 */ int getSize(void); 复制代码
注意点:
函数说明:
/** * 根据错误码error返回具体错误信息 * @param error 错误码 * @return String 错误码对应的错误信息 */ static String errorToString(int error); 复制代码
错误码定义:
/// HTTP client errors #define HTTPC_ERROR_CONNECTION_REFUSED (-1) #define HTTPC_ERROR_SEND_HEADER_FAILED (-2) #define HTTPC_ERROR_SEND_PAYLOAD_FAILED (-3) #define HTTPC_ERROR_NOT_CONNECTED (-4) #define HTTPC_ERROR_CONNECTION_LOST (-5) #define HTTPC_ERROR_NO_STREAM (-6) #define HTTPC_ERROR_NO_HTTP_SERVER (-7) #define HTTPC_ERROR_TOO_LESS_RAM (-8) #define HTTPC_ERROR_ENCODING (-9) #define HTTPC_ERROR_STREAM_WRITE (-10) #define HTTPC_ERROR_READ_TIMEOUT (-11) 复制代码
函数源码:
/** * converts error code to String * @param error int * @return String */ String HTTPClient::errorToString(int error) { switch(error) { case HTTPC_ERROR_CONNECTION_REFUSED: return F("connection refused");//拒绝连接,一般多是权限问题 case HTTPC_ERROR_SEND_HEADER_FAILED: return F("send header failed");//请求头错误 case HTTPC_ERROR_SEND_PAYLOAD_FAILED: return F("send payload failed");//发送请求数据错误 case HTTPC_ERROR_NOT_CONNECTED: return F("not connected");//没有和服务器建立连接 case HTTPC_ERROR_CONNECTION_LOST: return F("connection lost");//连接断开 case HTTPC_ERROR_NO_STREAM: return F("no stream"); case HTTPC_ERROR_NO_HTTP_SERVER: return F("no HTTP server");//没有找到Http server case HTTPC_ERROR_TOO_LESS_RAM: return F("too less ram");//内存不够用 case HTTPC_ERROR_ENCODING: return F("Transfer-Encoding not supported");//不支持该传输编码 case HTTPC_ERROR_STREAM_WRITE: return F("Stream write error");//写数据流失败 case HTTPC_ERROR_READ_TIMEOUT: return F("read Timeout");//读取数据超时 default: return String(); } } 复制代码
讲了那么多的理论知识,该开始实际操作了,请看以下几个例子。
源码:
/** * Demo: * 演示Http请求天气接口信息 * @author 单片机菜鸟 * @date 2019/09/09 */ #include <ESP8266WiFi.h> #include <ArduinoJson.h> #include <ESP8266HTTPClient.h> //以下三个定义为调试定义 #define DebugBegin(baud_rate) Serial.begin(baud_rate) #define DebugPrintln(message) Serial.println(message) #define DebugPrint(message) Serial.print(message) const char* AP_SSID = "xxxxx"; // XXXXXX -- 使用时请修改为当前你的 wifi ssid const char* AP_PSK = "xxxxxx"; // XXXXXX -- 使用时请修改为当前你的 wifi 密码 const char* HOST = "http://api.seniverse.com"; const char* APIKEY = "wcmquevztdy1jpca"; //API KEY const char* CITY = "guangzhou"; const char* LANGUAGE = "zh-Hans";//zh-Hans 简体中文 会显示乱码 const unsigned long BAUD_RATE = 115200; // serial connection speed const unsigned long HTTP_TIMEOUT = 5000; // max respone time from server // 我们要从此网页中提取的数据的类型 struct WeatherData { char city[16];//城市名称 char weather[32];//天气介绍(多云...) char temp[16];//温度 char udate[32];//更新时间 }; HTTPClient http; String GetUrl; String response; WeatherData weatherData; void setup() { // put your setup code here, to run once: WiFi.mode(WIFI_STA); //设置esp8266 工作模式 DebugBegin(BAUD_RATE); DebugPrint("Connecting to ");//写几句提示,哈哈 DebugPrintln(AP_SSID); WiFi.begin(AP_SSID, AP_PSK); //连接wifi WiFi.setAutoConnect(true); while (WiFi.status() != WL_CONNECTED) { //这个函数是wifi连接状态,返回wifi链接状态 delay(500); DebugPrint("."); } DebugPrintln(""); DebugPrintln("WiFi connected"); DebugPrintln("IP address: " + WiFi.localIP()); //拼接get请求url 博哥后面考虑看看是否可以封装一个方法来用用 不需要自己一个个拼装这个url GetUrl = String(HOST) + "/v3/weather/now.json?key="; GetUrl += APIKEY; GetUrl += "&location="; GetUrl += CITY; GetUrl += "&language="; GetUrl += LANGUAGE; //设置超时 http.setTimeout(HTTP_TIMEOUT); //设置请求url http.begin(GetUrl); //以下为设置一些头 其实没什么用 最重要是后端服务器支持 http.setUserAgent("esp8266");//用户代理版本 http.setAuthorization("esp8266","boge");//用户校验信息 } void loop() { //心知天气 发送http get请求 int httpCode = http.GET(); if (httpCode > 0) { Serial.printf("[HTTP] GET... code: %d/n", httpCode); //判断请求是否成功 if (httpCode == HTTP_CODE_OK) { //读取响应内容 response = http.getString(); DebugPrintln("Get the data from Internet!"); DebugPrintln(response); //解析响应内容 if (parseUserData(response, &weatherData)) { //打印响应内容 printUserData(&weatherData); } } } else { Serial.printf("[HTTP] GET... failed, error: %s/n", http.errorToString(httpCode).c_str()); } http.end(); delay(1000);//每1s调用一次 } /** * @Desc 解析数据 Json解析 * 数据格式如下: * { * "results": [ * { * "location": { * "id": "WX4FBXXFKE4F", * "name": "北京", * "country": "CN", * "path": "北京,北京,中国", * "timezone": "Asia/Shanghai", * "timezone_offset": "+08:00" * }, * "now": { * "text": "多云", * "code": "4", * "temperature": "23" * }, * "last_update": "2017-09-13T09:51:00+08:00" * } * ] *} */ bool parseUserData(String content, struct WeatherData* weatherData) { // -- 根据我们需要解析的数据来计算JSON缓冲区最佳大小 // 如果你使用StaticJsonBuffer时才需要 // const size_t BUFFER_SIZE = 1024; // 在堆栈上分配一个临时内存池 // StaticJsonBuffer<BUFFER_SIZE> jsonBuffer; // -- 如果堆栈的内存池太大,使用 DynamicJsonBuffer jsonBuffer 代替 DynamicJsonBuffer jsonBuffer; JsonObject& root = jsonBuffer.parseObject(content); if (!root.success()) { DebugPrintln("JSON parsing failed!"); return false; } //复制我们感兴趣的字符串 strcpy(weatherData->city, root["results"][0]["location"]["name"]); strcpy(weatherData->weather, root["results"][0]["now"]["text"]); strcpy(weatherData->temp, root["results"][0]["now"]["temperature"]); strcpy(weatherData->udate, root["results"][0]["last_update"]); // -- 这不是强制复制,你可以使用指针,因为他们是指向“内容”缓冲区内,所以你需要确保 // 当你读取字符串时它仍在内存中 return true; } // 打印从JSON中提取的数据 void printUserData(const struct WeatherData* weatherData) { DebugPrintln("Print parsed data :"); DebugPrint("City : "); DebugPrint(weatherData->city); DebugPrint(", /t"); DebugPrint("Weather : "); DebugPrint(weatherData->weather); DebugPrint(",/t"); DebugPrint("Temp : "); DebugPrint(weatherData->temp); DebugPrint(" C"); DebugPrint(",/t"); DebugPrint("Last Updata : "); DebugPrint(weatherData->udate); DebugPrintln("/r/n"); } 复制代码
源码解析:setup中配置好url,串口参数,以及httpclient,并设置了client的请求头。loop中,每个1s去请求一次get服务,把获取回来的天气信息通过json库转成具体对应的数值。
测试结果: [外链图片转存失败(img-KFEZjksz-1562303373408)( www.arduino.cn/data/attach… )]
源码:
/** * Demo: * 演示Http请求天气接口信息,演示响应头操作 * @author 单片机菜鸟 * @date 2019/09/09 */ #include <ESP8266WiFi.h> #include <ArduinoJson.h> #include <ESP8266HTTPClient.h> //以下三个定义为调试定义 #define DebugBegin(baud_rate) Serial.begin(baud_rate) #define DebugPrintln(message) Serial.println(message) #define DebugPrint(message) Serial.print(message) const char* AP_SSID = "TP-LINK_5344"; // XXXXXX -- 使用时请修改为当前你的 wifi ssid const char* AP_PSK = "6206908you11011010"; // XXXXXX -- 使用时请修改为当前你的 wifi 密码 const char* HOST = "http://api.seniverse.com"; const char* APIKEY = "wcmquevztdy1jpca"; //API KEY const char* CITY = "guangzhou"; const char* LANGUAGE = "zh-Hans";//zh-Hans 简体中文 会显示乱码 const char *keys[] = {"Content-Length","Content-Type","Connection","Date"};//需要收集的响应头的信息 const unsigned long BAUD_RATE = 115200; // serial connection speed const unsigned long HTTP_TIMEOUT = 5000; // max respone time from server; HTTPClient http; String GetUrl; String response; void setup() { // put your setup code here, to run once: WiFi.mode(WIFI_STA); //设置esp8266 工作模式 DebugBegin(BAUD_RATE); DebugPrint("Connecting to ");//写几句提示,哈哈 DebugPrintln(AP_SSID); WiFi.begin(AP_SSID, AP_PSK); //连接wifi WiFi.setAutoConnect(true); while (WiFi.status() != WL_CONNECTED) { //这个函数是wifi连接状态,返回wifi链接状态 delay(500); DebugPrint("."); } DebugPrintln(""); DebugPrintln("WiFi connected"); DebugPrintln("IP address: " + WiFi.localIP()); //拼接get请求url 博哥后面考虑看看是否可以封装一个方法来用用 不需要自己一个个拼装这个url GetUrl = String(HOST) + "/v3/weather/now.json?key="; GetUrl += APIKEY; GetUrl += "&location="; GetUrl += CITY; GetUrl += "&language="; GetUrl += LANGUAGE; //设置超时 http.setTimeout(HTTP_TIMEOUT); //设置请求url http.begin(GetUrl); //以下为设置一些头 其实没什么用 最重要是后端服务器支持 http.setUserAgent("esp8266");//用户代理版本 http.setAuthorization("esp8266","boge");//用户校验信息 http.addHeader("myname","cainiaobo"); //设置获取响应头的信息 http.collectHeaders(keys,4); } void loop() { //心知天气 发送http get请求 int httpCode = http.GET(); if (httpCode > 0) { Serial.printf("[HTTP] GET... code: %d/n", httpCode); //判断请求是否成功 if (httpCode == HTTP_CODE_OK) { //读取响应内容 response = http.getString(); DebugPrintln("Get the data from Internet!"); DebugPrintln(response); DebugPrintln(String("Content-Length:")+ http.header("Content-Length")); DebugPrintln(String("Content-Type:")+ http.header("Content-Type")); DebugPrintln(String("Connection:")+ http.header("Connection")); DebugPrintln(String("Date:")+ http.header("Date")); } } else { Serial.printf("[HTTP] GET... failed, error: %s/n", http.errorToString(httpCode).c_str()); } http.end(); delay(1000);//每1s调用一次 } 复制代码
源码解析:
测试结果: [外链图片转存失败(img-NzXLOsaR-1562303373408)( www.arduino.cn/data/attach… )]
注意点: