——閑扯:
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。小郭在此感謝大家的支持!我會一如既往的為大家奉獻更多的東西。
