四月初就開始著手寫Web服務(wù)器蔽氨,因為一些事耽擱了一個月焰檩,最近又在提交代碼了多柑,文章列表:
這不是一個寫Web服務(wù)器的教程遥椿,只是做的過程的記錄,因為是先寫代碼后寫文章的掏父,有些過程我直接憑記憶在這里寫下來笋轨,可能會有疏漏。
建立 TCP 連接
這一部分參考 Liso Project 的 start_code赊淑、深入理解計算機系統(tǒng) 第二版 10-12 章爵政、UNIX網(wǎng)絡(luò)編程 卷1 第三版 1-4 章。
首先作為一個 Web服務(wù)器陶缺,要能夠監(jiān)聽端口钾挟、等待TCP連接、建立TCP連接饱岸,這是基本要求掺出。因為使用C語言開發(fā),所以要用 UNIX 的套接字API苫费,TCP連接建立與釋放的流程如下:
其中:
- socket() 用于創(chuàng)建一個套接字結(jié)構(gòu)體汤锨,這里需要使用的協(xié)議族、協(xié)議類型等百框。這一步一般不會出問題泥畅。
- bind() 用于綁定到一個本地端口。出錯的原因一般有端口已經(jīng)被占用、非 Root 用戶綁定知名端口位仁。
- listen() 用于說明這個套接字用于被動接收連接請求。
- accept() 會導(dǎo)致程序陷入睡眠方椎,直到系統(tǒng)中斷提醒程序有新連接建立聂抢。
- read() 也是一個 I/O操作,會導(dǎo)致程序陷入睡眠棠众,這個時候內(nèi)核開始從網(wǎng)卡的Buffer里面拷貝數(shù)據(jù)到內(nèi)存里琳疏,一旦拷貝完了,系統(tǒng)中斷讓進程切回來繼續(xù)執(zhí)行闸拿。
這部分對應(yīng)的代碼:
#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#define BUFFER_SIZE 4096
#define FORK_CHILD_PID 0
typedef struct http_mod {
int sockfd;
struct sockaddr_in addr;
} http_mod;
http_mod* http_init(uint16_t port) {
http_mod* m = (http_mod*) malloc(sizeof(http_mod*));
m->sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (m->sockfd == -1) {
fprintf(stderr, "Create socket failed.");
free(m);
return NULL;
}
m->addr.sin_family = AF_INET;
m->addr.sin_port = htons(port);
m->addr.sin_addr.s_addr = htonl(INADDR_ANY);
int bind_ret = bind(m->sockfd, (struct sockaddr*) &(m->addr), sizeof(m->addr));
if (bind_ret != 0) {
fprintf(stderr, "bind to %d failed!", port);
if(close(m->sockfd)) {
fprintf(stderr, "Close socked failed!\n");
}
free(m);
return NULL;
}
int listen_ret = listen(m->sockfd, 5);
if (listen_ret != 0) {
fprintf(stderr, "Failed to listen!\n");
if(close(m->sockfd)) {
fprintf(stderr, "Close socked failed!\n");
}
free(m);
return NULL;
}
fprintf(stdout, "start http mod successfully! \n");
return m;
}
//TODO: replace fork with IO multiplex
int start_receive_conn(http_mod *http) {
struct sockaddr_in client_sock_addr;
socklen_t cs_size = sizeof(client_sock_addr);
fprintf(stdout, "Waiting for a connection, localport: %d\n", ntohs(http->addr.sin_port));
while (1) {
int new_sock = accept(http->sockfd, (struct sockaddr*)(&client_sock_addr), &cs_size);
if (new_sock == -1) {
fprintf(stderr, "Accept connections failed!\n");
if (close(http->sockfd)) {
fprintf(stderr, "Close socket failed!\n");
}
return -1;
} else {
fprintf(stdout, "Receive connection from %s\n", inet_ntoa(client_sock_addr.sin_addr));
}
if (fork() == FORK_CHILD_PID) {
handle_request_loop(new_sock);
if (close(new_sock)) {
fprintf(stderr, "Close real sock failed! \n'");
}
fprintf(stdout, "Close connection from %s", inet_ntoa(client_sock_addr.sin_addr));
exit(0);
}
if (close(new_sock)) {
fprintf(stderr, "Close real sock failed! \n'");
return -1;
}
}
if (close(http->sockfd)) {
fprintf(stderr, "Close sock failed! \n'");
}
fprintf(stdout, "Liso has stopped.\n");
return 0;
}
void liso_init(struct arguments* arg) {
http_mod* hm = http_init(arg->port);
if (hm == NULL) {
fprintf(stderr, "Initialize http module failed!\n");
return;
}
start_receive_conn(hm);
}
因為程序在調(diào)用 accept() 建立了新連接后就不處于監(jiān)聽狀態(tài)了空盼,此時別的客戶端是無法和服務(wù)端建立連接的。所以必須要處理并發(fā)問題新荤,一個最簡單的處理辦法是用 fork() 的方式支持并發(fā):
每次建立了一個TCP連接后揽趾,就調(diào)用fork() 派生出一個子進程,此時子進程和父進程所有的內(nèi)存數(shù)據(jù)苛骨、文件打開列表都是一樣的篱瞎,這時可以讓子進程繼續(xù)處理數(shù)據(jù),而父進程把剛剛建立的連接關(guān)閉掉痒芝,繼續(xù)調(diào)用 accept() 等待客戶端連接就可以支持并發(fā)了俐筋。
但是這樣弊端也很大,那就是支持并發(fā)所需要的開銷太大了严衬。更好的方法是用線程和IO多路復(fù)用澄者,但是出于快速寫一個架子的考慮,我先讓這個“服務(wù)器”可以踉踉蹌蹌地跑起來再說请琳,多路復(fù)用先加入到 TODO List 里面去粱挡。
解析 HTTP 請求
這一部分參考 編譯原理(龍書) 3到4 章、Flex&Bison 開發(fā)文檔单起、RFC 2616(HTTP/1.1 標(biāo)準(zhǔn))抱怔、RFC 2396。
解決一個計算機問題嘀倒,建立一個穩(wěn)定可復(fù)現(xiàn)屈留、可調(diào)試的一個觀察點還是很必要的,尤其我在TCP協(xié)議之上做數(shù)據(jù)傳輸测蘑,如果無法觀察到實際傳輸?shù)臄?shù)據(jù)是很惱火的灌危,所以我要抓包看數(shù)據(jù)。一開始我想用 WireShark碳胳,后來發(fā)現(xiàn)那玩意從源碼編譯起來有點麻煩勇蝙,直接用 apt 安裝了一個 tcpdump。
RFC文檔看的是 2616挨约,這是 CS-15-441/641 Project1 的項目要求給的參考文檔味混,似乎這個文檔已經(jīng)被新的標(biāo)準(zhǔn)所替代产雹,但是絕大部分內(nèi)容還是可以參考的。
HTTP請求的格式如下:
仔細(xì)研究了一下格式翁锡,發(fā)現(xiàn)其實用C語言代碼解析一下就很方便了蔓挖,完全沒有必要用Flex和Bison,但是既然文檔那么要求馆衔,就試一下這兩個工具瘟判。
Flex 是用于詞法分析的工具,它把一個輸入的字符串分割成一個個詞法單元送給語法分析工具 Bison角溃。Flex 和 Bison 在 Ubuntu 中都可以通過 apt 安裝拷获。在 Flex 中,我需要定義一些正則表達式來匹配詞法單元减细,F(xiàn)lex 文件格式如下:
%{
定義詞法單元匆瓜,可以用 #define 表示
%}
聲明部分
%%
轉(zhuǎn)換規(guī)則
%%
輔助函數(shù)
Flex 和 Bison 結(jié)合起來使用的時候, %{ %} 中的詞法單元可以不定義邪财,放到 Bison 文件中去定義陕壹。轉(zhuǎn)換規(guī)則是重點,每行規(guī)則以一個正則表達式開始树埠,后面再跟一個代碼塊糠馆。代碼塊里可以執(zhí)行一些邏輯,比如調(diào)用輔助函數(shù)怎憋,返回詞法單元給 Bison又碌。
轉(zhuǎn)換規(guī)則這里有幾個坑點:
- 我一開始每行加了\t來indent一下,這里不能加绊袋,不要從行首開始寫正則表達式
- Flex支持的正則表達式和我平時用的正則表達式不太一樣毕匀,有些符號如 \w 是不支持的
- 正斜杠(forward slash)符號 / 會被轉(zhuǎn)義,要想不轉(zhuǎn)義前面加上 \ 是沒有用的癌别,得用雙引號括起來:"/"
這里放一部分規(guī)則:
輔助函數(shù)那里皂岔,如果不使用 Bison 的話,需要定義一個 yywrap() 函數(shù):
int yywrap() {
return 1;
}
使用 flex xxx.l 命令就可以把 xxx.l 文件編譯為 c 文件展姐,然后調(diào)用 yylex() 函數(shù)就會開始解析輸入的數(shù)據(jù)躁垛。 默認(rèn)是從 stdin 解析的。
Bison文件的格式和 Flex 類似圾笨。需要定義產(chǎn)生式教馆。Bison 會根據(jù)產(chǎn)生式自動生成語法解析器。