使用C#開發HTTP服務器系列之Hello World

各位朋友你們好,我是秦元培,歡迎你們關注個人博客。從今天起,我將開始撰寫一組關於HTTP服務器開發的系列文章。我爲何會有這樣的想法呢?由於人們對Web技術存在誤解,認爲網站開發是Web技術的所有。其實在今天這樣一個時代,Web技術可謂是無處不在,不管是傳統軟件開發仍是移動應用開發都離不開Web技術,因此在個人認識中,任何使用了HTTP協議實現數據交互均可以認爲是Web技術的一種體現,並且當咱們說起服務器開發的時候,咱們經常說起Java或者PHP。但是這些重要嗎?不,在我看來服務器開發和語言無關,和IIS、Tomcat、Apache、Ngnix等等咱們熟知的服務器軟件無關。Web技術能夠像一個網站同樣經過瀏覽器來訪問,一樣能夠像一個服務同樣經過程序來調用,因此在接下來的時間裏,我將和你們一塊兒見證如何使用C#開發一個基本的HTTP服務器,但願經過這些可以讓你們更好的認識Web技術。javascript

至繁至簡的HTTP

  咱們對HTTP協議最直觀的認識應該是來自瀏覽器,由於在互聯網時代咱們都是經過瀏覽器這個入口來接觸互聯網的,而到了移動互聯網時代咱們開始思考新的互聯網入口。在這個過程當中咱們有創新的模式不斷涌現出來,一樣有併購、捆綁、壟斷等形式的惡性競爭此起彼伏,所謂「痛並快樂着」。我想說的是,HTTP是一個簡單與複雜並存的東西,那麼什麼是HTTP呢?咱們在瀏覽器中輸入URL的時候,早已任性地連「http」和「www」都省略了吧,因此我相信HTTP對人們來講依然是一個陌生的東西。php

  HTTP是超文本傳輸協議(HyperText Transfer Protocol)的簡稱,它創建在C/S架構的應用層協議,熟悉這部份內容的朋友應該清楚,TCP/IP協議是協議層的內容,它定義了計算機間通訊的基礎協議,咱們熟悉的HTTP、FTP、Telnet等協議都是創建在TCP/IP協議基礎上的。在HTTP協議中,客戶端負責發起一個Request,該Request中含有請求方法、URL、協議版本等信息,服務端在接受到該Request後會返回一個Response,該Response中含有狀態碼、響應內容等信息,這一模型稱爲請求/響應模型。HTTP協議迄今爲止發展出3個版本:html

  • 0.9版本:已過期。該版本僅支持GET一種請求方法,不支持請求頭。由於不支持POST方法,因此客戶端沒法向服務器傳遞太多信息。
  • HTTP/1.0版本:這是第一個在通信中指定版本號的HTTP協議版本,至今依然被普遍採用,特別是在代理服務器中。
  • HTTP/1.1版本:目前採用的版本。持久鏈接被默認採用,並能很好地配合代理服務器工做。相對1.0版本,該版本在緩存處理、帶寬優化及網絡鏈接地使用、錯誤通知地管理、消息在網絡中的發送等方面都有顯著的區別。

  HTTP協議通訊的核心是HTTP報文,根據報文發送者的不一樣,咱們將其分爲請求報文和響應報文。其中,由客戶端發出的HTTP報文稱爲請求報文,由服務端發出的報文稱爲響應報文。下面咱們來着重瞭解和認識這兩種不一樣的報文:前端

  • 請求報文:請求報文一般由瀏覽器來發起,當咱們訪問一個網頁或者請求一個資源的時候都會產生請求報文。請求報文一般由HTTP請求行、請求頭、消息體(可選)三部分組成,服務端在接收到請求報文後根據請求報文請求返回數據給客戶端,因此咱們一般講的服務端開發其實是指在服務端接收到信息之後處理的這個階段。下面是一個基本的請求報文示例:
