Preise

Architektur im Überblick

Quelldateien

Diese Seite wurde aus den folgenden Quelldateien erstellt:

Komponentenübersicht

Tinyhttpd ist ein minimaler HTTP-Server in C, der eine übersichtliche, modulare Architektur mit klar getrennten Verantwortlichkeiten implementiert. Das System besteht aus einer Hauptsteuerung, einem Netzwerk-Listener, einer Request-Verarbeitungseinheit und einer CGI-Ausführungsumgebung.

Die funktionale Gliederung orientiert sich an den Kernfunktionen, die in der Dokumentation definiert sind. Jede Funktion übernimmt eine spezifische Aufgabe innerhalb des Request-Response-Zyklus. Die zentrale Einheit accept_request steuert die gesamte Anfrageverarbeitung und delegiert an spezialisierte Hilfsfunktionen wie cat für statische Dateien oder execute_cgi für dynamische Inhalte (README.md:16-31).

正在加载图表渲染器...

Erklärung der Architekturkomponenten:

  • Hauptprogramm: Initialisiert den Server-Socket und betreibt die Endlosschleife zur Verbindungsaufnahme. Die main()-Funktion orchestriert alle Komponenten durch Thread-Erzeugung für jede eingehende Verbindung (httpd.c:489-516).
  • Anfrageverarbeitung: Parst HTTP-Requests, extrahiert Methode und URL, und entscheidet über statische Auslieferung oder CGI-Ausführung. Die Funktion accept_request fungiert als zentraler Dispatcher.
  • CGI-Subsystem: Implementiert die Prozessisolierung für dynamische Inhalte mittels fork() und Pipe-Kommunikation zwischen Eltern- und Kindprozess.
  • HTTP-Antwort: Generiert standardisierte HTTP-Response-Header und Fehlermeldungen gemäß RFC-Spezifikationen.

Server-Initialisierung und Hauptschleife

Die Initialisierungsphase erfolgt durch die Funktion startup, die einen TCP-Socket erstellt, diesen an einen Port bindet und in den Listen-Modus versetzt. Der Server startet entweder auf einem angegebenen Port oder wählt automatisch einen freien Port.

Der Hauptloop in main() implementiert ein ereignisgesteuertes Modell mit Thread-basierter Nebenläufigkeit. Bei jeder neuen Verbindung (accept) wird ein separater Thread erzeugt, der die Funktion accept_request ausführt (httpd.c:498-511).

Startphase im Detail:

  1. Socket-Erstellung: Die startup-Funktion initialisiert die Netzwerkkommunikation durch Aufruf von socket(), bind() und listen().
  2. Port-Binding: Der Server wird an Port 4000 oder einen vom Betriebssystem zugewiesenen freien Port gebunden.
  3. Verbindungsannahme: Die accept()-Funktion blockiert, bis eine Client-Verbindung eintrifft.
正在加载图表渲染器...

Erklärung der Sequenz:

  • Der Server initialisiert den Socket einmalig beim Start und betritt dann eine Endlosschleife zur Verbindungsaufnahme (README.md:35-36).
  • Jede eingehende Verbindung löst die Erzeugung eines neuen Threads aus, der unabhängig vom Hauptthread läuft.
  • Die Thread-basierte Architektur ermöglicht die gleichzeitige Bearbeitung mehrerer Anfragen, wobei jeder Thread seinen eigenen Socket-Deskriptor erhält.

Fehlerbehandlung:

Bei Fehlern in der accept()-Funktion wird error_die() aufgerufen, das eine Fehlermeldung über perror ausgibt und den Prozess beendet. Fehler bei der Thread-Erzeugung werden über perror protokolliert, führen aber nicht zum Serverabbruch.

Anfrageverarbeitungs-Pipeline

Die Verarbeitung einer HTTP-Anfrage durchläuft mehrere definierte Phasen, die von der Funktion accept_request orchestriert werden. Diese Pipeline implementiert eine klare Trennung zwischen Parsing, Routing und Content-Generierung.

Phase 1: Request-Parsing

