返回

使用 C#开发 HTTP 服务器系列之更简单的实现方式

AI 摘要
秦元培在博客中分享了关于HTTP服务器开发的系列文章,强调了从头开始构建Web服务器的乐趣和重要性,提到了HTTP协议的基本原理和无状态特性,以及如何利用TcpListener/TcpClient和HttpListener实现HTTP服务器的两种不同方法。他指出了在设计HTTP服务器时需要考虑保持状态的问题,并介绍了Cookie的应用。最后表示这些内容只是开发HTTP服务器的延伸,鼓励读者进一步探索。

各位朋友大家好,我是秦元培,欢迎大家关注我的博客,我的博客地址是http://qinyuanpei.com。到目前为止,我已经发布了 3 篇 HTTP 服务器开发的系列文章。对我个人而言,我非常享受这个从无到有的过程,或许我现在写的这个 Web 服务器有各种不完美的因素,可是当有一天我需要一个轻量级的服务器的时候,我在无形中是不是比别人多了一种选择呢?我们常常提到“不要重复造轮子”,可事实上这并不能成为我们“不造轮子”的理由,虽然我们有各种各样的服务器软件、有各种各样的服务端框架可以供我们选择,可是在动手写这个系列文章前,我对 Web 服务器的印象无非是因为我是用 LAWP(Linux + Apache + MySQL + PHP)搭建过 Wordpress 博客而已。虽然在对动态页面(如.aspx、.jsp、.php 等)的处理上,可能会和静态页面有所不同,但是我庆幸我了解了这个过程以及它的内部原理,这种跨语言、跨平台的设计思路是任何框架或者标准都无法告诉我的。或许有人会问我,为什么不在最开始的时候就选择更简单的实现方法,那么在这篇文章中你将会找到答案。

从原理说起

我们知道 HTTP 服务器其实是一个“服务端循环监听客户端请求然后响应客户端请求”的请求/响应模型,在这个模型中请求通常是由浏览器来发起的,而服务端负责响应客户端的请求。这是我们通常意义上的认识,可是当我们了解到 HTTP 协议的实质以后就会明白,不管是客户端还是服务端,从本质上来讲都是 Socket 通信,只要我们能够发送符合 HTTP 协议规范的报文就可以啦。

所以我们立刻就能够想到无论是 Unity 引擎中的 WWW 还是.NET 平台下的 WebClient,它们之所以能够向服务器发起请求,无一例外地是它们都遵循了 HTTP 协议的规范。从这个角度来讲,人类社会存在各种各样的问题,本质上都是存在游离于规范以外的不公平的现象。还记得我们在这个系列中提到的请求报文和响应报文的结构是什么样的吗?此时此刻我们发自内心地向创造 HTTP 协议的先驱们致敬,因为这个协议我们构建起了连接人与人的社交网络,可是同样因为这个协议我们和人越来越远、和手机越来越近。

HTTP 协议是一种无状态的应用层协议,这个无状态该怎么理解呢?我这里想借助聊天机器人这个实例来解释这个问题,我们都知道聊天机器人是一种问答型的程序,程序每次都可以根据提问者的问题给出,一个从人类角度来看完全合理的答案。然而从目前我了解到的聊天机器人的技术现状来看,具备自然语言理解的机器人程序基本没有,所以在这样的大背景下,机器人程序实际上是没有上下文理解的能力的。

好了,现在我们回到 HTTP 协议,首先聊天机器人的问答模式是不是和 HTTP 协议中的请求/响应模式非常相似呢?其次,我们在设计 HTTP 服务器的时候,每次在向客户端返回响应报文以后,我们就关闭了 Socket 连接,这意味着每次的请求和响应完全都是独立的,那么这样是不是就和聊天机器人不能理解上下文非常相似了呢?所以综合下来,我们理解的无状态其实就是说 HTTP 请求和响应完全独立,即在客户端中不会存储服务端的响应,在服务端中同样不会存储客户端的请求。

这样难免引发一个问题,如果我需要在不同请求和响应中保持状态该怎么做呢?这个在不同的服务器软件中有不同的技术实现,这里我们说一种最通用的 Cookie。Cookie 是存储在客户端中的一个数据,在发起下一轮请求时这个参数会被加入到参数列表中然后传递给服务器,服务器会对客户端传递的参数进行验证,以此来判断本轮请求和上轮请求间是否存在上下文联系。

两种不同的实现

到目前为止我们了解的 HTTP 服务器开发,实际上由两部分组成,即 Socket 通信和请求-响应模型。基于这两点考虑,我们这里提供两种快速实现 Web 服务器的具体思路,这是在我们理解了 HTTP 协议实质以后,从原理出发想到的解决方案,为什么我不建议在刚开始就学习这些东西呢?因为我觉得学习有时候其实就是一个不断开阔视野和思路的过程吧。好了,下面我们来说说这两种不同实现方式的具体思路吧!

基于 TcpListener/TcpClienr 改进 Socket