/* HTTP請求行 */ GET / HTTP/1.1 /* 請求頭部 */ Accept: text/html, application/xhtml+xml, image/jxr, */* Accept-Encoding: gzip, deflate Accept-Language: zh-Hans-CN, zh-Hans; q=0.5 Connection: Keep-Alive Host: localhost:4000 User-Agent: Mozilla/5.0 (Windows NT 10.0; Trident/7.0; rv:11.0) like Gecko /* 消息體 */
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 響應報文:響應報文是指在服務端接收並處理了客戶端的請求信息之後,服務端發送給客戶端的HTTP報文,服務端開發的重要工做就是處理來自客戶端的請求,因此這是咱們開發一個HTTP服務器的核心工做。和請求報文相似,響應報文由HTTP狀態行、響應頭、消息體(可選)三部分組成。例如咱們一般熟悉的200和404分別表示鏈接正常和沒法訪問資源這兩種響應狀態。下面是一個基本的響應報文示例:
/* HTTP狀態行 */ HTTP/1.1 200 OK /* 響應頭部 */ Content-Type: text/html;charset=utf-8 Connection: keep-alive Server: Microsoft-IIS/7.0 Date: Sun, 12 Jun 2016 11:00:42 GMT X-Powered-By: Hexo /* 消息體 */
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

  這裏須要說明的是,實際的請求報文和響應報文會由於服務端設計的不一樣,和這裏的報文示例略有不一樣,報文中頭部信息參數種類比較多,我不打算在這裏詳細解釋每一個參數的含義,咱們只須要對報文格式有一個基本的認識便可,想了解這些內容的朋友能夠閱讀這裏。在請求報文中咱們注意到第一行,即HTTP請求行指明當前請求的方法。因此下面咱們來講說HTTP協議的基本請求方法。常見的方法有GET、POST、HEAD、DELETE、OPTIONS、TRACE、CONNECT,咱們這裏選取最經常使用的兩種方式,即GET和PSOT來說解:java

  • GET:最爲常見的一種請示方式。當客戶端從服務器讀取文檔或者經過一個連接來訪問頁面的時候,都是採用GET方式來請求的。GET請求的一個顯著標志是其請求參數附加在URL後,例如」/index.jsp?id=100&option=bind」這種形式即爲GET方式請求。GET方式對用戶而言,傳遞參數過程是透明的,由於用戶能夠經過瀏覽器地址欄直接看到參數,因此這種方式更適合用來設計API,即在不須要驗證身份或者對安全性要求不高的場合,須要注意的是GET方式請求對參數長度由必定限制。
  • POST:POST克服了GET方式對參數長度存在限制的缺點,以鍵-值形式將參數封裝在HTTP請求中,因此從理論上講它對參數長度沒有限制(實際上會由於瀏覽器和操做系統的限制而大打折扣),並且對用戶來說參數傳遞過程是不可見的,因此它是一種相對安全的參數傳遞方式。一般用戶登陸都會採起這種方式,咱們在編寫爬蟲的時候遇到須要登陸的狀況一般都須要使用POST方式進行模擬登陸。

Socket與HTTP的緊密聯繫

  到目前爲止,咱們基本上搞清楚了HTTP是如何運做的,這偏偏符合普通人對技術的認知水平,或許在普通人看起來很是簡單的東西,對技術人員來說永遠都是複雜而深奧的,因此從這個角度來說,我覺的咱們更應該向技術人員致敬,由於是技術人員讓這些通過其簡化之後的複雜流程以一種產品的形態走進了你個人生活,感謝有技術和技術人員的存在,讓咱們這個世界更加美好。好了,如今咱們來思考這樣一個問題,Socket和HTTP有一種怎樣的關聯?這是由於咱們目前全部對HTTP的理解都是一種形而上學上的理解,它如今僅僅是一種協議,但是協議離真正的應用很遙遠不是嗎?因此咱們須要考慮如何去實現這樣一種協議。咱們注意到HTTP是創建在TCP/IP協議上的,因此HTTP的協議應該考慮用TCP/IP協議的實現來實現,考慮到Socket是TCP/IP協議的一種實現,因此咱們很是容易地想到應該用Socket來構建一個HTTP服務器,由此咱們找到了Socket和HTTP的緊密聯繫。python

  在找到Socket和HTTP的緊密聯繫之後,咱們如今就能夠開始着手來設計一個HTTP服務器了。咱們的思路是這樣的,首先咱們在服務端建立一個Socket來負責監聽客戶端鏈接。每次客戶端發出請求後,咱們根據請問報文來判斷客戶端的請求類型,而後根據不一樣的請求類型進行相應的處理,這樣咱們就設計了一個基本的HTTP服務器。mysql

