一不小心写了个WEB服务器

时间:2023-03-08 16:30:29
一不小心写了个WEB服务器

开场

  Web服务器是啥玩意? 是那个托管了我的网站的机器么? No,虽然那个也是服务器,但是我们今天要说的Web服务器主要是指像IIS这样一类的,用于处理request并返回response的工具,没错我们可以说它是一个工具,不就是一个应用程序吗?谁不会写应用程序呀,等着,三分钟就搞一个出来。

Web Server的介绍

  我们先来看一下web server主要干什么?
一不小心写了个WEB服务器

  这图很熟悉么?我是直接从小坦克的那篇http协议里面拿过来的,但是要注意的是,图中的Web Server是指的那台机器。我们网站的文件可能放在它上面的某一个磁盘目录下,但是接收request并且最后返回给我们的response的却不是机器本身,它就是我们今天的开场web server。一般我们ASP.NET网站开发时所指的web server就是IIS了,但是还有一些开源的像Apache,Lighttpd, Nginx等在php和java领域以及开源社区都有很大的名声,并且Apache才是被使用最多的web server(大概占60%左右的市场)。

  虽然说web server的主要工作是处理request返回response,但是一些主流的web server还包括了很多其它的扩展模块

  • 应用程序生命周期管理
  • 认证
  • 授权
  • 缓存
  • 安全
  • 队列处理
  • 压缩
  • 线程管理
  • ......

  当然,上面这些功能呢,我们一个也不会实现,:(  我们今天只实现对一个静态站点的访问,其实我的静态站点里面也就一个页面。但是这只是一个思路,给大家留下足够的想象空间,更重要的是好戏还在后头!

类库介绍

  • HttpListener: http协议监听器。
  • HttpListenerContext:包含resquest 和 response信息的一个上下文对象。
  • HttpListenerRequest:包含请求信息,头,体等。
  • HttpListenerResponse:包含响应信息,头,体等。

  我们今天就主要借助以上4个类来帮助实现我们的web server,这4个类都是包含在System.Net命名空间下,并且是在2.0的时候就已经存在了,所以并不是什么新鲜事了。我们创建了一个控制台应用程序,然后在不到3分钟的时间内写了以下代码。

public static HttpListener listener = new HttpListener();
// 暂时把程序启动目标设置为我们网站的根目录
public static string startUpPath = System.IO.Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
static void Main(string[] args)
{
listener.Start(); // 使用本机IP地址监听
listener.Prefixes.Add("http://192.168.1.100/");
Thread t = new Thread(new ThreadStart(clientListener));
t.Start();
Console.Write("Web server started...");
while (true)
{
string s = Console.ReadLine();
Console.Write("Web server ended...");
}
} public static void clientListener()
{
while (true)
{
try
{
HttpListenerContext request = listener.GetContext();
// 从线程池从开一个新的线程去处理请求
ThreadPool.QueueUserWorkItem(processRequest, request);
}
catch (Exception e) { Console.WriteLine(e.Message); }
}
}

  //处理请求的代码

public static void processRequest(object listenerContext)
{
try
{
var context = (HttpListenerContext)listenerContext;
string filename = context.Request.RawUrl.Remove(0, 1);
string path = Path.Combine(startUpPath, filename);
byte[] msg;
if (!File.Exists(path))
{
Console.WriteLine("文件未找到,找错了!");
context.Response.StatusCode = (int)HttpStatusCode.NotFound;
msg = File.ReadAllBytes(startUpPath + "\\webroot\\error.html");
}
else
{
context.Response.StatusCode = (int)HttpStatusCode.OK;
msg = File.ReadAllBytes(path);
}
context.Response.ContentLength64 = msg.Length;
using (Stream s = context.Response.OutputStream)
{
// 直接将文件写入response
s.Write(msg, 0, msg.Length);
}
}
catch
{
}
}

一不小心写了个WEB服务器

  接下来,我放了一个简单的index.html和一个images文件夹在我们应用程序的bin目录下,然后按F5启动这个控制台应用程序,最后输入我们的http://192.168.1.100/index.html,你们将会看到:

一不小心写了个WEB服务器

  怎么样?有图有真相,我们这个小小的web server已经可以处理一个静态的站点了,包括css文件js文件都没有问题。当然对于HttpListener的用法,如果大家感兴趣可以继续研究,我们这里就点到为止。因为如果你觉得写一个小小的web server是本文的重点,那么我只能说,少年,你实在是太年轻了!

  好的,让我们重新开始吧!

本文概述

  为什么会有一个关于自定义web server的例子摆在本文概述的前面呢?本文又到底是要阐述一个什么样的话题呢?让我们把时钟拔到2周以前,也就是我的上一篇博客,通过介绍ASP.NET Identity的登录原理引入了微软开源家族中的又一个亮点产品OWin(Open web interface for .net),关于什么是OWin,我们在上一篇博客中已经有了比较具体的介绍,我就不打算重复了。简而言之,它是一个有着潜力可以让ASP.NET MVC脱离 IIS(我想通过这里,你或许可以猜到我们为什么会有前面的那个demo),或者说可以让我们用全新的方式开发基于.NET的WEB应用程序的。

  问题一:ASP.NET开发的网站能Host在除了IIS以外的其它server上么?
  问题二:基于.NET的来开发web应用程序的方式除和ASP.NET Web Form和ASP.NET MVC以外,还有其它方式么?

IIS到底哪里错了?

  由于篇幅的原因,今天我们先来回答第一个问题。到目前为止,ASP.NET开发的网站是不能托管在除了IIS以外的Web服务器之上的,至少很难,为什么呢?我们要从ASP.NET的管道模型开始说起, 上周你们不是推荐了那篇ASP.NET是如何在IIS工作的 么?我借鉴一下里面的那张.NET运行时的序列图:    

  一不小心写了个WEB服务器

  但是今天我们不是讲IIS是如何工作的,我们把上面用到的对象列出来看一下:

  • ISAPIRuntime: System.Web.Hosting.ISAPIRuntime, System.Web
  • HttpWorkerRequest: System.Web.HttpWorkerRequest, System.Web
  • HttpRuntime: System.Web.Runtime, System.Web
  • HttpApplicationFactory: System.Web.HttpApplicationFactory, System.Web
  • HttpApplication: System.Web.HttpApplication, System.Web
  • HttpContext: System.Web.HttpContext, System.Web
  • HttpRequest: System.Web.HttpRequest, System.Web
  • HttpReponse:  System.Web.HttpResponse, System.Web

System.Web是属于.NET Framewok的一部份

  大家可以发现,这些类全部是被放到了System.Web这个dll中的,包括其中没有列出来的Session,IHttpModule和IHttpHandle同样也是。那么这个dll有什么问题么?这个dll本身没有问题,问题在于它是.NET Framework的一部份,回顾一下.NET Framework多久更新一次?2-3年? 当然.NET Framework 2-3年更新一次并没有什么错,因为毕竟它是非常底层的东西,必须保证它的稳定性的健壮性。但是Web这个词汇本身就是一个更新换代非常快的东西,万一它有个什么bug,我们也得等个2-3年,这就直接导致了如果想要对这些相关的功能做一些改进或者优化,等它出来也得等2到3年(一个程序员的青春有几个3年啊!)

  不过ASP.NET Team吸取了教训,现在的Web API就已经完全摆脱了对System.Web的依懒,所以Web API是用Nuget来发布版本的,.NET Framework 10年多的时间才到4.5,而Web API不到两年的时间已经接近了12个release 现在是 2.1 。 这也使得Web API能够更好的拥抱变化,更快的响应开发者以及开源社区的需求,当然Web API本身也是开源的。

  为什么ASP.NET MVC没有放到.NET Framework中,也是这个原因。

  还有一个问题是,所有的这些东西全部放在System.Web中,随着时间的推移,这个dll就会越来越大,越来越复杂。

HttpModule是基于IIS管道的

  在上一篇文章中,我们讲到为什么要解耦服务器与应用程序时,我们也提到了IIS的处理模型,从上到下,IIS给我们暴露了这样的一些事件,而我们开发自定义的HttpModule就是绑定这些事件来做一些处理。设想一下,如果我要在Authorization之后实现多个HttpModule,并且要按照指定顺序来执行怎么办?

  一不小心写了个WEB服务器

  我们并不能改变以上管道中每一个结点中的执行顺序,而我们自定义的HttpModule是按照我们在web.config中定义的顺序被添加的。这里的局限性是,这条管道就是这么多个执行过程,我们只能够在其中的某一个结点之前,或之后来做一些事件。又或者我想关掉其中的某些步骤(比如说我不要Authentication),怎么办?

ASP.NET 多数Modules默认全部开启

  我们可以用VS2013新建一个空白的MVC站点,记住是完全空白的,然后我们可以看一下有哪些HttpModule是在工作的。我们只需要建一个HomeController加一个Index的Action就可以了。

public class HomeController : Controller
{
public void Index()
{
HttpApplication httpApps = ControllerContext.HttpContext.ApplicationInstance;
// 获取所有 http module
HttpModuleCollection httpModules = httpApps.Modules; Response.Write(string.Format("一共有{0}个 HttpModule</br>", httpModules.Count.ToString()));
foreach (string activeModule in httpModules.AllKeys)
{
Response.Write(activeModule + "</br>");
}
}
}

  大家可以看到,OutputCache,Session,WindowsAuthentication,FormsAuthentiation, RoleManager, Profile等等,这些你在项目中真的有用到么?如果没有,你有关闭他们么?

  如果不使用它们,这些Module是需要手动在config文件里面移除的。但是大多数情况下,程序员们并不会想到去移除他们,这其实是一个性能上的损失。

  一不小心写了个WEB服务器

  当然我们并不能因为这一些问题就否认IIS,就算是ASP.NET在当初设计的时候也是被认为它就是要被托管在IIS上的。但是它又不具有很好扩展性,同时ASP.NET也是时候要考虑开放了,特别是在Node.js以及一些开源前端MVVM框架的影响下,Web后端开发有逐渐要被取代的趋势,所以OWin来了,它为了解决这些问题而来,一切都还是我们所熟悉的,但是却给了我们更灵活的开发方式。

随心所欲-建立你自己的管道

  我们上篇有说到OWin只是一套定义,它本身不具备任何代码。它主要定义了服务器在处理resquest所需要的一些信息(大多都是http协议里面要求的),和一个应用程序代理。  

一不小心写了个WEB服务器

  IDictionary<string,object>叫做环境变量,这个将要贯穿我们整个处理管道的集合里面存储了我们所需要的所有信息。而后面的Task,代表着管道的下一个结点,我们可以调用Invoke方法处理流程交给下一个结点。

  就是这么简单,在这套定义的帮助下,我们完全摆了上面提到了System.Web中的所有类,HttpApplication, HttpContext, HttpRequest, HttpResponse全部都不需要了。什么HttpModule, HttpHandler 这些玩意就让他们成为历史吧!

OWin环境变量都包含哪些?

  首先,环境变量是可以在生一个处理结点的时候随意添加的。其次OWin有定义一些必须的环境变量,因为没有这些是不能构成一个完整的Request的。

一不小心写了个WEB服务器

  为了让大家更好的理解我们上面所讲的自定义管道的概念,我们来做一个小小的demo。注意我们下面用的的所有类库是来自微软的另外一个开源项目Katana,我们说Owin只是一套定义,而Katana,则是微软对于Owin的一套实现。大家不要觉得Katana陌生,现在你用VS2013新建一个MVC5的项目都会自动引用相关的dll(Owin.dll, Microsoft.Owin.dll) ,也会自动添加Startup的配置类。 关于Katana的源码,大家可以到CodePlex上去下载。下面是对Katana项目结构的一个简单介绍:

一不小心写了个WEB服务器

  好了,知道了Katana的存在,我们就可以来看我们的Demo了,我们打算这样干:

  1. 建立一个空的MVC站点
  2. 从Nuget中添加Microsoft.Owin.Host.SystemWeb
  3. 添加Startup配置类

Microsoft.Owin.Host.SystemWeb

  这个dll可以让OWin接管IIS的请求,虽然同样是托管在IIS,但是所有的请求都会被OWin来处理。在OWin的4层结构中(Applicaton->Middleware->Server->Host),Microsoft.Owin.Host.SystemWeb属于Server层,还有一个同样也在Server层的是Microsoft.Owin.Host.HttpListener,这个可以实现利用控制台程序现实自托管,就可以完全摆脱IIS了。

Startup配置类

  要使用Owin的应用程序都要有一个叫Startup的类,在这个类里面有一个Configuration的方法,这两个名字是默认约定,必须用同样的名字才会被Owin找到。你也可以用Attribute和在web.config文件中配置的方式来定义这个类,详情见Startup。我们在Configuration方法里面,就可以定义我们自己的管道了。我们可以通过Use来添加自己的管道的处理步骤,并且可以自己设置处理顺序。

public void Configuration(IAppBuilder app)
{
app.Use(async (context, next) =>
{
await context.Response.WriteAsync(" Authentication... ");
await next();
}); app.Use(async (context, next) =>
{
await context.Response.WriteAsync("Authorization... ");
await next();
}); app.Use(async (context, next) =>
{
context.Response.ContentType = "text/plain";
await context.Response.WriteAsync("Hello World!");
});
}

  我们需要在web.config中加入一个配置,让OWin处理所有的请求:

  <appSettings>
<add key="owin:HandleAllRequests" value="true"/>
</appSettings>

  这样的话,不管我们输入什么URL,都会返回同样的结果,因为不管哪个URL,对应的都是我们上面所写的代码。一不小心写了个WEB服务器

用Microsoft.Owin.Host.HttpListener实现自寄宿

  上面的网站我们依旧是托管在IIS中的,但是我们今天的主题是摆脱IIS,所以接下来我们就来利用Owin的自托管功能。

  1. 新建一个控制台程序
  2. 拷贝我们上面建立的Startup类
  3. 用Nuget安装 Microsoft.Owin.Hosting 和 Microsoft.Owin.HttpListener

  我们需要在Main方法中加入下面的一段代码去启动我们的网站。

class Program
{
static void Main(string[] args)
{
using (WebApp.Start<Startup>(
new StartOptions(url: "http://localhost:7000")))
{
Console.ReadLine();
}
Console.ReadLine();
}
}

  按F5启动我们的控制台程序之后,我们就可以通过浏览器访问我们的7000端口了。

一不小心写了个WEB服务器

  当然,结果和我们Host在IIS上是一样的。

一切都在IDictionary<string,object>集合中

  当我们用控制台程序自寄宿的时候,没有IIS,没有System.Web,那么我们的Request信息和Response信息从何而来呢?

一不小心写了个WEB服务器

  首先,我们可以看到其实这里的Context,Reuqest, Response都已经不是原来的了,Katana自己有一些对应的类来封装了这些信息。但是就算是没有这些类,我们也可以很方便的拿到Request和Reponse,因为他们全部都在我们所讲的环境变量中。

一不小心写了个WEB服务器

  我们的Request Header, Url, Method等都被放到了这个环境变量的集合中,包括Response Header, Response Body, Response Status等同样也是。而这个环境变量会从一开始,一直到最后结束,在整个管道的每一步中我们都能够访问得到,并且可以添加和修改。就是这样最后得到一个Http Response返回给客户端的。

用Middleware来串成一个完整的管道

  其实我们上面的3个Use方法已经构成了一个完整的管道,但是不具有通用性,而且因为我们的Demo十分的简单,代码量少才允许我们那样写。但是在真正的开发过程中,我们要将Use中的代码转换成Middleware,打包成dll供其它项目使用。

  IAppBuilder 提供了一个Use的重载可以把一个Middleware作为泛型参数传进去来实现将这个Middleware注册进Owin的管道。下面模拟一下AuthenticationMiddleware和AuthorizationMiddleware的实现,我们可以直接从OwinMiddleware继承。

class AuthenticationMiddleware : OwinMiddleware
{
public AuthenticationMiddleware(OwinMiddleware next) : base(next) { } // 主要逻辑入口
public override async Task Invoke(IOwinContext context)
{
await context.Response.WriteAsync("Authentication....");
     // 如果你想在这里中断整个管道,下面这句话不调就可以了。
await Next.Invoke(context);
}
} class AuthorizationMiddleware : OwinMiddleware
{
public AuthorizationMiddleware(OwinMiddleware next) : base(next) { } // 主要逻辑入口
public override async Task Invoke(IOwinContext context)
{
await context.Response.WriteAsync("Authorization....");
await Next.Invoke(context);
}
}

 这里要注意的是,所有的Middleware构造函数都接收一个OwinMiddleware作为参数传给基类,基类会把它作为下一下Middleware,和我们上面用到的Next一样都是为了确定管道继续进行下去。那我们就可以用下面这种办法来注册我们的Middleware了。

public void Configuration(IAppBuilder app)
{
app.Use<AuthenticationMiddleware>(); app.Use<AuthorizationMiddleware>(); app.Use(async (context, next) =>
{
context.Response.ContentType = "text/plain";
await context.Response.WriteAsync("Hello World!");
});
}

  是不是比之前一堆的Use方法要简洁了很多?如果这还不够的话,我们还可以学习ASP.NET Identity Middleware以及WEB Api Owin Middleware的作法,为IAppBuilder添加扩展方法,这样调用都甚至都不需要知道我们Middleware的类名,只需要调用扩展方法就可以了,比如说Web Api的app.UseWebAPI()。

用Microsoft.Owin.StaticFiles来实现静态站点的托管

  我们可以接着上面的控制台程序继续添加代码,用Nuget下载Microsoft.Owin.StaticFiles,然后在Startup里面添加下面的代码。

一不小心写了个WEB服务器

同样,我们还是用控制台托管的方式:

一不小心写了个WEB服务器

就是这么几行代码,我们就用Owin实现了一个静态网站的的Web服务器了,因为我把站点的根目录指向了我们文章一开始那个站点的根目录,所以结果当然是一样的,但是请注意,我是换了端口的!

一不小心写了个WEB服务器

大功告成,但是为什么要前最前面那个Demo,因为Owin的Host就是用同样的方法实现的,只不过进行了一些封装而已,有兴趣的朋友也可以自己开载Katana的源码进行阅读,我后面也会继续写关于Owin的博客。

YY一下Owin的未来

   Owin(Open web interface for .NET)为了解放.NET而来,摆脱了.NET Framework的束缚,摆脱了IIS的束缚,ASP.NET才可以跑得越来越快。.NET的世界会越来越精彩,我们已经看到Web API可以用Owin来托管,SignalR也可以用Owin来托管,静态文件同样用Owin来托管,再加上Owin这种开放式的,可插拔式的设计,最后还是开源的,我相信会有越来越多的Framework加入到Owin中来。我们文中看到Owin已经是可以实现动态生成Reponse,那我们可以大胆猜测一下,ASP.NET MVC会不会加入到Owin中来,那么这样的话ASP.NET MVC也可以托管在Owin上了,同时ASP.NET Team也表示,Owin很快就会支持MONO !那ASP.NET 是不是可以跨平台了(当然现在也可以),但是有了Owin这样一个框架在这里面以后,一切都会变得更容易一些!所以小伙伴们要Hold住了,小纳不是说了么,对开发者好,为什么不去做呢? 那就做吧!