IoT设备数据上传优化

在物联网(IoT)设备的数据上传过程中,经常面临内存容量有限的问题。尤其是当需要上传大量数据到服务器时,传统的HTTP传输方式可能会因为内存不足而失败。本文将介绍如何通过分块传输编码(chunked transfer encoding)来优化这一过程,从而在不增加额外内存负担的情况下,有效地将数据上传到服务器。

ESP32和ESP8266是两款流行的IoT开发板,它们虽然功能强大,但RAM容量有限。ESP32有520kB的RAM,而ESP8266仅有80kB供用户使用。这意味着在发送数据时,不能将所有数据一次性加载到内存中,而需要采用流式传输。

传统的HTTP传输需要指定Content-Length头部,这对于动态生成的内容来说并不方便。为了解决这个问题,分块传输编码应运而生。它允许数据以小单位或“块”的形式发送,每个块都有一个已知的长度。这样,就不需要Content-Length头部,并且可以流式传输动态生成的内容。

准备工作

在开始编码之前,需要做一些准备工作:

  • 安装Arduino IDE,并配置好适合硬件的板管理器。
  • 准备一块ESP32开发板,或者ESP8266开发板(后者未经过测试)。
  • 安装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设备。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485