從頭開始設計HTTP服務器

  好了,如今咱們要開始從頭設計一個HTTP服務器了,在此以前,咱們首先來爲整個項目設計下面的基本約束。我一直很是好奇爲何有的開發者會如此強烈地依賴框架。尤爲是在Web開發領域,MVC和MVVM基本上是耳熟能詳到爛俗的詞彙。我我的更加認同這是一種思想。什麼是思想呢?思想是你知道其絕妙處而絕口不提,卻在潛移默化中心照不宣的運行它。可事實上是什麼樣呢?無數開發者被框架所禁錮,由於咱們缺乏了犯錯的機會。因此我在這裏不想再說起Java、php、.NET在Web開發領域裏那些廣爲人知的框架,由於我認爲忘掉這些框架能夠幫助咱們更好的理解框架,下面我就來用個人這種方法告訴你們什麼叫作MVC?react

  什麼叫作MVC?咱們都知道MVC由模型、視圖、控制器三部分組成,但是它們的實質是什麼呢?我想這個問題可能沒有人想過,由於咱們的時間都浪費在配置XML文檔節點上。(我說的就是Java裏的配置狂魔)git

  首先,模型是什麼呢?模型對程序員而言能夠是一個實體類,亦能夠是一張數據表,而這兩種認知僅僅是由於咱們看待問題的角度不一樣而已,爲了讓這兩種認知模型統一,咱們想到了ORM、想到了根據數據表生成實體類、想到了在實體類中使用各類語法糖,而這些在我看來很是無聊的東西,居然可讓咱們不厭其煩地製造出各類框架,對程序員而言我仍是喜歡理解爲實體類。程序員

  其次,視圖是什麼呢?視圖在我看來是一個函數,它返回的是一個HTML結構的文本,而它的參數是一個模型,一個通過咱們實例化之後的對象,因此控制器所作的工做無非是從數據庫中獲取數據,而後將其轉化爲實體對象,再傳遞給視圖進行綁定而已。這樣聽起來,咱們對MVC的理解是否是就清晰了?而如今前端領域興起的Vue.jsReact,從本質上來說是在糾結控制器的這部分工做該有前端來完成仍是該有後端來完成而已。

  MVC中有一個路由的概念,這個概念咱們能夠和HTTP中請求行來對應起來,咱們知道發出一個HTTP請求的時候,咱們可以從請求報文中得到請求方法、請求地址、請求參數等一系列信息,服務器正是根據這些信息來處理客戶端請求的。那麼,路由究竟是什麼呢?路由就是這裏的請求地址,它能夠是實際的文件目錄、能夠是虛擬化的Web API、能夠是項目中的文件目錄,而一切的一切都在於咱們如何定義路由,例如咱們定義的路由是」http://www.zhihu.com/people/vczh「,從某種意義上來說,它和」http://www.zhihu.com/people/?id=vczh「是同樣的,由於服務器老是能股一眼看出這些語法糖的區別。

  雖然我在不遺餘力地避免造成對框架的依賴,但是在設計一個項目的時候,咱們依然須要作些宏觀上的規劃,我設計的一個原則就是簡單、輕量,我不喜歡重度產品,我喜歡小而美的東西,就像我喜歡C#這門語言而不喜歡ASP.NET同樣,由於我喜歡Nancy這個名字挺起來文藝而使用起來簡單、開心的東西。我不會像某語言同樣喪心病狂地使用接口和抽象類的,在我這裏總體設計是很是簡單的: 
