Skip to content

Allow HTTP client to send other requests if no other clients are waiting #7412

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
6 tasks done
ZakCodes opened this issue Jun 29, 2020 · 2 comments · Fixed by #7414
Closed
6 tasks done

Allow HTTP client to send other requests if no other clients are waiting #7412

ZakCodes opened this issue Jun 29, 2020 · 2 comments · Fixed by #7414

Comments

@ZakCodes
Copy link
Contributor

Basic Infos

  • This issue complies with the issue POLICY doc.
  • I have read the documentation at readthedocs and the issue is not addressed there.
  • I have tested that the issue is present in current master branch (aka latest git).
  • I have searched the issue tracker for a similar issue.
  • If there is a stack dump, I have decoded it.
  • I have filled out all fields below.

Platform

  • Hardware: ESP8266MOD (ESP-12E I think)
  • Core Version: 52f4cc8 (June 29th 2020)
  • Development Env: Arduino IDE (v1.8.12)
  • Operating System: Arch Linux

Settings in IDE

  • Module: NodeMCU 1.0 (ESP-12E Module)
  • Flash Size: 4MB
  • lwip Variant: v2 Lower Memory
  • CPU Frequency: 160MHz
  • Upload Speed: 921600

Problem Description

The HTTP server always closes connections to it after 2s (HTTP_MAX_CLOSE_WAIT) and never accepts many requests from the same socket. It always returns the header Connection: close to tell the client to close the connection.

This is extremely inneficient because the clients must establish a new TCP, and possibly a new TLS session, for each request. It is understandable considering that the server cannot handle multiple requests in parallel, so you don't want a single client to connect to the server and send requests to it forever and block all the other clients.

The image below shows just how inneficient this is by showing a connection to an HTTPS server on a NodeMCU running at 160MHz with an encryption key of 512bits.

This gets even worse when the encryption key is 2048bits as you can see below.

These two images show how most of the delay is caused by the TLS setup is. This delay can get worse because the TLS setup is also highly dependent on the latency because there's a lot of back and forth.

By reusing the same connection for multiple subsequent requests, we can get rid of the TLS setup for all the subsequent requests and see a great performance boost. There will also be a performance boost for unencrypted requests, but it won't be as significant.

To prevent a client to block all the other clients, I suggest to only allow the current client to stay connected if no other clients are waiting.

MCVE Sketch

#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();
}

Benchmarking the solution

I've created a script in ruby that should be helpful to benchmark the solution:

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

If you change the DOMAIN variable to google.com, the result is:

                   user     system      total        real
keep-alive:    0.087922   0.011206   0.099128 (  3.915354)
close:         0.292852   0.128215   0.421067 (  8.163557)

We can see the performance gained by keeping the connection alive.

If you change the DOMAIN variable to the IP of your controller, the result is:

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

We can see that there is currently no performance gain by keeping the connection alive.

Debug Messages

n/a

@earlephilhower
Copy link
Collaborator

@ZakCodes, looks like you're 90% of the way there. Why not submit a pull request with your suggested solution?

@ZakCodes
Copy link
Contributor Author

I was going to do it. It's just that I prefer to always create an issue so that someone else can work on a solution to the problem if my pull request doesn't get accepted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants