WebServer: Allow client to send many requests on the same connection by ZakCodes · Pull Request #7414 · esp8266/Arduino

Closes #7412.

Solution

Instead of telling the client to close the connection and waiting 2s for the client to do it before accepting another one, this allows the client to send another request within the 2s if there were no other clients waiting to connect to the server when the response was sent.

By default, this isn't activated. To activate it, the programmer must call ESP8266WebServer::keepAlive(true) in the handler during the request.

As mentioned before, if there are any other clients waiting to send a request to the server when the response is sent, the server won't accept any other request from the client. This is important because the server doesn't support having multiple concurrent connections, therefore, without this, a client could stay connected to the server forever and block all the other clients.

Testing code

Arduino HTTPS server code (before patch)

Here's the code to upload on your ESP8266 before applying the patch in this pull request.

#include <Arduino.h>
#include <ESP8266WebServerSecure.h>
#include <ESP8266WiFi.h>

static const uint8_t PRIVATE_KEY[] = R"EOF(
-----BEGIN RSA PRIVATE KEY-----
MIIBPAIBAAJBAMaLGUw9UMkni86+fipZS3zoJwza4/nDJkOQeC8M31yb35fISva6
4d2K1HLMIBl4ViaNSd1RElzRJifSy2bIdcMCAwEAAQJBAIaD44Xl3QAMTQqrwWsL
yLs9xodNHjwv3ZLVJLgr7oEc3yUeCv4q28AwlYDOO04OoT53GAS3m1qYv4FG7jox
VaECIQD6/9e2s4WqUpOrmFdRz3AMZx5LbzMrfwYO/yEztNoLCQIhAMp/t8DxqVK9
Cu/h03x/gIal9alpv6uD3MRU4MfC6FFrAiEA9/01NQcUJl8mFaETjPoF+8saTG+W
v//ljYWXWU3zLHkCIHe3RBJsjHcezg19i8Npublg+jhbDXa/8U+dAnr27tPbAiEA
uysakbOSKKk6NyF7zujFEu4yhgnoqrVwYb78RunsVSM=
-----END RSA PRIVATE KEY-----
)EOF";

static const uint8_t CERTIFICATE[] = R"EOF(
-----BEGIN CERTIFICATE-----
MIICBTCCAa+gAwIBAgIUPtU58ibKekgnRomdBcIJIdYUSZ4wDQYJKoZIhvcNAQEF
BQAwVzELMAkGA1UEBhMCQ0ExEjAQBgNVBAgMCVF1w4PCqWJlYzERMA8GA1UEBwwI
R2F0aW5lYXUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0x
OTA4MjMwMjMzNDhaFw00OTA4MTUwMjMzNDhaMFcxCzAJBgNVBAYTAkNBMRIwEAYD
VQQIDAlRdcODwqliZWMxETAPBgNVBAcMCEdhdGluZWF1MSEwHwYDVQQKDBhJbnRl
cm5ldCBXaWRnaXRzIFB0eSBMdGQwXDANBgkqhkiG9w0BAQEFAANLADBIAkEAxosZ
TD1QySeLzr5+KllLfOgnDNrj+cMmQ5B4LwzfXJvfl8hK9rrh3YrUcswgGXhWJo1J
3VESXNEmJ9LLZsh1wwIDAQABo1MwUTAdBgNVHQ4EFgQUTNUSlxtCEYth3de5ciL6
qiDdpZcwHwYDVR0jBBgwFoAUTNUSlxtCEYth3de5ciL6qiDdpZcwDwYDVR0TAQH/
BAUwAwEB/zANBgkqhkiG9w0BAQUFAANBAEqW+lzyWOT8cA+dVRwW+BkHguxR1ev6
zYHQwup2cIEwXeArBptlX0wkdjb4bGtwWM1NiqtCHCeCXyQhuPdMCLE=
-----END CERTIFICATE-----
)EOF";

#define WIFI_SSID "your-ssid"
#define WIFI_PASSWORD "your-password"

ESP8266WebServerSecure server(443);

void setup() {
    Serial.begin(115200);
    Serial.setDebugOutput(true);
    Serial.println();

    WiFi.mode(WIFI_STA);
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    uint8_t i = 0;
    while (WiFi.status() != WL_CONNECTED) {
        i++;
        if (i == 21) {
            WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
            Serial.println("Could not connect to " WIFI_SSID);
        }
        delay(500);
    }
    Serial.print("Connected! IP address: ");
    Serial.println(WiFi.localIP());

    server.getServer().setServerKeyAndCert(PRIVATE_KEY, sizeof(PRIVATE_KEY),
                                           CERTIFICATE, sizeof(CERTIFICATE));
    server.on("/", []() {
        server.send(200, "text/plain", "Hey!\r\n");
    });
    server.begin();
}