* IServer.cs:定義服務器接口,該接口定義了OnGet()、OnPost()、OnDefault()、OnListFiles()四個方法,分別用來響應GET請求、響應POST請求、響應默認請求、列取目錄,咱們這裏的服務器類HttpServer須要實現該接口。 
* Request.cs:封裝來自客戶端的請求報文繼承自BaseHeader。 
* Response.cs:封裝來自服務端的響應報文繼承自BaseHeader。 
* BaseHeader.cs: 封裝通用頭部和實體頭部。 
* HttpServer.cs: HTTP服務器基類需實現IServer接口。

  由於我這裏但願實現的是一種全局上由我來控制,細節上由你來決定的面向開發者的設計思路,這和一般的面向大衆的產品思路是徹底不一樣的。例如委託或者事件的一個重要意義就是,它可讓程序按照設計者的思路來運行,同時知足使用着在細節上的控制權。因此,在寫完這個項目之後,咱們就能夠無需再關注客戶端和服務端如何通訊這些細節,而將更多的精力放在服務器接收到了什麼、如何處理、怎樣返回這樣的問題上來,這和框架但願咱們將精力放在業務上的初衷是同樣的,但是事實上關注業務對開發者來說是趨害的,對公司來說則是趨利的。當你發現你由於熟悉了業務而逐漸淪落爲框架填充者的時候,你有足夠的理由來喚起心裏想要控制一切的慾望。世界很大、人生很短,這原本就是一個矛盾的存在,當咱們習慣在框架中填充代碼的時候,你是否會想到人生原本沒有這樣的一個框架?

  好了,如今咱們來開始編寫這個Web服務器中通訊的基礎部分。首先咱們須要建立一個服務端Socket來監聽客戶端的請求。若是你熟悉Socket開發,你將指望看到下面這樣的代碼:

/// <summary> /// 開啓服務器 /// </summary> public void Start() { if(isRunning) return; //建立服務端Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); serverSocket.Bind(new IPEndPoint(IPAddress.Parse(ServerIP), ServerPort)); serverSocket.Listen(10); isRunning = true; //輸出服務器狀態 Console.WriteLine("Sever is running at http://{0}:{1}/.", ServerIP, ServerPort); //鏈接客戶端 while(isRunning) { Socket clientSocket = serverSocket.Accept(); Thread requestThread = new Thread(() =>{ ProcessRequest(clientSocket);}); requestThread.Start(); } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

這裏咱們使用isRunning來表示服務器是否運行,顯然當服務器處在運行狀態時,它應該返回。咱們這裏使用ServerIP和ServerPort分別表示服務端IP和端口,建立服務端Socket這裏就再也不贅述了,由於這是很是簡單而基礎的東西。當服務器處在運行狀態時咱們接受一個客戶端請求,並使用一個獨立的線程來處理請求,客戶端請求的處理咱們這裏提供了一個叫作ProcessRequest的方法,它具體都作了什麼工做呢?咱們繼續往下看:

/// <summary> /// 處理客戶端請求 /// </summary> /// <param name="handler">客戶端Socket</param> private void ProcessRequest(Socket handler) { //構造請求報文 HttpRequest request = new HttpRequest(handler); //根據請求類型進行處理 if(request.Method == "GET"){ OnGet(request); }else if(request.Method == "POST"){ OnPost(request); }else{ OnDefault(); } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

