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

你是否有过这样的需求——想运行 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 该地址查看所有代码。

 
 
 
标签: C#ASP.NET

C# 编写简易 ASP.NET Web 服务器的更多相关文章

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

    原文 http://www.cnblogs.com/lcomplete/p/use-csharp-write-aspnet-web-server.html 如果你想获得更好的阅读体验,可以前往我在 g ...

  2. 使用 C# 编写简易 ASP.NET Web 服务器 ---- 模拟IIS的处理过程

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

  3. 网络知识 - 简易的自定义Web服务器

    简易的自定义Web服务器 基于浏览器向服务端发起请求 两台主机各自的进程之间相互通信,需要协议.IP地址和端口号,IP表示了主机的网络地址,而端口号则表示了主机上的某个进程的地址,IP加Port统称为 ...

  4. Jexus V5.8.0正式发布:跨平台的ASP.NET WEB服务器

    Jexus Web Server V5.8.0 已于今日(12月10日)正式发布,下载地址:http://www.linuxdot.net/. Jexus v5.8.0有如下的更新: 1,为反向代理增 ...

  5. Android与Asp.Net Web服务器的文件上传下载BUG汇总[更新]

    遇到的问题: 1.java.io.IOException: open failed: EINVAL (Invalid argument)异常,在模拟器中的sd卡创建文件夹和文件时报错 出错原因可能是: ...

  6. (转)推荐一个在Linux/Unix上架设ASP.NET的 WEB服务器--Jexus

    在Linux/Unix上架设ASP.NET WEB服务器,有两个可选方式,一种是Mono+XSP,一种是Mono+Jexus,其它的方式,比如 Apache+mod_mono.Nginx+FastCg ...

  7. 第十八篇:简易版web服务器开发

    在上篇有实现了一个静态的web服务器,可以接收web浏览器的请求,随后对请求消息进行解析,获取客户想要文件的文件名,随后根据文件名返回响应消息:那么这篇我们对该web服务器进行改善,通过多任务.非阻塞 ...

  8. 搭个 Web 服务器(一)

    导读 我相信,如果你想成为一个更好的开发者,你必须对日常使用的软件系统的内部结构有更深的理解,包括编程语言.编译器与解释器.数据库及操作系统.Web 服务器及 Web 框架.而且,为了更好更深入地理解 ...

  9. 打造一款属于自己的web服务器——开篇

    JVM总结慢慢来吧,先插播一篇水文,来介绍下最近业余一直在写的一个小项目——easy-httpserver(github).适合新手学习,大神们路过即可^_^. 一.这是个什么玩意? easy-htt ...

随机推荐

  1. 【甘道夫】Ubuntu群集配置 - 免费登陆

    引言 这是几年前写的文章,但一直以来该问题被反复问到.所以我决定将它又一次搬上屏幕. 正文 三个节点:masternode    slavenode1   slavenode2 第一步:全部节点分别生 ...

  2. outlook自动备份

    在 这里 下载 http://support.microsoft.com/kb/238782

  3. JS 数组array方法push, pop, unshift, shift, slice,splice,contact, join, sort

    Array:数组对象用来在单独的变量名中存储一系列的值   定义数组:         1. var arrayObj = new Array();         2. var arrayObj = ...

  4. Swift入门教程:基本语法(一)

          简介:                                                                                       什么是S ...

  5. jQuery.extend()源码解读

    // extend方法为jQuery对象和init对象的prototype扩展方法// 同时具有独立的扩展普通对象的功能jQuery.extend = jQuery.fn.extend = funct ...

  6. oracle 存储过程的基本语法

    原文:oracle 存储过程的基本语法 1.基本结构 CREATE OR REPLACE PROCEDURE 存储过程名字(    参数1 IN NUMBER,    参数2 IN NUMBER) I ...

  7. SQL点滴7—使用SQL Server的attach功能出现错误及解决方法

    原文:SQL点滴7-使用SQL Server的attach功能出现错误及解决方法 今天用SQL Server 2008的attach功能附加一个数据库,出了点问题,提示的错误是: Unable to ...

  8. 快速构建Windows 8风格应用28-临时应用数据

    原文:快速构建Windows 8风格应用28-临时应用数据 本篇博文主要介绍临时应用数据概览.如何构建临时应用数据. 一.临时应用数据概览 临时应用数据相当于网页中缓存,这些数据文件是不能够漫游的,并 ...

  9. Zend Server更新至6.2版本——虚拟主机全方位管理

    Zend Server自从发布6.0以来,并支持云服务,成为很多PHP程序所选择的Web服务器. Zend Server 6.2版本从更新内容来看,解决了Web服务器与虚拟主机之间的协同管理.并在细节 ...

  10. WTIR Updating Page

    NO REPLY. ############### #14090704# ###############