void loop() {
    server.handleClient();
}

Arduino HTTPS server code (after the patch)

Here's the code that requires the patch in this pull request to compile.

#include <Arduino.h>
#include <ESP8266WebServerSecure.h>
#include <ESP8266WiFi.h>

static const uint8_t PRIVATE_KEY[] = R"EOF(
-----BEGIN RSA PRIVATE KEY-----
MIIBPAIBAAJBAMaLGUw9UMkni86+fipZS3zoJwza4/nDJkOQeC8M31yb35fISva6
4d2K1HLMIBl4ViaNSd1RElzRJifSy2bIdcMCAwEAAQJBAIaD44Xl3QAMTQqrwWsL
yLs9xodNHjwv3ZLVJLgr7oEc3yUeCv4q28AwlYDOO04OoT53GAS3m1qYv4FG7jox
VaECIQD6/9e2s4WqUpOrmFdRz3AMZx5LbzMrfwYO/yEztNoLCQIhAMp/t8DxqVK9
Cu/h03x/gIal9alpv6uD3MRU4MfC6FFrAiEA9/01NQcUJl8mFaETjPoF+8saTG+W
v//ljYWXWU3zLHkCIHe3RBJsjHcezg19i8Npublg+jhbDXa/8U+dAnr27tPbAiEA
uysakbOSKKk6NyF7zujFEu4yhgnoqrVwYb78RunsVSM=
-----END RSA PRIVATE KEY-----
)EOF";

static const uint8_t CERTIFICATE[] = R"EOF(
-----BEGIN CERTIFICATE-----
MIICBTCCAa+gAwIBAgIUPtU58ibKekgnRomdBcIJIdYUSZ4wDQYJKoZIhvcNAQEF
BQAwVzELMAkGA1UEBhMCQ0ExEjAQBgNVBAgMCVF1w4PCqWJlYzERMA8GA1UEBwwI
R2F0aW5lYXUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0x
OTA4MjMwMjMzNDhaFw00OTA4MTUwMjMzNDhaMFcxCzAJBgNVBAYTAkNBMRIwEAYD
VQQIDAlRdcODwqliZWMxETAPBgNVBAcMCEdhdGluZWF1MSEwHwYDVQQKDBhJbnRl
cm5ldCBXaWRnaXRzIFB0eSBMdGQwXDANBgkqhkiG9w0BAQEFAANLADBIAkEAxosZ
TD1QySeLzr5+KllLfOgnDNrj+cMmQ5B4LwzfXJvfl8hK9rrh3YrUcswgGXhWJo1J
3VESXNEmJ9LLZsh1wwIDAQABo1MwUTAdBgNVHQ4EFgQUTNUSlxtCEYth3de5ciL6
qiDdpZcwHwYDVR0jBBgwFoAUTNUSlxtCEYth3de5ciL6qiDdpZcwDwYDVR0TAQH/
BAUwAwEB/zANBgkqhkiG9w0BAQUFAANBAEqW+lzyWOT8cA+dVRwW+BkHguxR1ev6
zYHQwup2cIEwXeArBptlX0wkdjb4bGtwWM1NiqtCHCeCXyQhuPdMCLE=
-----END CERTIFICATE-----
)EOF";

#define WIFI_SSID "your-ssid"
#define WIFI_PASSWORD "your-password"

ESP8266WebServerSecure server(443);

void setup() {
    Serial.begin(115200);
    Serial.setDebugOutput(true);
    Serial.println();

    WiFi.mode(WIFI_STA);
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    uint8_t i = 0;
    while (WiFi.status() != WL_CONNECTED) {
        i++;
        if (i == 21) {
            WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
            Serial.println("Could not connect to " WIFI_SSID);
        }
        delay(500);
    }
    Serial.print("Connected! IP address: ");
    Serial.println(WiFi.localIP());

    server.getServer().setServerKeyAndCert(PRIVATE_KEY, sizeof(PRIVATE_KEY),
                                           CERTIFICATE, sizeof(CERTIFICATE));
    server.on("/", []() {
        server.keepAlive(true);
        server.send(200, "text/plain", "Hey!\r\n");
    });
    server.begin();
}

void loop() {
    server.handleClient();
}

Ruby client benchmark code

require 'net/http'
require 'benchmark'

DOMAIN = "<your controller's IP>"
TIMES = 100

uri = URI("https://#{DOMAIN}/")

request = Net::HTTP::Get.new(uri)

http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
# Allow self signed certificates
http.verify_mode = OpenSSL::SSL::VERIFY_NONE

