在前面章節的博客中,博主介紹了ESP8266WiFi庫 Tcp client的用法,並模擬了Http請求。可是,能夠看出經過WiFiClient模擬Http請求,咱們須要本身拼裝Http請求協議,稍微不當心就很容易拼接錯誤。
那麼有沒有針對Http請求操做的庫呢?答案確定是有的,這就是博主本篇須要跟你們講述的知識——ESP8266HTTPClient庫。
請注意,ESP8266HTTPClient庫不屬於ESP8266WiFi庫的一部分,因此須要引入html
#include <ESP8266HTTPClient.h>
複製代碼
博主說過,Http是基於Tcp協議之上的,因此你在ESP8266HTTPClient源碼中會看到TcpClient的蹤影。git
#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協議,更加深刻的瞭解請自行查閱資料。github
HTTP協議是Hyper Text Transfer Protocol的縮寫,簡稱超文本傳輸協議,用於從WWW服務器傳輸文本到本地瀏覽器的傳送協議。
HTTP是一個基於TCP/IP通訊協議來傳遞數據,瀏覽器做爲HTTP客戶端經過URL向HTTP服務端即WEB服務器發送全部請求。WEB服務器根據接收到的請求後,向客戶端發送響應信息。 web
[外鏈圖片轉存失敗(img-VhNm8NA9-1562303373406)(www.arduino.cn/data/attach…)]json
HTTP是一個應用層協議,由請求和響應構成,是一個標準的客戶端服務器模型。HTTP默認的端口號是80,HTTPS的端口號是443。
瀏覽網頁是HTTP主要應用,但不表明只用於網頁瀏覽。HTTP只是一種協議,只要通訊雙方遵照這個協議,HTTP就能用。後端
一次HTTP操做稱爲一個事務,工做流程可分爲4步:api
以上四步驟,只要其中一步出現錯誤,那麼就會產生錯誤信息返回給客戶端。數組
客戶端發送一個HTTP請求到服務器,請求信息包括如下格式:瀏覽器
請求行以一個方法符號開頭,以空格分開,後面跟着請求的URI和協議的版本。bash
請求例子,使用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…)]
注意點:
這一篇章,主要講解了Http協議的client,你們只須要記住一句話就好——Http是基於Tcp協議之上的應用層協議。
ESP技術開發交流羣 622368884,不喜勿加