自己動手實現網路伺服器(Web Server)——基於C#

来源:http://www.cnblogs.com/deali/archive/2017/10/16/7676484.html
-Advertisement-
Play Games

前言 最近在學習網路原理,突然萌發出自己實現一個網路伺服器的想法,並且由於第三代小白機器人的開發需要,我把之前使用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.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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前面我們針對 SVG 的解析和繪製做了介紹,SVG 是圖片的一種形式,而另一種很重要的圖片是:點陣圖,包括 png、jpeg、bmp 等格式。點陣圖的基本規則是,組成的基本元素是像素點,由寬度 * 高度個像素組成,每個像素存儲了一個點的顏色和位置信息,顏色信息可以是 ARGB、RGBA、BGR 或 YU ...
  • http://www.cnblogs.com/fengxuehuanlin/p/5631664.html 關於xml是屬於一個比較重要的東西,在平時開發的過程中,這塊內容最主要的是要掌握XML內容的讀取和寫入操作。 xml可作為小型資料庫用來存儲數據。 html主要用來顯示數據,XAML前臺設計。 ...
  • 方法過濾器 使用 和註解屬性 實現 使用方式 1. 自定義方法過濾器 可分別定義方法 執行前過濾器 , 方法 執行結束過濾器 , 方法 異常過濾器 執行前過濾器繼承 抽象類, 實現 抽象方法, 參數 為運行時攔截方法的參數列表 /// /// 自定義執行前過濾器 /// public class C ...
  • 一些文章: 反射插件插件 http://bbs.csdn.net/topics/391950257?page=1 反射窗體 http://www.sufeinet.com/thread-2984-1-1.html http://www.cnblogs.com/mumupudding/p/460740 ...
  • 1.複習泛型集合List<T>Dictionary<Tkey,Tvalue>裝箱和拆箱裝箱:把值類型轉換為引用類型拆箱:把引用類型轉換為值類型 我們應該儘量避免在代碼中發生裝箱或者拆箱文件流FileStream StreamReader和StreamWriter多態:虛方法、抽象類、介面虛方法:抽象 ...
  • 解決 Operation is not supported on this platform 異常 直接上代碼: public class RSAHelper { /// <summary> /// 私鑰簽名 /// </summary> /// <param name="signStr"></pa ...
  • 問題:sqlite3使用ef框架操作資料庫報錯 問題原因:資料庫文件沒有訪問許可權 結局方案:可以將資料庫文件所在的文件夾的訪問許可權添加Everyone用戶許可權。 錯誤:"System.Data.Entity.Core.EntityException: 在提供程式連接上啟動事務時出錯。有關詳細信息,請 ...
  • 在MVC中,controller中的Action和View中的.cshtml文件名稱有一個對應的關係。 當不對應時,有以下幾種情況發生: 一、找不到視圖的錯誤 請求URL:http://localhost:13850/Customer/Create controller中有對應的Action: Vi ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...