開場
Web服務器是啥玩意? 是那個托管了我的網站的機器么? No,雖然那個也是服務器,但是我們今天要說的Web服務器主要是指像IIS這樣一類的,用於處理request並返回response的工具,沒錯我們可以說它是一個工具,不就是一個應用程序嗎?誰不會寫應用程序呀,等着,三分鍾就搞一個出來。
Web Server的介紹
我們先來看一下web server主要干什么?
這圖很熟悉么?我是直接從小坦克的那篇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
{
}
}

接下來,我放了一個簡單的index.html和一個images文件夾在我們應用程序的bin目錄下,然后按F5啟動這個控制台應用程序,最后輸入我們的http://192.168.1.100/index.html,你們將會看到:

怎么樣?有圖有真相,我們這個小小的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運行時的序列圖:

但是今天我們不是講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,並且要按照指定順序來執行怎么辦?

我們並不能改變以上管道中每一個結點中的執行順序,而我們自定義的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文件里面移除的。但是大多數情況下,程序員們並不會想到去移除他們,這其實是一個性能上的損失。

當然我們並不能因為這一些問題就否認IIS,就算是ASP.NET在當初設計的時候也是被認為它就是要被托管在IIS上的。但是它又不具有很好擴展性,同時ASP.NET也是時候要考慮開放了,特別是在Node.js以及一些開源前端MVVM框架的影響下,Web后端開發有逐漸要被取代的趨勢,所以OWin來了,它為了解決這些問題而來,一切都還是我們所熟悉的,但是卻給了我們更靈活的開發方式。
隨心所欲-建立你自己的管道
我們上篇有說到OWin只是一套定義,它本身不具備任何代碼。它主要定義了服務器在處理resquest所需要的一些信息(大多都是http協議里面要求的),和一個應用程序代理。

IDictionary<string,object>叫做環境變量,這個將要貫穿我們整個處理管道的集合里面存儲了我們所需要的所有信息。而后面的Task,代表着管道的下一個結點,我們可以調用Invoke方法處理流程交給下一個結點。
就是這么簡單,在這套定義的幫助下,我們完全擺了上面提到了System.Web中的所有類,HttpApplication, HttpContext, HttpRequest, HttpResponse全部都不需要了。什么HttpModule, HttpHandler 這些玩意就讓他們成為歷史吧!
OWin環境變量都包含哪些?
首先,環境變量是可以在生一個處理結點的時候隨意添加的。其次OWin有定義一些必須的環境變量,因為沒有這些是不能構成一個完整的Request的。

為了讓大家更好的理解我們上面所講的自定義管道的概念,我們來做一個小小的demo。注意我們下面用的的所有類庫是來自微軟的另外一個開源項目Katana,我們說Owin只是一套定義,而Katana,則是微軟對於Owin的一套實現。大家不要覺得Katana陌生,現在你用VS2013新建一個MVC5的項目都會自動引用相關的dll(Owin.dll, Microsoft.Owin.dll) ,也會自動添加Startup的配置類。 關於Katana的源碼,大家可以到CodePlex上去下載。下面是對Katana項目結構的一個簡單介紹:

好了,知道了Katana的存在,我們就可以來看我們的Demo了,我們打算這樣干:
- 建立一個空的MVC站點
- 從Nuget中添加Microsoft.Owin.Host.SystemWeb
- 添加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,對應的都是我們上面所寫的代碼。
用Microsoft.Owin.Host.HttpListener實現自寄宿
上面的網站我們依舊是托管在IIS中的,但是我們今天的主題是擺脫IIS,所以接下來我們就來利用Owin的自托管功能。
- 新建一個控制台程序
- 拷貝我們上面建立的Startup類
- 用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端口了。

當然,結果和我們Host在IIS上是一樣的。
一切都在IDictionary<string,object>集合中
當我們用控制台程序自寄宿的時候,沒有IIS,沒有System.Web,那么我們的Request信息和Response信息從何而來呢?

首先,我們可以看到其實這里的Context,Reuqest, Response都已經不是原來的了,Katana自己有一些對應的類來封裝了這些信息。但是就算是沒有這些類,我們也可以很方便的拿到Request和Reponse,因為他們全部都在我們所講的環境變量中。

我們的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里面添加下面的代碼。

同樣,我們還是用控制台托管的方式:

就是這么幾行代碼,我們就用Owin實現了一個靜態網站的的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住了,小納不是說了么,對開發者好,為什么不去做呢? 那就做吧!
