本身動手實現網絡服務器(Web Server)——基於C#

  

前言

最近在學習網絡原理,忽然萌發出本身實現一個網絡服務器的想法,而且因爲第三代小白機器人的開發須要,我把以前使用python、PHP寫的那部分代碼都遷移到了C#(別問我爲何這麼喜歡C#),以前使用PHP就是用來處理網絡請求的,如今遷移到C#了,而Linux系統上並無IIS服務器,天然不能使用ASP.Net,因此這個時候本身實現一個功能簡單的網絡服務器就恰到好處地解決這些問題了。javascript

基本原理

Web Server在一個B/S架構系統中起到的做用不只多並且至關重要,Web開發者大部分時候並不須要瞭解它的詳細工做機制。雖然不一樣的Web Server可能功能並不徹底同樣,可是如下三個功能幾乎是全部Web Server必須具有的:php

  • 接收來自瀏覽器端的HTTP請求
  • 將請求轉發給指定Web站點程序(後者由Web開發者編寫,負責處理請求)
  • 向瀏覽器發送請求處理結果

下圖顯示Web Server在整個Web架構系統中所處的重要位置:css

如上圖,Web Server起到了一個「承上啓下」的做用(雖然並無「上下」之分),它負責鏈接用戶和Web站點。html

每一個網站就像一個個「插件」,只要網站開發過程當中遵循了Web Server提出的規則,那麼該網站就能夠「插」在Web Server上,咱們即可以經過瀏覽器訪問網站。前端

太長不看版原理

瀏覽器想要拿到哪一個文件(html、css、js、image)就和服務器發請求信息說我要這個文件,而後服務器檢查請求合不合法,若是合法就把文件數據傳回給瀏覽器,這樣瀏覽器就能夠把網站顯示出來了。(一個網站通常會包含n多個文件)java

話很少說,直接上代碼

在C#中有兩種方法能夠簡單實現Web服務器,分別是直接使用Socket和使用封裝好的HttpListener。python

由於後者比較方便一些,因此我選擇使用後者。web

這是最簡單的實現一個網絡服務器,能夠處理瀏覽器發過來的請求,而後將指定的字符串內容返回。json

