使用 C# 编写简易 ASP.NET Web 服务器

简介: 原文 http://www.cnblogs.com/lcomplete/p/use-csharp-write-aspnet-web-server.html 如果你想获得更好的阅读体验,可以前往我在 github 上的博客进行阅读,http://lcomplete.github.io/blog/2013/07/16/use-csharp-write-aspnet-web-server/。

原文 http://www.cnblogs.com/lcomplete/p/use-csharp-write-aspnet-web-server.html

如果你想获得更好的阅读体验,可以前往我在 github 上的博客进行阅读,http://lcomplete.github.io/blog/2013/07/16/use-csharp-write-aspnet-web-server/


 

你是否有过这样的需求——想运行 ASP.NET 程序,又不想安装 IIS 或者 Visual Studio?我想如果你经常编写 ASP.NET 程序的话,应该或多或少都会碰到这种情况。除了使用 IIS 和 VS,我们还有哪些方式可以运行 ASP.NET 程序呢,自己写一个支持 ASP.NET 的 Web 服务器怎么样?NO NO NO,如果你只是想找个这样的工具的话,那完全没必要,我们知道使用 VS 可以运行 ASP.NET 程序,那么我们就可以找出 VS 所调用的程序,将其拷贝到没有 VS 和 IIS 的环境中运行,就能运行 ASP.NET 程序了,安装了 VS 的朋友可以到 C:\Program Files\Common Files\Microsoft Shared\DevServer\ 这个目录里面找找看,这个程序的使用方式如下。

WebDev.WebServer.EXE /port:80 /path:"c:\mysite" /vpath:"/"

怎么样?不错吧,轻而易举地就解决了文章开头所说的问题了。当然这并不是本篇文章的重点,如果你不满足于只知道这个用法,那可以继续往下阅读,接下来,我们将使用 C# 编写一个支持 ASP.NET 的 Web 服务器,看看这一切究竟是如何运作的。

C# 中有着许多丰富的类库,使用不同的类库,我们可以站在不同的抽象层级去编写一个 Web 服务器,比如在 System.Net 命名空间下提供了一个 HttpListener 类,使用这个类,我们可以很容易地创建一个简单的 Web 服务器,但是这个类隐藏了很多实现的细节,为了避免知其然不知其所以然,我们将使用网络框架最底层的 Socket 类来编写这个程序。

预备知识

正式编写这个程序之前,让我们先来了解一些基础知识。编写一个 Web Server,必需要了解 HTTP 协议,它是万维网的基础,位于 TCP/IP 协议栈的应用层。

  1. HTTP 协议

    HTTP 协议是一个基于请求与响应模式、无状态的应用层协议,HTTP 请求主要包括三部分:请求行、请求报头、请求正文,下面是一个请求示例。

    复制代码
    GET /lcomplete/AspNetServer HTTP/1.1 
    Host: github.com
    Connection: keep-alive
    Cache-Control: max-age=0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.72 Safari/537.36
    
    postdata #可选的消息体
    复制代码

    第一行是请求行,该行又分为3个部分,分别是动作、URI 和 HTTP 协议版本,后面的 {key}: {value} 格式的行为报头,如果请求为 post 动作的话,则报头后面的post数据为请求正文,需要注意报头和请求正文之间必需以(回车+换行)分割。

    Web 服务器接收到一个请求后,就会将请求解析成上面3个部分,并开始处理应答,响应也由3个部分组成:状态行、响应报头、响应正文,响应报头和正文同样使用进 行分割,状态行为HTTP协议版本、状态码、状态描述组成,响应报头与请求报头格式相同,只不过请求报头由服务器解释并处理,响应报头由浏览器解释并处 理,最后的响应正文便是我们所熟悉的 HTML。

    了解了 HTTP 协议的基础知识后,我们可以很容易地构建出一个支持静态文件的 HTTP 服务器,但是如何处理 ASP.NET 动态内容呢,这就要求我们熟悉 ASP.NET 的 HTTP 架构、管道机制、应用程序生命周期和宿主环境。

  2. ASP.NET 运行时机制

    ASP.NET 被特意设计成避免依赖 IIS,它的底层架构采用了管道机制,管道由一系列处理 HTTP 消息的对象组成,每个 HTTP 请求都要经过这些对象,每个对象都执行一些自己职责之内的任务。

    HttpRuntime 类是管道的入口,它负责开始处理请求,管理首先执行 HttpRuntime 类上的静态方法 ProcessRequest ,这个方法接收一个 HttpWorkerRequest 对象参数,该对象包含了当前请求的相关信息,HttpRuntime 类使用这个请求信息构建 HttpContext 对象,其中包含了 HttpRequest 和 HttpResponse 属性,然后根据上下文获取 HttpApplication 对象,之后请求交给 HttpApplication 对象进行处理。

    处理请求时,HttpApplication 会执行一系列任务,其中包括为请求调用合适的 IHttpHandler 类的 ProcessRequest 方法,例如,如果请求针对某页,则使用该页的实例处理该请求,另外 HttpApplication 中还维护了 IHttpModule 对象列表,它可以在页面实例处理请求前后进行一些额外的工作。

    管道机制是完全自主的,不需要依附于 IIS 上,不过管道并没有接收 HTTP 请求的能力,我们需要自己编写这部分代码,当收到请求时,创建 HttpWorkerRequest 对象并提供给 HttpRuntime.ProcessRequest 方法调用以启动管道。

    要处理 ASP.NET 请求,还需要创建一个应用程序域以托管 HTTP 管道,我们可以使用 ApplicationHost.CreateApplicationHost 方法创建应用程序域,该方法接收3个参数:宿主类型、虚拟路径和物理路径,宿主类型需要跨域应用程序边界,所以需要继承自 MarshalByRefObject 类,并提供与其交互的方法,例如至少要提供一个方法使得可以提交 ASP.NET 请求以进行处理。

    了解了 ASP.NET 的运行机制后,再来看看编写 ASP.NET 服务器需要使用到哪些类,首先我们需要使用 ApplicationHost 创建应用程序域以获得处理 ASP.NET 请求的能力,接收到请求后构造HttpWorkerRequest (该类是抽象类,需要定义它的子类)对象,交由 HttpRuntime 类进行处理,接下来的事情就由 HTTP 管道处理了。

    好了,预备知识已经讲解完毕,下面让我们进入编码实战。