接下來咱們能夠注意到咱們這裏根據客戶端Soket構造了一個請求報文,其實就是在請求報文的構造函數中經過解析客戶端發來的消息,而後將其和咱們這裏定義的HttpRequest類對應起來。咱們這裏能夠看到,根據請求方法的不一樣,咱們這裏分別採用OnGet、OnPost和OnDefault三個方法進行處理,而這些是定義在IServer接口中並在HttpServer類中聲明爲虛方法。嚴格來說,這裏應該有更多的請求方法類型,但是由於我這裏寫系列文章的關係,我想目前暫時就實現Get和Post兩種方法,因此這裏你們若是感興趣的話能夠作更深層次的研究。因此,如今咱們就明白了,由於這些方法都被聲明爲虛方法,因此咱們只須要HttpServer類的子類中重寫這些方法就能夠了嘛,這好像離我最初的設想愈來愈近了呢。關於請求報文的構造,你們能夠到http://github.com/qinyuanpei/HttpServer/中來了解,實際的工做就是解析字符串而已,這些微小的工做實在不值得在這裏單獨來說。

  咱們今天的正事兒是什麼呢?是Hello World啊,因此咱們須要想辦法讓這個服務器給咱們返回點什麼啊,接下來咱們繼承HttpServer類來寫一個具體的類MyServer,和指望的同樣,咱們僅僅須要重寫相關方法就能夠寫一個基本的Web服務器,須要注意的是子類須要繼承父類的構造函數。咱們一塊兒來看代碼:

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.IO; namespace HttpServerLib { public class MyServer : HttpServer { public MyServer(string ipAddress, int port) : base(ipAddress, port) { } public override void OnGet(HttpRequest request) { HttpResponse response = new HttpResponse("<html><body><h1>Hello World</h1></body></html>", Encoding.UTF8); response.StatusCode = "200"; response.Server = "A Simple HTTP Server"; response.Content_Type = "text/html"; ProcessResponse(request.Handler, response); } } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

能夠注意到咱們這裏構造了一個HttpResponse,這是我這裏定義的HTTP響應報文,咱們這裏響應的內容是一段簡單的HTML採用UTF-8編碼。在構造完HttpResponse之後咱們設定了它的相關狀態,熟悉Web開發的朋友應該能夠想到這是抓包工具抓包時獲得的服務端報文信息,最近博主最喜歡的某個妹子寫真集網站開始反爬蟲了,所以博主之前寫的Python腳本如今執行會被告知403,這是一個禁止訪問的狀態碼。解決方案其實很是簡單地,將HTTP請求假裝成一個「瀏覽器」便可,思路就是在HTTP請求報文中增長相關字段,這樣就能夠「騙」過服務器,固然更深層次的「欺騙」就是Cookie和Session級別的假裝了,這個話題咱們有時間再說。這裏咱們設定狀態碼爲200,這是一個正常的請求,其次ContentType等字段能夠自行閱讀HTTP協議中頭部字段的相關資料,最後咱們經過ProcessResponse這個方法來處理響應,其內部是一個使用Socket發送消息的基本實現,詳細的設計細節你們能夠看項目代碼。

  如今讓咱們懷着無比激動的心情運行咱們的服務器,此時服務器運行狀況是:

服務器運行狀況

這樣是否是有一種恍若隔世的感受啊,每次打開Hexo的時候看到它自帶的本地服務器,感受很是高大上啊,結果萬萬沒想到有朝一日你就本身實現了它,這叫作「長大之後我就成了你嗎」?哈哈,如今是見證奇蹟的時刻:

瀏覽器運行狀況

瀏覽器懷着對將來無限的憧憬,自豪地寫下「Hello World」,正如不少年前詩人北島在絕望中寫下的《相信將來》同樣,或許生活中眼前都是苟且,但是隻要心中有詩和遠方,咱們就永遠不會迷茫。好了,至此這個系列第一篇Hello World終於寫完了,簡直如釋重負啊,第一篇須要理解和學習的東西實在太多了,原本打算在文章後附一份詳細的HTTP頭部字段說明,但是由於這些概念實在太枯燥,而使用Markdown編寫表格時表格內容過可能是寫做者的無盡痛苦。關於這個問題,你們能夠從這裏找到答案。下期再見!

相關文章
相關標籤/搜索