简易HTTP服务器实现指南

在本文中,将探讨如何实现一个简单的HTTP服务器类,这个类可以被嵌入到项目中,或者用于学习HTTP协议。高性能的Web服务通常托管在像IIS、Apache或Tomcat这样的稳定Web服务器上。然而,HTML是一种非常灵活的用户界面语言,它可以在几乎任何应用程序或后端服务器上提供HTML用户界面。在这些情况下,外部Web服务器的开销和配置复杂性通常不值得。需要的是一个简单的HTTP类,可以轻松地嵌入以服务简单的Web请求。这个类满足了这个需求。

使用代码

首先,让回顾一下如何使用这个类,然后将深入了解它如何运作的一些细节。从继承HttpServer类并为两个抽象方法handleGETRequest和handlePOSTRequest提供实现开始。

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(""); p.outputStream.WriteLine("当前时间: " + DateTime.Now.ToString()); p.outputStream.WriteLine("url : {0}", p.http_url); p.outputStream.WriteLine("
"); p.outputStream.WriteLine(""); p.outputStream.WriteLine(""); p.outputStream.WriteLine("
"); } public override void handlePOSTRequest(HttpProcessor p, StreamReader inputData) { Console.WriteLine("POST request: {0}", p.http_url); string data = inputData.ReadToEnd(); p.outputStream.WriteLine(""); p.outputStream.WriteLine("

"); p.outputStream.WriteLine("postbody:

{0}
", data); } }

一旦提供了简单的请求处理器,就必须在端口上实例化服务器,并为服务器监听器启动一个线程。

HttpServer httpServer = new MyHttpServer(8080); Thread thread = new Thread(new ThreadStart(httpServer.listen)); thread.Start();

如果编译并运行示例项目,应该能够将选择的Web浏览器指向http://localhost:8080来查看上述简单的HTML页面。让简要地看一下幕后发生了什么。

服务器组件

这个简单的Web服务器分为两个组件。HttpServer类在传入端口上打开一个TcpListener,并坐在一个循环中处理传入的TCP连接请求,使用AcceptTcpClient()。这是处理传入TCP连接的第一步。传入的请求到达"众所周知的端口",并且这个接受过程为服务器与此客户端通信创建了一个新的端口对。这个新的端口对是TcpClient会话。这使主要接受端口可以自由地接受新的连接。正如在下面的代码中看到的,每次监听器返回一个新的TcpClient,HttpServer就创建一个新的HttpProcessor并为其启动一个新线程。这个类还包含了子类必须实现的抽象方法,以便产生响应。

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

此时,新的客户端-服务器TCP连接被移交给HttpProcessor,在其自己的线程中。HttpProcessor的工作是正确解析HTTP头,并将控制权交给适当的抽象方法处理器实现。让看一下HTTP头处理的一些一小部分。HTTP请求的第一行类似于以下内容:

GET /myurl HTTP/1.0

在process()中设置输入和输出流之后,HttpProcessor调用parseRequest(),接收并解析上述HTTP请求行。

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

HTTP请求行总是由三部分组成,所以简单地使用string.Split()调用来将其分成三部分。下一步是从客户端接收并解析HTTP头。每个头行包括一个形式为KEY:Value的类型。一个空行表示HTTP头的结束。readHeaders代码如下:

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++; // strip any spaces } string value = line.Substring(pos, line.Length - pos); Console.WriteLine("header: {0}:{1}", name, value); httpHeaders[name] = value; } }

对于每一行,寻找冒号(separator),获取前面的字符串作为名称,后面的字符串作为值。当到达一个空的头行时,返回,因为已经接收了所有头。此时,足够了解如何处理简单GET或POST,所以将调度到适当的处理器。在POST的情况下,在接受POST数据时有一些复杂性需要处理。一个请求头包括POST数据的content-length。虽然希望让子类的handlePOSTRequest实际处理POST数据,只需要允许他们从流中请求content-length字节,否则他们将被卡在输入流上等待永远不会到来的数据。在这个简单的服务器中,用一个肮脏但有效的策略来处理这种情况,即在将这些数据发送到POST处理器之前,将所有POST数据读入一个MemoryStream。这并不理想,原因有很多。首先,POST数据可能很大。事实上,它可能是一个文件上传,在这种情况下,将其缓冲到内存中可能不是高效的,甚至是不可能的。理想情况下,会创建某种类型的流模仿器,可以设置为限制自己到content-length字节,但除此之外就像一个普通的流一样。这将允许POST处理器直接从流中拉取数据,而不需要在内存中缓冲的开销。然而,这也意味着更多的代码。在许多嵌入式HTTP服务器中,POST请求根本不需要,所以通过简单地限制POST输入数据不超过10MB来避免这种情况。

返回数据的内容类型

在这个简单的服务器中,content-type的返回数据被简化了。在HTTP协议中,服务器总是向浏览器发送它应该期望的数据的MIME-Type。在writeSuccess()中,可以看到这个服务器总是指示一个content-type为text/html。如果希望返回其他内容类型,需要扩展这个方法,以允许处理器在将数据发送给客户端之前提供内容类型响应。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485