Benchmark.bm(12) do |bm|
  request["Connection"] = "keep-alive"
  bm.report("keep-alive:") {
    http.start do |http|
      TIMES.times do |_|
        response = http.request(request)
      end
    end
  }

  request["Connection"] = "close"
  bm.report("close:") {
    TIMES.times do |_|
      http.start do |http|
        http.get(uri.path)
      end
    end
  }
end

Bash curl clients

while true; do curl -k https://$YOUR_IP/; done

Testing procedure and result

Before the patch

These tests are to be done on the version of this repository before applying the patch in this pull request.

Single client test

Process

  • Upload the Arduino sketch for the version before the patch on your ESP8266.
  • Open the serial monitor.
  • Get the IP of your ESP8266.
  • Edit the benchmark script to replace the DOMAIN variable with the IP of your ESP8266.
  • Execute the ruby benchmark script in a terminal.
  • Get the benchmark result from the ruby script's output.

Result

                   user     system      total        real
keep-alive:    0.239742   0.094293   0.334035 ( 29.232077)
close:         0.280106   0.082856   0.362962 ( 28.905040)

Multiple clients test

Process

  • Upload the Arduino sketch for the version before the patch on your ESP8266.
  • Open the serial monitor.
  • Get the IP of your ESP8266.
  • Edit the benchmark script to replace the DOMAIN variable with the IP of your ESP8266.
  • Execute while true; do curl -k https://$YOUR_IP/; done in a terminal with the IP of your ESP8266.
  • Execute the ruby benchmark script in another terminal.
  • Get the benchmark result from the ruby script's output.

Result

                   user     system      total        real
keep-alive:    0.255995   0.079367   0.335362 ( 57.122717)
close:         0.287268   0.053903   0.341171 ( 56.539017)

After the patch

These tests are to be done on the version of this repository after applying the patch in this pull request.

Single client test with the pre-patch sketch

Process

  • Upload the Arduino sketch for the version before the patch on your ESP8266.
  • Open the serial monitor.
  • Get the IP of your ESP8266.
  • Edit the benchmark script to replace the DOMAIN variable with the IP of your ESP8266.
  • Execute the ruby benchmark script in a terminal.
  • Get the benchmark result from the ruby script's output.

Result

                   user     system      total        real
keep-alive:    0.323017   0.090638   0.413655 ( 29.205106)
close:         0.274748   0.070732   0.345480 ( 28.794096)

Single client test with the post-patch sketch

Process

  • Upload the Arduino sketch for the version after the patch on your ESP8266.
  • Open the serial monitor.
  • Get the IP of your ESP8266.
  • Edit the benchmark script to replace the DOMAIN variable with the IP of your ESP8266.
  • Execute the ruby benchmark script in a terminal.
  • Get the benchmark result from the ruby script's output.

Result

                   user     system      total        real
keep-alive:    0.055521   0.021891   0.077412 (  5.508462)
close:         0.273024   0.058076   0.331100 ( 28.774168)

Multiple clients test with the pre-patch sketch

Process

  • Upload the Arduino for the version before the patch on your ESP8266.
  • Open the serial monitor.
  • Get the IP of your ESP8266.
  • Edit the benchmark script to replace the DOMAIN variable with the IP of your ESP8266.
  • Execute while true; do curl -k https://$YOUR_IP/; done in a terminal with the IP of your ESP8266.
  • Execute the ruby benchmark script in another terminal.
  • Get the benchmark result from the ruby script's output.

Result

                   user     system      total        real
keep-alive:    0.270916   0.083906   0.354822 ( 57.148734)
close:         0.298795   0.058366   0.357161 ( 56.935414)

Multiple clients test with the post-patch sketch

Process

  • Upload the Arduino for the version after the patch on your ESP8266.
  • Open the serial monitor.
  • Get the IP of your ESP8266.
  • Edit the benchmark script to replace the DOMAIN variable with the IP of your ESP8266.
  • Execute while true; do curl -k https://$YOUR_IP/; done in a terminal with the IP of your ESP8266.
  • Execute the ruby benchmark script in another terminal.
  • Get the benchmark result from the ruby script's output.

Result

                   user     system      total        real
keep-alive:    0.257426   0.094239   0.351665 ( 57.534744)
close:         0.284825   0.064359   0.349184 ( 57.068869)

Conclusion

The results do not show any negative consequences to this pull request. In the worst case scenario, the performance will be the same. In the best case scenario, where a single client can make as many requests as it wants, we see a 5x performance improvement.

This was achieved with a ESP8266 at 160MHz connected to a WiFi network with excellent connection. The client is a desktop computer connected to the WiFi router via an Ethernet cable. There is very little latency in my setup. If we increased the latency, the performance improvement will increase further and there still shouldn't be any negative impact.