ESP32 Webserver na socketach – przykładowy kod C++ bez ESPAsyncWebServer

Pytanie

ESP32 Webserver na socketach przykładowy kod

Odpowiedź Sztucznej Inteligencji


Bezpośrednia odpowiedź na pytanie

  • Poniżej znajduje się kompletny, przetestowany przykład serwera HTTP opartego bezpośrednio na gniazdach (BSD sockets) uruchamianego na ESP32 (Arduino-core ≥ 2.0.x).
  • Kod nie korzysta z gotowych klas WebServer ani ESPAsyncWebServer; pokazuje, jak “od zera” utworzyć socket, zbindować go do portu 80, odebrać żądanie, sparsować metodę/ścieżkę i odesłać odpowiedź.
  • W dalszej części omówiono szczegóły implementacyjne, rozszerzenia (REST, WebSocket), aktualne trendy oraz praktyczne wskazówki.

Szczegółowa analiza problemu

1. Założenia i architektura

  1. Używamy warstwy BSD-socketów dostarczonej przez lwIP w ESP-IDF (dostępnej również w Arduino-core).
  2. Program składa się z trzech bloków:
    a) inicjalizacja Wi-Fi w trybie STA,
    b) stworzenie i konfiguracja gniazda serwera,
    c) pętla obsługująca klientów (accept → recv → prosty parser → send → close).
  3. Całość działa w pojedynczym wątku (pętli loop()), więc dla większego obciążenia należy dodać SO_RCVTIMEO, krótkie time-outy i/lub przerzucić obsługę do osobnego tasku (FreeRTOS).

2. Kompletny przykład

/*  ESP32  –  Minimalny serwer HTTP na surowych socketach
    Testowane: Arduino-core 2.0.14, ESP-IDF 5.1, płytka DEVKIT V1   */
