tinyhttp是一個用c寫的輕量級的http server陡舅,相比較nginx贞瞒,apache這類的server徐鹤,它完全和他們不是一個量級的東西,像是小山包和喜馬拉雅山的區(qū)別颂碘,但是看看這個源碼族淮,對了解http協(xié)議和 http server的原理是超級有幫助的!F就俊祝辣!
tinyhttp是用c寫的,相比較用python或則是其他高級語言實現(xiàn)的http server切油,c語言可以更加從底層的讓讀者明白一個瀏覽器的請求是如何被server相應(yīng)蝙斜,然后server是如何response。大概用一天的時間讀完澎胡,真的是把我爽到了孕荠,強(qiáng)烈推薦!9ニ稚伍!
另外,如果你有空戚宦,可以看一下web bench的源碼个曙,這個是http壓測工具,模擬并發(fā)的生成http請求受楼】寻幔可以結(jié)合起來看一下呼寸,還是可以學(xué)到很多東西的。
源碼:https://github.com/zhaozhengcoder/rebuild-the-wheel/tree/master/tinyhttpd
tinyhttp的框架
下圖是看源碼的時候猴贰,手畫的一個流程圖对雪。
我把一些核心代碼和相應(yīng)的注釋貼在這里,如果你感興趣全部米绕,可以移步我的github瑟捣。
https://github.com/zhaozhengcoder/rebuild-the-wheel/tree/master/tinyhttpd
main函數(shù):
int main(void)
{
int server_sock = -1;
//監(jiān)聽的端口
u_short port = 4000;
int client_sock = -1;
struct sockaddr_in client_name;
socklen_t client_name_len = sizeof(client_name);
pthread_t newthread;
server_sock = startup(&port);
printf("httpd running on port %d\n", port);
while (1)
{
//等待socket建立連接
client_sock = accept(server_sock,(struct sockaddr *)&client_name,&client_name_len);
if (client_sock == -1)
error_die("accept");
//對于一個socket連接(即一個http請求),創(chuàng)建一個線程去處理
if (pthread_create(&newthread , NULL, (void *)accept_request, (void *)(intptr_t)client_sock) != 0)
perror("pthread_create");
}
close(server_sock);
return(0);
}
accept_request函數(shù):
//對于每一個http請求栅干,都會創(chuàng)建一個線程迈套,線程去執(zhí)行這個函數(shù)去處理請求
//注意:client是一個文件句柄,在accept_request函數(shù)里面非驮,只讀了這個句柄的第一行交汤,得到了請求的方法和url
void accept_request(void *arg)
{
int client = (intptr_t)arg;
char buf[1024];
size_t numchars;
char method[255];
char url[255];
char path[512];
size_t i, j;
struct stat st;
int cgi = 0; /* becomes true if server decides this is a CGI */
char *query_string = NULL;
//獲得請求的第一行,請求的第一行往往是 : GET / HTTP/1.1
numchars = get_line(client, buf, sizeof(buf));
printf("buf : %s",buf);
//分析請求
i = 0; j = 0;
while (!ISspace(buf[i]) && (i < sizeof(method) - 1))
{
method[i] = buf[i];
i++;
}
j=i;
method[i] = '\0';
//server 支持get 和post 兩種方法劫笙,如果是其他的方法芙扎,就不支持了,返回狀態(tài)碼501填大,服務(wù)器不支持這個方法
if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
{
unimplemented(client);
return;
}
//對于是post的請求戒洼,把cgi(common gateway interface)的flag 設(shè)為1,表示這個需要cgi來處理
if (strcasecmp(method, "POST") == 0)
cgi = 1;
//獲得請求的url
i = 0;
while (ISspace(buf[j]) && (j < numchars))
j++;
while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < numchars))
{
url[i] = buf[j];
i++; j++;
}
url[i] = '\0';
printf("url is %s \n",url); //比如說允华,這個請求的url可能是 /index.html 圈浇,或則是 /index.html?id=100
//如果是get方法,判斷這個get請求靴寂,是否是帶有參數(shù)的請求
if (strcasecmp(method, "GET") == 0)
{
query_string = url;
while ((*query_string != '?') && (*query_string != '\0'))
query_string++;
if (*query_string == '?')
{
cgi = 1;
*query_string = '\0';
query_string++;
}
}
//sprintf()函數(shù):將格式化的數(shù)據(jù)寫入字符串
sprintf(path, "htdocs%s", url); //獲取請求文件路徑
printf("path is :%s \n",path);
//如果路徑是一個目錄磷蜀,那么就給這個路徑加上index.html ,表示默認(rèn)的請求
if (path[strlen(path) - 1] == '/')
strcat(path, "index.html");
//根據(jù)路徑找文件,并獲取path文件信息保存到結(jié)構(gòu)體st中百炬,-1表示尋找失敗
if (stat(path, &st) == -1) {
//如果尋找失敗
while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */
numchars = get_line(client, buf, sizeof(buf));
not_found(client);
}
else
{
if ((st.st_mode & S_IFMT) == S_IFDIR)
strcat(path, "/index.html");
if ((st.st_mode & S_IXUSR) ||
(st.st_mode & S_IXGRP) ||
(st.st_mode & S_IXOTH) )
cgi = 1;
//如果cgi==0褐隆,表示僅是一個get請求,沒有帶參數(shù)
if (!cgi){
printf("\n to execute server_file \n");
serve_file(client, path);
}
else{
//表示是post方法或者是帶有參數(shù)的get方法
printf("\n to execute execute_cgi \n");
execute_cgi(client, path, method, query_string);
}
}
close(client);
}
execute_cgi 函數(shù)
//對于帶有參數(shù)的get請求和 post請求剖踊,這兩類并不能直接返回一個靜態(tài)的html文件庶弃,需要cgi
//cgi是common gateway interface的簡稱
//談一下我對cgi的理解,就是對于不能直接返回靜態(tài)頁面的請求德澈,這些請求一定是需要在服務(wù)器上面運(yùn)行一段代碼歇攻,然后返回一個結(jié)果
//具體一點的談:
//比如一個 get請求 /index?uid=100,它可能對應(yīng)的場景是返回id=100用戶的頁面,這顯然不是一個靜態(tài)的頁面梆造,需要動態(tài)的生成缴守,然后服務(wù)器把這個id=100的參數(shù)拿到,去執(zhí)行本地的一個 xxx.cgi 文件,
//執(zhí)行這個文件的時候斧散,參數(shù)是id=100供常,然后將執(zhí)行這個文件的輸出返回給瀏覽器 可以參考 : http://www.runoob.com/python/python-cgi.html
//注意:client是一個文件劇本摊聋,在accept_request函數(shù)里面鸡捐,只讀了第一行,在execute_cgi函數(shù)里面麻裁,把剩下的讀完
void execute_cgi(int client, const char *path,const char *method, const char *query_string)
{
printf ("\n in function execute cgi ! \n");
char buf[1024];
int cgi_output[2];
int cgi_input[2];
pid_t pid;
int status;
int i;
char c;
int numchars = 1;
int content_length = -1;
buf[0] = 'A'; buf[1] = '\0';
if (strcasecmp(method, "GET") == 0)
while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */
numchars = get_line(client, buf, sizeof(buf));
else if (strcasecmp(method, "POST") == 0) /*POST*/
{
numchars = get_line(client, buf, sizeof(buf));
while ((numchars > 0) && strcmp("\n", buf))
{
buf[15] = '\0';
if (strcasecmp(buf, "Content-Length:") == 0)
content_length = atoi(&(buf[16]));
numchars = get_line(client, buf, sizeof(buf));
printf("buf : %s",buf);
}
if (content_length == -1) {
bad_request(client);
return;
}
}
else/*HEAD or other*/
{
}
if (pipe(cgi_output) < 0) {
cannot_execute(client);
return;
}
if (pipe(cgi_input) < 0) {
cannot_execute(client);
return;
}
if ( (pid = fork()) < 0 ) {
cannot_execute(client);
return;
}
sprintf(buf, "HTTP/1.0 200 OK\r\n");
send(client, buf, strlen(buf), 0);
if (pid == 0) /* child: CGI script */
{
char meth_env[255];
char query_env[255];
char length_env[255];
dup2(cgi_output[1], STDOUT);
dup2(cgi_input[0], STDIN);
close(cgi_output[0]);
close(cgi_input[1]);
sprintf(meth_env, "REQUEST_METHOD=%s", method);
putenv(meth_env);
if (strcasecmp(method, "GET") == 0) {
sprintf(query_env, "QUERY_STRING=%s", query_string);
printf("qery_env : %s ",query_env);
putenv(query_env);
}
else { /* POST */
sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
putenv(length_env);
}
printf("\npath : %s",path);
//執(zhí)行外部腳本
execl(path,path, NULL);
exit(0);
} else { /*父進(jìn)程 */
close(cgi_output[1]);
close(cgi_input[0]);
//注意:這段代碼的意識是箍镜,如果請求是post類型,post的請求是在正文里面有post的具體數(shù)據(jù)的
if (strcasecmp(method, "POST") == 0)
for (i = 0; i < content_length; i++) {
//在這里讀的就是post請求的具體參數(shù)煎源,父子進(jìn)程共享文件句柄色迂,然后這個socet的header部分已經(jīng)讀完了,在往下讀手销,就是post的正文了
recv(client, &c, 1, 0);
printf("c: %c \n",c);
//將讀到的數(shù)據(jù)寫給子進(jìn)程
write(cgi_input[1], &c, 1);
}
while (read(cgi_output[0], &c, 1) > 0)
send(client, &c, 1, 0);
close(cgi_output[0]);
close(cgi_input[1]);
waitpid(pid, &status, 0);
}
}
others
假如不借鑒源碼的思路歇僧,自己去寫一個http server,那么下面的幾個問題锋拖,可能需要考慮如何去解決诈悍。
如何去處理并發(fā)的http請求?
源碼給出的思路是兽埃,對每一個來的請求侥钳,創(chuàng)建一個線程出處理并發(fā)的請求。如果換成你柄错,你會怎么做舷夺?你的解決方案可以支持高并發(fā)嗎?對于一個http請求售貌,如果從請求里面解析到關(guān)鍵的字段信息给猾,比如
http method,是get颂跨,post敢伸,put,delete毫捣,還是head?
url是什么详拙?
如果是post類型的請求,post的參數(shù)是在http請求的正文里面的蔓同,那么怎么讀取出來他們饶辙?他們的長度是如何確定的?對于帶有參數(shù)的get方法斑粱,和post方法弃揽,你的服務(wù)器如何去處理?
對于問題2,
這個需要了解http的格式矿微,http請求的格式痕慢,如下圖:
判斷http 請求header的每一行的標(biāo)志是 :\r\n
判斷http請求header和請求正文的標(biāo)志是:兩個\r\n (如上圖)
對于,一個post請求涌矢,請求的正文里面是post的請求數(shù)據(jù)掖举,header里面的content-length指明了post請求的數(shù)據(jù)的長度。
可以參考:http://www.reibang.com/p/f5a5db039737
對于問題3:
帶有參數(shù)的get請求娜庇,和post請求塔次,服務(wù)器沒有辦法簡單的返回一個靜態(tài)的文件,服務(wù)器需要在服務(wù)器端將相應(yīng)的頁面“計算”出來名秀,然后返回給瀏覽器励负。
假如,瀏覽器發(fā)送了一個請求 /index?id=100匕得,請求id=100的人的主頁
那么继榆,服務(wù)器需要“計算”出來id=100的這個人的主頁的的頁面。這個就需要cgi來幫忙了汁掠,cgi可以理解為在服務(wù)器端可以執(zhí)行的小腳本略吨。服務(wù)器收到這個請求之后,執(zhí)行index.cgi (這個文件是提前寫好了调塌,專門來處理這樣的請求) 晋南,服務(wù)器執(zhí)行index.cgi ,參數(shù)是id=100羔砾,然后“計算”出網(wǎng)頁的數(shù)據(jù)负间,返回給瀏覽器。
舉個例子姜凄,cgi的小腳本長什么樣子:
可以參考:http://www.runoob.com/python/python-cgi.html
這是我在看源碼的時候政溃,給自己提的幾個問題√恚看懂tinyhttp應(yīng)該是理解http server的第一步吧董虱,nginx正在前面等你,一起前進(jìn)吧申鱼,少年~