各位朋友大家好,我是秦元培,歡迎大家關注我的博客,我的博客地址是http://qinyuanpei.com。在這個系列文章的第一篇中,我們着重認識和了解了HTTP協議,並在此基礎上實現了一個可交互的Web服務器,即當客戶端訪問該服務器的時候,服務器能夠返回並輸出一個簡單的“Hello World”。現在這個服務器看起來非常簡陋,為此我們需要在這個基礎上繼續開展工作。今天我們希望為這個服務器增加主頁支持,即當我們訪問這個服務器的時候,它可以向我們展示一個定制化的服務器主頁。通常情況下網站的主頁被定義為index.html,而在動態網站技術中它可以被定義為index.PHP。了解這些將有助於幫助我們認識Web技術的實質,為了方便我們這里的研究,我們以最簡單的靜態頁面為例。
大意失荊州
首先我們可以認識到的一點是,網站主頁是一個網站默認展示給訪問者的頁面,所以對服務器而言,它需要知道兩件事情,第一客戶端當前請求的這個頁面是不是主頁,第二服務端應該返回什么內容給客戶端。對這兩個問題,我們在目前設計的這個Web服務器中都可以找到答案的。因為HTTP協議中默認的請求方法是GET,所以根據HttpRequest的實例我們可以非常容易的知道,當前請求的方法類型以及請求地址。我們來看一個簡單的客戶端請求報文:
GET / HTTP/1.1 Accept: text/html, application/xhtml+xml, image/jxr, */* Accept-Language: zh-Hans-CN,zh-Hans;q=0.5 User-Agent: Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.10586 Accept-Encoding: gzip, deflate Host: localhost:4040 Connection: Keep-Alive
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
我們在這里可以非常清晰地看到,客戶端當前發出的請求是GET類型,而其請求的地址是”/”,這表示請求頁面為主頁,而實際上我們將Host字段和這個地址組合起來,就能得到一個完整的地址,這正是我們在HTML結構中編寫超鏈接的時候使用相對地址的原因。好了,在明白了這樣兩件事情具體的運作機理以后,下面我們來繼續編寫相關邏輯來實現如何向訪問者展示一個網站主頁。
public override void OnGet(HttpRequest request) { //判斷請求類型和請求頁面 if(request.Method == "GET" && request.URL == "/") { //構造響應報文 HttpResponse response; //判斷主頁文件是否存在,如存在則讀取主頁文件否則返回404 if(!File.Exists(ServerRoot + "index.html")){ response = new HttpResponse("<html><body><h1>404 - Not Found</h1></body></html>", Encoding.UTF8); response.StatusCode = "404"; response.Content_Type = "text/html"; response.Server = "ExampleServer"; }else{ response = new HttpResponse(File.ReadAllBytes(ServerRoot + "index.html"), Encoding.UTF8); response.StatusCode = "200"; response.Content_Type = "text/html"; response.Server = "ExampleServer"; } //發送響應 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
- 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
可以注意到在這里我們首先根據請求方法和請求地址來判斷當前客戶端是否在請求主頁頁面,然后我們判斷在服務器目錄下是否存在index.html文件,如果該文件存在就讀取文件並返回給客戶端,否則我們將返回給客戶端一個404的狀態,熟悉Web開發的朋友應該會知道這個狀態碼表示的是無法找到請求資源,類似地我們還可以想到的狀態碼有200、501等等,通常來講,這些狀態碼的定義是這樣的:
* 1XX:指示信息-表示請求已接收,繼續處理。
* 2XX:成功-表示請求已被成功接受、理解和處理。
* 3XX: 重定向-表示完成請求需要更進一步的操作。
* 4XX:客戶端錯誤-表示請求錯誤或者無法實現。
* 5XX:服務端錯誤-表示服務器未提供正確的響應。
具體來講,常見的狀態代碼描述如下:
狀態碼 | 狀態描述 |
---|---|
200 OK | 客戶端請求成功 |
400 Bad Request | 客戶端請求錯誤且不能被服務器所理解 |
401 Unauthorized | 請求未經授權需要使用WWW-Authenticate報頭域 |
403 Forbidden | 服務器收到請求但拒絕提供服務 |
404 Not Found | 請求資源不存在 |
500 Internal Server Error | 服務器發生不可預期的錯誤 |
503 Server Unavailable | 服務器當前不能處理客戶端的請求 |
為了簡化需求,我們這里假設服務器目錄下只有一個主頁文件index.html,實際上像IIS、Apache等大型的服務器軟件都是支持多個主頁文件的,而且同時支持靜態頁面和動態頁面,所以這里就涉及到一個優先級的問題,無論是在Apache還是IIS中我們可以找到對主頁優先級設置的選項。所謂優先級,其實就是對這些主頁文件重要性的一種排序,在實際設計的過程中,會優先讀取優先級較高的主頁文件,如該文件不存在則退而求其次,以此類推。在讀取主頁文件的時候,我們需要注意的一點是編碼類型,因為無論是客戶端還是服務端在其各自的頭部信息里都聲明了它可以接受的編碼類型,所以服務器端在響應請求的時候應該注意和客戶端保持一致,這樣可以避免“雞同鴨講”問題的發生,進而提高溝通效率。我們這里在說技術,可是人何嘗不是這樣啊,我感覺我們生活和工作中90%的時間都被用來溝通了,可是這恰恰說明了溝通的重要性啊。好了,下面我們來測試下我們編寫的主頁:
龍潛在淵
咦?這個頁面顯示的結果怎么和我們期望的不一樣啊,看起來這是一個因為樣式丟失而引發的錯誤啊,不僅如此我們發現頁面中的圖片同樣丟失了。首先我們檢查下靜態頁面是否有問題,這個怎么可能嘛?因為這是博主采用Hexo生成的靜態頁面,所以排除頁面本身的問題后,我們不得不開始重新思考我們的設計。我們靜下心來思考這樣一個問題:在瀏覽器加載一個頁面的過程中難道只有靜態頁面和服務器發生交互嗎?這顯然不是啊,因為傻子都知道一個網頁最起碼有HTML、CSS和JavaScript三部分組成,所以我們決定在Chrome中仔細看看瀏覽器在加載網頁的過程中都發生了什么。按下”F12”打開開發者工具對網頁進行監聽:
WTF!感覺在這里懵逼了是不是?你沒有想到服務器在這里會響應如此多的請求吧?所以我們自作聰明地認為只要響應靜態頁面的請求就好了,這完全就是在作死啊!這里我的理解是這樣的,對頁面來講服務器在讀取它以后會返回給客戶端,所以對客戶端而言這部分響應是完全可見的,而頁面中關聯的CSS樣式和javascript腳本則可能是通過瀏覽器緩存下載到本地,然后再根據相對路徑引用並應用到整個頁面中來的,而為了區分這些不同類型的資源,我們需要在響應報文中的Content-Type字段中指明內容的類型,所以現在我們就清楚了,首先在請求頁面的時候存在大量關聯資源,這些資源必須通過響應報文反饋給客戶端,其次這些資源由不同的類型具體體現在響應報文的Content-Type字段中。因此,我們在第一段代碼的基礎上進行修改和完善,最終編寫出了下面的代碼:
public override void OnGet(HttpRequest request) { if(request.Method == "GET") { ///獲取客戶端請求地址 ///鏈接形式1:"http://localhost:4050/assets/styles/style.css"表示訪問指定文件資源, ///此時讀取服務器目錄下的/assets/styles/style.css文件。 ///鏈接形式2:"http://localhost:4050/assets/styles/"表示訪問指定頁面資源, ///此時讀取服務器目錄下的/assets/styles/style.index文件。 //當文件不存在時應返回404狀態碼 string requestURL = request.URL; requestURL = requestURL.Replace("/", @"\").Replace("\\..", ""); //判斷地址中是否存在擴展名 string extension = Path.GetExtension(requestURL); //根據有無擴展名按照兩種不同鏈接進行處 string requestFile = string.Empty; if(extension != ""){ requestFile = ServerRoot + requestURL; }else{ requestFile = ServerRoot + requestURL + "index.html"; } //構造HTTP響應 HttpResponse response = ResponseWithFile(requestFile); //發送響應 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
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 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
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
注意到我在代碼中寫了兩種不同形式的鏈接的分析:
-
鏈接形式1:”http://localhost:4050/assets/styles/style.css“表示訪問指定文件資源,此時讀取服務器目錄下的/assets/styles/style.css文件。
-
鏈接形式2:”http://localhost:4050/assets/styles/“表示訪問指定頁面資源,此時讀取服務器目錄下的/assets/styles/style.index文件。
首先我們判斷這兩種形式是根據擴展名來判斷的,這樣我們可以獲得一個指向目標文件的地址requestFile。這里提供一個輔助方法ResponseWithFile,這是一個從文件中構造響應報文的方法,其返回類型是一個HttpResponse,當文件不存在時將返回給客戶端404的錯誤代碼,我們一起來看它具體如何實現:
/// <summary> /// 使用文件來提供HTTP響應 /// </summary> /// <param name="fileName">文件名</param> private HttpResponse ResponseWithFile(string fileName) { //准備HTTP響應報文 HttpResponse response; //獲取文件擴展名以判斷內容類型 string extension = Path.GetExtension(fileName); //獲取當前內容類型 string contentType = GetContentType(extension); //如果文件不存在則返回404否則讀取文件內容 if(!File.Exists(fileName)){ response = new HttpResponse("<html><body><h1>404 - Not Found</h1></body></html>", Encoding.UTF8); response.StatusCode = "404"; response.Content_Type = "text/html"; response.Server = "ExampleServer"; }else{ response = new HttpResponse(File.ReadAllBytes(fileName), Encoding.UTF8); response.StatusCode = "200"; response.Content_Type = contentType; response.Server = "ExampleServer"; } /返回數據 return 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
- 28
- 29
- 30
- 31
- 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
- 28
- 29
- 30
- 31
同樣的,因為在響應報文中我們需要指明資源的類型,所以這里使用一個叫做GetContentType的輔助方法,該方法定義如下,這里僅僅選擇了常見的Content-Type類型來實現,有興趣的朋友可以自行了解更多的內容並在此基礎上進行擴展:
/// <summary> /// 根據文件擴展名獲取內容類型 /// </summary> /// <param name="extension">文件擴展名</param> /// <returns></returns> protected string GetContentType(string extension) { string reval = string.Empty; if(string.IsNullOrEmpty(extension)) return null; switch(extension) { case ".htm": reval = "text/html"; break; case ".html": reval = "text/html"; break; case ".txt": reval = "text/plain"; break; case ".css": reval = "text/css"; break; case ".png": reval = "image/png"; break; case ".gif": reval = "image/gif"; break; case ".jpg": reval = "image/jpg"; break; case ".jpeg": reval = "image/jgeg"; break; case ".zip": reval = "application/zip"; break; } return reval; }
- 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
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 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
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
風雨過后終見彩虹
好啦,到目前為止,關於靜態Web服務器的編寫我們基本上告一段落啦!其實這篇文章寫的不是特別順利,因為我幾乎是在不斷否認自我的情況下,一邊調試一邊寫這篇文章的。整篇文章總結下來其實就兩個點,第一,Web服務器在加載一個頁面的時候會發起無數個請求報文,除了頁面相關的請求報文以外大部分都是和資源相關的請求,所以HTML頁面的優化實際上就是從資源加載這個地方入手的。第二,不同的資源有不同的類型,具體表現在響應報文的Content-Type字段上,構造正確的Content-Type能讓客戶端了解到這是一個什么資源。好了,現在我們可以氣定神閑的驗證我們的勞動成果啦,這里我以我本地的Hexo生成的靜態博客為例演示我的Web服務器,假設我的博客是存放在”D:\Hexo\public”這個路徑下,所以我可以直接在Web服務器中設置我的服務器目錄:
ExampleServer server = new ExampleServer("127.0.0.1",4050); server.SetServerRoot("D:\\Hexo\\public"); server.Start();
- 1
- 2
- 3
- 1
- 2
- 3
現在打開瀏覽器就可以看到:
如此激動人心的時候,讓我們踏歌長行、夢想永續,下期見!