C# 中的简单 HTTP 服务器






4.94/5 (74投票s)
线程化的同步 HTTP 服务器抽象类,用于响应 HTTP 请求
引言
本文介绍了一个简单的 HTTP 服务器类,您可以将其集成到您自己的项目中,或通过阅读来了解更多关于 HTTP 协议的知识。
背景
高性能的 Web 服务通常托管在 IIS、Apache 或 Tomcat 等稳定可靠的 Web 服务中。然而,HTML 作为一种灵活的用户界面语言,可以方便地从几乎任何应用程序或后端服务器中提供 HTML UI。在这些情况下,外部 Web 服务器的开销和配置复杂性通常不值得费力。需要的是一个可以轻松嵌入的简单 HTTP 类,用于处理简单的 Web 请求。此类正满足了这一需求。
Using the Code
首先,我们回顾一下如何使用该类,然后深入探讨其工作原理的一些细节。我们通过继承 HttpServer
类,并为两个 abstract
方法 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("<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>{0}</pre>", data);
}
}
一旦提供了简单的请求处理器,就需要在一个端口上实例化服务器,并为主要的服务器监听器启动一个线程。
HttpServer httpServer = new MyHttpServer(8080);
Thread thread = new Thread(new ThreadStart(httpServer.listen));
thread.Start();
如果您编译并运行示例项目,您应该能够使用您选择的 Web 浏览器访问 https://:8080 来查看上面渲染的简单 HTML 页面。让我们简要了解一下幕后发生了什么。
这个简单的 Web 服务器分为两个组件。HttpServer
类在一个传入端口上打开一个 TcpListener
,然后在一个循环中通过 AcceptTcpClient
() 处理传入的 TCP 连接请求。这是处理传入 TCP 连接的第一步。传入的请求到达了我们的“知名端口”,此接受过程创建了一个新的端口对供服务器与此客户端通信。这个新的端口对就是我们的 TcpClient
会话。这使得我们的主接受端口可以自由地接受新连接。正如您在下面的代码中看到的,每次监听器返回一个新的 TcpClient
时,HttpServer
都会创建一个新的 HttpProcessor
并为其启动一个新线程来运行。此类还包含我们的子类必须实现的 abstract
方法,以便生成响应。
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 头,并将控制权交给适当的 abstract
方法处理程序实现。让我们看一些 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;
}
}
对于每一行,我们查找冒号 () 分隔符,将前面的字符串作为名称,后面的字符串作为值。当遇到一个空头行时,我们返回,因为我们已经收到了所有头。
此时,我们已经掌握了足够的信息来处理简单的 GET
或 POST
请求,然后我们分派到相应的处理程序。对于 post 请求,在接受 post 数据方面有一些技巧需要处理。请求头之一包含 post 数据的 content-length
。虽然我们希望让我们的子类的 handlePOSTRequest
来实际处理 post 数据,但我们只需要允许他们从 stream
中读取 content-length
字节,否则他们将阻塞在输入流上等待永不会到达的数据。在这个简单的服务器中,我们通过将所有 post 数据读取到 MemoryStream
中,然后再将这些数据发送到 POST
处理程序来处理这种情况,这是一种简单但有效但“脏”的策略。这并非理想,原因有几点。首先,post 数据可能很大。实际上,它可能是一个文件上传,在这种情况下,将其缓冲到内存中可能效率不高,甚至不可能。理想情况下,我们应该创建一个某种流模拟器,它可以设置为限制自身读取 content-length
字节,但否则会像普通流一样工作。这将允许 POST
处理程序直接从流中读取数据,而无需缓冲到内存的开销。但是,这也需要更多的代码。在许多嵌入式 HTTP 服务器中,根本不需要 post 请求,因此我们通过简单地将 POST
输入数据限制在 10MB 以内来避免这种情况。
这个简单服务器的另一个简化之处是返回数据的 content-type
。在 HTTP 协议中,服务器始终将服务器应该期望的数据的 MIME-Type
发送给浏览器。在 writeSuccess()
中,您可以看到该服务器始终指示 content-type
为 text/html
。如果您希望返回其他内容类型,您将需要扩展此方法,以便允许您的处理程序在将数据发送给客户端之前提供内容类型响应。
关注点
这个 SimpleHttpServer
只实现了 HTTP/1.0 规范一个非常基础的子集。HTTP 规范的后续版本包含更复杂且非常有价值的改进,包括压缩、会话保持活动、分块响应等等。然而,由于 HTTP 的出色和简单的设计,您会发现即使是这个非常基础的代码也能够提供与现代 Web 浏览器兼容的页面。
其他类似的嵌入式服务器包括:
- C# 简易 HTTP 服务器骨架
- 嵌入式 .NET HTTP 服务器
- System.Net HttpListener(.NET Framework 3.5 SP1 及更高版本)
- http://zeroconf.codeplex.com/ 中提供的 HTTP 服务器
历史
- 2010 年 12 月 19 日:发布初始版本
- 2010 年 12 月 22 日:从输入端移除了
StreamReader
,以便我们可以正确获取原始POST
数据 - 2012 年 3 月 2 日:修正了行终止符,使用 WriteLine() 发送 \r\n 而不是仅发送 \n