Socket寫的Web服務器——帶詳細圖解


——閑扯:  

    Socket是大家都很熟悉的.NET處理底層硬件通信的類。比如:物聯網中的一個器件要與其他器件相通信,那就必須使用到Socket來實現。但是我對Socket的中文翻譯很不滿意:Socket的中文翻譯是“套接字”。我請問一下各位讀者朋友,我如果只告訴你“套接字”你會知道這是什么嗎? Socket的英文含義是:“插座、開關”,但你能通過“套接字”知道Socket的原意嗎?

       Socket就像一根電話線,連接通兩端的電話。讓電話可以實現通信。我們聲明一個Socket對象從實例開始監聽的那一刻開始,Socket就像一個電話插座一樣,隨時監聽等待消息的傳入,而我們建立連接就像把插頭插在這個插座上一樣,一插即可通訊。效果和寓意正如英文的原意:插座、開關相符。      

       很多的外國技術文獻翻譯過來很難讓人想象到它原本的意思,這是最失敗的地方。而且直接音譯的“套接字”也很難跟讀音['sɑːkɪt]的Socket聯系起來,反而更像讀音['tɑːɡɪt]的target.很多晦澀難懂的專業技術名詞,你只要查看其英文原意,往往都會恍然大悟、醍醐灌頂。我不知道“前輩”們為何會這樣翻譯,我以為一個東西的翻譯可以有更好的選擇,最起碼不能翻譯的太偏、太晦澀,以至於我們這些后來人很難接受。

       我認為Socket譯為“通信插座”更為恰當。我們設置一個Socket對象的實例開始監聽,就像設置一個電話插座在那一樣,誰撥我這個“IP地址和端口”,我就接通誰。我覺得Socket翻譯成“套接字”相對於林語堂大師翻譯的“humor:幽默,sofa:沙發”相比,太讓人無法接受了。

      總結:我推薦大家盡量去讀英文原文的技術資料,去英文編程的技術網站和論壇去看。本人英語6級,雖然沒有考過托福、雅思之類的,但是感覺看懂這些英文資料還是比較容易。 這或許受益於本人考研究生時對英語系統的復習,英語幾乎每一個單詞都有它的來歷,‘漢字靠形造詞,英語靠音造詞’這是導致東西方文化、思想的區別的根源,也是我對學習英語最深的體會。

——正文:

  我們用過了IIS服務器,也了解了IIS服務器的實現原理和機制(讀者如果不清楚,可以跟着我寫完這個模擬的服務器,相信你就會明白了)。那么我們能不能手寫一個類似於IIS的Web服務器呢?注意哦!我們這里寫的是web服務器,而服務器有多種:FTP服務器(文件服務器)、POP3服務器(郵箱服務器)等,不過我想底層也應該大同小異.

 開始:

1、首先新建一個空白的解決方案,命名為WebServer.注意圖中紅色箭頭的說明。

 

 2、在解決方案中添加一個WinForm應用程序,命名為“WebServer”,新建一個Winform窗體,並將窗體重命名為:"ServerForm".

3、拖動控件,進行如下布局:

4、對控件進行重命名操作:參考如圖中所示。(希望讀者養成規范的、良好的重命名的習慣)

5、布局完畢,剩下就是寫程序了。寫程序之前,我們需要先分析一下我們寫Web服務器的思路

我們的思路:
(1)、先建立一個負責監聽的“電話插座”——Socket,這個“電話插座”以指定的“IP地址和端口”作為“電話號碼”,隨時等待接通每一個撥打此“號碼”(連接到此IP和端口)的人(在這里是程序進程)的電話。
(2)、因為我們當前的電話插座需要處理很多通信,所以每接通一個"電話"(接收到連接到該IP和端口的請求),我們就復制一個“電話插座”單獨為該“電話”服務。(在這里我們會用到多線程的知識。 )
(3)、電話撥通了,但是我們需要懂雙方的語言。也就是雙方需要說同一門語言,或最起碼有一個共同的互相都能懂得的語言約定。這就是HTTP協議。那么我們的瀏覽器和服務器之間的HTTP協議是什么樣子的呢?往下看。