class Program { static void Main(string[] args) { string port = "8080"; HttpListener httpListener = new HttpListener(); httpListener.Prefixes.Add(string.Format("http://+:{0}/", port)); httpListener.Start(); httpListener.BeginGetContext(new AsyncCallback(GetContext), httpListener); //開始異步接收request請求 Console.WriteLine("監聽端口:" + port); Console.Read(); } static void GetContext(IAsyncResult ar) { HttpListener httpListener = ar.AsyncState as HttpListener; HttpListenerContext context = httpListener.EndGetContext(ar); //接收到的請求context(一個環境封裝體) httpListener.BeginGetContext(new AsyncCallback(GetContext), httpListener); //開始 第二次 異步接收request請求 HttpListenerRequest request = context.Request; //接收的request數據 HttpListenerResponse response = context.Response; //用來向客戶端發送回覆 response.ContentType = "html"; response.ContentEncoding = Encoding.UTF8; using (Stream output = response.OutputStream) //發送回覆 { byte[] buffer = Encoding.UTF8.GetBytes("要返回的內容"); output.Write(buffer, 0, buffer.Length); } } } 

這個簡單的代碼已經能夠實現用於小白機器人的網絡請求處理了,由於大體只用到GET和POST兩種HTTP方法,只須要在GetContext方法裏判斷GET、POST方法,而後分別給出響應就能夠了。瀏覽器

可是咱們的目的是開發一個真正的網絡服務器,固然不能只知足於這樣一個專用的服務器,咱們要的是能夠提供網頁服務的服務器。

那就繼續吧。

根據個人研究,提供網頁訪問服務的服務器作起來確實有一點麻煩,由於須要處理的東西不少。須要根據瀏覽器請求的不一樣文件給出不一樣響應,處理Cookies,還要處理編碼,還有各類出錯的處理。

首先咱們要肯定一下咱們的服務器要提供哪些文件的訪問服務。

這裏我用一個字典結構來保存。

/// <summary> /// MIME類型 /// </summary> public Dictionary<string, string> MIME_Type = new Dictionary<string, string>() { { "htm", "text/html" }, { "html", "text/html" }, { "php", "text/html" }, { "xml", "text/xml" }, { "json", "application/json" }, { "txt", "text/plain" }, { "js", "application/x-javascript" }, { "css", "text/css" }, { "bmp", "image/bmp" }, { "ico", "image/ico" }, { "png", "image/png" }, { "gif", "image/gif" }, { "jpg", "image/jpeg" }, { "jpeg", "image/jpeg" }, { "webp", "image/webp" }, { "zip", "application/zip"}, { "*", "*/*" } }; 

劇透一下:其中有PHP類型是咱們後面要使用CGI接入的方式使咱們的服務器支持PHP。

我在QFramework中封裝了一個QHttpWebServer模塊,這是其中的啓動代碼。

/// <summary> /// 啓動本地網頁服務器 /// </summary> /// <param name="webroot">網站根目錄</param> /// <returns></returns> public bool Start(string webroot) { //觸發事件 if (OnServerStart != null) OnServerStart(httpListener); WebRoot = webroot; try { //監聽端口 httpListener.Prefixes.Add("http://+:" + port.ToString() + "/"); httpListener.Start(); httpListener.BeginGetContext(new AsyncCallback(onWebResponse), httpListener); //開始異步接收request請求 } catch (Exception ex) { Qdb.Error(ex.Message, QDebugErrorType.Error, "Start"); return false; } return true; } 

如今把網頁服務器的核心處理代碼貼出來。

這個代碼只是作了基本的處理,對於網站的主頁只作了html後綴的識別。

後來我在QFramework中封裝的模塊作了更多的細節處理。

/// <summary> /// 網頁服務器相應處理 /// </summary> /// <param name="ar"></param> private void onWebResponse(IAsyncResult ar) { byte[] responseByte = null; //響應數據 HttpListener httpListener = ar.AsyncState as HttpListener; HttpListenerContext context = httpListener.EndGetContext(ar); //接收到的請求context(一個環境封裝體) httpListener.BeginGetContext(new AsyncCallback(onWebResponse), httpListener); //開始 第二次 異步接收request請求 //觸發事件 if (OnGetRawContext != null) OnGetRawContext(context); HttpListenerRequest request = context.Request; //接收的request數據 HttpListenerResponse response = context.Response; //用來向客戶端發送回覆 //觸發事件 if (OnGetRequest != null) OnGetRequest(request, response); if (rawUrl == "" || rawUrl == "/") //單純輸入域名或主機IP地址 fileName = WebRoot + @"\index.html"; else if (rawUrl.IndexOf('.') == -1) //不帶擴展名,理解爲文件夾 fileName = WebRoot + @"\" + rawUrl.SubString(1) + @"\index.html"; else { int fileNameEnd = rawUrl.IndexOf('?'); if (fileNameEnd > -1) fileName = rawUrl.Substring(1, fileNameEnd - 1); fileName = WebRoot + @"\" + rawUrl.Substring(1); } //處理請求文件名的後綴 string fileExt = Path.GetExtension(fileName).Substring(1); if (!File.Exists(fileName)) { responseByte = Encoding.UTF8.GetBytes("404 Not Found!"); response.StatusCode = (int)HttpStatusCode.NotFound; } else { try { responseByte = File.ReadAllBytes(fileName); response.StatusCode = (int)HttpStatusCode.OK; } catch (Exception ex) { Qdb.Error(ex.Message, QDebugErrorType.Error, "onWebResponse"); response.StatusCode = (int)HttpStatusCode.InternalServerError; } } if (MIME_Type.ContainsKey(fileExt)) response.ContentType = MIME_Type[fileExt]; else response.ContentType = MIME_Type["*"]; response.Cookies = request.Cookies; //處理Cookies response.ContentEncoding = Encoding.UTF8; using (Stream output = response.OutputStream) //發送回覆 { try { output.Write(responseByte, 0, responseByte.Length); } catch (Exception ex) { Qdb.Error(ex.Message, QDebugErrorType.Error, "onWebResponse"); response.StatusCode = (int)HttpStatusCode.InternalServerError; } } } 

這樣就能夠提供基本的網頁訪問了,通過測試,使用Bootstrap,Pure等前端框架的網頁均可以完美訪問,性能方面通常般。(在QFramework的封裝中我作了一點性能優化,有一點提高)我以爲要在性能方面作提高仍是要在多線程處理這方面作優化,因爲篇幅關係,就不把多線程版本的代碼貼出來了。

接下來咱們還要實現服務器的PHP支持。

首先定義兩個字段。

/// <summary> /// 是否開啓PHP功能 /// </summary> public bool PHP_CGI_Enabled = true; /// <summary> /// PHP執行文件路徑 /// </summary> public string PHP_CGI_Path = "php-cgi"; 

接下來在網頁服務的核心代碼裏作PHP支持的處理。

//PHP處理 string phpCgiOutput = ""; Action phpProc = new Action(() => { try { string argStr = ""; if (request.HttpMethod == "GET") { if (rawUrl.IndexOf('?') > -1) argStr = rawUrl.Substring(rawUrl.IndexOf('?')); } else if (request.HttpMethod == "POST") { using (StreamReader reader = new StreamReader(request.InputStream)) { argStr = reader.ReadToEnd(); } } Process p = new Process(); p.StartInfo.CreateNoWindow = false; //不顯示窗口 p.StartInfo.RedirectStandardOutput = true; //重定向輸出 p.StartInfo.RedirectStandardInput = false; //重定向輸入 p.StartInfo.UseShellExecute = false; //是否指定操做系統外殼進程啓動程序 p.StartInfo.FileName = PHP_CGI_Path; p.StartInfo.Arguments = string.Format("-q -f {0} {1}", fileName, argStr); p.Start(); StreamReader sr = p.StandardOutput; while (!sr.EndOfStream) { phpCgiOutput += sr.ReadLine() + Environment.NewLine; } responseByte = sr.CurrentEncoding.GetBytes(phpCgiOutput); } catch (Exception ex) { Qdb.Error(ex.Message, QDebugErrorType.Error, "onWebResponse->phpProc"); response.StatusCode = (int)HttpStatusCode.InternalServerError; } }); if (fileExt == "php" && PHP_CGI_Enabled) { phpProc(); } else { if (!File.Exists(fileName)) { responseByte = Encoding.UTF8.GetBytes("404 Not Found!"); response.StatusCode = (int)HttpStatusCode.NotFound; } else { try { responseByte = File.ReadAllBytes(fileName); response.StatusCode = (int)HttpStatusCode.OK; } catch (Exception ex) { Qdb.Error(ex.Message, QDebugErrorType.Error, "onWebResponse"); response.StatusCode = (int)HttpStatusCode.InternalServerError; } } } 

這樣就實現了基於PHP-CGI的PHP支持了,通過測試,基本的php頁面均可以支持,可是須要使用curl和xml這類擴展的暫時還沒辦法。須要作更多的工做。

接下來我會給服務器作一個GUI界面,供你們測試。

同時也會把QFramework框架發佈,有興趣的可使用基於QFramework的服務器封裝。


博客原文地址:http://blog.deali.cn/?p=875

個人微信公衆號:DealiAxy

相關文章
相關標籤/搜索