在物联网(IoT)设备的数据上传过程中,经常面临内存容量有限的问题。尤其是当需要上传大量数据到服务器时,传统的HTTP传输方式可能会因为内存不足而失败。本文将介绍如何通过分块传输编码(chunked transfer encoding)来优化这一过程,从而在不增加额外内存负担的情况下,有效地将数据上传到服务器。
ESP32和ESP8266是两款流行的IoT开发板,它们虽然功能强大,但RAM容量有限。ESP32有520kB的RAM,而ESP8266仅有80kB供用户使用。这意味着在发送数据时,不能将所有数据一次性加载到内存中,而需要采用流式传输。
传统的HTTP传输需要指定Content-Length
头部,这对于动态生成的内容来说并不方便。为了解决这个问题,分块传输编码应运而生。它允许数据以小单位或“块”的形式发送,每个块都有一个已知的长度。这样,就不需要Content-Length
头部,并且可以流式传输动态生成的内容。
在开始编码之前,需要做一些准备工作:
ESPDateTime
库。分块传输编码本质上是HTTP协议的一个扩展层。它使用Transfer-Encoding: chunked
头部,而不是Content-Length
头部,来告诉接收方数据将以块的形式发送。每个块由一个十六进制的值开始,表示块的长度,后面跟着一个回车换行符,然后是指定长度的数据,最后再跟一个回车换行符。最后一个块的长度为0。
例如,发送“Hello World!”:
5
Hello
7
World!
0
这样,就可以在RAM中只保留一个块的空间,而不是整个文档,从而减少了内存需求。
在C++中,可以使用类,但为了简单起见,本项目采用过程式代码。首先,定义一个函数来发送一个块:
void httpWriteChunked(const char* sz) {
int cl = (sz) ? strlen(sz) : 0;
if (cl > 0) {
char szt[1024];
sprintf(szt, "\r\n%x\r\n%s\r\n", cl, sz);
_client.print(szt);
} else {
_client.print("0\r\n\r\n");
}
}
这个函数检查传入的字符串是否为空,然后获取字符串长度(如果为空则为0),并按照分块传输编码的格式发送数据。如果是最后一个块(传入的字符串为空或NULL),则发送0和两个回车换行符。
接下来是setup()
函数,它负责初始化和发送数据:
void setup() {
Serial.begin(115200);
WiFi.begin(SSID, PASSWORD);
for (int i = 0; i < 30 && WL_CONNECTED != WiFi.status(); ++i) {
delay(500);
}
if (WL_CONNECTED != WiFi.status()) {
Serial.println("Could not connect to WiFi network");
while (true);
}
DateTime.setServer(NTP_SERVER);
DateTime.begin();
if (!DateTime.isTimeValid()) {
Serial.println("Could not fetch Internet time");
while (true);
}
long int dnow = DateTime.utcTime();
if (!_client.connect(REST_SERVER, 80)) {
Serial.println("Could not connect to server");
while (true);
}
char sz[1536];
sprintf_P(sz, PSTR("POST %S HTTP/1.1\r\nHost: %S\r\n"), REST_PATH, REST_SERVER);
strcat_P(sz, PSTR("Accept: application/json\r\n"));
strcat_P(sz, PSTR("Content-Type: application/json\r\n"));
strcat_P(sz, PSTR("Transfer-Encoding: chunked\r\nConnection: close\r\n\r\n"));
_client.print(sz);
httpWriteChunked("{ \"write_api_key\":\"YCKKPCFMQTDQKGHK\",\"updates\":[");
for (int i = 0; i < 5; ++i) {
String str;
time_t tts = (time_t)(dnow - (5 - i));
DateTimeClass dt(tts);
if (i > 0)
str = dt.format(",{\"created_at\":\"%Y-%m-%d %H:%M:%S +0000\",\"field1\":\"");
else
str = dt.format("{ \"created_at\":\"%Y-%m-%d %H:%M:%S +0000\",\"field1\":\"");
strcpy(sz, str.c_str());
char szn[32];
strcat(sz, itoa(i, szn, 10));
strcat(sz, "\"}");
httpWriteChunked(sz);
}
httpWriteChunked("]}");
httpWriteChunked(NULL);
String line = _client.readStringUntil('\n');
if (0 != strncmp("HTTP/1.1 202 ", line.c_str(), 13)) {
Serial.println();
Serial.println("HTTP request failed:");
Serial.println(line);
}
while (_client.connected()) {
line = _client.readStringUntil('\n');
if (line == "\r") {
Serial.println("Success! Visit https://thingspeak.com/channels/1243886 for data");
}
}
_client.stop();
}
这个函数首先初始化串口通信,然后尝试连接到WiFi网络。一旦连接成功,从NTP服务器获取当前时间,然后连接到Thingspeak服务器并开始发送数据。首先发送请求行和一些头部信息,然后发送第一个块,它包含了API密钥。接下来,在一个循环中发送5个条目,每次发送一个条目。这样,就不需要超过1.5kB的缓冲区,这是关键。
在每个条目中,生成一个时间戳。创建当前时间以及前4秒的时间戳。这是因为Thingspeak每秒最多显示一个数据点,所以让服务器相信从5秒前开始创建这些条目。然后,发送最后的JSON终止序列和空块终止符。
之后,开始读取响应。实际上,并不关心响应的内容,只需要HTTP状态行,因为它告诉操作是否成功。
通过使用分块传输编码,可以有效地将大量数据上传到服务器,而不会耗尽设备的RAM。这种方法不仅适用于ESP32和ESP8266,还可以通过一些修改适用于其他Arduino兼容的SoC设备。