Einführung
Dieser Artikel verwendet C#, um die einfachste HTTP-Serverklasse zu implementieren. Sie können sie in Ihr eigenes Projekt einbetten oder den Code lesen, um mehr über das HTTP-Protokoll zu erfahren.
Hintergrund
Hochleistungs-WEB-Anwendungen werden im Allgemeinen auf leistungsstarken WEB-Servern wie IIS, Apache und Tomcat erstellt. Allerdings ist HTML eine sehr flexible UI-Markup-Sprache, was bedeutet, dass jede Anwendung und jeder Back-End-Dienst Unterstützung für die HTML-Generierung bieten kann. In diesem kleinen Beispiel verbrauchen Server wie IIS und Apache zu viele Ressourcen. Wir müssen selbst einen einfachen HTTP-Server implementieren und ihn in unsere Anwendung einbetten, um WEB-Anfragen zu verarbeiten. Wir brauchen nur eine Klasse, um es zu implementieren, es ist sehr einfach.
Code-Implementierung
Sehen wir uns zunächst die Verwendung von Klassen an und analysieren dann die spezifischen Details der Implementierung. Hier erstellen wir eine Klasse, die von HttpServer erbt und die beiden abstrakten Methoden handleGETRequest und handlePOSTRequest implementiert:
public class MyHttpServer : HttpServer { public MyHttpServer(int port) : base(port) { } public override void handleGETRequest(HttpProcessor p) { Console.WriteLine("request: {0}", p.http_url); p.writeSuccess(); p.outputStream.WriteLine("<html><body><h1>test server</h1>"); p.outputStream.WriteLine("Current Time: " + DateTime.Now.ToString()); p.outputStream.WriteLine("url : {0}", p.http_url); p.outputStream.WriteLine("<form method=post action=/form>"); p.outputStream.WriteLine("<input type=text name=foo value=foovalue>"); p.outputStream.WriteLine("<input type=submit name=bar value=barvalue>"); p.outputStream.WriteLine("</form>"); } public override void handlePOSTRequest(HttpProcessor p, StreamReader inputData) { Console.WriteLine("POST request: {0}", p.http_url); string data = inputData.ReadToEnd(); p.outputStream.WriteLine("<html><body><h1>test server</h1>"); p.outputStream.WriteLine("<a href=/test>return</a><p>"); p.outputStream.WriteLine("postbody: <pre class="brush:php;toolbar:false">{0}", data); } }
Wenn wir mit der Verarbeitung einer einfachen Anfrage beginnen, müssen wir einen separaten Thread starten, um einen Port abzuhören, z Port 8080:
HttpServer httpServer = new MyHttpServer(8080); Thread thread = new Thread(new ThreadStart(httpServer.listen)); thread.Start();
Wenn Sie dieses Projekt kompilieren und ausführen, sehen Sie den generierten Beispielinhalt auf der Seite unter der Browseradresse http://localhost:8080. Werfen wir einen kurzen Blick auf die Implementierung dieser HTTP-Server-Engine.
Dieser WEB-Server besteht aus zwei Komponenten. Eine davon ist die HttpServer-Klasse, die dafür verantwortlich ist, TcpListener zu starten, um den angegebenen Port abzuhören, und die Methode AcceptTcpClient() verwendet, um TCP-Verbindungsanforderungen in einer Schleife zu verarbeiten Schritt bei der Verarbeitung von TCP-Verbindungen. Dann kommt die Anfrage am „angegebenen“ Port an und es wird ein neues Portpaar erstellt, um die TCP-Verbindung vom Client zum Server zu initialisieren. Dieses Portpaar ist die Sitzung von TcpClient, sodass unser Hauptport weiterhin neue Verbindungsanfragen empfangen kann. Aus dem folgenden Code können wir ersehen, dass jedes Mal, wenn der Listener einen neuen TcpClien erstellt, die HttpServer-Klasse einen neuen HttpProcessor erstellt und dann einen Thread für den Betrieb startet. Die HttpServer-Klasse enthält außerdem zwei abstrakte Methoden, die Sie implementieren müssen.
public abstract class HttpServer { protected int port; TcpListener listener; bool is_active = true; public HttpServer(int port) { this.port = port; } public void listen() { listener = new TcpListener(port); listener.Start(); while (is_active) { TcpClient s = listener.AcceptTcpClient(); HttpProcessor processor = new HttpProcessor(s, this); Thread thread = new Thread(new ThreadStart(processor.process)); thread.Start(); Thread.Sleep(1); } } public abstract void handleGETRequest(HttpProcessor p); public abstract void handlePOSTRequest(HttpProcessor p, StreamReader inputData); }
Auf diese Weise wird eine neue TCP-Verbindung von HttpProcessor in einem eigenen Thread verarbeitet. Die Aufgabe von HttpProcessor besteht darin, den HTTP-Header korrekt zu analysieren und die korrekt implementierte abstrakte Methode zu steuern. Werfen wir einen Blick auf die Verarbeitung von HTTP-Headern. Die erste Codezeile für die HTTP-Anfrage lautet wie folgt:
GET /myurl HTTP/1.0
Nachdem die Eingabe und Ausgabe von Process() festgelegt wurde, ruft HttpProcessor auf parseRequest( )-Methode.
public void parseRequest() { String request = inputStream.ReadLine(); string[] tokens = request.Split(' '); if (tokens.Length != 3) { throw new Exception("invalid http request line"); } http_method = tokens[0].ToUpper(); http_url = tokens[1]; http_protocol_versionstring = tokens[2]; Console.WriteLine("starting: " + request); }
Die HTTP-Anfrage besteht aus drei Teilen, daher müssen wir sie nur mit der Methode string.Split() in drei Teile aufteilen. Der nächste Schritt besteht darin, die HTTP-Anfrage zu empfangen und zu analysieren Header von den Client-Informationen, jede Datenzeile in den Header-Informationen wird in Form eines Schlüsselwerts (Schlüsselwert) gespeichert. Eine leere Zeile zeigt das Ende der HTTP-Header-Informationen an Lesen Sie die HTTP-Header-Informationen:
public void readHeaders() { Console.WriteLine("readHeaders()"); String line; while ((line = inputStream.ReadLine()) != null) { if (line.Equals("")) { Console.WriteLine("got headers"); return; } int separator = line.IndexOf(':'); if (separator == -1) { throw new Exception("invalid http header line: " + line); } String name = line.Substring(0, separator); int pos = separator + 1; while ((pos < line.Length) && (line[pos] == ' ')) { pos++; // 过滤掉所有空格 } string value = line.Substring(pos, line.Length - pos); Console.WriteLine("header: {0}:{1}",name,value); httpHeaders[name] = value; } }
An diesem Punkt haben wir gesehen, wie mit einfachen GET- und POST-Anfragen umgegangen wird, die jeweils den richtigen Handlern zugewiesen sind. In diesem Beispiel gibt es ein heikles Problem, das beim Senden von Daten gelöst werden muss, nämlich dass die Anforderungsheaderinformationen die Längeninformationen der gesendeten Daten enthalten. Wenn wir hoffen, dass die handlePOSTRequest-Methode in der Unterklasse HttpServer ist Wenn wir die Daten korrekt verarbeiten können, müssen wir die Informationen zur Datenlänge und Inhaltslänge zusammen in den Datenstrom einfügen. Andernfalls wird der Absender blockiert und wartet auf Daten, die niemals ankommen. Wir verwenden eine weniger elegante, aber sehr effektive Methode, um mit dieser Situation umzugehen, nämlich die Daten in einen MemoryStream einzulesen, bevor sie an die POST-Verarbeitungsmethode gesendet werden. Dieser Ansatz ist aus folgenden Gründen nicht ideal: Wenn die gesendeten Daten groß sind oder sogar eine Datei hochgeladen wird, ist es für uns nicht angemessen oder überhaupt nicht möglich, die Daten im Speicher zwischenzuspeichern. Die ideale Methode besteht darin, die Länge des Beitrags zu begrenzen. Beispielsweise können wir die Datenlänge auf 10 MB begrenzen.
Eine weitere Vereinfachung dieser einfachen Version des HTTP-Servers ist der Rückgabewert von content-type. Im HTTP-Protokoll sendet der Server immer den MIME-Typ der Daten an den Client und teilt dem Client mit, welchen Typ er haben muss Daten erhalten. In der Methode writeSuccess() sehen wir, dass der Server immer den Typ text/html sendet. Wenn Sie andere Typen hinzufügen müssen, können Sie diese Methode erweitern.