Die Funktion get_line liest eine Zeile vom Socket und normalisiert Zeilenenden zu einheitlichen Linefeeds. Anschließend werden HTTP-Methode (GET/POST) und URL extrahiert. Bei GET-Anfragen mit Parametern wird der Query-String durch das Fragezeichen separiert (README.md:37-44).

Phase 2: URL-Normalisierung und Routing

Die URL wird in einen Dateipfad im htdocs-Verzeichnis umgewandelt. Verzeichnisanfragen werden durch Anhängen von index.html ergänzt. Das System prüft, ob die angeforderte Datei existiert und ob es sich um eine ausführbare Datei handelt.

Phase 3: Content-Auslieferung

  • Statische Dateien: Bei parameterlosen GET-Requests wird die Datei direkt über cat gelesen und an den Socket gesendet.
  • Dynamische Inhalte: Bei POST-Requests, GET-Requests mit Parametern oder ausführbaren Dateien wird execute_cgi aufgerufen.
正在加载图表渲染器...

Erklärung der Verzweigungen:

  • Die Entscheidung zwischen statischer und dynamischer Auslieferung basiert auf der HTTP-Methode und dem Vorhandensein von Parametern.
  • Die Funktion serve_file kapselt den Aufruf von headers und cat für statische Inhalte.
  • Bei CGI-Ausführung werden Umgebungsvariablen wie REQUEST_METHOD, QUERY_STRING und CONTENT_LENGTH gesetzt.

Abschluss der Verarbeitung:

Nach erfolgreicher Antwortübertragung wird die Verbindung geschlossen, da HTTP ein verbindungsloses Protokoll ist (README.md:55-57).

CGI-Subsystem und Interprozesskommunikation

Das CGI-Subsystem ist der komplexeste Teil der Architektur und implementiert die Kommunikation zwischen dem Serverprozess und einem separaten CGI-Kindprozess über Pipes.

Pipe-Architektur:

Zwei Pipes werden erstellt: cgi_input für Daten vom Server zum CGI-Programm (STDIN des Kindprozesses) und cgi_output für Daten vom CGI-Programm zum Server (STDOUT des Kindprozesses).

Prozess-Forking:

Nach dem fork()-Aufruf laufen zwei Prozesse parallel:

  • Kindprozess: Leitet STDOUT auf cgi_output[1] und STDIN auf cgi_input[0] um, setzt Umgebungsvariablen und führt das CGI-Programm via execl aus.
  • Elternprozess: Schließt nicht benötigte Pipe-Enden, schreibt bei POST-Requests die Daten in cgi_input[1] und liest die CGI-Ausgabe von cgi_output[0].

Umgebungsvariablen:

VariableBedeutungSetzung bei
REQUEST_METHODHTTP-MethodeAlle Requests
QUERY_STRINGURL-ParameterGET mit Parametern
CONTENT_LENGTHBody-LängePOST-Requests

Die Pipe-Enden müssen sorgfältig verwaltet werden, um Deadlocks zu vermeiden. Der Elternprozess schließt nach dem Schreiben der POST-Daten das Schreibende von cgi_input, um EOF an den Kindprozess zu signalisieren.

Kernfunktionen im Detail

accept_request

Verantwortlichkeit: Zentrale Dispatcher-Funktion für eingehende HTTP-Anfragen. Koordiniert Parsing, Routing und Antwortgenerierung.

Eintrittspunkt: Wird als Thread-Funktion von main() aufgerufen und erhält den Client-Socket-Deskriptor als Argument.

Schlüsseldatenstrukturen:

  • char method[255]: HTTP-Methode
  • char url[255]: Angeforderte URL
  • char path[512]: Serverseitiger Dateipfad
  • char *query_string: Zeiger auf URL-Parameter

Aufrufkette: accept_requestget_lineserve_file / execute_cgiheaders / cat

execute_cgi

Verantwortlichkeit: Führt CGI-Programme in einem isolierten Kindprozess aus und managed die Pipe-Kommunikation.

Eintrittspunkt: Wird von accept_request aufgerufen, wenn dynamische Content-Generierung erforderlich ist.

Schlüsseldatenstrukturen:

  • int cgi_input[2]: Pipe für STDIN des CGI-Prozesses
  • int cgi_output[2]: Pipe für STDOUT des CGI-Prozesses
  • pid_t pid: Prozess-ID nach fork()