编码实战

还记得文章开头的命令吗?运行一个网站需要提供3个必要的东西,端口、网站物理路径、网站虚拟路径,在程序开始运行时需要得到这3个参数。

复制代码
static void Main(string[] args)
{
    int port;
    string dir = Directory.GetCurrentDirectory();
    if(args.Length==0 || !int.TryParse(args[0],out port))
    {
        port = 45758; //端口
    }
    InitHostFile(dir);
    SimpleHost host= (SimpleHost) ApplicationHost.CreateApplicationHost(typeof (SimpleHost), "/", dir);
    host.Config("/", dir); //配置虚拟路径和物理路径
    WebServer server = new WebServer(host, port);
    server.Start();
}
//需要拷贝执行文件 才能创建ASP.NET应用程序域
private static void InitHostFile(string dir)
{
    string path = Path.Combine(dir, "bin");
    if (!Directory.Exists(path))
        Directory.CreateDirectory(path);
    string source = Assembly.GetExecutingAssembly().Location;
    string target = path + "/" + Assembly.GetExecutingAssembly().GetName().Name + ".exe";
    if(File.Exists(target))
        File.Delete(target);
    File.Copy(source, target);
}
复制代码

为了便于测试,我将这3个参数都写死了,端口默认使用45758,物理路径使用当前程序所在目录,虚拟路径使用根目录,这两个路径信息保存在 host 对象中。由于 Application.CreateApplicationHost 方法期望在 GAC 或指定的物理路径中的 bin 目录中找到宿主类型所在的程序集,所以在创建应用程序域之前先将当前程序拷贝到了物理路径的 bin 目录中,创建完应用程序域后初始化 WebServer 对象,调用该对象的 Start 方法以启动服务器。在 WebServer 中保留了 host 的引用,当处理 ASP.NET 请求时会使用到,我们先看一下启动服务器的方法。

复制代码
public void Start()
{
    _serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    _serverSocket.ExclusiveAddressUse = true;
    _serverSocket.Bind(new IPEndPoint(IPAddress.Any, Port));
    _serverSocket.Listen(1000);
    IsRuning = true;
    Console.WriteLine("Serving HTTP on 0.0.0.0 port " + Port + " ...");
    new Thread(OnStart).Start();
}
private void OnStart(object state)
{
    while (IsRuning)
    {
        try
        {
            Socket socket = _serverSocket.Accept();
            ThreadPool.QueueUserWorkItem(AcceptSocket, socket);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
            Thread.Sleep(100);
        }
    }
}
private void AcceptSocket(object state)
{
    if (IsRuning)
    {
        Socket socket = state as Socket;
        HttpProcessor processor = new HttpProcessor(_host, socket);
        processor.ProcessRequest();
    }
}
复制代码

在 Start 方法中,创建了一个全局的 socket 对象,使其监听指定端口,并新开了一个线程用于处理客户端请求,当接收到客户端请求后,将其交给 HttpProcessor 对象处理。