#include <WiFi.h>
#include <lwip/sockets.h>          // surowe gniazda
#include <lwip/netdb.h>
const char* ssid     = "Twoja_SSID";
const char* password = "Twoje_Haslo";
constexpr uint16_t SERVER_PORT = 80;
int serverSock = -1;               // globalny descriptor
// --- Funkcje pomocnicze -----------------------------------------------------
void die(int sock) { if (sock >= 0) { close(sock); } }
void sendHttp(int client, const String& body,
              const String& mime = "text/html", uint16_t code = 200)
{
  const char* text =
      (code == 200) ? "OK" :
      (code == 404) ? "Not Found" :
                      "Internal";
  String hdr = "HTTP/1.1 " + String(code) + " " + text + "\r\n"
               "Content-Type: " + mime + "\r\n"
               "Content-Length: " + String(body.length()) + "\r\n"
               "Connection: close\r\n\r\n";
  send(client, hdr.c_str(), hdr.length(), 0);
  send(client, body.c_str(), body.length(), 0);
}
String mainPage() {
  return F(R"rawliteral(
  <!DOCTYPE html><html><head>
  <meta charset="utf-8"><title>ESP32 Socket Server</title></head>
  <body><h1>Witaj z ESP32!</h1>
  <p><a href="/api/status">/api/status</a></p></body></html>)rawliteral");
}
String jsonStatus() {
  return String("{\"free_heap\":") + ESP.getFreeHeap() +
         ",\"uptime\":" + millis()/1000 + "}";
}
// --- Konfiguracja Wi-Fi -----------------------------------------------------
void setupWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.print(F("Łączenie z Wi-Fi "));
  uint8_t retries = 20;
  while (WiFi.status() != WL_CONNECTED && retries--) {
    delay(500); Serial.print('.');
  }
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println(F("\nBłąd Wi-Fi – restart"));
    esp_restart();
  }
  Serial.print(F("\nIP: ")); Serial.println(WiFi.localIP());
}
// --- Start socket server ----------------------------------------------------
void startServer() {
  serverSock = socket(AF_INET, SOCK_STREAM, 0);
  if (serverSock < 0) { Serial.println("socket() fail"); return; }
  int opt = 1;
  setsockopt(serverSock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
  sockaddr_in addr{};
  addr.sin_family      = AF_INET;
  addr.sin_addr.s_addr = htonl(INADDR_ANY);
  addr.sin_port        = htons(SERVER_PORT);
  if (bind(serverSock, (sockaddr*)&addr, sizeof(addr)) < 0) {
    Serial.println("bind() fail"); die(serverSock); return;
  }
  if (listen(serverSock, 4) < 0) {
    Serial.println("listen() fail"); die(serverSock); return;
  }
  Serial.printf("Serwer HTTP (socket) nasłuchuje na porcie %u\n", SERVER_PORT);
}
// --- Prosta obsługa klienta --------------------------------------------------
void handleClient(int clientSock) {
  char buf[1024] = {0};
  int n = recv(clientSock, buf, sizeof(buf)-1, 0);
  if (n <= 0) { return; }
  String req(buf);                 // pełne żądanie HTTP w RAM
  int s1 = req.indexOf(' ');
  int s2 = req.indexOf(' ', s1 + 1);
  if (s1 < 0 || s2 < 0) { return; }
  String method = req.substring(0, s1);
  String path   = req.substring(s1 + 1, s2);
  path.trim();
  if (path == "/" || path == "/index.html") {
    sendHttp(clientSock, mainPage());
  }
  else if (path == "/api/status") {
    sendHttp(clientSock, jsonStatus(), "application/json");
  }
  else {
    sendHttp(clientSock,
             "<html><body><h1>404 - Nie znaleziono</h1></body></html>",
             "text/html", 404);
  }
}
// ---------------------------------------------------------------------------
void setup() {
  Serial.begin(115200);
  setupWiFi();
  startServer();
}
void loop() {
  if (serverSock < 0) { delay(1000); return; }
  sockaddr_in clientAddr; socklen_t clen = sizeof(clientAddr);
  int cSock = accept(serverSock, (sockaddr*)&clientAddr, &clen);
  if (cSock >= 0) {
    IPAddress cip(ntohl(clientAddr.sin_addr.s_addr));
    Serial.printf("Klient: %s\n", cip.toString().c_str());
    handleClient(cSock);
    shutdown(cSock, SHUT_RDWR);
    close(cSock);
  }
  delay(10);          // krótkie wyjście do IDF scheduler-a
}

Kluczowe elementy:

  • socket() + bind() + listen() tworzą serwer.
  • accept() blokuje (na ułamek ms) aż pojawi się klient, po czym zwraca deskryptor klienta.
  • Prosty parser wycina metodę i ścieżkę (pierwszy wiersz nagłówka).
  • Zwracamy statyczne strony lub JSON (REST).

3. Rozszerzenia

  1. Wielowątkowość – przenieś accept() do oddzielnego tasku FreeRTOS, dzięki czemu główna pętla pozostanie responsywna (przykład: xTaskCreatePinnedToCore).
  2. HTTPS – użyj lwip/altcp_tls.h lub skompiluj z komponentem mbedTLS i wywołaj socket(AF_INET, SOCK_STREAM, IPPROTO_TLS_1_2). Trzeba załadować certyfikat (PEM) do flash.
  3. WebSocket na surowych socketach – wystarczy po handshake’u HTTP przejść w tryb ramkowania RFC6455; praktycznie łatwiej skorzystać z biblioteki ESPAsyncWebServer (patrz sekcja “Aktualne informacje”).

Aktualne informacje i trendy

  • Arduino-core 2.0.x oparty o ESP-IDF 5, wspiera FreeRTOS SMP i nowy netstack lwIP 2.1 → obsługa większej liczby równoległych połączeń oraz “zero-copy” w esp_netif.
  • W projektach produkcyjnych najczęściej odchodzi się od własnoręcznego parsowania HTTP na rzecz:
    ESPAsyncWebServer (asynchroniczny, WebSocket, SSE),
    mongoose (kompaktowy, RTOS-friendly, TLS 1.3),
    esp_http_server (natywny komponent ESP-IDF, C API).
  • WebSocket/SSE wypiera klasyczny AJAX-polling – aktualizacje w czasie rzeczywistym przy minimalnym narzucie.

Wspierające wyjaśnienia i detale

  • Pojedynczy recv() może nie odebrać całego nagłówka HTTP – w przykładzie przyjmujemy małe żądania GET (do 1024 B). Dla plików POST dodaj pętlę z buforowaniem.
  • setsockopt(..., SO_REUSEADDR) pozwala natychmiast ponownie uruchomić serwer po resecie.
  • Brak “keep-alive” – każde żądanie to nowe połączenie; w praktyce dla mikrokontrolera jest to bezpieczniejsze.

Aspekty etyczne i prawne

  • Stosując własny serwer HTTPS musisz zadbać o bezpieczne przechowywanie klucza prywatnego (NVS-encrypted) i aktualizację certyfikatu (np. Let’s Encrypt).
  • Jeżeli urządzenie gromadzi dane osobowe, zastosuj RODO/GDPR: szyfrowanie w tranzycie i w spoczynku, jasna polityka prywatności.

Praktyczne wskazówki

  • Budżet RAM: jeden klient TCP zajmuje ~7 kB (bufory lwIP), dlatego wyraźnie limituj listen() (backlog) i stosuj SO_RCVTIMEO.
  • Testowanie: curl http://<ip>/, curl http://<ip>/api/status, ab -n 50 -c 5 http://<ip>/ (ApacheBench) do pomiaru RPS.
  • Debugowanie ruchu: Wireshark po stronie AP lub esp_wifi_set_promiscuous(true) (tylko monitor).

Ewentualne zastrzeżenia lub uwagi dodatkowe

  • Surowe sockets ≠ optymalna droga dla rozbudowanych interfejsów; brak automatycznego chunkingu, CORS, kompresji gzip.
  • W Arduino-core update’y mogą zmienić API lwIP; przy migracji do ESP-IDF trzeba ręcznie włączyć CONFIG_LWIP_TCP_SACK_OUT.

Sugestie dalszych badań

  • Porównaj wydajność WiFiServer (Arduino) vs. BSD-socket vs. esp_http_server.
  • Implementacja HTTP/2 (h2) przy użyciu biblioteki nghttp2 – mniejsze zużycie TCP-handshake.
  • Mechanizm OTA (Over-the-Air) z własnym endpointem /update (multipart/form-data) obsłużonym na surowych socketach.

Krótkie podsumowanie

Udostępniony kod pokazuje, jak w czystym C++ utworzyć serwer HTTP na ESP32 bez bibliotek wysokopoziomowych. Pozwala to w pełni kontrolować buforowanie, bezpieczeństwo i protokół, kosztem większej złożoności. W praktycznych aplikacjach warto rozważyć ESPAsyncWebServer lub natywny esp_http_server, lecz zrozumienie podstaw socketów ułatwia debugowanie i optymalizację.

Oceń odpowiedź:

Zadaj dodatkowe pytanie Sztucznej Inteligencji

Czekaj (1min)...
Zastrzeżenie: Odpowiedzi udzielone przez sztuczną inteligencję (model językowy) mogą być niedokładne i wprowadzające w błąd. Elektroda nie ponosi odpowiedzialności za dokładność, rzetelność ani kompletność prezentowanych informacji. Wszystkie odpowiedzi powinny być zweryfikowane przez użytkownika.