前面我們實現了TCP服務器和客戶端的簡單應用,接下來我們實現一個基於TCP協議的應用協議,那就是HTTP超文本傳輸協議
1、HTTP協議簡介
超文本傳輸協議(Hyper Text Transfer Protocol),簡稱HTTP,是一種基於TCP的應用層協議,也是目前為止最為流行的應用層協議之一,可以說HTTP協議是萬維網的基石。
HTTP是一種客戶端請求、服務器應答式的應用層傳輸協議,也就是說服務器端是不可能主動向客戶端發送數據的。在網絡正常的情況下請求和響應都是一一對應的。而這個請求和響應也就是后端開發人員經常看到的Request和Response。
首先,我們來看客戶器端的請求,HTTP請求報文由請求行、請求頭、空白行以及請求體組成。其報文格式如下:
我們來說一說請求行,它由請求方法字段、URL字段和HTTP協議版本字段3個字段組成,它們用空格分隔。需要理解的是請求方法,HTTP協議的請求方法有GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE、CONNECT幾種。先對常用的幾種說明如下:
- GET方法,意思是獲取URL指定的資源,這個請求方式是最簡單的也是最常用的。使用GET 方法時,可以將請求參數和對應的值附加在 URI 后面,利用一個問號(“?”)將資源的URI和請求參數隔開,參數之間使用與符號(“&”)隔開,因此傳遞參數長度也受到了限制,而且與隱私相關的信息也直接暴露在URI中。比如/index.jsp?username=holmofy&password=123123
- HEAD方法,與GET用法相同,但沒有響應體,使用場合沒有GET多。比如下載前使用HEAD發送請求,通過ContentLength響應字段,來了解網絡資源的大小;或者通過LastModified響應字段來判斷本地緩存資源是否要更新。
- POST方法,一般用提交信息或數據,請求服務器進行處理(例如提交表單或者上傳文件)。表單使用POST相對GET來說還是比較隱秘的,而且GET的URL有長度限制,而上傳大文件就必須要使用POST了。
- OPTIONS方法,該方法用於請求服務器告知其支持哪些其他的功能和方法。通過OPTIONS 方法,可以詢問服務器具體支持哪些方法,或者服務器會使用什么樣的方法來處理一些特殊資源。可以說這是一個探測性的方法,客戶端通過該方法可以在不訪問服務器上實際資源的情況下就知道處理該資源的最優方式。這個選項在跨域HTTP請求的情況出現的比較多,這里有一片關於跨域請求的文章,其中有一張圖很好的解釋了什么是跨域HTTP請求。
客戶端發出HTTP請求,服務端接收后,會向客戶端發送響應信息。所以接下來,我們來看看服務器端的響應報文。HTTP響應報文由響應行、響應頭、空白行以及響應體組成。其報文格式如下:
在響應報文中,非常重要的就是響應行,其中響應行中最重要的就是HTTP的狀態碼。HTTP協議中狀態碼有三位數字組成,第一位數字定義了響應的類別,有以下五種:
- 1XX:信息提示。表示請求已被服務器接受,但需要繼續處理,范圍為100~101。
- 2XX:請求成功。服務器成功處理了請求。范圍為200~206。
- 3XX:客戶端重定向。重定向狀態碼用於告訴客戶端瀏覽器,它們訪問的資源已被移動,並告訴客戶端新的資源位置。客戶端收到重定向會重新對新資源發起請求。范圍為300~305。
- 4XX:客戶端信息錯誤。客戶端可能發送了服務器無法處理的東西,比如請求的格式錯誤,或者請求了一個不存在的資源。范圍為400~415。
- 5XX:服務器出錯。客戶端發送了有效的請求,但是服務器自身出現錯誤,比如Web程序運行出錯。范圍是500~505。
我們開發過程有一些狀態碼比較常見,我們對其簡單說明如下:
2、HTTP客戶端設計
我們已經說過了,HTTP協議是基於TCP運行的,那么佷顯然我們要實現一個HTTP客戶端其本質上首先是要實現一個TCP客戶端。
在實現TCP客戶端的基礎上,我們要讓這個客戶端能夠實現HTTP協議的基本操作。所以我們需要為客戶端構造請求報文。關於請求報文的格式前面已經介紹過了,我們根據這個格式來構造,因為我們只是簡單的一個HTTP客戶端測試,所以我們采用GET方法。我們構造報文如下:
"GET https://www.cnblogs.com/foxclever/ HTTP/1.1\r\n"
"Host:www.cnblogs.com:80\r\n\r\n";
對於HTTP協議具有專門的端口號,所以我們采用這個制定的端口號來實現。而實現的流程與一般TCP客戶端是一樣的。
3、HTTP客戶端實現
經過上述的分析以及我們前面實現TCP客戶端的經驗,實現HTTP客戶端已經沒有問題。與TCP客戶端一般,我們將HTTP客戶端分成4個函數來實現。首先依然是實現HTTP客戶端的初始化:
1 /* HTTP客戶端初始化配置*/ 2 void Http_Client_Initialization(void) 3 { 4 struct tcp_pcb *tcp_client_pcb; 5 ip_addr_t ipaddr; 6 7 /* 將目標服務器的IP寫入一個結構體,為pc機本地連接IP地址 */ 8 IP4_ADDR(&ipaddr,httpServerIP[0],httpServerIP[1],httpServerIP[2],httpServerIP[3]); 9 10 /* 為tcp客戶端分配一個tcp_pcb結構體 */ 11 tcp_client_pcb = tcp_new(); 12 13 /* 綁定本地端號和IP地址 */ 14 tcp_bind(tcp_client_pcb, IP_ADDR_ANY, TCP_HTTP_CLIENT_PORT); 15 16 if (tcp_client_pcb != NULL) 17 { 18 /* 與目標服務器進行連接,參數包括了目標端口和目標IP */ 19 tcp_connect(tcp_client_pcb, &ipaddr, TCP_HTTP_SERVER_PORT, HTTPClientConnected); 20 21 tcp_err(tcp_client_pcb, HTTPClientConnectError); 22 } 23 }
我們很容易發現,上述初始化的代碼其實就是TCP客戶端的初始化代碼,除了所使用的端口不一樣外,其它都一樣。也是在初始化代碼中實現了兩個函數的注冊:一是使用tcp_connect注冊連接完成的處理回調函數;二是使用tcp_err注冊了連接錯誤處理回調函數。很明顯接下來我們需要實現這兩個函數。
連接到服務器成功后的回調函數是tcp_connected_fn類型。在客戶端建立一個連接后,內核會調用這個函數。在這個函數中,客戶端回想服務器發送最初的操作請求,並且會在這個函數中注冊數據接收處理回調函數。
1 /* HTTP客戶端連接到服務器回調函數 */ 2 static err_t HTTPClientConnected(void *arg, struct tcp_pcb *pcb, err_t err) 3 { 4 char clientString[]="GET https://www.cnblogs.com/foxclever/ HTTP/1.1\r\n" 5 6 "Host:www.cnblogs.com:80\r\n\r\n"; 7 8 /* 配置接收回調函數 */ 9 tcp_recv(pcb, HTTPClientCallback); 10 11 /* 發送一個建立連接的問候字符串*/ 12 tcp_write(pcb,clientString, strlen(clientString),0); 13 14 return ERR_OK; 15 }
這個代碼也是與普通TCP客戶端一樣,只是為了應用於HTTP協議,我們發送的請求字符串需要按照HTTP的格式來設定。對HTTP客戶端連接服務器錯誤回調函數,它是tcp_err_fn類型,在這個程序中主要完成連接異常結束時的一些處理,可以釋放一些必要的資源。在這個函數被內核調用時,連接實際上已經斷開,相關控制塊也已經被刪除。所以在這個函數中我們可以重新初始化連接及其資源。在這里我們就是使用它來重新初始化TCP客戶端。
1 /* HTTP客戶端連接服務器錯誤回調函數 */ 2 static void HTTPClientConnectError(void *arg, err_t err) 3 { 4 /* 重新啟動連接 */ 5 Http_Client_Initialization(); 6 }
最后我們需要實現的是HTTP客戶端接收到數據后的數據處理回調函數。這個函數其實就是我們前面連接成功時,注冊過的HTTP客戶端數據接收處理函數。這個函數是tcp_recv_fn類型。這是使用RAW API實現HTTP客戶端功能最重要的一個函數,因為它決定HTTP客戶端的具體功能。
1 /* HTTP客戶端接收到數據后的數據處理回調函數 */ 2 static err_t HTTPClientCallback(void *arg, struct tcp_pcb *pcb, struct pbuf *tcp_recv_pbuf, err_t err) 3 { 4 struct pbuf *tcp_send_pbuf; 5 char echoString[]="GET https://www.cnblogs.com/foxclever/ HTTP/1.1\r\n" 6 7 "Host:www.cnblogs.com:80\r\n\r\n"; 8 9 if (tcp_recv_pbuf != NULL) 10 { 11 /* 更新接收窗口 */ 12 tcp_recved(pcb, tcp_recv_pbuf->tot_len); 13 14 /* 將接收到的服務器內容回顯*/ 15 tcp_write(pcb,echoString, strlen(echoString), 1); 16 tcp_send_pbuf = tcp_recv_pbuf; 17 tcp_write(pcb, tcp_send_pbuf->payload, tcp_send_pbuf->len, 1); 18 19 pbuf_free(tcp_recv_pbuf); 20 } 21 else if (err == ERR_OK) 22 { 23 tcp_close(pcb); 24 Http_Client_Initialization(); 25 26 return ERR_OK; 27 } 28 29 return ERR_OK; 30 }
同樣,這段代碼也是除了要按HTTP協議構造響應信息外,其他部分與普通TCP客戶端類似。
4、結論
與前一篇實現HTTP服務器是基於TCP服務器實現的一樣,這里我們實現HTTP客戶端是基於TCP客戶端來實現的。在我們前面已經實現TCP客戶端的情況下,開發HTTP客戶端應用就顯得簡單了。在這一篇我們基於LwIP實現了一個簡單的HTTP客戶端應用,我們並對其進行了簡單的測試。再歷程中我們只是實現了GET方法,但經測試設計是正確的。如果需要設計其他方法的HTTP應用只需在此基礎上添加即可。
歡迎關注: