源碼地址https://github.com/zhuangqh/a...html
看到像這個給tinyhttpd寫README的倉庫都有1k star的時候,我真的好氣?,因此我也寫一個用c寫HTTP靜態文件服務器的教程,並且性能更好。git
c socket編程面向的是傳輸層。咱們在這一層上來收發HTTP報文。github
HTTP請求報文格式以下:編程
因爲咱們是靜態文件服務器,因此有效的請求報文是 GET url 的格式。咱們只要解析這個url,而後發送對應的文件就OK了。這個是基本的思路。數組
我仿照UNP中對函數進行包裝的方式。對基礎函數進行包裝,在代碼中只使用包裝過的函數。
UNIX函數大多會將函數的調用狀態做爲返回值。如Socket函數,若是返回值小於零,則是調用出錯,這種狀況咱們直接結束進程並報錯。瀏覽器
int Socket(int family, int type, int protocol) { int n; if ( (n = socket(family, type, protocol)) < 0) err_sys("socket error"); return(n); }
服務器啓動並監聽的流程是這樣的:首先調用socket()建立一個服務端的套接字,而後使用bind()將套接字綁定在一個指定的端口上。調用listen()將套接字從CLOSED狀態轉換到LISTEN狀態。而accept會返回已鏈接隊列的對頭。咱們對accept返回的描述符的讀寫就是對客戶端的收發操做。服務器
這篇教程選用的併發模型是線程池,每一個線程分別accept的形式。併發
ReqHandler這個struct存放的是對客戶端描述符的處理函數和在pthread_t數組的下標。下標用於後面的pthread_createsocket
主進程在建立完線程後任務就完成了,因此它一直阻塞等待就好。函數
tptr = Calloc(THREAD_NUM, sizeof(pthread_t)); // tptr是一個pthread_t的數組。在啓動的時候可給出線程池線程的數量,不指明則使用默認值8。 ReqHandler rh; rh.handler = accept_request; for (int i = 0; i < nthreads; i++) { rh.index = i; thread_make(rh); } for ( ; ; ) pause(); // everything done by threads
在線程建立時,將ReqHandler裏的請求處理函數傳遞給它
Pthread_create(&tptr[rh.index], NULL, &thread_main, (void *) (rh.handler));
在各個線程中分別accept,這個有個問題,他們不該該同時accept。因此咱們在進入accept這個函數前加上互斥鎖。
Pthread_mutex_lock(&mmlock); connfd = Accept(listenfd, cliaddr, &clilen); Pthread_mutex_unlock(&mmlock);
在服務器搭起來以後,咱們就能夠幹正事了。accept_request
這個函數解析出HTTP的請求方法和URL並做出響應。
咱們知道HTTP的每一行是以/r/n
結束,那麼getline該怎麼作呢?一個字符一個字符地讀,並逐一判斷是否爲/r/n
序列的方法顯然比較慢。因此咱們作一個本身的緩衝區。預先在客戶端描述符connfd讀入多個字符。再在緩衝區裏一個字符一個字符地判斷。緩衝區讀完後,再讀一次connfd。這樣能大大減小讀取connfd的次數。
獲取到HTTP的請求方法後,若是方法不是GET,咱們直接返回501錯誤。說明這個方法咱們尚未實現。
可能有人會對下面這種寫法感到疑惑。其實編譯器在作預處理的時候會把連着的字符串合併的。因此下面這種寫法跟寫在一對雙引號裏是同樣的。
void unimplemented(int sockfd) { char msg[] = "HTTP/1.1 501 Method Not Implemented\r\n" SERVER_STRING "Content-Type: text/plain\r\n" "\r\n" "method not implemented\r\n"; Write(sockfd, msg, strlen(msg)); }
而後判斷URL的文件是否存在。這裏咱們多作了一步處理,若是URL是以/
結尾的,瀏覽器會自動給它加上index.html
,因此咱們也按照這個來。
咱們用open的形式打開文件,而不是標準IO的fopen。open能拿到該文件的描述符。這在咱們下一步傳輸文件時比較方便。若是文件不存在,直接返回404。
void serve_file(int sockfd, const char *filepath) { int filefd = open(filepath, O_RDONLY); // open file for read if (filefd == -1) { not_found(sockfd); } else { set_header(sockfd, filepath); send_file(sockfd, filefd); Close(filefd); } }
向客戶端發送文件,還得設置好響應報文中Content-Type的值,告訴對方這是一個什麼文件。這裏咱們須要一張表,根據文件的後綴名查詢Content-Type。天然是使用Hash表,衝突用鏈表的形式解決。具體請看源碼。
傳輸文件時,就是一對read write,UNIX一切皆文件的優雅就此體現。
void send_file(int sockfd, int filefd) { char buf[MAXLINE]; int cnt = 0; while ((cnt = read(filefd, buf, MAXLINE)) > 0) { Write(sockfd, buf, cnt); } }
具體代碼請查看文章開頭的github地址,我把要點和總體思路講完,剩下就是看代碼了?