前言 最近在學習網路原理,突然萌發出自己實現一個網路伺服器的想法,並且由於第三代小白機器人的開發需要,我把之前使用python、PHP寫的那部分代碼都遷移到了C#(別問我為什麼這麼喜歡C#),之前使用PHP就是用來處理網路請求的,現在遷移到C#了,而Linux系統上並沒有IIS伺服器,自然不能使用A ...
前言
最近在學習網路原理,突然萌發出自己實現一個網路伺服器的想法,並且由於第三代小白機器人的開發需要,我把之前使用python、PHP寫的那部分代碼都遷移到了C#(別問我為什麼這麼喜歡C#),之前使用PHP就是用來處理網路請求的,現在遷移到C#了,而Linux系統上並沒有IIS伺服器,自然不能使用ASP.Net,所以這個時候自己實現一個功能簡單的網路伺服器就恰到好處地解決這些問題了。
基本原理
Web Server在一個B/S架構系統中起到的作用不僅多而且相當重要,Web開發者大部分時候並不需要瞭解它的詳細工作機制。雖然不同的Web Server可能功能並不完全一樣,但是以下三個功能幾乎是所有Web Server必須具備的:
- 接收來自瀏覽器端的HTTP請求
- 將請求轉發給指定Web站點程式(後者由Web開發者編寫,負責處理請求)
- 向瀏覽器發送請求處理結果
下圖顯示Web Server在整個Web架構系統中所處的重要位置:
如上圖,Web Server起到了一個“承上啟下”的作用(雖然並沒有“上下”之分),它負責連接用戶和Web站點。
每個網站就像一個個“插件”,只要網站開發過程中遵循了Web Server提出的規則,那麼該網站就可以“插”在Web Server上,我們便可以通過瀏覽器訪問網站。
太長不看版原理
瀏覽器想要拿到哪個文件(html、css、js、image)就和伺服器發請求信息說我要這個文件,然後伺服器檢查請求合不合法,如果合法就把文件數據傳回給瀏覽器,這樣瀏覽器就可以把網站顯示出來了。(一個網站一般會包含n多個文件)
話不多說,直接上代碼
在C#中有兩種方法可以簡單實現Web伺服器,分別是直接使用Socket和使用封裝好的HttpListener。
因為後者比較方便一些,所以我選擇使用後者。
這是最簡單的實現一個網路伺服器,可以處理瀏覽器發過來的請求,然後將指定的字元串內容返回。
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