很久沒輸出了,知識仍是要寫下總結才能讓思路更加清晰。最近在學習計算機網絡相關的知識,來聊聊如何編寫一個建議的HTTP服務器。html
這個http server的實現源代碼我放在了個人github上,有興趣的話能夠點擊查看哦。git
HTTP服務器,就是一個運行在主機上的程序。程序啓動了以後,會一直在等待其餘全部客戶端的請求,接收到請求以後,處理請求,而後發送響應給客戶端。客戶端和服務器之間使用HTTP協議進行通訊,全部遵循HTTP協議的程序均可以做爲客戶端。github
先直接上代碼,而後再詳細說明實現細節。編程
#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" int main() { /* 定義server和client的文件描述符 */ int server_fd = -1; int client_fd = -1; u_short port = PORT; struct sockaddr_in client_addr; struct sockaddr_in server_addr; socklen_t client_addr_len = sizeof(client_addr); char buf[BUFF_SIZE]; char recv_buf[BUFF_SIZE]; char hello_str[] = "Hello world!"; int hello_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, (struct sockaddr *)&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, (struct sockaddr *)&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); return 0; }
代碼寫好以後,運行測試一下,將上面代碼保存到server.c,而後編譯程序:瀏覽器
gcc server.c -o server
./server運行服務器
服務器運行,監聽9001端口。再用netstat
命令查看:
網絡
server程序在監聽9001端口,運行正確。接着用瀏覽器訪問http://localhost:9001socket
成功輸出了Hello World函數
再嘗試用telnet
去模擬HTTP請求:學習
一、成功鏈接
二、發送HTTP請求
三、HTTP響應結果
上面是一個最簡單的server程序,代碼比較簡單,省去一些細節,下面經過代碼來學習一下socket的編程細節。
建立一個套接字,經過各參數指定套接字的類型。
int socket(int family, int type, int protocol);
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,結構以下所示:
struct sockaddr_in { uint8_t sin_len; /* 結構體的長度 */ sa_family_t sin_family; /* IP協議族,IPV4是AF_INET */ in_port_t sin_port; /* 一個16比特的TCP/UDP端口地址 */ struct in_addr sin_addr; /* 32比特的IPV4地址,網絡字節序 */ char sin_zero[8]; /* 未使用字段 */ };
注:sockaddr_in是Internet socket address structure的縮寫。
struct in_addr { in_addr_t s_addr; };
套接字地址結構的做用是爲了將ip地址和端口號傳遞到socket函數,寫成結構體的方式是爲了抽象。看成爲一個參數傳遞進任何套接字函數時,套接字地址結構老是以引用方式傳遞。然而,協議族有不少,所以以這樣的指針做爲參數之一的任何套接字函數必須處理來自全部支持的任何協議族的套接字地址結構。使用void *
做爲通用的指針類型,所以,套接字函數被定義爲以指向某個通用套接字結構的一個指針做爲其參數之一,正以下面的bind函數原型同樣。
int bind(int, struct sockaddr *, socklen_t);
這就要求,對這些函數的任何調用都必需要將指向特定於協議的套接字地址結構的指針進行強制類型轉換,變成某個通用套接字地址結構的指針。例如:
struct sockaddr_in addr; bind(sockfd, (struct sockaddr *)&addr , sizeof(addr));
對於全部socket函數而言,sockaddr的惟一用途就是對指向特定協議的套接字地址結構的指針執行強制類型轉換,指向要綁定給sockfd的協議地址。
將套接字地址結構綁定到套接字
int bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
sockfd:socket描述符,惟一標識一個socket。bind函數就是將這個描述字綁定一個名字。
addr:一個sockaddr指針,指向要綁定給sockfd的協議地址。一個socket由ip和端口號惟一肯定,而sockaddr就包含了ip和端口的信息
地址的長度
綁定了socket以後,就可使用該socket開始監聽請求了。
將sockfd從未鏈接的套接字轉換成一個被動套接字,指示內核應接受指向該套接字的鏈接請求。
int listen(int sockfd, int backlog);
listen函數會將套接字從CLOSED狀態轉換到LISTEN狀態,第二個參數規定內核應該爲相應套接字排隊的最大鏈接個數。
關於backlog參數,內核爲任何一個給定的監聽套接字維護兩個隊列:
一、未完成鏈接隊列,在隊列裏面的套接字處於SYN_RCVD狀態
二、已完成隊列,處於ESTABLISHED狀態
兩個隊列之和不超過backlog的大小。
listen完成以後,socket就處於LISTEN狀態,此時的socket調用accept函數就能夠接受客戶端發來的請求了。
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
用於從已完成鏈接隊列頭返回下一個已完成鏈接,若是已完成鏈接隊列爲空,那麼進程就會被阻塞。所以調用了accept函數以後,進程就會被阻塞,直到有新的請求到來。
第一個參數sockfd是客戶端的套接字描述符,第二個是客戶端的套接字地址結構,第三個是套接字地址結構的長度。
若是accept成功,那麼返回值是由內核自動生成的全新描述符,表明所返回的客戶端的TCP鏈接。
對於accept函數,第一個參數稱爲監聽套接字描述符,返回值稱爲已鏈接套接字。服務器僅建立監聽套接字,它一直存在。已鏈接套接字由服務器進程接受的客戶鏈接建立,當服務器完成某個鏈接的響應後,相應的已鏈接套接字就被關閉了。
accept函數返回時,會返回套接字描述符或出錯指示的整數,以及引用參數中的套接字地址和該地址的大小。若是對返回值不感興趣,能夠把兩個引用參數設爲空。
accept以後,一個TCP鏈接就創建起來了,接着,服務器就接受客戶端的請求信息,而後作出響應。
ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags); ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);
分別用於從客戶端讀取信息和發送信息到客戶端。在此不作過多的解釋。
能夠看到,在bind函數和accept函數裏面,都有一個套接字地址結構長度的參數,區別在於一個是值形式,另外一個是引用形式。套接字地址結構的傳遞方式取決於該結構的傳遞方向:是從進程到內核,仍是從內核到進程。
一、從進程到內核:bind、connect、sendto。
函數將指針和指針所指內容的大小都傳給了內核,因而內核知道到底須要從進程複製多少數據進來。
二、從內核到進程:
accept、recvfrom、getsockname、getperrname。
這四個函數的結構大小是以只引用的方式傳遞。
由於當函數被調用時,結構大小是一個值,它告訴內核該結構的大小,這樣內核在寫該結構時不至於越界;當函數返回時,結構大小又是一個結果,它告訴內核在該結構中究竟存儲了多少信息。
發送響應給客戶端時,發送的報文要遵循HTTP協議,HTTP的響應報文格式以下:
<status-line> <headers> <blank line> [<response-body>]
第一行status-line,狀態欄,格式:HTTP版本 狀態碼 狀態碼錶明文字
headers是返回報文的類型,長度等信息,接着是一個空行,而後是響應報文的實體。
一個HTTP響應報文例子:
HTTP/1.1 200 OK Content-Type: text/html;charset=utf-8 Content-Length: 122 <html> <head> <title>Hello Server</title> </head> <body> Hello Server </body> </html>
最後close函數關閉套接字,時刻保持關閉文件描述符是一個很好的編程習慣。
雖然不少東西看起來很簡單,但只有本身真正動手作一遍,才發現其中的簡單,以後才能說這些基礎是最簡單的。要更好和更深刻地理解系統的知識,你必須從新一點一點地從新構建一次。
這個http server的實現源代碼我放在了個人github上,有興趣的話能夠點擊查看哦。