Die Klasse ESP8266WebServer ist eher weniger gut dokumentiert. Hier einige Dinge, die im Laufe der Zeit klarer geworden sind.
Inhaltsverzeichnis
2.2 Empfang und Analyse des HTTP-Request
2.2.1 HTTP-Method und Dateipfad
2.3.1 Konstruktor / Destruktor
2.3.3 Webserver starten / stoppen
Hinweis: Erweiterte Debugging-Ausgaben über die serielle Schnittstelle erhält man, wenn man die Konstante DEBUG_ESP_HTTP_SERVER definiert.
Hinweis: In neueren Versionen der ESP8266-Bibliothek wurden inkompatible Änderungen vorgenommen. Diese Seite wurde am 5.9.2023 auf die zu der Zeit aktuelle Version 3.1.2 angepasst.
Zunächst einige zentrale Begriffe zu HTTP.
Eine ausführliche Behandlung findet man bei CodeProjekt: The HTTP series von Vladimir Pecanac.
Zur Identifizierung der zurückzuliefernden Daten wird vom HTTP-Client ein URL (Uniform Resource Locator) im HTTP-Header an den HTTP-Server übermittelt. Eine URL hat prinzipiell den folgenden Aufbau (ohne Leerzeichen!):
<Ressourcentyp> :// <User>
: <Passwort> @ <Host.Domain.TLD>
: <Port> / <Pfad/Datei>
? Parametername = Parameterwert &
Parametername = Parameterwert …
http ://
anonym : My$pw @
ullisroboterseite.de : 80 / index.html
? search = ATmega &
datesince = 2016-01-01
Einige der Angaben sind optional.
Ein HTTP-Request ist ein Text-Element und hat prinzipiell folgenden Aufbau (elektronik-kompendium, w3.org):
<Method>
<URL>
HTTP/<Version> <CRLF>
<HeaderName>:
<HeaderValue> <CRLF>
<HeaderName>:
<HeaderValue> <CRLF>
<CRLF>
//Leerzeile!
<MessageBody>
...
GET /esp8266-webserver-klasse.html
HTTP/1.1↵
Host:
ullisroboterseite.de↵
User-Agent: Mozilla/5.0 (Windows;
U; Windows NT 5.1; de; rv:1.9.1.2) Gecko/20090729 Firefox/3.5.2 (.NET CLR 3.5.30729)↵
...
↵
Die URL selbst besteht aus einer Pfadangabe zu der gewünschten Datei und einem optionalen Query-String:
<Path> ? <ParameterName>
= <ParameterValue> [ & <ParameterName>
= <ParameterValue> ... ]
www.example.org/suche?stichwort=wiki&ausgabe=liste
Hinweis: Die folgenden Angaben beziehen sich auf die Version 2.3.0 des Arduino core for ESP8266 WiFi chip.
ESP8266WebServer ist i.W. ein Wrapper um die Klasse WiFiServer. WiFiServer ist ein allgemeiner TCP-Server, ESP8266WebServer behandelt die Besonderheiten des HTTP-Protokolls.
Nach der Initialisierung einer Instanz der Klasse (ctor, begin()) muss das Programm dafür sorgen, dass regelmäßig überprüft wird, ob ein neuer HTTP-Request eingetroffen ist (handleClient()). Ein erhaltener Request wird analysiert und in seine Bestandteile zerlegt. Diese Elemente werden über entsprechende Methoden veröffentlicht (z.B. uri()).
Anhand der HTTP-Methode (z.B. GET) und der angeforderten Datei (z.B. "/config.html") wird eine vorher registrierte (on()) Bearbeitungsmethode (Callback, Handler) ermittelt und aufgerufen. Diese stellt den Seitentext zusammen und sendet ihn zum Anforderer zurück (send()). Wurde kein entsprechender Handler registriert, wird die registrierte Standard-Bearbeitungsmethode (onNotFound()) aufgerufen. Wurde auch diese nicht definiert, wird eine Fehlerseite zurückgeliefert (404, Not found).
Neben den Handlern für die Seitenbearbeitung gibt es einen, der einen angeforderten Datei-Upload ausführt.
Für die Entgegennahme von HTTP-Anfragen ist die Methode handleClient() zuständig. Hier wird nachgeschaut, ob ein neuer HTTP-Request vorliegt und dieser anschließend analysiert. Aus diesem Grund muss handleClient() regelmäßig (loop()) aufgerufen werden.
Die eigentliche Analyse wird von der geschützten Methode _parseRequest() erledigt. Folgende Felder werden gefüllt:
HTTPMethod _currentMethod;
String _currentUri;
RequestHandler* _currentHandler;
_currentHandler wird anhand von _currentMethod und _currentUri in der Handler-Liste (s. Methode on()) gesucht.
Außerdem wird die Parameter-Liste aufgebaut (s.u.) und die angeforderten Header-Elemente (s.u.) extrahiert.
Die ermittelten Daten sind über öffentliche Methoden abrufbar:
Methode | Funktion |
---|---|
String uri() |
Ruft den (nackten) Pfad auf die angeforderte Datei ab. |
HTTPMethod method() |
Ruft den Typ des HTTP-Requests ab. Mögliche Werte sind: HTTP_ANY, HTTP_GET, HTTP_POST, HTTP_PUT, HTTP_PATCH, HTTP_DELETE, HTTP_OPTIONS |
Auf in der URL hinterlegte Parameter (Query-String) kann über den folgenden Methoden-Komplex zugegriffen werden:
Methode | Funktion |
---|---|
int args() |
Ruft die Anzahl der gespeicherten Parameter ab. |
String arg(int i) |
Ruft den Parameter-Wert mit dem nullbasierten Index i ab. Liefert einen leeren String, wenn der Index außerhalb des definierten Bereichs liegt. |
String argName(int i) |
Ruft den Parameter-Namen mit dem nullbasierten Index i ab. Liefert einen leeren String, wenn der Index außerhalb des definierten Bereichs liegt. |
String arg(String name) |
Ruft den Paramter-Wert mit dem Namen name ab. Liefert einen leeren String, wenn der ein Header mit dem angegeben Namen nicht gespeichert wurde. |
bool hasArg(String name) |
Prüft, ob der Paramter mit den Namen name existiert. |
Serial.print("Paramter: ");
Serial.println(webserver.args());
for (int i = 0; i < webserver.args(); i++)
Serial.printf("Paramter[%i]: %s: %s\n", i, webserver.argName(i).c_str(), webserver.arg(i).c_str());
http://192.168.xxx.xxx/?m=1&q=33
Paramter: 2
Paramter[0]: m: 1
Paramter[1]: q: 33
Die statische Methode urlDecode() tauscht Ersatzzeichen im übergegeben String gegen reguläre Zeichen aus. Parameterwerte werden während der Analyse dekodiert. Die HTTP-Adresse
https://translate.google.de/translate?hl=de&sl=en&u=http://www.esp8266.com/viewtopic.php%3Fp%3D48622&prev=search
enthält den Parameter u mit dem Wert http://www.esp8266.com/viewtopic.php%3Fp%3D48622.
%3F und %3D werden ersetzt durch
? und =. Der Methodenaufruf arg("u")
liefert dann:
http://www.esp8266.com/viewtopic.php?p=48622
Um Zugriff auf die Header zu erhalten, muss vorab festgelegt werden, welche Header-Werte gespeichert werden sollen. Diese geschieht über die Methode collectHeaders(). Dieser Methode wird eine Liste der gewünschten Namen übergeben:
// Gewünschte Header-Elemente festlegen
const char* Headers[] = {"User-Agent", "Connection"};
...
// Speicherung anfordern
webserver.collectHeaders(Headers, sizeof(Headers)/ sizeof(Headers[0]));
...
// Gespeicherte Header-Elemente ausgeben
Serial.print("Headers: ");
Serial.println(webserver.headers());
for (int i = 0; i < webserver.headers(); i++)
Serial.printf("Header[%i]: %s\n", i, webserver.header(i).c_str());
Headers: 3
Header[0]:
Header[1]: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:45.0) Gecko/20100101 Firefox/45.0
Header[2]: keep-alive
Der Zugriff auf die Daten erfolgt durch den Methodenkomplex:
Methode | Funktion |
---|---|
int headers() |
Ruft die Anzahl der gespeicherten Header ab. |
String header(int i) |
Ruft den Header-Wert mit dem nullbasierten Index i ab. Liefert einen leeren String, wenn der Index außerhalb des definierten Bereichs liegt. |
String headerName(int i) |
Ruft den Header-Namen mit dem nullbasierten Index i ab. Liefert einen leeren String, wenn der Index außerhalb des definierten Bereichs liegt. |
String header(String name) |
Ruft den Header-Wert mit dem Namen name ab. Liefert einen leeren String, wenn der ein Header mit dem angegeben Namen nicht gespeichert wurde. |
bool hasHeader(String name) |
Prüft, ob der Header mit den Namen name existiert. |
Der Header "Host" wird unabhängig von dieser Liste in dem geschützten Feld _ostHeader gesichert und über die öffentliche Methode hostHeader() zur Verfügung gestellt.
Um den oben beschriebenen Funktionsumfang ausnutzen zu können, muss die ESP8266WebServer-Klasse entsprechend initialisiert werden.
Methode | Funktion |
---|---|
ESP8266WebServer(int port = 80) |
Initialisiert eine neue Instanz der ESP8266WebServer-Klasse.
Der Standard-Port ist 80. Diese Version des Konstruktors sollte verwandt werden, nur eine einzelne Netzwerkverbindung besteht. Der Webserver wird dann an diese gebunden. |
ESP8266WebServer(IPAddress addr, int port = 80) |
Initialisiert eine neue Instanz der ESP8266WebServer-Klasse
und bindet sie an die angegebene IP-Adresse. Der Standard-Port ist 80. Diese Version des Konstruktors muss verwandt werden, wenn mehr als eine einzelne Netzwerkverbindung besteht. |
~ESP8266WebServer() |
Gibt die der Instanz zugewiesenen Ressourcen wieder frei. |
Zur Bearbeitung eingehender HTTP-Request müssen entsprechende Methoden kodiert werden. Die Methoden sind wie folgt deklariert:
typedef std::function<void(void)> THandlerFunction;
Also z.B. void handleIndex(void) zur Bearbeitung einer Requests auf die Datei "index.html". Die hinterlegten Methoden müssen der ESP8266WebServer-Instanz bekannt gemacht werden:
Methode | Funktion |
---|---|
void on(const char* uri, THandlerFunction handler) |
Erstellt einen neue Instanz der FunctionRequestHandler-Klasse
und fügt sie der Handler-Liste hinzu. fn wird für eine beliebige HTTP-Method aufgerufen, wenn ein HTTP-Request mit der angegebenen URI eintrifft. Als Upload-Handler wird der durch onFileUpload() hinterlegte Handler genutzt (s. Formulare). |
void on(const char* uri, HTTPMethod method, THandlerFunction fn) |
Erstellt einen neue Instanz der FunctionRequestHandler-Klasse und fügt sie der Handler-Liste
hinzu. fn wird aufgerufen, wenn ein HTTP-Request mit der angegebenen URI und der angegeben Methode eintrifft. Als Upload-Handler wird der durch onFileUpload() hinterlegte Handler genutzt (s. Formulare). |
void on(const char* uri, HTTPMethod method, THandlerFunction fn, THandlerFunction
ufn) |
Erstellt einen neue Instanz der FunctionRequestHandler-Klasse und fügt sie der Handler-Liste
hinzu. fn wird aufgerufen, wenn ein HTTP-Request mit der angegebenen URI und der angegeben Methode eintrifft. Als Upload-Handler wird der in ufn hinterlegte Handler genutzt (s. Formulare). |
void onNotFound(THandlerFunction fn) |
fn wird aufgerufen, wenn keine andere Methode für URI/Method-Kombination des Request hinterlegt ist. |
void onFileUpload(THandlerFunction fn) |
fn wird in der geschützten Variablen _fileUploadHandler abgelegt. Diese hinterlegte Funktion wird bei den Upload-Requests aufgerufen, für die kein eigener Upload-Handler angegeben wurde. |
void addHandler(RequestHandler* handler) |
Fügt die angegebene Instanz der RequestHandler-Klasse der Handler-Liste hinzu. |
void serveStatic(const char* uri, fs::FS& fs, const char* path, const char* cache_header
= NULL ) |
Definiert einen Handler zum Upload einer Datei die mit der HTTP-Method
GET angefordert werden. uri: URI für den dieser Handler gelten soll fs: Dateisystem aus dem die Datei geladen werden soll. path: Absoluter Pfad zur Datei Bespiel: serveStatic("/favicon.png", SPIFFS, "/espressif.png") Wenn die Datei "/favicon.png" angefordert wird, wird die Datei "/espressif.png" aus dem Dateisystem SPIFFS zurückgeliefert. Zeigt path auf eine Directory, wird versucht, die Datei "<path>/index.html" zu laden. Im vierten Parameter können Angaben zum Cache-Control gemacht werden, d.h. die Angabe wie lange die Datei im Browser-Cache gehalten werden soll. Also cache_header := "max-age=2000" . Die Angabe erfolgt in
Sekunden. |
Die Methode begin() bindet den WebServer an TCP, d.h. TCP-Pakete an den im WebServer festgelegten Port werden an den WebServer weitergegeben.
close() oder gleichwertig stop() heben die Bindung wieder auf.
Um Daten an den Client zurück zu senden (HTTP-Response) wird die Methode send() genutzt. Die drei Parameter haben folgende Bedeutung:
Parameter | Bedeutung |
---|---|
int code |
HTTP response code, 200 oder 404 |
char* content_type |
HTTP content type, wie "text/plain" oder "image/png" |
String& content |
Der content |
send() steht in verschiedenen Varianten zur Verfügung.
Mit
kann der Response auch einzeln aufgebaut werden.
Authentifizierungsdaten (Account-Name und Passwort) werden über einen speziellen Header übertragen. Name und Passwort sind verschlüsselt:
Authorization: Basic dWxsaToxMjM=
Für die Authentifizierung stehen zwei Methoden zur Verfügung:
Methode | Funktion |
---|---|
bool authenticate(const char * username, const char * password) |
Fragt ab, ob Authentifizierungsdaten für das angegebene Konto vorliegt. |
void requestAuthentication() |
Fordert Authentifizierungsdaten vom Browser an. |
Eine typische Kodierung ist:
if (!webserver.authenticate(User, Password))
return webserver.requestAuthentication();
Sind mehrere Accounts für den Zugriff auf die Seite zugelassen, ist die Methode authenticate() mehrfach aufzurufen.
Die Gestaltung von Formularen ist bei selfhtml beschrieben. Wesentlich sind Formular-Elemente zur Eingabe von Daten (input, textarea) und button-Elemente zum Auslösen von Aktionen.
Das Klicken auf Formular-Elemente mit dem type-Attribut "submit" führen zur Übertragung der Formular-Daten
der Felder mit den Tags <input> oder <textarea> an den Server. Die eingegebenen Daten
werden im MessageBody
übertagen.
Das Format ist <name>=<value><crlf><name>=<value><crlf>...
Eine HTML-Seite "test.html" mit dem folgenden form-Element:
<form action="site.html" method="POST">
<input name="ButtonName" type="submit" value="ButtonValue">
</form>
führt im Browser zur Anzeige der Schaltfläche
Wird diese gedrückt, wird die Seite "site.html" (= action-Attribut) mit folgendem Request angefordert:
POST /site.html HTTP/1.1
Host: 192.168.xxx.xxx
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:45.0) Gecko/20100101 Firefox/45.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: de,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://192.168.xxx.xxx/test.html
Authorization: Basic dWxsaToxMjM=
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 22
ButtonName=ButtonValue <= MessageBody
MessageBody erhält den Inhalt "ButtonName=ButtonValue". Dies entspricht den Angaben bei name und value des input-Elements.
Die ESP8266WebServer-Klasse hängt diese Daten an den Query-String
an, erzeugt also ein neues Parameter-Element (s.o.). Die Methode arg("ButtonName")
würde also den Wert "ButtonValue" liefern.
Interner Code:
searchStr += plainBuf; // searchStr := QueryString, MessageBody := plainBuf
Beginnt der MessageBody mit "{", "[" oder enthält kein "=" ( wie sieht das HTML dazu aus), wird das Parameter-Element mit den Namen "plain" erzeugt.
Interner Code:
// searchStr := QueryString, MessageBody := plainBuf
if(plainBuf[0] == '{' || plainBuf[0] == '[' || strstr(plainBuf, "=") == NULL)
{ //plain post json or other data
searchStr += "plain=";
searchStr += plainBuf;
...
Wie ein HTML-form-Element gestaltet sein muss, um eine Datei hoch zu laden, ist bei selfhtml beschrieben. Das form-Element muss das enctype="multipart/form-data"-Attribut besitzen. Ein input-Element mit dem type-Attribut "file" dient zur Auswahl der Datei, eine Schaltfläche zum hochladen.
<form action="site.html" enctype="multipart/form-data" method="POST">
Datei hochladen
<input name="datei" size="50" type="file">
<button>Hochladen</button>
</form>
Die ESP8266WebServer-Klasse erkennt, dass eine Datei hochgeladen werden soll und und ruft den zur uri und HTTP-Method registrierten Upload-Handler auf. Die Datei-Informationen werden über die Methode HTTPUpload& upload() in Form einer Referenz auf eine HTTPUpload-Instanz zur Verfügung gestellt.
typedef struct {
HTTPUploadStatus status;
String filename;
String name; // Dateiname (?) bei Content-Disposition
String type; // Content type ("text/plain", ...)
size_t totalSize; // File Size
size_t currentSize; // Size of data currently in buf
uint8_t buf[HTTP_UPLOAD_BUFLEN];
} HTTPUpload;
Die registrierte Upload-Methode wird ggf. mehrfach aufgerufen, bis der komplette Dateiinhalt übertragen wurde. Die jeweils auszuführen Aktion kann anhand von HTTPUpload::status ermittelt werden:
Status | Aktion |
---|---|
UPLOAD_FILE_START | Datei öffnen |
UPLOAD_FILE_WRITE | Datei schreiben |
UPLOAD_FILE_END | Datei schließen |
UPLOAD_FILE_ABORTED | Datei schließen und löschen |
Ein Standard-Upload-Handler könnte wie folgt aussehen:
void handleUpload()
{ static File fsUploadFile; // File-Handle zur Datei
HTTPUpload& upload = webserver.upload(); // Upload-Daten
String UploadFileName; // Datei-Name
switch (upload.status)
{ case UPLOAD_FILE_START:
UploadFileName = upload.filename;
if (!UploadFileName.startsWith("/"))
UploadFileName = "/" + UploadFileName;
fsUploadFile = SPIFFS.open(UploadFileName, "w");
break;
case UPLOAD_FILE_WRITE:
if (!fsUploadFile)
break;
fsUploadFile.write(upload.buf, upload.currentSize);
break;
case UPLOAD_FILE_END:
if (!fsUploadFile)
break;
fsUploadFile.close();
break;
case UPLOAD_FILE_ABORTED:
if (!fsUploadFile)
break;
fsUploadFile.close();
SPIFFS.remove(UploadFileName);
break;
default:
break;
}
}
Die folgende Applikation verbindet sich mit einem WLAN, versucht zu HTTP-Anfragen passende Dateien aus dem SPIFFS zurück zu liefern. Das Filesystem enthält die Dateien index.html, ulli.html, espressif.png.
Die einzige Besonderheit im Code ist die Zeile
webserver.serveStatic("/", SPIFFS, "/index.html");
Die Eingabe der reinen IP-Adresse wird auf '/index.html' umgelenkt.
Index.html
<!DOCTYPE HTML>
<html lang="de">
<head>
<title>ESP8266-MinWebServer</title>
<link href="espressif.png" rel="shortcut icon" type="image/x-icon">
<meta charset="UTF-8">
</head>
<body>
<h1>ESP8266-MinWebServer</h1>
<p>Hier geht's zur <a href="ulli.html">zweiten Seite</a></p>
</body>
</html>
ulli.html
<!DOCTYPE HTML>
<html lang="de">
<head>
<meta content="de" http-equiv="Content-Language">
<title>ESP8266-MinWebServer</title>
<link href="espressif.png" rel="shortcut icon" type="image/x-icon">
<meta charset="UTF-8">
</head>
<body>
<p style="font-family: Arial, Helvetica, sans-serif">Dokumentation zu diesem Projekt findet man auf
der Seite <a href="http://ullisroboterseite.de/esp8266-webserver-klasse.html">ESP8266
Webserver</a> in</p>
<h1 style="font-family: Arial, Helvetica, sans-serif"><a href="http://ullisroboterseite.de">Ullis
Roboter Seite</a></h1>
</body>
</html>
ESP8266-Code:
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <detail\mimetable.h>
String ssid("your ssid");
String password("your password");
ESP8266WebServer webserver(80); // Webserver-Instanz für Port 80 erstellen
void notFound(); // Sendet "Not Found"-Seite
void handleUnknown(); // Liefert Web-Seiten aus dem SPIFFS
void setup()
{ Serial.begin(74880);
Serial.println("\n");
Serial.flush();
delay(100);
Serial.println(" ");
Serial.println("----------------------------");
Serial.println("Project: ESP8266-MinWebServer");
// Initialize file system.
if (!SPIFFS.begin())
{ Serial.println("SPIFFS nicht initialisiert!");
while (1) // ohne SPIFFS geht es sowieso nicht...
{ yield();
}
}
Serial.println("SPIFFS ok");
// Mit WLAN verbinden
Serial.println("Connecting to " + ssid);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid.c_str(), password.c_str());
while (WiFi.waitForConnectResult() != WL_CONNECTED)
{ Serial.println("Connection Failed! Rebooting...");
delay(5000);
ESP.restart();
}
Serial.print("Connected to " + ssid + ", IP: " );
Serial.println(WiFi.localIP());
// Webserver initilaisieren
// Lenkt die Abfrage unbekanter Dateien auf handleUnknown
webserver.onNotFound(handleUnknown);
// Die Abfrage auf die reine URL '/' wird auf '/index.html' umgelenkt
webserver.serveStatic("/", SPIFFS, "/index.html");
webserver.begin(); // Web-Server starten
Serial.println("HTTP Server running on Port 80");
}
void loop()
{ webserver.handleClient(); // auf neuen HTTP-Request prüfen
}
// Sendet "Not Found"-Seite
void notFound()
{ String HTML = F("<html><head><title>404 Not Found</title></head><body>"
"<h1>Not Found</h1>"
"<p>The requested URL was not found on this webserver.</p>"
"</body></html>");
webserver.send(404, "text/html", HTML);
}
// Es wird versucht, die angegebene Datei aus dem SPIFFS hochzuladen
void handleUnknown()
{ String filename = webserver.uri();
File pageFile = SPIFFS.open(filename, "r");
if (pageFile)
{ String contentTyp = mime::getContentType(filename);
size_t sent = webserver.streamFile(pageFile, contentTyp);
pageFile.close();
}
else
notFound();
}
In der Methode handleUnknown() wird versucht, eine HTTP-Anfrage, für die kein separater Handler definiert ist, über die Rückgabe einer Datei aus dem SPIFFS zu befriedigen. Bei send(..) muss man einen “Content-Type” mitgeben, z.B. “text/plain”. In dem Bespiel zu ESP8266WebServer-Klasse gibt es eine eigene Passage, die aus einer Datei-Endung den Content-Type ableitet:
if(path.endsWith(".src")) path = path.substring(0, path.lastIndexOf("."));
else if(path.endsWith(".htm")) dataType = "text/html";
else if(path.endsWith(".css")) dataType = "text/css";
else if(path.endsWith(".js")) dataType = "application/javascript";
else if(path.endsWith(".png")) dataType = "image/png";
...