編寫一個簡易的 HTTP 伺服器程式
好久沒輸出了,知識還是要寫下總結才能讓思路更加清晰。最近在學習計算機網路相關的知識,來聊聊如何編寫一個建議的HTTP伺服器。
HTTP 伺服器
HTTP伺服器,就是一個執行在主機上的程式。程式啟動了之後,會一直在等待其他所有客戶端的請求,接收到請求之後,處理請求,然後傳送響應給客戶端。客戶端和伺服器之間使用HTTP協議進行通訊,所有遵循HTTP協議的程式都可以作為客戶端。
先直接上程式碼,然後再詳細說明實現細節。
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596 | #include <stdio.h>#include <ctype.h>#include <sys/types.h>#include <netinet/in.h> |
測試執行
程式碼寫好之後,執行測試一下,將上面程式碼儲存到server.c,然後編譯程式:
1 | gcc server.c-oserver |
./server執行
伺服器執行,監聽9001埠。再用netstat
命令檢視:
server程式在監聽9001埠,執行正確。接著用瀏覽器訪問http://localhost:9001
成功輸出了Hello World
再嘗試用telnet
去模擬HTTP請求:
- 1、成功連線
- 2、傳送HTTP請求
- 3、HTTP響應結果
上面是一個最簡單的server程式,程式碼比較簡單,省去一些細節,下面通過程式碼來學習一下socket的程式設計細節。
啟動server的流程
socket 函式
建立一個套接字,通過各引數指定套接字的型別。
1 | intsocket(intfamily,inttype,intprotocol); |
- family:協議族。AF_INET:IPV4協議;AF_INET6:IPv6協議;AF_LOCAL:Unix域協議;AF_ROUTE:路由套接字;AF_KEY:金鑰套接字
- type:套接字型別。SOCK_STREAM : 位元組流套接字;SOCK_DGRAM:資料包套接字;SOCK_SEGPACKET:有序分組套接字;SOCK_RAW:原始套接字
- protocol:某個協議型別常量。TCP:0,UDP :1, SCTP :2
套接字地址結構
在socket程式設計中,大部分函式都用到一個指向套接字地址結構的指標作為引數。針對不同的協議型別,會有不同的結構體定義格式,對於ipv4,結構如下所示:
1234567 | structsockaddr_in{uint8_t sin_len;/* 結構體的長度 */sa_family_t sin_family;/* IP協議族,IPV4是AF_INET */in_port_t sin_port;/* 一個16位元的TCP/UDP埠地址 */structin_addr sin_addr;/* 32位元的IPV4地址,網路位元組序 */charsin_zero[8];/* 未使用欄位 */};注:sockaddr_in是**Internet socket address structure**的縮寫。 |
ip地址結構
123 | structin_addr{in_addr_t s_addr;}; |
套接字地址結構的作用是為了將ip地址和埠號傳遞到socket函式,寫成結構體的方式是為了抽象。當作為一個引數傳遞進任何套接字函式時,套接字地址結構總是以引用方式傳遞。然而,協議族有很多,因此以這樣的指標作為引數之一的任何套接字函式必須處理來自所有支援的任何協議族的套接字地址結構。使用void *
作為通用的指標型別,因此,套接字函式被定義為以指向某個通用套接字結構的一個指標作為其引數之一,正如下面的bind函式原型一樣。
1 | intbind(int,structsockaddr *,socklen_t); |
這就要求,對這些函式的任何呼叫都必須要將指向特定於協議的套接字地址結構的指標進行強制型別轉換,變成某個通用套接字地址結構的指標。例如:
12 | structsockaddr_in addr;bind(sockfd,(structsockaddr *)&addr,sizeof(addr)); |
對於所有socket函式而言,sockaddr的唯一用途就是對指向特定協議的套接字地址結構的指標執行強制型別轉換,指向要繫結給sockfd的協議地址。
bind函式
將套接字地址結構繫結到套接字
1 | intbind(sockfd,(structsockaddr *)&addr,sizeof(addr));*sockfd:socket描述符,唯一標識一個socket。bind函式就是將這個描述字繫結一個名字。*addr:一個sockaddr指標,指向要繫結給sockfd的協議地址。一個socket由ip和埠號唯一確定,而sockaddr就包含了ip和埠的資訊地址的長度 |
綁定了socket之後,就可以使用該socket開始監聽請求了。
listen函式
將sockfd從未連線的套接字轉換成一個被動套接字,指示核心應接受指向該套接字的連線請求。
1 | intlisten(intsockfd,intbacklog);listen函式會將套接字從CLOSED狀態轉換到LISTEN狀態,第二個引數規定核心應該為相應套接字排隊的最大連線個數。 |
關於backlog引數,核心為任何一個給定的監聽套接字維護兩個佇列: > * 1、未完成連線佇列,在佇列裡面的套接字處於SYN_RCVD狀態 > * 2、已完成佇列,處於ESTABLISHED狀態
兩個佇列之和不超過backlog的大小。
listen完成之後,socket就處於LISTEN狀態,此時的socket呼叫accept函式就可以接受客戶端發來的請求了。
accept函式
1 | intaccept(intsockfd,structsockaddr *cliaddr,socklen_t *addrlen);用於從已完成連線佇列頭返回下一個已完成連線,如果已完成連線佇列為空,那麼程序就會被阻塞。因此呼叫了accept函式之後,程序就會被阻塞,直到有新的請求到來。 |
第一個引數sockfd是客戶端的套接字描述符,第二個是客戶端的套接字地址結構,第三個是套接字地址結構的長度。
如果accept成功,那麼返回值是由核心自動生成的全新描述符,代表所返回的客戶端的TCP連線。
對於accept函式,第一個引數稱為監聽套接字描述符,返回值稱為已連線套接字。伺服器僅建立監聽套接字,它一直存在。已連線套接字由伺服器程序接受的客戶連線建立,當伺服器完成某個連線的響應後,相應的已連線套接字就被關閉了。
accept函式返回時,會返回套接字描述符或出錯指示的整數,以及引用引數中的套接字地址和該地址的大小。如果對返回值不感興趣,可以把兩個引用引數設為空。
accept之後,一個TCP連線就建立起來了,接著,伺服器就接受客戶端的請求資訊,然後做出響應。
recv和send函式
12 | ssize_t recv(intsockfd,void*buff,size_t nbytes,intflags);ssize_t send(intsockfd,constvoid*buff,size_t nbytes,intflags); |
分別用於從客戶端讀取資訊和傳送資訊到客戶端。在此不做過多的解釋。
套接字地址結構大小和值-結果引數
可以看到,在bind函式和accept函式裡面,都有一個套接字地址結構長度的引數,區別在於一個是值形式,另一個是引用形式。套接字地址結構的傳遞方式取決於該結構的傳遞方向:是從程序到核心,還是從核心到程序。
1、從程序到核心:bind、connect、sendto。 函式將指標和指標所指內容的大小都傳給了核心,於是核心知道到底需要從程序複製多少資料進來。
2、從核心到程序: accept、recvfrom、getsockname、getperrname。 這四個函式的結構大小是以只引用的方式傳遞。 因為當函式被呼叫時,結構大小是一個值,它告訴核心該結構的大小,這樣核心在寫該結構時不至於越界;當函式返回時,結構大小又是一個結果,它告訴核心在該結構中究竟儲存了多少資訊。
HTTP響應報文
傳送響應給客戶端時,傳送的報文要遵循HTTP協議,HTTP的響應報文格式如下:
1234 | <status-line><headers><blank line>[<response-body>] |