6、HTTP的協議分為:請求報文協議和響應報文協議。而無論是請求報文還是響應報文,其標准格式都是:頭(header)、體(content).如:請求頭,請求體;響應頭,響應體。    

    (1)、下面來看一下我們的請求協議的報文是什么樣子的:我們熟知的網頁對服務器的請求分為get請求和post請求。

     a、get請求圖(沒有“請求體”): (那么get請求的請求體到哪里去了呢?請讀者思考一下,相信很容易就想出答案)

     

    b、post請求圖(請求頭和請求體都有):請注意請求頭和請求體之間的空行。這是HTTP協議請求報文的約定。

    

    (2)、下面讓我們來看一下響應協議的報文是什么樣子的

     

7、了解了請求協議的報文和響應協議的報文整體格式之后,我們需要進一步分析里面的“有用”的內容。回顧上面的請求報文圖我們發現:

在第一行中包含了,請求方法、請求資源地址。

     

     好了我們拿到對方請求的報文之后,就可以截取這些“有用”的內容(注意:這里並不是說其他內容沒有用,我們只是模擬Web服務器的主要功能),將響應的請求資源,以“響應協議報文”的格式,發送過去。這樣瀏覽器也就會自動解讀你發送的數據,我們的Web服務器也就實現了!

 

8、源代碼開始了:

首先是ServerForm窗體的代碼:

//*************************************************************************
//
//File Name:            ServerForm.cs
//
//Tables:               Nothing               
//
//Author:               GuoHenghai
//
//Create Date:          6/08/2013
//
//*************************************************************************
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Windows.Forms;

namespace WebServer
{
    public partial class ServerForm : Form
    {
        public ServerForm()
        {
            InitializeComponent();
            CheckForIllegalCrossThreadCalls = false;
        }
        private void btnStart_Click(object sender, EventArgs e)
        {
            // 第一步,設置頂級的監聽端口的Socket對象
            Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            // 准備Socket綁定方法的參數對象IPEndPoint
            IPAddress ipAddress;
 
            if (!IPAddress.TryParse(txtIP.Text.Trim(), out ipAddress)) // 判斷當前的IP地址欄數據是否可正常轉換為IP地址
            {
                return;
            }
            int port;
            if (!int.TryParse(txtPort.Text.Trim(), out port))// 判斷當前的Port是否能轉換為數字
            {
                return;
            }

            IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, port);
            // 開始頂級Socket的綁定和監聽
            try
            {
                serverSocket.Bind(ipEndPoint);
                serverSocket.Listen(10);
                SetLogText("服務器已經開啟...");
                // 設置線程,進行連接Socket對象的處理
                Thread thread=new Thread(Listen);
                thread.IsBackground = true;// 必須設置成為后線程,后台線程在窗體關閉的時候,會自動結束自己線程運行
                thread.Start(serverSocket);// 將監聽的的頂級Socket對象作為參數傳入線程委托中的函數里面去
            }
            catch (Exception ex)
            {
                // 捕獲到異常 
                SetLogText("服務器已經開啟,您無需重復開啟!");
                SetLogText("  >詳細信息:\r\n   "+ex.Message);
            }
        }

        // 設置處理每一次監聽到的連接的方法
        private void Listen(object o)
        {
            Socket serverSocket = o as Socket;
            while (true)
            {
                // 將服務監聽到的連接,轉換成一個Socket對象,后面將使用該連接的Socket進行HTTP請求的接收和響應的處理。
                Socket connSocket = serverSocket.Accept();
                SetLogText(connSocket.RemoteEndPoint+":已建立連接!");
                // 嘗試進行HTTP請求的接收和處理
                try
                {
                    // 聲明接收HTTP請求的二進制字節數組
                    // 將接收到的二進制字節存放到聲明的二進制字節數組中去
                    byte[] buffer=new byte[1024*1024];
                    int realLen = connSocket.Receive(buffer);

                    // 如果接收到的HTTP請求是空的,則關閉當前連接的Socket對象,返回進行下一次連接的監聽。
                    if (realLen <= 0)
                    {
                        // 禮貌地關閉該連接Socket對象
                        connSocket.Shutdown(SocketShutdown.Both);
                        connSocket.Close();
                        SetLogText(connSocket.RemoteEndPoint + ":0字節請求,當前連接已關閉!");
                        return;
                    }

                    // 如果接收到的HTTP請求是正常的,則進行HTTP請求報文的分析,並生成HTTP響應報文
                    string content = Encoding.UTF8.GetString(buffer,0,realLen); // 讀取HTTP請求報文
                    SetLogText(content);// 將該請求報文記錄到服務器日志中
                    // 將有用的報文信息轉換成Request(請求)對象;
                    Request request=new Request(content);
                    // 分析請求報文,進行HTTP響應處理
                    RequestStaticOrDynamicPage(request.RawUrl,connSocket);
                }
                catch (Exception)
                { 
                    // 提示異常的發生,並跳出死循環
                    SetLogText("當前連接發生異常,請重啟服務!");
                    // 一旦接收異常,關閉此次連接的Socket
                    connSocket.Close();
                   
                    break;
                }
            }
        }

        /// <summary>
        /// 判斷請求的是動態頁面還是靜態頁面,並分別針對,進行HTTP響應處理
        /// </summary>
        /// <param name="rawUrl"></param>
        /// <param name="connsocket"></param>
        private void RequestStaticOrDynamicPage(string rawUrl, Socket connsocket)
        {
            // 根據請求文件的后綴名進行判斷
            string ext = Path.GetExtension(rawUrl);
            switch (ext)
            {
                case ".aspx":
                case ".asp":
                case ".php":
                case ".jsp":
                    // 動態頁面的處理 (挖坑,讀者自己來把這里補充完整)
                    break;
                default:
                    // 靜態頁面的處理
                    ProcessStaticPageRequest(rawUrl,connsocket);
                    break;                    
            }
        }

        /// <summary>
        /// 處理HTTP的靜態頁面請求
        /// </summary>
        /// <param name="rawUrl"></param>
        /// <param name="connsocket"></param>
        private void ProcessStaticPageRequest(string rawUrl,Socket connsocket)
        {
            // 拼接物理路徑的字符串,檢測當前物理路徑的文件是否存在
            // 注意 Path.Combine()方法中,第二個開始以后的參數,開頭的 / 要去掉,否則拼接出來的路徑將從后面的
            // 以 / 的字符串開始進行拼接,也就是忽略掉, / 前面的拼接路徑字符串
            rawUrl = rawUrl.TrimStart('/');
            string physicalPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory,"web",rawUrl);

            // 進行檢測當前請求的文件是否存在
            if (File.Exists(physicalPath))
            {
               // 文件存在,讀取到文件流中,拼接到HTTP響應對象——Response中的“響應報文”中的響應體中。
                using (FileStream fs=new FileStream(physicalPath,FileMode.Open))
                {
                    // 聲明存儲文件流的二進制字節數組
                    // 將文件流讀取到聲明好的二進制字節數組中去
                    byte[] buffer=new byte[fs.Length];
                    fs.Read(buffer, 0, buffer.Length);

                    // 准備發送響應報文
                    string ext = Path.GetExtension(rawUrl);
                    Response response=new Response(200,buffer,ext);
                    // 發送響應報文,關閉當前Socket連接,注意在這里體現了HTTP協議的無狀態根本原因
                    connsocket.Send(response.GetResponse());
SetLogText(connsocket.RemoteEndPoint+":已關閉連接.");
                    connsocket.Close();
                    
                }
            }
            else
            {
                // 404 頁面不存在處理
                // 埋坑,讀者可以在這里設置一個專門提示的頁面,提示用戶當前訪問資源不存在
            }
        }

        /// <summary>
        /// 設置日志文本框的記錄方法
        /// </summary>
        /// <param name="msg"></param>

        private void SetLogText(string msg)
        {
            txtLog.AppendText(msg + "\r\n");
        }
    }
}

Request對象的代碼:

//*************************************************************************
//
//File Name:            Request.cs
//
//Tables:               Nothing               
//
//Author:               GuoHenghai
//
//Create Date:          6/08/2013
//
//*************************************************************************
using System;

namespace WebServer
{
    class Request
    {
        #region 私有屬性
        private string _rawUrl;
        private string _method;

        public string RawUrl
        {
            get { return _rawUrl; }
            set { _rawUrl = value; }
        }

        public string Method
        {
            get { return _method; }
            set { _method = value; }
        } 
        #endregion
        #region 構造函數-屬性初始化器

        public Request(string content)
        {
            // 按行分解請求報文
            string[] lines = content.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
            // 按空格分解請求報文中的第一行,並初始化該對象的兩個屬性
            this.Method = lines[0].Split(' ')[0];
            this.RawUrl = lines[0].Split(' ')[1];
        }

        #endregion
    }
}

Response對象的代碼:

using System.Collections.Generic;
using System.Text;

//*************************************************************************
//
//File Name:            Response.cs
//
//Tables:               Nothing               
//
//Author:               GuoHenghai
//
//Create Date:          6/08/2013
//
//*************************************************************************
namespace WebServer
{
    class Response
    {

        #region 私有字段、屬性
        private int _codeStatus;
        private int _contentLength;
        private string _contentType;
        private byte[] _buffer;

        public int CodeStatus
        {
            get { return _codeStatus; }
            set { _codeStatus = value; }
        }

        public int ContentLength
        {
            get { return _contentLength; }
            set { _contentLength = value; }
        }

        public string ContentType
        {
            get { return _contentType; }
            set { _contentType = value; }
        }

        public byte[] Buffer
        {
            get { return _buffer; }
            set { _buffer = value; }
        }
        #endregion

        #region 構造函數——屬性初始化器

        public Response(int codeStatus,byte[] buffer,string ext)
        {
            FillCodeStaDic();
            this.Buffer = buffer;
            this.CodeStatus = codeStatus;
            this.ContentLength = buffer.Length;
            GetContentType(ext);
        }
        Dictionary<int,string> codeStatusDic=new Dictionary<int, string>();

        /// <summary>
        /// 填充狀態碼 字典 
        /// </summary>
        private void FillCodeStaDic()
        {
            codeStatusDic[200] = "OK";
            codeStatusDic[404] = "請求頁面不存在!";
            //...挖坑,讀者可以在這里進行詳細的補充
        }

        /// <summary>
        /// 根據請求文件的后綴名,確定響應體的類型
        /// </summary>
        /// <param name="ext"></param>
        void GetContentType(string ext)
        {
            switch (ext)
            {
                case ".css":
                    this.ContentType = "text/css";
                    break;
                case ".gif":
                    this.ContentType = "image/gif";
                    break;
                case ".ico":
                    this.ContentType = "image/x-icon";
                    break;
                case ".jpe":
                case ".jpeg":
                case ".jpg":
                    this.ContentType = "image/jpeg";
                    break;
                case "bmp":
                    this.ContentType = "image/bmp";
                    break;
                case ".js":
                    this.ContentType = "application/x-javascript";
                    break;
                case "stm":
                case ".htm":
                case ".html":
                    this.ContentType = "text/html";
                    break;
                // ...挖坑,讀者可以在這里進行詳細的補充
            }
        }

        /// <summary>
        /// 拼接響應報文
        /// </summary>
        public byte[] GetResponse()
        {
            // 拼接響應報文頭
            StringBuilder sb=new StringBuilder();
            sb.Append("HTTP/1.0 "+this.CodeStatus+" "+codeStatusDic[this.CodeStatus]+"\r\n");
            sb.Append("Content-Type: "+this.ContentType+"\r\n");
            sb.Append("Content-Length: "+this.ContentLength+"\r\n");
            sb.Append("Server: ghhSever/1.0\r\n");
            sb.Append("X-Powered-By: MannyGuo\r\n");// 大家可以模擬下面的響應報文進行添加,注意格式必須要一致(末尾換行)
            sb.Append("\r\n");
            // 構建響應報文頭
            byte[] header = Encoding.UTF8.GetBytes(sb.ToString());
            // 構建響應報文體
            byte[] content = this.Buffer;
            // 裝載響應報文
            List<byte>bList=new List<byte>();
            bList.AddRange(header);
            bList.AddRange(content);

            return bList.ToArray();
        }

        #region 響應報文分析
        /*
        HTTP/1.0 200 OK
        Content-Type: text/html
        Content-Length: 337
        Connection: keep-alive
        Date: Sun, 09 Jun 2013 04:50:44 GMT
        Server: Apache
        X-Powered-By: PHP/5.2.5
        Content-Encoding: gzip
        Vary: Accept-Encoding
        Age: 37928
        Via: 1.0 fe91fd60a17845818d57d903e10536ce.cloudfront.net (CloudFront)
        X-Cache: Hit from cloudfront
        X-Amz-Cf-Id: WKYiDsukwM6go6_K9lF207F72tlhGB6Wv1wgRutHWslDdd_7MoUpdw==
        
        50 
         */
        
        #endregion
        #endregion
    }
}

9、演示效果:

為了演示效果,我們需要在程序的debug目錄下新建一個Web文件夾,里面放一個測試用的1.html

  

 運行我們自己手寫的Web服務器,啟動服務。在瀏覽器地址中輸入“IP地址:端口號/頁面(或者資源)”,就可以看到效果了。

10、上一篇文章,短短3天內瀏覽量超過了1000。小郭在此感謝大家的支持!我會一如既往的為大家奉獻更多的東西。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM