一、使用 esp8266 实现 HTTP 客户端协议
在 arduinoIDE 中,并没有专门的 HTTP 协议客户端库。但是我们可以用 TCP 协议来自动手动实现。
1.1 HTTP 请求报文简介
所谓请求报文,即是基于 TCP/IP 协议发送的一串规范字符,这串规范字符描述了当前请求的具体细节。
一个 HTTP请求报文至少要包括请求行和头部字段,其中包括了:
请求方法(method):是一个动词,如 GET/POST,表示对资源的操作;
请求目标(URI):通常是一个 URI,标记了请求方法要操作的资源;
版本号(Version):表示报文使用的 HTTP 协议版本。
这三个部分通常使用空格(space)来分隔,最后要用 CRLF 换行表示结束。
GET /update HTTP/1.1
在这个请求行里,“GET”是请求方法,“/update”是请求目标,“HTTP/1.1”是版本号,把这三部分连起来,意思就是“服务器你好,我想获取网站 /update 目录下内容,我用的协议版本号是 1.1,请不要用 1.0 或者 2.0 回复我。”
请求行之后便是头部字段了,请求行或状态行再加上头部字段集合就构成了 HTTP 报文里完整的请求头或响应头。
在这里我只介绍 Host 字段,它属于请求字段,只能出现在请求头里,它同时也是唯一一个 HTTP/1.1 规范里要求必须出现的字段,也就是说,如果请求头里没有 Host,那这就是一个错误的报文。
Host 字段告诉服务器这个请求应该由哪个主机来处理,当一台计算机上托管了多个虚拟主机的时候,服务器端就需要用 Host 字段来选择,有点像是一个简单的“路由重定向”。
1.2 esp8266 要发送的报文
在这篇案例中我们让 esp8266 发送一个最简单的报文。
GET /update HTTP/1.1
Host: 192.168.0.123
这个报文使用了 GET 请求,使用HTTP1.1协议 访问了 http://192.168.0.123/update 这个连接,其作用和把他粘贴到浏览器是一样的。不过浏览器会在头部字段添加一些本机信息,这些对本章内容并不重要,我们不需要添加这些。
我们只需要把这些字符串按 TCP 协议发送出去,服务端即会返回给我们想要的数据。
需要注意的是,HTTP 协议是一问一答的,在服务端返回数据后即可断开这个 TCP 连接,客户端或者服务端主动断开都可以。想再次和服务端使用 HTTP 协议通讯只需要再次启动一个 TCP 连接即可。
1.2 HTTP 请求案例
本案例实现一个对 http://192.168.0.123/update 进行请求,服务器返回值控制 esp8266 LED 灯亮的实例。
#include <ESP8266WiFi.h>
const char* host = "192.168.0.102"; // 网络服务器IP
const int httpPort = 8888; // http端口80
const char* ssid = "home";
const char* password = "123456";
void setup(){
Serial.begin(9600);
Serial.println("");
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH);
//设置ESP8266工作模式为无线终端模式
WiFi.mode(WIFI_STA);
//开始连接wifi
WiFi.begin(ssid, password);
int i = 0;
while (WiFi.status() != WL_CONNECTED) { // 尝试进行wifi连接。
delay(1000);
Serial.print(i++); Serial.print(' ');
}
// WiFi连接成功后将通过串口监视器输出连接成功信息
Serial.println("");
Serial.print("Connected to ");
Serial.println(WiFi.SSID()); // WiFi名称
Serial.print("IP address:\t");
Serial.println(WiFi.localIP()); // IP
}
void loop(){
wifiClientRequest();
delay(3000);
}
void wifiClientRequest(){
WiFiClient client; // 建立WiFiClient对象
bool buttonState; // 储存服务器按钮状态变量
Serial.print("Connecting to "); Serial.print(host);
// 连接服务器
if (client.connect(host, httpPort)){
Serial.println(" Success!");
// 建立客户端请求信息
String httpRequest = String("GET /update") + " HTTP/1.1\r\n" +
"Host: " + host + "\r\n";
// 发送客户端请求
Serial.println("Sending request: ");Serial.print(httpRequest);
client.print(httpRequest);
// 获取服务器响应信息中的按钮状态信息
while (client.connected() || client.available()){
if(client.find("buttonState:")){
buttonState = client.parseInt();
Serial.print("buttonState: " );
Serial.println(buttonState);
}
}
} else{
Serial.println(" failed!");
}
Serial.println("===============");
client.stop(); // 停止客户端
// 根据服务器按键状态点亮或熄灭LED
buttonState == 0 ? digitalWrite(LED_BUILTIN, LOW) : digitalWrite(LED_BUILTIN, HIGH);
}
如果要使用这个案例,我们使用工具创建一个 TCP Server:
在同一局域网下,esp8266 填入正确的电脑 IP。 连接后向 esp8266 发送响应报文,通过这种形式模拟一个 web 服务器:
HTTP/1.0 200 OK
Content-Type: text/plain
Content-Length: 14
buttonState: 0
发送后点击关闭连接,这样才是一个完整的 http 请求。之后 esp8266 的 LED 灯即会点亮。
我们在代码中的逻辑是:如果服务器不端口连接,就会一直接受信息。这符合 HTTP 的协议。
// 获取服务器响应信息中的按钮状态信息
while (client.connected() || client.available()){
if(client.find("buttonState:")){
buttonState = client.parseInt();
Serial.print("buttonState: " );
Serial.println(buttonState);
}
二、esp8266 解析JSON
2.1 JSON 概述
JSON数据以“名”“值”对呈现。数据“名”“值”由冒号分隔。JSON数据的书写格式是:
"info": {
"name" : "taichi-maker",
"website" : "www.taichi-maker.com"
}
当然也可以有 JSON 数组
"info": [
{
"name" : "taichi-maker",
"website" : "www.taichi-maker.com"
},
{
"year": 2020,
"month": 12,
"day": 30
}
]
2.2 单一对象JSON解析
#include <ArduinoJson.h>
void setup() {
Serial.begin(9600);
Serial.println("");
// 重点2:即将解析的json文件
String json = "{\"name\":\"taichi-maker\",\"number\":1}";
// 重点1:DynamicJsonDocument对象
const size_t capacity = json.length()*2;
DynamicJsonDocument doc(capacity);
// 重点3:反序列化数据
deserializeJson(doc, json);
// 重点4:获取解析后的数据信息
String nameStr = doc["name"].as<String>();
int numberInt = doc["number"].as<int>();
// 通过串口监视器输出解析后的数据信息
Serial.print("nameStr = ");Serial.println(nameStr);
Serial.print("numberInt = ");Serial.println(numberInt);
}
void loop() {}
关于 JsonDocument 缓冲区大小下边的连接有更详细的讲解。How to determine the capacity of the JsonDocument? | ArduinoJson 6
不过我在参考其他例子中,发现有很多人直接把字符串长度 *2
// 重点1:DynamicJsonDocument对象
const size_t capacity = json.length()*2;
DynamicJsonDocument doc(capacity);
这样也许会造成一些内存浪费,但他是极其方便的。我推荐使用这种办法。
2.3 解析网络 JSON 心知天气
在心知天气中,我们看到获得天气的接口是这样的,先用 postman 来试试~
可见成功返回了 json 对象
{
"results": [
{
"location": {
"id": "WX4FBX****KE4F",
"name": "Beijing",
"country": "CN",
"path": "Beijing,Beijing,China",
"timezone": "Asia/Shanghai",
"timezone_offset": "+08:00"
},
"now": {
"text": "Sunny",
"code": "0",
"temperature": "-1"
},
"last_update": "2023-01-22T09:48:23+08:00"
}
]
}
示例代码:
#include <ESP8266WiFi.h>
#include <ArduinoJson.h>
const char* host = "api.seniverse.com"; // 网络服务器IP
const int httpPort = 80; // http端口80
const char* ssid = "home";
const char* password = "123456";
// 心知天气HTTP请求所需信息
String reqUserKey = "SG0vWTMFqCxPaCyyv"; // 私钥
String reqLocation = "Beijing"; // 城市
String reqUnit = "c"; // 摄氏/华氏
void setup() {
Serial.begin(9600);
Serial.println("");
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH);
//设置ESP8266工作模式为无线终端模式
WiFi.mode(WIFI_STA);
//开始连接wifi
WiFi.begin(ssid, password);
int i = 0;
while (WiFi.status() != WL_CONNECTED) { // 尝试进行wifi连接。
delay(1000);
Serial.print(i++);
Serial.print(' ');
}
}
void loop() {
wifiClientRequest();
delay(3000);
}
void wifiClientRequest() {
WiFiClient client; // 建立WiFiClient对象
// 建立心知天气API当前天气请求资源地址
String reqRes = "/v3/weather/now.json?key=" + reqUserKey + +"&location=" + reqLocation + "&language=en&unit=" + reqUnit;
String httpRequest = String("GET ") + reqRes + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Connection: close\r\n\r\n";
Serial.println("");
Serial.print("Connecting to ");
Serial.print(host);
// 尝试连接服务器
if (client.connect(host, 80)) {
Serial.println(" Success!");
// 向服务器发送http请求信息
client.print(httpRequest);
Serial.println("Sending request: ");
// 获取并显示服务器响应状态行
// 获取并显示服务器响应状态行
String status_response = client.readStringUntil('\n');
Serial.print("status_response: ");
Serial.println(status_response);
// 使用find跳过HTTP响应头
if (client.find("\r\n\r\n")) {
Serial.println("Found Header End. Start Parsing.");
}
//获得 json 正文
String str = client.readString();
Serial.println(str);
DynamicJsonDocument doc(str.length()*2);
deserializeJson(doc, a);
JsonObject results_0 = doc["results"][0];
JsonObject results_0_now = results_0["now"];
const char* results_0_now_text = results_0_now["text"]; // "Sunny"
const char* results_0_now_code = results_0_now["code"]; // "0"
const char* results_0_now_temperature = results_0_now["temperature"]; // "32"
const char* results_0_last_update = results_0["last_update"]; // "2020-06-02T14:40:00+08:00"
// 通过串口监视器显示以上信息
String results_0_now_text_str = results_0_now["text"].as<String>();
int results_0_now_code_int = results_0_now["code"].as<int>();
int results_0_now_temperature_int = results_0_now["temperature"].as<int>();
String results_0_last_update_str = results_0["last_update"].as<String>();
Serial.println(F("======Weahter Now======="));
Serial.print(F("Weather Now: "));
Serial.print(results_0_now_text_str);
Serial.print(F(" "));
Serial.println(results_0_now_code_int);
Serial.print(F("Temperature: "));
Serial.println(results_0_now_temperature_int);
Serial.print(F("Last Update: "));
Serial.println(results_0_last_update_str);
Serial.println(F("========================"));
}
}
其中返回来的值会有响应头和响应体两部分,按照http标准,他们会空一行。
范例如下:
所以在代码中我们用 \r\n\r\n 跳过这个响应头。之后获得的都是响应体 JSON 了。
// 使用find跳过HTTP响应头
if (client.find("\r\n\r\n")) {
Serial.println("Found Header End. Start Parsing.");
}
在这里先使用 string 对象保存 余下的 JSON,之后打印。
我们按照之前的说明,将 JSON 长度 *2 作为解析堆栈长度。
//获得 json 正文
String str = client.readString();
Serial.println(str);
DynamicJsonDocument doc(str.length()*2);
deserializeJson(doc, a);
重点就是这些,解析部分没什么好说的,仔细看看把代码跑跑就行。
运行结果: