在本文中,将探讨如何实现一个简单的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("");
}
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。如果希望返回其他内容类型,需要扩展这个方法,以允许处理器在将数据发送给客户端之前提供内容类型响应。