如果说使用 Socket 从头开始编写 HTTP 服务器是一个“刀耕火种”时代的缩影,那么使用 TcpListener/TcpClient 则是让我们开始进入“青铜铸犁”的农耕时代。和 Sokcet 相比,TcpListener/TcpClient 是.NET 对 Socket 的进一步封装,在这个体系下,TcpListener 负责监听和接收传入的连接请求,在该类中仅需要传入一个网络终端信息就可以完成服务端的初始化,而无需设置网络通信协议等细节性的内容。调用 Start 方法后即可以开始监听,这里我们使用 AcceptTcpClient 方法来阻塞进程直到接受到一个客户端请求为止,该方法将返回一个 TcpClient 对象,我们可以借助它完成和客户端的通信。下面我们来一起看基本的代码实现:

public void Start()
{
    if (isRunning)
        return;

    //创建TcpListener
    serverListener = new TcpListener(IPAddress.Parse(ServerIP), ServerPort);
    //开始监听
    serverListener.Start(10);
    isRunning = true;

    //输出服务器状态
    Console.WriteLine("Sever is running at http://{0}:{1}/.", ServerIP, ServerPort);

    while (isRunning)
    {
        //获取客户端连接
        TcpClient acceptClient = serverListener.AcceptTcpClient();

        //获取请求报文
        NetworkStream netstream = acceptClient.GetStream();

        //解析请求报文
        byte[] bytes = new byte[1024];
        int length = netstream.Read(bytes, 0, bytes.Length);
        string requestString = Encoding.UTF8.GetString(bytes, 0, length);

        //以下为响应报文(略)
    }
}

我个人感觉这种形式和原生的 Socket 在实现上区别不是非常大,按照这种思路继续往下设计,我的 HttpRequest 和 HttpResponse 可能都需要进行改进,因为在我的设计中,我是在尽可能地隐藏 Socket 通信的细节,因为我不想让使用者觉察到他这是在使用 Socket 进行通信,这里细心的朋友可能会发现,这里的 TcpListener/TcpClient 都保留了常见的 Socket 用法如同步通信和异步通信的支持等,所以在使用 cpListener/TcpClient 其实没有必要纠结它的这套流程,如果你喜欢继续使用 Socket 通信的经验和方法就可以了。这里我们仅提供一种延伸思路。具体的代码实现大家顺着这个思路继续下去就好啦。

基于 HttpListener 实现请求-响应模型

下面我们再来说说基于 HttpListener 实现请求-响应模型,它和改进 Socket 不同,它对我们编写一个 Web 服务器的意义主要体现在它提供了一个非常规范的接口,类似我这里的 HttpResponse 和 HttpRequest 以及 OnPost、OnGet 等接口这些设计。这个让我不喜欢的一点是它在设置服务器 IP 地址和端口的时候非常别扭,其思路和我的设计是非常相似的,下面我们来一起看代码:

public void Listen()
{
    if (!HttpListener.IsSupported)
        throw new InvalidOperationException(
            "请确保使用WindowsXP以上版本的Windows!");

    //初始化Http监听器
    listener = new HttpListener();

    //初始化服务器URL
    string[] prefixes = new string[] { address };
    foreach (string prefix in prefixes)
    {
        listener.Prefixes.Add(prefix);
    }

    //开启服务器
    listener.Start();

    //监听服务器
    while (isActive)
    {
        HttpListenerContext context = listener.GetContext();
        HttpListenerRequest request = context.Request;
        HttpListenerResponse response = context.Response;
        if (request.HttpMethod == "GET") {
            OnGetRequest(request, response);
        } else {
            OnPostRequest(request, response);
        }
    }
}

好了,现在这个东西就非常简单了,因为我们只需要继承 HttpServerBase 这个类然后重写相关方法就可以了,而请求报文和响应报文中的相关属性都在 HttpListenerRequest 和 HttpListenerResponse 这两个类中封装好了,我们直接使用就好了。在没有写这个系列文章前,可能我会对这种方案充满好奇,可是当我了解到这一切的实质以后,我反而更加喜欢使用我设计的 HTTP 服务器了,因为这些东西在我看来区别真的可以忽略。

One More Thing

关于今天本文中提到的两种方案,我都是作为 HTTP 服务器开发延伸出来的内容来写出来给大家看,所以这块儿内容我都是点到为止不打算给出完整的实现,如果有兴趣的朋友可以顺着我这个思路区继续改进。这个系列文章中的示例代码主要来自我的项目HttpServer,大家到我的 GIthub 上去了解更多细节。到目前为止我觉得 HTTP 服务器快发这块儿我能写的内容都基本上写完了,因为是一边写代码一边写博客,所以有时候博客中如果有写得不好或者写的不明白的地方,希望大家能够谅解,同时希望大家在博客中给我积极留言,下一篇我想简单写一下 RESTful API 的相关问题,写完这一篇整个系列就结束了,我还是想说写文章真的很累啊,希望大家继续支持,下期见。

Built with Hugo v0.126.1
Theme Stack designed by Jimmy
已创作 274 篇文章,共计 1038468 字