若是你想得到更好的閱讀體驗,能夠前往我在 github 上的博客進行閱讀,http://lcomplete.github.io/blog/2013/07/16/use-csharp-write-aspnet-web-server/。html
你是否有過這樣的需求——想運行 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\ 這個目錄裏面找找看,這個程序的使用方式以下。git
WebDev.WebServer.EXE /port:80 /path:"c:\mysite" /vpath:"/"
怎麼樣?不錯吧,垂手可得地就解決了文章開頭所說的問題了。固然這並非本篇文章的重點,若是你不知足於只知道這個用法,那能夠繼續往下閱讀,接下來,咱們將使用 C# 編寫一個支持 ASP.NET 的 Web 服務器,看看這一切到底是如何運做的。github
C# 中有着許多豐富的類庫,使用不一樣的類庫,咱們能夠站在不一樣的抽象層級去編寫一個 Web 服務器,好比在 System.Net 命名空間下提供了一個 HttpListener 類,使用這個類,咱們能夠很容易地建立一個簡單的 Web 服務器,可是這個類隱藏了不少實現的細節,爲了不知其然不知其因此然,咱們將使用網絡框架最底層的 Socket 類來編寫這個程序。web
預備知識
正式編寫這個程序以前,讓咱們先來了解一些基礎知識。編寫一個 Web Server,必須要了解 HTTP 協議,它是萬維網的基礎,位於 TCP/IP 協議棧的應用層。跨域
-
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 架構、管道機制、應用程序生命週期和宿主環境。app
-
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();//確保鏈接關閉 } }
處理的步驟以下:
- 解析請求數據,從創建的 socket 鏈接處獲取請求數據,將其解析爲RequestInfo對象。
- 判斷請求是否有效,無效則響應 400 錯誤,有效則進行下一步處理。
- 判斷請求的是否爲靜態內容,是則輸出文件響應。
- 判斷請求是否爲目錄,是則輸出目錄下的子文件夾和文件的連接,與 IIS 目錄服務相似。
- 不爲靜態內容和目錄時,則交給 host 對象處理(使用ASP.NET HTTP 運行時進行處理)。
- 處理完後確保鏈接關閉。
其中輸出響應是構造狀態行、響應報頭和響應正文,接着經過 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 該地址查看全部代碼。