复制代码
public void ProcessRequest()
{
    try
    {
        RequestInfo requestInfo = ParseRequest();
        if (requestInfo != null)
        {
            string staticContentType = GetStaticContentType(requestInfo);
            if (!string.IsNullOrEmpty(staticContentType))
            {
                WriteFileResponse(requestInfo.FilePath, staticContentType);
            }
            else if (requestInfo.FilePath.EndsWith("/"))
            {
                WriteDirResponse(requestInfo.FilePath);
            }
            else
            {
                _host.ProcessRequest(this, requestInfo);
            }
        }
        else
        {
            SendErrorResponse(400);
        }
    }
    finally
    {
        Close();//确保连接关闭
    }
}
复制代码

处理的步骤如下:

  1. 解析请求数据,从建立的 socket 连接处获取请求数据,将其解析为RequestInfo对象。
  2. 判断请求是否有效,无效则响应 400 错误,有效则进行下一步处理。
  3. 判断请求的是否为静态内容,是则输出文件响应。
  4. 判断请求是否为目录,是则输出目录下的子文件夹和文件的链接,与 IIS 目录服务类似。
  5. 不为静态内容和目录时,则交给 host 对象处理(使用ASP.NET HTTP 运行时进行处理)。
  6. 处理完后确保连接关闭。

其中输出响应是构造状态行、响应报头和响应正文,接着通过 socket 发送给客户端的过程。相信看到这里,大家已经对整个交互过程有了一个了解,剩下的最后一个问题就是如何处理动态内容。

为了与 ASP.NET 的应用程序域交互,我们需要将请求信息提交给宿主对象 host 进行处理,下面是我们实现的宿主类。

复制代码
public class SimpleHost : MarshalByRefObject
{
    public string PhysicalDir { get; private set; }
    public string VituralDir { get; private set; }
    public void Config(string vitrualDir, string physicalDir)
    {
        VituralDir = vitrualDir;
        PhysicalDir = physicalDir;
    }
    public void ProcessRequest(HttpProcessor processor, RequestInfo requestInfo)
    {
        WorkerRequest workerRequest = new WorkerRequest(this, processor, requestInfo);
        HttpRuntime.ProcessRequest(workerRequest);
    }
}
复制代码

在 ProcessRequest 方法中,创建了 HttpWorkerRequest 的子类 WorkerRequest 对象,并提交给 HttpRuntime 进行处理。WorkerRequest 类中实现了 HttpWorkerRequest 中的抽象方法,其中包括 GetRawUrl 、GetHttpVerbName 等等这一类获取请求相关信息的方法,HTTP 管道调用这些方法以获取请求数据,同时它还包含类似 FlushResponse 这类输出响应的方法,HTTP 管道最终会调用这类方法向客户端发送数据,下面是 FlushResponse 方法的实现,在该方法中我们使用 HttpProcessor 对象向 socket 客户端发送响应数据。

复制代码
public override void FlushResponse(bool finalFlush)
{
    if (!_isHeaderSent)
    {
        _processor.SendHeaders(_statusCode, _responseHeaders, -1, finalFlush);
        _isHeaderSent = true;
    }
    for (int i = 0; i < _responseBodyBytes.Count; i++)
    {
        byte[] data = _responseBodyBytes[i];
        _processor.SendResponse(data);
    }
    _responseBodyBytes = new List<byte[]>();
    if (finalFlush)
        _processor.Close();
}
复制代码

到这一步,我们已经可以运行 ASP.NET 程序了,但是只实现抽象方法还不能提供足够的信息给 HTTP 管道,例如 HTTP 管道无法得知 POST 数据和 Cookie 数据,要提供这些信息我们还需要重写一些虚拟方法,如 GetKnownRequestHeader 、GetPreloadedEntityBody 等等,实现一些必要的方法之后,ASP.NET 程序就能够良好地运行了。

总结

编写支持 ASP.NET 的 Web 服务器,并不是一件难事,这得益于 ASP.NET 优雅的设计,只要向运行时提供必要的信息,HTTP 管道就能够正确地进行处理。

文中只贴了一小部分代码,你可以通过 https://github.com/lcomplete/AspNetServer 该地址查看所有代码。

目录
相关文章
|
24天前
|
开发框架 .NET C#
在 ASP.NET Core 中创建 gRPC 客户端和服务器
本文介绍了如何使用 gRPC 框架搭建一个简单的“Hello World”示例。首先创建了一个名为 GrpcDemo 的解决方案,其中包含一个 gRPC 服务端项目 GrpcServer 和一个客户端项目 GrpcClient。服务端通过定义 `greeter.proto` 文件中的服务和消息类型,实现了一个简单的问候服务 `GreeterService`。客户端则通过 gRPC 客户端库连接到服务端并调用其 `SayHello` 方法,展示了 gRPC 在 C# 中的基本使用方法。
35 5
在 ASP.NET Core 中创建 gRPC 客户端和服务器
|
27天前
|
XML 前端开发 JavaScript
PHP与Ajax在Web开发中的交互技术。PHP作为服务器端脚本语言,处理数据和业务逻辑
本文深入探讨了PHP与Ajax在Web开发中的交互技术。PHP作为服务器端脚本语言,处理数据和业务逻辑;Ajax则通过异步请求实现页面无刷新更新。文中详细介绍了两者的工作原理、数据传输格式选择、具体实现方法及实际应用案例,如实时数据更新、表单验证与提交、动态加载内容等。同时,针对跨域问题、数据安全与性能优化提出了建议。总结指出,PHP与Ajax的结合能显著提升Web应用的效率和用户体验。
40 3
|
2月前
|
监控 网络安全 调度
Quartz.Net整合NetCore3.1,部署到IIS服务器上后台定时Job不被调度的解决方案
解决Quartz.NET在.NET Core 3.1应用中部署到IIS服务器上不被调度的问题,通常需要综合考虑应用配置、IIS设置、日志分析等多个方面。采用上述策略,结合细致的测试和监控,可以有效地提高定时任务的稳定性和可靠性。在实施任何更改后,务必进行充分的测试,以验证问题是否得到解决,并监控生产环境的表现,确保长期稳定性。
90 1
|
2月前
|
网络协议 Unix Linux
一个.NET开源、快速、低延迟的异步套接字服务器和客户端库
一个.NET开源、快速、低延迟的异步套接字服务器和客户端库
|
2月前
|
Java PHP
PHP作为广受青睐的服务器端脚本语言,在Web开发中占据重要地位。理解其垃圾回收机制有助于开发高效稳定的PHP应用。
【10月更文挑战第1天】PHP作为广受青睐的服务器端脚本语言,在Web开发中占据重要地位。其垃圾回收机制包括引用计数与循环垃圾回收,对提升应用性能和稳定性至关重要。本文通过具体案例分析,详细探讨PHP垃圾回收机制的工作原理,特别是如何解决循环引用问题。在PHP 8中,垃圾回收机制得到进一步优化,提高了效率和准确性。理解这些机制有助于开发高效稳定的PHP应用。
53 3
|
2月前
|
API C#
异步轮询 Web API 的实现与 C# 示例
异步轮询 Web API 的实现与 C# 示例
87 0
|
3月前
|
开发框架 JavaScript 前端开发
|
4月前
|
JavaScript 搜索推荐 前端开发
从零搭建到部署:Angular与Angular Universal手把手教你实现服务器端渲染(SSR),全面解析及实战指南助你提升Web应用性能与SEO优化效果
【8月更文挑战第31天】服务器端渲染(SSR)是现代Web开发的关键技术,能显著提升SEO效果及首屏加载速度,改善用户体验。Angular Universal作为官方SSR解决方案,允许在服务器端生成静态HTML文件。本文通过具体示例详细介绍如何使用Angular Universal实现SSR,并分享最佳实践。首先需安装Node.js和npm。
101 1
|
4月前
|
API C# 开发框架
WPF与Web服务集成大揭秘:手把手教你调用RESTful API,客户端与服务器端优劣对比全解析!
【8月更文挑战第31天】在现代软件开发中,WPF 和 Web 服务各具特色。WPF 以其出色的界面展示能力受到欢迎,而 Web 服务则凭借跨平台和易维护性在互联网应用中占有一席之地。本文探讨了 WPF 如何通过 HttpClient 类调用 RESTful API,并展示了基于 ASP.NET Core 的 Web 服务如何实现同样的功能。通过对比分析,揭示了两者各自的优缺点:WPF 客户端直接处理数据,减轻服务器负担,但需处理网络异常;Web 服务则能利用服务器端功能如缓存和权限验证,但可能增加服务器负载。希望本文能帮助开发者根据具体需求选择合适的技术方案。
201 0
|
4月前
|
Rust 安全 开发者
惊爆!Xamarin 携手机器学习,开启智能应用新纪元,个性化体验与跨平台优势完美融合大揭秘!
【8月更文挑战第31天】随着互联网的发展,Web应用对性能和安全性要求不断提高。Rust凭借卓越的性能、内存安全及丰富生态,成为构建高性能Web服务器的理想选择。本文通过一个简单示例,展示如何使用Rust和Actix-web框架搭建基本Web服务器,从创建项目到运行服务器全程指导,帮助读者领略Rust在Web后端开发中的强大能力。通过实践,读者可以体验到Rust在性能和安全性方面的优势,以及其在Web开发领域的巨大潜力。
45 0