1. 程式人生 > >編寫一個簡易的 HTTP 伺服器程式

編寫一個簡易的 HTTP 伺服器程式

好久沒輸出了,知識還是要寫下總結才能讓思路更加清晰。最近在學習計算機網路相關的知識,來聊聊如何編寫一個建議的HTTP伺服器。

HTTP 伺服器

HTTP伺服器,就是一個執行在主機上的程式。程式啟動了之後,會一直在等待其他所有客戶端的請求,接收到請求之後,處理請求,然後傳送響應給客戶端。客戶端和伺服器之間使用HTTP協議進行通訊,所有遵循HTTP協議的程式都可以作為客戶端。

先直接上程式碼,然後再詳細說明實現細節。

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596 #include <stdio.h>#include <ctype.h>#include <sys/types.h>#include <netinet/in.h>
#include <sys/socket.h>#include <unistd.h>#include <stdlib.h>#include <string.h>#include <sys/stat.h>#define PORT 9001#define QUEUE_MAX_COUNT 5#define BUFF_SIZE 1024#define SERVER_STRING "Server: hoohackhttpd/0.1.0\r\n"intmain(){/* 定義server和client的檔案描述符 */intserver_fd=-1;intclient_fd
=-1;u_short port=PORT;structsockaddr_in client_addr;structsockaddr_in server_addr;socklen_t client_addr_len=sizeof(client_addr);charbuf[BUFF_SIZE];charrecv_buf[BUFF_SIZE];charhello_str[]="Hello world!";inthello_len=0;/* 建立一個socket */server_fd=socket(AF_INET,SOCK_STREAM,0);if(server_fd==-1){perror("socket");exit(-1);}memset(&server_addr,0,sizeof(server_addr));/* 設定埠,IP,和TCP/IP協議族 */server_addr.sin_family=AF_INET;server_addr.sin_port=htons(PORT);server_addr.sin_addr.s_addr=htonl(INADDR_ANY);/* 繫結套接字到埠 */if(bind(server_fd,(structsockaddr *)&server_addr,sizeof(server_addr))<0){perror("bind");exit(-1);}/* 啟動socket監聽請求,開始等待客戶端發來的請求 */if(listen(server_fd,QUEUE_MAX_COUNT)<0){perror("listen");exit(-1);}printf("http server running on port %d\n",port);while(1){/* 呼叫了accept函式,阻塞了程式,直到接收到客戶端的請求 */client_fd=accept(server_fd,(structsockaddr *)&client_addr,&client_addr_len);if(client_fd<0){perror("accept");exit(-1);}printf("accept a client\n");printf("client socket fd: %d\n",client_fd);/* 呼叫recv函式接收客戶端發來的請求資訊 */hello_len=recv(client_fd,recv_buf,BUFF_SIZE,0);printf("receive %d\n",hello_len);/* 傳送響應給客戶端 */sprintf(buf,"HTTP/1.0 200 OK\r\n");send(client_fd,buf,strlen(buf),0);strcpy(buf,SERVER_STRING);send(client_fd,buf,strlen(buf),0);sprintf(buf,"Content-Type: text/html\r\n");send(client_fd,buf,strlen(buf),0);strcpy(buf,"\r\n");send(client_fd,buf,strlen(buf),0);sprintf(buf,"Hello World\r\n");send(client_fd,buf,strlen(buf),0);/* 關閉客戶端套接字 */close(client_fd);}close(server_fd);return0;}

測試執行

程式碼寫好之後,執行測試一下,將上面程式碼儲存到server.c,然後編譯程式:

1 gcc server.c-oserver

./server執行

runserver

伺服器執行,監聽9001埠。再用netstat命令檢視: server_netstat

server程式在監聽9001埠,執行正確。接著用瀏覽器訪問http://localhost:9001

browser_server

成功輸出了Hello World

再嘗試用telnet去模擬HTTP請求:

telnet_http

  • 1、成功連線
  • 2、傳送HTTP請求
  • 3、HTTP響應結果

上面是一個最簡單的server程式,程式碼比較簡單,省去一些細節,下面通過程式碼來學習一下socket的程式設計細節。

啟動server的流程

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));*sockfdsocket描述符,唯一標識一個socketbind函式就是將這個描述字繫結一個名字。*addr:一個sockaddr指標,指向要繫結給sockfd的協議地址。一個socketip和埠號唯一確定,而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>]