Skip to content

Commit ef0e78b

Browse files
committed
Add ChunkDecodingStream and ChunkDecodingClient
Closes bblanchon/ArduinoJson#2105
1 parent ff84214 commit ef0e78b

10 files changed

Lines changed: 838 additions & 6 deletions

File tree

.github/workflows/ci.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,52 +54,61 @@ jobs:
5454
- core: arduino:avr
5555
board: arduino:avr:leonardo
5656
eeprom: true
57+
httpclient: false
5758
softwareserial: true
5859

5960
- core: esp8266:esp8266
6061
board: esp8266:esp8266:huzzah
6162
eeprom: true
63+
httpclient: true
6264
softwareserial: true
6365
index_url: https://arduino.esp8266.com/stable/package_esp8266com_index.json
6466

6567
- core: esp32:esp32
6668
board: esp32:esp32:esp32
6769
eeprom: true
70+
httpclient: true
6871
softwareserial: false
6972
index_url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
7073

7174
- core: arduino:samd
7275
board: arduino:samd:mkr1000
7376
eeprom: false
77+
httpclient: false
7478
softwareserial: false
7579

7680
- core: STMicroelectronics:stm32
7781
board: STMicroelectronics:stm32:Nucleo_32
7882
eeprom: true
83+
httpclient: false
7984
softwareserial: true
8085
index_url: https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json
8186

8287
- core: stm32duino:STM32F1
8388
board: stm32duino:STM32F1:mapleMini
8489
eeprom: false
90+
httpclient: false
8591
softwareserial: false
8692
index_url: http://dan.drown.org/stm32duino/package_STM32duino_index.json
8793

8894
- core: stm32duino:STM32F4
8995
board: stm32duino:STM32F4:blackpill_f401
9096
eeprom: false
97+
httpclient: false
9198
softwareserial: false
9299
index_url: http://dan.drown.org/stm32duino/package_STM32duino_index.json
93100

94101
- core: rp2040:rp2040
95102
board: rp2040:rp2040:rpipico
96103
eeprom: true
104+
httpclient: false
97105
softwareserial: true
98106
index_url: https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json
99107

100108
- core: arduino:mbed_rp2040
101109
board: arduino:mbed_rp2040:pico
102110
eeprom: false
111+
httpclient: false
103112
softwareserial: false
104113

105114
steps:
@@ -115,6 +124,10 @@ jobs:
115124
- name: Install core
116125
run: arduino-cli core install --additional-urls "${{ matrix.index_url }}" ${{ matrix.core }}
117126

127+
- name: Build ChunkDecoding
128+
if: matrix.httpclient
129+
run: arduino-cli compile --library . --warnings all -b ${{ matrix.board }} "examples/ChunkDecoding/ChunkDecoding.ino"
130+
118131
- name: Build EepromRead
119132
if: matrix.eeprom
120133
run: arduino-cli compile --library . --warnings all -b ${{ matrix.board }} "examples/EepromRead/EepromRead.ino"

README.md

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ For example, with this library, you can:
2727
* debug your program more easily by logging what it sends to a Web service
2828
* send large data with the [Wire library](https://www.arduino.cc/en/reference/wire)
2929
* use a `String`, EEPROM, or `PROGMEM` with a stream interface
30+
* decode HTTP chunks
3031

3132
Read on to see how StreamUtils can help you!
3233

@@ -37,11 +38,11 @@ How to add buffering to a Stream?
3738
### Buffering read operations
3839

3940
Sometimes, you can significantly improve performance by reading many bytes at once.
40-
For example, [according to SPIFFS's wiki](https://github.com/pellepl/spiffs/wiki/Performance-and-Optimizing#reading-files), reading read files in chunks of 64 bytes is much faster than reading them one byte at a time.
41+
For example, [according to SPIFFS's wiki](https://github.com/pellepl/spiffs/wiki/Performance-and-Optimizing#reading-files), reading files in chunks of 64 bytes is much faster than reading them one byte at a time.
4142

4243
![ReadBufferingStream](https://github.com/bblanchon/ArduinoStreamUtils/raw/master/extras/images/ReadBuffer.svg)
4344

44-
To buffer the input, decorate the original `Stream` with `ReadBufferingStream`. For example, suppose your program reads a JSON document from SPIFFS like that:
45+
To buffer the input, decorate the original `Stream` with `ReadBufferingStream`. For example, suppose your program reads a JSON document from SPIFFS like this:
4546

4647
```c++
4748
File file = SPIFFS.open("example.json", "r");
@@ -61,7 +62,7 @@ Unfortunately, this optimization is only possible if:
6162
1. `Stream.readBytes()` is declared `virtual` in your Arduino Code (as it's the case for ESP8266), and
6263
2. the derived class has an optimized implementation of `readBytes()` (as it's the case for SPIFFS' `File`).
6364

64-
When possible, prefer `ReadBufferingClient` to `ReadBufferingStream` because `Client` defines a `read()` method similar to `readBytes()`, except that this one is `virtual` on all platforms.
65+
When possible, prefer `ReadBufferingClient` to `ReadBufferingStream` because `Client` defines a `read()` method similar to `readBytes()`, except this one is `virtual` on all platforms.
6566

6667
If memory allocation fails, `ReadBufferingStream` behaves as if no buffer was used: it forwards all calls to the upstream `Stream`.
6768

@@ -74,7 +75,7 @@ For example, writing to `WiFiClient` one byte at a time is very slow; it's much
7475

7576
![WriteBufferingStream](https://github.com/bblanchon/ArduinoStreamUtils/raw/master/extras/images/WriteBuffer.svg)
7677

77-
To add a buffer, decorate the original `Stream` with `WriteBufferingStream`. For example, if your program sends a JSON document via `WiFiClient`, like that:
78+
To add a buffer, decorate the original `Stream` with `WriteBufferingStream`. For example, if your program sends a JSON document via `WiFiClient` like this:
7879

7980
```c++
8081
serializeJson(doc, wifiClient);
@@ -166,7 +167,7 @@ char response[256];
166167
client.readBytes(response, 256);
167168
```
168169

169-
Then decorate `client` and replace the calls:
170+
Then, decorate `client` and replace the calls:
170171

171172
```c++
172173
LoggingStream loggingClient(client, Serial);
@@ -187,7 +188,7 @@ These extra bits increase the amount of traffic but allow correcting any one-bit
187188
If you use this encoding on an 8-bit channel, it effectively doubles the amount of traffic. However, if you use an [`HardwareSerial`](https://www.arduino.cc/reference/en/language/functions/communication/serial/) instance (like `Serial`, `Serial1`...), you can slightly reduce the overhead by configuring the ports as a 7-bit channel, like so:
188189
189190
```c++
190-
// Initialize serial port with 9600 bauds, 7-bits of data, no parity, and one stop bit
191+
// Initialize serial port with 9600 bauds, 7 bits of data, no parity, and one stop bit
191192
Serial1.begin(9600, SERIAL_7N1);
192193
```
193194

@@ -346,6 +347,49 @@ Serial.println(stream.readString());
346347
347348
`ProgmemStream`'s constructor also supports `const __FlashStringHelper*` (the type returned by the `F()` macro) and an optional second argument to specify the size of the buffer.
348349
350+
How to decode HTTP chunks?
351+
--------------------------
352+
353+
HTTP servers can send their response in multiple parts using [Chunked Transfer Encoding](https://en.wikipedia.org/wiki/Chunked_transfer_encoding). Clients using HTTP 1.1 must support this encoding as it's not optional and is dictated by the server.
354+
355+
`ChunkDecodingStream` and `ChunkDecodingClient` are decorators that decode the chunks and make the response available as a regular stream.
356+
357+
![ChunkDecodingStream](https://github.com/bblanchon/ArduinoStreamUtils/raw/master/extras/images/ChunkDecodingStream.svg)
358+
359+
Here is an example using `HTTPClient`:
360+
361+
```c++
362+
// Initialize HTTPClient
363+
HTTPClient http;
364+
http.begin(client, url);
365+
366+
// Tell HTTPClient to collect the Transfer-Encoding header
367+
// (by default HTTPClient discards the response headers)
368+
const char *keys[] = {"Transfer-Encoding"};
369+
http.collectHeaders(keys, 1);
370+
371+
// Send the request
372+
int status = http.GET();
373+
if (status != 200) return;
374+
375+
// Create the raw and decoded stream
376+
Stream& rawStream = http.getStream();
377+
ChunkDecodingStream decodedStream(http.getStream());
378+
379+
// Choose the stream based on the Transfer-Encoding header
380+
Stream& response = http.header("Transfer-Encoding") == "chunked" ? decodedStream : rawStream;
381+
382+
// Read the response
383+
JsonDocument doc;
384+
deserializeJson(doc, response);
385+
386+
// Close the connection
387+
http.end();
388+
```
389+
390+
Note that `HTTPClient` already performs chunk decoding **if** you use `getString()`, but you might want to use `getStream()` to avoid buffering the entire response in memory.
391+
392+
Also, you can avoid chunked transfer encoding by downgrading the HTTP version to 1.0. `HTTPClient` allows you to do that by calling `useHTTP10(true)` before sending the request.
349393

350394
Summary
351395
-------
@@ -367,6 +411,7 @@ See the equivalence table below.
367411
| Error correction (decode only) | `HammingDecodingClient` | `HammingDecodingStream` | |
368412
| Error correction (encode only) | `HammingEncodingClient` | `HammingEncodingStream` | `HammingPrint` |
369413
| Error correction (encode & decode) | `HammingClient` | `HammingStream` | |
414+
| Decode HTTP chunks | `ChunkDecodingClient` | `ChunkDecodingStream` | |
370415

371416
Prefer `XxxClient` to `XxxStream` because, unlike `Stream::readBytes()`, `Client::read()` is virtual on all cores and therefore allows optimized implementations.
372417

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#if defined(ARDUINO_ARCH_ESP8266)
2+
#include <ESP8266HTTPClient.h>
3+
#include <ESP8266WiFi.h>
4+
#elif defined(ARDUINO_ARCH_ESP32)
5+
#include <HTTPClient.h>
6+
#include <WiFi.h>
7+
#include <WiFiClientSecure.h>
8+
#else
9+
#error Unsuported platform
10+
#endif
11+
12+
#include <StreamUtils.h>
13+
14+
// WiFi network configuration.
15+
const char* WIFI_SSID = "*****EDIT*****";
16+
const char* WIFI_PASSWORD = "*****EDIT*****";
17+
18+
void setup() {
19+
// Initialize Serial Port
20+
Serial.begin(115200);
21+
while (!Serial)
22+
continue;
23+
24+
// Connect to the WLAN
25+
WiFi.mode(WIFI_STA);
26+
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
27+
while (WiFi.status() != WL_CONNECTED) {
28+
Serial.println(F("Connecting to Wifi..."));
29+
delay(500);
30+
}
31+
32+
// Initialize the SSL library
33+
WiFiClientSecure client;
34+
client.setInsecure(); // ignore server's certificate
35+
36+
// Send the request
37+
HTTPClient http;
38+
http.begin(client, F("https://jigsaw.w3.org/HTTP/ChunkedScript"));
39+
40+
// Ask HTTPClient to collect the Transfer-Encoding header
41+
// (by default it discards all headers)
42+
const char* keys[] = {"Transfer-Encoding"};
43+
http.collectHeaders(keys, 1);
44+
45+
Serial.println(F("Sending request..."));
46+
int status = http.GET();
47+
if (status != 200) {
48+
Serial.print(F("Unexpected HTTP status: "));
49+
Serial.println(status);
50+
return;
51+
}
52+
53+
// Get a reference to the stream
54+
Stream& rawStream = http.getStream();
55+
ChunkDecodingStream decodedStream(http.getStream());
56+
Stream& response =
57+
http.header("Transfer-Encoding") == "chunked" ? decodedStream : rawStream;
58+
59+
// Read and print the response
60+
char buffer[256];
61+
size_t n = 0;
62+
do {
63+
n = response.readBytes(buffer, sizeof(buffer));
64+
Serial.write(buffer, n);
65+
} while (n == sizeof(buffer));
66+
67+
// Disconnect
68+
http.end();
69+
70+
Serial.println(F("Done!"));
71+
}
72+
73+
void loop() {}
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// StreamUtils - github.com/bblanchon/ArduinoStreamUtils
2+
// Copyright Benoit Blanchon 2019-2024
3+
// MIT License
4+
5+
#include "StreamUtils/Clients/ChunkDecodingClient.hpp"
6+
#include "StreamUtils/Clients/MemoryClient.hpp"
7+
#include "StreamUtils/Clients/SpyingClient.hpp"
8+
#include "StreamUtils/Prints/StringPrint.hpp"
9+
10+
#include "doctest.h"
11+
12+
using namespace StreamUtils;
13+
14+
TEST_CASE("ChunkDecodingClient") {
15+
MemoryClient upstream(128);
16+
StringPrint log;
17+
SpyingClient spy{upstream, log};
18+
ChunkDecodingClient client{spy};
19+
20+
// Most of the tests are in ChunkDecodingStreamTest.cpp
21+
// This file only tests the client-specific methods
22+
23+
SUBCASE("read() merge chunks") {
24+
uint8_t buffer[32];
25+
26+
upstream.print(
27+
"4\r\n"
28+
"XXXX\r\n"
29+
"4\r\n"
30+
"YYYY\r\n"
31+
"0\r\n"
32+
"\r\n");
33+
REQUIRE(client.read(buffer, 32) == 8);
34+
REQUIRE(buffer[0] == 'X');
35+
REQUIRE(buffer[4] == 'Y');
36+
}
37+
38+
SUBCASE("read() waits timeout") {
39+
uint8_t buffer[32];
40+
41+
upstream.print(
42+
"4\r\n"
43+
"XXXX\r\n");
44+
REQUIRE(client.read(buffer, 32) == 4);
45+
REQUIRE(buffer[0] == 'X');
46+
47+
REQUIRE(log.str() ==
48+
"read(1) -> 1" // 1
49+
"read(1) -> 1" // \r
50+
"read(1) -> 1" // \n
51+
"read(4) -> 4" // XXXX
52+
"read(1) -> 1" // \r
53+
"read(1) -> 1" // \n
54+
"read(1) -> 0 [timeout]");
55+
}
56+
57+
SUBCASE("read() doen't wait if final chunk received") {
58+
uint8_t buffer[32];
59+
60+
upstream.print(
61+
"4\r\n"
62+
"XXXX\r\n"
63+
"0\r\n"
64+
"\r\n");
65+
REQUIRE(client.read(buffer, 32) == 4);
66+
REQUIRE(buffer[0] == 'X');
67+
REQUIRE(client.ended() == true);
68+
69+
REQUIRE(log.str() ==
70+
"read(1) -> 1" // 1
71+
"read(1) -> 1" // \r
72+
"read(1) -> 1" // \n
73+
"read(4) -> 4" // XXXX
74+
"read(1) -> 1" // \r
75+
"read(1) -> 1" // \n
76+
"read(1) -> 1" // 0
77+
"read(1) -> 1" // \r
78+
"read(1) -> 1" // \n
79+
"read(1) -> 1" // \r
80+
"read(1) -> 1" // \n
81+
);
82+
}
83+
}

0 commit comments

Comments
 (0)