Fehlerbehandlung: Bei fork()-Fehlern oder fehlgeschlagener execl-Ausführung wird cannot_execute() aufgerufen.

startup

Verantwortlichkeit: Initialisiert den Server-Socket und bereitet die Netzwerkadresse vor.

Eintrittspunkt: Wird einmalig von main() beim Serverstart aufgerufen.

Rückgabewert: Der Dateideskriptor des Server-Sockets.

cat

Verantwortlichkeit: Liest eine Datei und schreibt den Inhalt direkt auf den Socket.

Eintrittspunkt: Wird von serve_file nach dem Senden der Header aufgerufen.

Implementierung: Verwendet fopen, fgets und send in einer Schleife bis EOF.

Technische Entscheidungen und Architekturmuster

Thread-basierte Nebenläufigkeit

Entscheidung: Verwendung von POSIX-Threads (pthread_create) statt Prozessen für jede Verbindung.

Begründung: Threads teilen sich den Adressraum und sind ressourcenschonender als separate Prozesse. Für einen Lern-Server ist die Implementierung einfacher als bei Event-Loop-Architekturen.

Nachteil: Bei hoher Last kann die Thread-Anzahl das System limitieren. Kein Thread-Pool vorhanden.

CGI über Pipes statt Sockets

Entscheidung: Interprozesskommunikation über anonyme Pipes statt Loopback-Sockets.

Begründung: Pipes sind effizienter für lokale Kommunikation und einfacher zu implementieren. Die Pipe-Enden können direkt auf STDIN/STDOUT umgeleitet werden.

Nachteil: Bidirektionale Kommunikation erfordert zwei Pipes, was die Komplexität erhöht.

Blockierende I/O

Entscheidung: Verwendung blockierender accept()- und read()-Aufrufe.

Begründung: Vereinfacht den Codefluss und ist für einen minimalen Server ausreichend.

Nachteil: Skaliert nicht für hohe Verbindungszahlen.

Keine HTTP-Persistenz

Entscheidung: Jede Verbindung wird nach der Antwort geschlossen.

Begründung: HTTP/1.0-Modell ist einfacher zu implementieren und erfordert keine Connection-State-Verwaltung.

Technologie-Stack

TechnologieVerwendungszweckBegründungAlternative
POSIX ThreadsNebenläufigkeitLeichtgewichtiger als ProzesseEvent-Loop (libevent)
TCP SocketsNetzwerkkommunikationStandard für HTTPUnix Domain Sockets
PipesCGI-KommunikationEffizient für lokale IPCShared Memory
fork/execProzessisolierungSicherheit für CGIInterpreter einbetten
Standard-C-IODateizugriffPortabilitätMemory-Mapped I/O
getenv/setenvUmgebungsvariablenCGI-SpezifikationKonfigurationsdateien
perrorFehlerausgabeEinfache DiagnoseSyslog
Line-ParsingHTTP-VerarbeitungEinfache ImplementierungParser-Generator

Modulabhängigkeiten

正在加载图表渲染器...

Erklärung der Abhängigkeiten:

  • main ist die Wurzel des Abhängigkeitsgraphen und ruft nur startup und accept_request direkt auf.
  • accept_request hat die meisten Abhängigkeiten, da es als zentraler Dispatcher fungiert.
  • Die Fehlerbehandlungsfunktionen (bad_request, not_found, unimplemented, cannot_execute) sind Blattknoten ohne eigene Abhängigkeiten.

Konfiguration und Startparameter

Der Server wird mit fest codierten Standardwerten konfiguriert:

  • Port: Standardmäßig 4000, kann vom Betriebssystem geändert werden, wenn der Port belegt ist
  • Dokument-Root: htdocs-Verzeichnis relativ zum Arbeitsverzeichnis
  • Index-Datei: index.html für Verzeichnisanfragen
  • CGI-Unterstützung: Alle ausführbaren Dateien werden als CGI-Programme interpretiert

Die Konfiguration erfolgt zur Kompilierzeit durch Änderung der Quellcodedateien. Zur Laufzeit sind keine Konfigurationsparameter vorgesehen.