引言
做一個老實人挺好的,至少還覺得自己挺老實的.
再分享一首 自己喜歡的詩人的一首 情景詩. 每個人總會有問題,至少喜歡就好,
本文 參照
http 協議 http://www.cnblogs.com/rayray/p/3729533.html
html格式 http://blog.csdn.net/allenjy123/article/details/7375029
tinyhttpd 源碼 https://github.com/EZLippi/Tinyhttpd
附錄 本文最後完稿的資源
httpd 源碼打包 http://download.csdn.net/detail/wangzhione/9461441
通過本文練習, 至少會學會 Linux上fork用法, pipe管道用法0讀1寫, pthread用法等.
其它的都是業務解析內容.
前言
講的不好望見諒, 因為很多東西需要自己去寫一寫就有感悟了. 看懂源碼和會改源碼是兩碼事. 和 會優化更不同了.
凡事多練習. 不懂也都懂了. 我們先說一下總的結構.
client.c 是一個簡易的 測試 http請求的客戶端
httpd.c 使我們重點要說的 小型簡易的Linux上的http服務器
index.html 測試網頁 是client.c 想請求的網頁
Makefile 編譯文件.
這裡先總的展示一下 httpd.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <errno.h> #include <pthread.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/wait.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> // --------------------------------------- 輔助參數宏 ---------------------------------------------- /* * c 如果是空白字符返回 true, 否則返回false * c : 必須是 int 值,最好是 char 范圍 */ #define sh_isspace(c) \ ((c==' ')||(c>='\t'&&c<='\r')) //4.0 控制台打印錯誤信息, fmt必須是雙引號括起來的宏 #define CERR(fmt, ...) \ fprintf(stderr,"[%s:%s:%d][error %d:%s]" fmt "\r\n",\ __FILE__, __func__, __LINE__, errno, strerror(errno),##__VA_ARGS__) //4.1 控制台打印錯誤信息並退出, t同樣fmt必須是 ""括起來的字符串常量 #define CERR_EXIT(fmt,...) \ CERR(fmt,##__VA_ARGS__),exit(EXIT_FAILURE) //4.3 if 的 代碼檢測 #define IF_CHECK(code) \ if((code) < 0) \ CERR_EXIT(#code) // --------------------------------------- 輔助變量宏 和 聲明 ------------------------------------------ // char[]緩沖區大小 #define _INT_BUF (1024) // listen監聽隊列的大小 #define _INT_LIS (7) /* * 讀取文件描述符 fd 一行的內容,保存在buf中,返回讀取內容長度 * fd : 文件描述符 * buf : 保存的內容 * sz : buf 的大小 * : 返回讀取的長度 */ int getfdline(int fd, char buf[], int sz); // 返回400 請求解析失敗,客戶端代碼錯誤 extern inline void response_400(int cfd); // 返回404 文件內容, 請求文件沒有找見 extern inline void response_404(int cfd); // 返回501 錯誤, 不支持的請求 extern inline void response_501(int cfd); // 服務器內部錯誤,無法處理等 extern inline void response_500(int cfd); // 返回200 請求成功 內容, 後面可以加上其它參數,處理文件輸出 extern inline void response_200(int cfd); /* * 將文件 發送給客戶端 * cfd : 客戶端文件描述符 * path : 發送的文件路徑 */ void response_file(int cfd, const char* path); /* * 返回啟動的服務器描述符(句柄), 這裡沒有采用8080端口,防止沖突,用了隨機端口 * pport : 輸出參數和輸出參數, 如果傳入NULL,將不返回自動分配的端口 * : 返回 啟動的文件描述符 */ int serstart(uint16_t* pport); /* * 在客戶端鏈接過來,多線程處理的函數 * arg : 傳入的參數, 客戶端文件描述符 (int)arg * : 返回處理結果,這裡默認返回 NULL */ void* request_accept(void* arg); /* * 處理客戶端的http請求. * cfd : 客戶端文件描述符 * path : 請求的文件路徑 * type : 請求類型,默認是POST,其它是GET * query : 請求發送的過來的數據, url ? 後面那些數據 */ void request_cgi(int cfd, const char* path, const char* type, const char* query); /* * 主邏輯,啟動服務,可以做成守護進程. * 具體的實現邏輯, 啟動小型玩樂級別的httpd 服務 */ int main(int argc, char* argv[]) { pthread_attr_t attr; uint16_t port = 0; int sfd = serstart(&port); printf("httpd running on port %u.\n", port); // 初始化線程屬性 pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); for(;;){ pthread_t tid; struct sockaddr_in caddr; socklen_t clen = sizeof caddr; int cfd = accept(sfd, (struct sockaddr*)&caddr, &clen); if(cfd < 0){ CERR("accept sfd = %d is error!", sfd); break; } if(pthread_create(&tid, &attr, request_accept, (void*)cfd) < 0) CERR("pthread_create run is error!"); } // 銷毀吧, 一切都結束了 pthread_attr_destroy(&attr); close(sfd); return 0; } // ----------------------------------------- 具體的函數實現過程 ------------------------------------------------ /* * 讀取文件描述符 fd 一行的內容,保存在buf中,返回讀取內容長度 * fd : 文件描述符 * buf : 保存的內容 * sz : buf 的大小 * : 返回讀取的長度 */ int getfdline(int fd, char buf[], int sz) { char* tp = buf; char c; --sz; while((tp-buf)<sz){ if(read(fd, &c, 1) <= 0) //偽造結束條件 break; if(c == '\r'){ //全部以\r分割 if(recv(fd, &c, 1, MSG_PEEK)>0 && c == '\n') read(fd, &c, 1); else //意外的結束,填充 \n 結束讀取 *tp++ = '\n'; break; } *tp++ = c; } *tp = '\0'; return tp - buf; } // 返回400 請求解析失敗,客戶端代碼錯誤 inline void response_400(int cfd) { const char* estr = "HTTP/1.0 400 BAD REQUEST\r\n" "Server: wz simple httpd 1.0\r\n" "Content-Type: text/html\r\n" "\r\n" "<p>你的請求有問題,請檢查語法!</p>\r\n"; write(cfd, estr, strlen(estr)); } // 返回404 文件內容, 請求文件沒有找見 inline void response_404(int cfd) { const char* estr = "HTTP/1.0 404 NOT FOUND\r\n" "Server: wz simple httpd 1.0\r\n" "Content-Type: text/html\r\n" "\r\n" "<html>" "<head><title>你請求的界面被查水表了!</title></head>\r\n" "<body><p>404: 估計是回不來了</p></body>" "</html>"; //開始發送數據 write(cfd, estr, strlen(estr)); } // 返回501 錯誤, 請求解析失敗,不支持的請求 inline void response_501(int cfd) { const char* estr = "HTTP/1.0 501 Method Not Implemented\r\n" "Server: wz simple httpd 1.0\r\n" "Content-Type: text/html\r\n" "\r\n" "<html>" "<head><title>小伙子不要亂請求</title></head>\r\n" "<body><p>too young too simple, 年輕人別總想弄出個大新聞.</p></body>" "</html>"; //這裡還有一個好的做法是將這些內容定義在文件中輸出文件 write(cfd, estr, strlen(estr)); } // 服務器內部錯誤,無法處理等 inline void response_500(int cfd) { const char* estr = "HTTP/1.0 500 Internal Server Error\r\n" "Server: wz simple httpd 1.0\r\n" "Content-Type: text/html\r\n" "\r\n" "<html>" "<head><title>Sorry </title></head>\r\n" "<body><p>最近有點方了!</p></body>" "</html>"; write(cfd, estr, strlen(estr)); } // 返回200 請求成功 內容, 後面可以加上其它參數,處理文件輸出 inline void response_200(int cfd) { // 打印返回200的報文頭 const char* str = "HTTP/1.0 200 OK\r\n" "Server: wz simple httpd 1.0\r\n" "Content-Type: text/html\r\n" "\r\n"; write(cfd, str, strlen(str)); } /* * 將文件 發送給客戶端 * cfd : 客戶端文件描述符 * path : 發送的文件路徑 */ void response_file(int cfd, const char* path) { FILE* txt; char buf[_INT_BUF]; // 讀取報文頭,就是過濾 while(getfdline(cfd, buf, sizeof buf)>0 && strcmp("\n", buf)) ; // 這裡開始處理 文件內容 if((txt = fopen(path, "r")) == NULL) //文件解析錯誤,給它個404 response_404(cfd); else{ response_200(cfd); //發送給200的報文頭過去 // 先判斷文件內容存在 while(!feof(txt) && fgets(buf, sizeof buf, txt)) write(cfd, buf, strlen(buf)); } fclose(txt); } /* * 返回啟動的服務器描述符(句柄) * pport : 輸出參數和輸出參數, 如果傳入NULL,將不返回自動分配的端口 * : 返回 啟動的文件描述符 */ int serstart(uint16_t* pport) { int sfd; struct sockaddr_in saddr = { AF_INET }; IF_CHECK(sfd = socket(PF_INET, SOCK_STREAM, 0)); saddr.sin_port = !pport || !*pport ? 0 : htons(*pport); saddr.sin_addr.s_addr = INADDR_ANY; // 綁定一下端口信息 IF_CHECK(bind(sfd, (struct sockaddr*)&saddr, sizeof saddr)); if(pport && !*pport){ socklen_t clen = sizeof saddr; IF_CHECK(getsockname(sfd, (struct sockaddr*)&saddr, &clen)); *pport = ntohs(saddr.sin_port); } // 開啟監聽任務 IF_CHECK(listen(sfd, _INT_LIS)); return sfd; } /* * 在客戶端鏈接過來,多線程處理的函數 * arg : 傳入的參數, 客戶端文件描述符 (int)arg * : 返回處理結果,這裡默認返回 NULL */ void* request_accept(void* arg) { char buf[_INT_BUF], path[_INT_BUF>>1], type[_INT_BUF>>5]; char *lt, *rt, *query, *nb = buf; struct stat st; int iscgi, cfd = (int)arg; if(getfdline(cfd, buf, sizeof buf) <= 0){ //請求錯誤 response_501(cfd); close(cfd); return NULL; } // 合法請求處理 for(lt=type, rt=nb; !sh_isspace(*rt) && (lt-type)< sizeof type - 1; *lt++ = *rt++) ; *lt = '\0'; //已經將 buf中開始不為empty 部分塞入了 type 中 //同樣處理合法與否判斷, 出錯了直接返回錯誤結果 if((iscgi = strcasecmp(type, "POST")) && strcasecmp(type, "GET")){ response_501(cfd); close(cfd); return NULL; } // 在buf中 去掉空字符 while(*rt && sh_isspace(*rt)) ++rt; // 這裡得到路徑信息 *path = '.'; for(lt = path + 1; (lt-path)<sizeof path - 1 && !sh_isspace(*rt); *lt++ = *rt++) ; *lt = '\0'; //query url路徑就拼接好了 //單獨處理 get 獲取 ? 後面數據, 不是POST那就是GET if(iscgi != 0){ for(query = path; *query && *query != '?'; ++query) ; if(*query == '?'){ iscgi = 0; *query++ = '\0'; } } // type , path 和 query 已經構建好了 if(stat(path, &st) < 0){ while(getfdline(cfd, buf, sizeof buf)>0 && strcmp("\n", buf))// 讀取內容直到結束 ; response_404(cfd); close(cfd); return NULL; } // 合法情況, 執行,寫入,讀取權限 if ((st.st_mode & S_IXUSR) ||(st.st_mode & S_IXGRP) ||(st.st_mode & S_IXOTH)) iscgi = 0; if(iscgi) //沒有cgi response_file(cfd, path); else request_cgi(cfd, path, type, query); close(cfd); return NULL; } /* * 處理客戶端的http請求. * cfd : 客戶端文件描述符 * path : 請求的文件路徑 * type : 請求類型,默認是POST,其它是GET * query : 請求發送的過來的數據, url ? 後面那些數據 */ void request_cgi(int cfd, const char* path, const char* type, const char* query) { char buf[_INT_BUF]; int pocgi[2], picgi[2]; pid_t pid; int contlen = -1; //報文長度 char c; if(strcasecmp(type, "POST") == 0){ while(getfdline(cfd, buf, sizeof buf)>0 && strcmp("\n", buf)){ buf[15] = '\0'; if(!strcasecmp(buf, "Content-Length:")) contlen = atoi(buf + 16); } if(contlen == -1){ //錯誤的報文,直接返回錯誤結果 response_400(cfd); return; } } else{ // 讀取報文頭,就是過濾, 後面就假定是 GET while(getfdline(cfd, buf, sizeof buf)>0 && strcmp("\n", buf)) ; } //這裡處理請求內容, 先處理錯誤信息 if(pipe(pocgi) < 0){ response_500(cfd); return; } if(pipe(picgi) < 0){ //管道 是 0讀取, 1寫入 close(pocgi[0]), close(pocgi[1]); response_500(cfd); return; } if((pid = fork())<0){ close(pocgi[0]), close(pocgi[1]); close(picgi[0]), close(picgi[1]); response_500(cfd); return; } // 這裡就是多進程處理了, 先處理子進程 if(pid == 0) { // dup2 讓 前者共享後者同樣的文件表 dup2(pocgi[1], STDOUT_FILENO); //標准輸出算作 pocgi管道 的寫入端 dup2(picgi[0], STDIN_FILENO); //標准輸入做為picgif管道的讀取端 close(pocgi[0]); close(pocgi[1]); // 添加環境變量,用於當前會話中 sprintf(buf, "REQUEST_METHOD=%s", type); putenv(buf); // 繼續湊環境變量串,放到當前會話種 if(strcasecmp(buf, "POST") == 0) sprintf(buf, "CONTENT_LENGTH=%d", contlen); else sprintf(buf, "QUERY_STRING=%s", query); putenv(buf); // 成功的話調到 新的執行體上 execl(path, path, NULL); // 這行代碼原本是不用的, 但是防止 execl執行失敗, 子進程沒有退出.妙招 exit(EXIT_SUCCESS); } // 父進程, 為所欲為了,先發送個OK write(cfd, "HTTP/1.0 200 OK\r\n", 17); close(pocgi[1]); close(picgi[0]); if(strcasecmp(type, "POST") == 0){ int i; //將數據都寫入到 picgi 管道中, 讓子進程在 picgi[0]中讀取 => STDIN_FILENO for(i=0; i<contlen; ++i){ read(cfd, &c, 1); write(picgi[1], &c, 1); } } //從子進程中 讀取數據 發送給客戶端, 多線程跨進程阻塞模型 while(read(pocgi[0], &c, 1) > 0) write(cfd, &c, 1); close(pocgi[0]); close(picgi[1]); //等待子進程結束 waitpid(pid, NULL, 0); }
我們看見 上面 函數 解釋的很清楚, 對於 response_* 響應部分占了大頭的一半.其實本質也就200行左右. 很適合臨摹一下.
正文
現在到了正文,說的很水. 再扯一點. 自己學習反人類的庫libuv, 就是note.js 底層通信的那套網絡庫. 也就是看官方的demo
一個個的臨摹. 了解的. 也就會用. 後面也就簡易的看看源碼. 也就懂了. 最經看的深入之後還是覺得,越簡單越直白越好.封裝太多了,
容易繞暈自己,而且很多功能用不上,遇到bug了又得查看繁瑣的萬行源碼.
總結就是, 學好基礎 問題, 走到哪裡都容易, 至少能做. 做的好不好, 以後再說.
那我們分析了. 第一個 看下面函數聲明
// 返回400 請求解析失敗,客戶端代碼錯誤 extern inline void response_400(int cfd);
這裡使用了C的內聯函數, 內聯函數聲明必須要有inline.否則編譯器解析 函數名稱會不一致找不見. 再扯一點對於
strcasecmp 其實是 linux上提供的函數 , window上使用需要做額外配置. 說白了就是不跨平台. 下面一種跨平台的實現如下
/* * 這是個不區分大小寫的比較函數 * ls : 左邊比較字符串 * rs : 右邊比較字符串 * : 返回 ls>rs => >0 ; ls = rs => 0 ; ls<rs => <0 */ int str_icmp(const char* ls, const char* rs) { int l, r; if(!ls || !rs) return (int)(ls - rs); do { if((l=*ls++)>='a' && l<='z') l -= 'a' - 'A'; if((r=*rs++)>='a' && r<='z') r -= 'a' - 'A'; } while(l && l==r); return l-r; }
參照編譯器源碼給的一種實現. 性能方面基本上還可以. 這裡再扯一點. 為什麼C中常說用指針速度快.
分析如下 普通的 a[6] ,訪問過程是 先取a首地址,再取a+6地址後面 取*(a+6)的值.
而如果直接用 ptr = a, ++ => ptr -> a+6 那就省略了一步 a+6的問題. 所以快一點.
再扯一點 a[6]其實就是語法糖, 本質也就是 *(a + 6), 通過這個推廣, a[-1] 也合法 等價於 *(a - 1).
後面再簡單分析一下 細節
我們總的思路是 服務器httpd 采用多線程接收客戶端請求. 再分析報文, 主要是分get請求和post請求.
get請求直接請求, 如果get 後面有? 或post請求 走 cgi 動態處理界面.
說白都很簡單, http 是在tcp 基礎上添加了 http報文的基礎解析內容. 本質是業務邏輯的處理.
這裡繼續說一說 本文中采用的管道細節
//這裡處理請求內容, 先處理錯誤信息 if(pipe(pocgi) < 0){ response_500(cfd); return; } if(pipe(picgi) < 0){ //管道 是 0讀取, 1寫入 close(pocgi[0]), close(pocgi[1]); response_500(cfd); return; } if((pid = fork())<0){ close(pocgi[0]), close(pocgi[1]); close(picgi[0]), close(picgi[1]); response_500(cfd); return; }
這裡是請求失敗會相應釋放打開的端口. 理論上在exit之後系統會自動回收打開的端口.但是不及時.
對於上面管道 是 子進程充定向管道為標准輸入輸出. 父進程向管道中寫入給子進程標准輸入輸出. 這就是傳說的cgi.
最後說明一段代碼
/* * 主邏輯,啟動服務,可以做成守護進程. * 具體的實現邏輯, 啟動小型玩樂級別的httpd 服務 */ int main(int argc, char* argv[]) { pthread_attr_t attr; uint16_t port = 0; int sfd = serstart(&port); printf("httpd running on port %u.\n", port); // 初始化線程屬性 pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); for(;;){ pthread_t tid; struct sockaddr_in caddr; socklen_t clen = sizeof caddr; int cfd = accept(sfd, (struct sockaddr*)&caddr, &clen); if(cfd < 0){ CERR("accept sfd = %d is error!", sfd); break; } if(pthread_create(&tid, &attr, request_accept, (void*)cfd) < 0) CERR("pthread_create run is error!"); } // 銷毀吧, 一切都結束了 pthread_attr_destroy(&attr); close(sfd); return 0; }
這是主業務, 亮點在於 pthread_attr 這塊, 添加了線程分離屬性, 自己回收. 不需要內核繼續保存線程屍體.
最後記得釋放.
到這裡基本細節我們都說完了. 對於 serstart 中采用了隨機端口, 是為了不合 服務器可能的http服務8080端口沖突, 就來個隨機端口.
對於socket 采用0端口,意思就是操作系統隨機分配.
測試
下面我們開始測試測試 的 client.c 代碼
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> //4.0 控制台打印錯誤信息, fmt必須是雙引號括起來的宏 #define CERR(fmt, ...) \ fprintf(stderr,"[%s:%s:%d][error %d:%s]" fmt "\r\n",\ __FILE__, __func__, __LINE__, errno, strerror(errno),##__VA_ARGS__) //4.1 控制台打印錯誤信息並退出, t同樣fmt必須是 ""括起來的字符串常量 #define CERR_EXIT(fmt,...) \ CERR(fmt,##__VA_ARGS__),exit(EXIT_FAILURE) //4.3 if 的 代碼檢測 #define IF_CHECK(code) \ if((code) < 0) \ CERR_EXIT(#code) //待拼接的字符串 #define _STR_HTTP_1 "GET /index.html HTTP/1.0\r\nUser-Agent: Happy is good.\r\nHost: 127.0.0.1:" #define _STR_HTTP_3 "\r\nConnection: close\r\n\r\n" // 簡單請求一下 int main(int argc, char* argv[]) { char buf[1024]; int sfd; struct sockaddr_in saddr = { AF_INET }; int len, port; // argc 默認為1 第一個參數 就是 執行程序串 if((argc != 2) || (port=atoi(argv[1])) <= 0 ) CERR_EXIT("Usage: %s [port]", argv[0]); // 開始了,就這樣了 IF_CHECK(sfd = socket(PF_INET, SOCK_STREAM, 0)); saddr.sin_port = htons(port); saddr.sin_addr.s_addr = INADDR_ANY; IF_CHECK(connect(sfd, (struct sockaddr*)&saddr, sizeof saddr)); //開始發送請求 strcpy(buf, _STR_HTTP_1); strcat(buf, argv[1]); strcat(buf, _STR_HTTP_3); write(sfd, buf, strlen(buf)); //讀取所喲內容 while((len = read(sfd, buf, sizeof buf - 1))){ buf[len] = '\0'; printf("%s", buf); } putchar('\n'); close(sfd); return 0; }
這裡就簡單向httpd 發送get 請求 index.html界面. 這裡再扯一點, 這個httpd 許多細節沒有考慮,容錯性不是那麼健全.
這些都好做,只要理解了實現思路和詳細了解HTTP協議就可以寫出好的HTTP知識,當然TCP的功底不可或缺,這點也很有挑戰.
對於index.html 界面如下
<html> <head> <title> 有意思 </title> </head> <body> <p> 只有野獸不會欺騙 <p> </body> </html>
最後上 Makefile
all:httpd.out client.out httpd.out:httpd.c gcc -g -Wall -o $@ $^ -lpthread client.out:client.c gcc -g -Wall -o $@ $^
最後執行結果示意圖圖如下,先啟動 httpd服務器
後面開啟http測試機, 需要輸入端口34704 如下
到這裡我們至少簡單測試都過了.
一切都是那麼自然而然. 前提你要個節奏,這個你能堅持. 節奏很重要, 裝逼是次要的.下次有機會再分享
開發中需要用到的一些開發模型和細節. 或者分享簡單高效的網絡庫知識. 最後扯一點, 都是從不懂,一點都不懂
堅持臨摹開始的.後面就懂了, 只有不懂和痛苦,惡心才會有點意思.哈哈.
後記
錯誤是難免的, 歡迎交流, 拜~~~