簡(jiǎn)介
Tinyhttp是1個(gè)輕量型Http Server,使用C語(yǔ)言開(kāi)發(fā),全部代碼只500多行,還包括1個(gè)簡(jiǎn)單Client。
源碼剖析
Tinyhttp程序的邏輯為:1個(gè)無(wú)線(xiàn)循環(huán),1個(gè)要求,創(chuàng)建1個(gè)線(xiàn)程,以后線(xiàn)程函數(shù)處理每一個(gè)要求,然后解析HTTP要求,做1些判斷,以后判斷文件是不是可履行,不可履行,打開(kāi)文件,輸出給客戶(hù)端(閱讀器),可履行就創(chuàng)建管道,父子進(jìn)程進(jìn)行通訊。其整體處理流程以下:
每一個(gè)函數(shù)的作用以下:
// accept_request函數(shù):處理從套接字上監(jiān)聽(tīng)到的1個(gè)HTTP要求,此函數(shù)很大部份體現(xiàn)服務(wù)器處理要求流程。
void accept_request(void *);
// bad_request函數(shù):返回給客戶(hù)端這是個(gè)毛病要求,HTTP狀態(tài)碼400 Bad Request。
void bad_request(int);
// cat函數(shù):讀取服務(wù)器上某個(gè)文件寫(xiě)到socket套接字。
void cat(int, FILE *);
// cannot_execute函數(shù):處理產(chǎn)生在履行cgi程序時(shí)出現(xiàn)的毛病。
void cannot_execute(int);
// error_die函數(shù):把毛病信息寫(xiě)到perror并退出。
void error_die(const char *);
// execute_cgi函數(shù):運(yùn)行cgi程序的處理,是主要的函數(shù)。
void execute_cgi(int, const char *, const char *, const char *);
// get_line函數(shù):讀取套接字的1行,把回車(chē)換行等情況都統(tǒng)1為換行符結(jié)束。
int get_line(int, char *, int);
// headers函數(shù):把HTTP響應(yīng)的頭部寫(xiě)到套接字。
void headers(int, const char *);
// not_found函數(shù):處理找不到要求的文件時(shí)的情況。
void not_found(int);
// serve_file函數(shù):調(diào)用cat函數(shù)把服務(wù)器文件返回給閱讀器
void serve_file(int, const char *);
// startup函數(shù):初始化httpd服務(wù),包括建立套接字,綁定端口,進(jìn)行監(jiān)聽(tīng)等。
int startup(u_short *);
// unimplemented函數(shù):返回給閱讀器表明收到的HTTP要求所用的method不被支持。
void unimplemented(int);
分析其程序,流程為:main()——>startup()——>accept_request()——>execute_cgi()等。
核心函數(shù)
1)main()函數(shù)
// 服務(wù)器main函數(shù)
int main(void)
{
int server_sock = ⑴;
u_short port = 4000;
int client_sock = ⑴;
struct sockaddr_in client_name;
socklen_t client_name_len = sizeof(client_name);
pthread_t newthread;
// 建立1個(gè)監(jiān)聽(tīng)套接字,在對(duì)應(yīng)的端口建立httpd服務(wù)
server_sock = startup(&port);
printf("httpd running on port %d\n", port);
// 進(jìn)入循環(huán),服務(wù)器通過(guò)調(diào)用accept等待客戶(hù)真?zhèn)€連接,Accept會(huì)以阻塞的方式運(yùn)行,直到
// 有客戶(hù)端連接才會(huì)返回。連接成功后,服務(wù)器啟動(dòng)1個(gè)新的線(xiàn)程來(lái)處理客戶(hù)真?zhèn)€要求,處理
// 完成后,重新等待新的客戶(hù)端要求。
while (1)
{
// 返回1個(gè)已連接套接字,套接字收到客戶(hù)端連接要求
client_sock = accept(server_sock,
(struct sockaddr *)&client_name,
&client_name_len);
if (client_sock == ⑴)
error_die("accept");
// 派生線(xiàn)程用accept_request函數(shù)處理新要求。
/* accept_request(client_sock); */
if (pthread_create(&newthread , NULL, (void *)accept_request, (void *)&client_sock) != 0)
perror("pthread_create");
}
// 出現(xiàn)意外退出的時(shí)候,關(guān)閉socket
close(server_sock);
return(0);
}
2)startup()函數(shù)
// startup函數(shù):依照TCP連接的正常流程順次調(diào)用socket,bind,listen函數(shù)。
// 監(jiān)聽(tīng)套接字端口既可以指定也能夠動(dòng)態(tài)分配1個(gè)隨機(jī)端口
int startup(u_short *port)
{
int httpd = 0;
struct sockaddr_in name;
// 創(chuàng)建1個(gè)socket,建立socket連接
httpd = socket(PF_INET, SOCK_STREAM, 0);
if (httpd == ⑴)
error_die("socket");
// 填充結(jié)構(gòu)體
memset(&name, 0, sizeof(name));
name.sin_family = AF_INET;
name.sin_port = htons(*port);
name.sin_addr.s_addr = htonl(INADDR_ANY);
// 將socket綁定到對(duì)應(yīng)的端口上
if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
error_die("bind");
// 如果當(dāng)前指定的端口是0,則動(dòng)態(tài)隨機(jī)分配1個(gè)端口
if (*port == 0) /* if dynamically allocating a port */
{
socklen_t namelen = sizeof(name);
// 1.getsockname()可以取得1個(gè)與socket相干的地址
// 1)服務(wù)器端可以通過(guò)它得到相干客戶(hù)端地址
// 2)客戶(hù)端可以得到當(dāng)前已連接成功的socket的IP和端口
// 2.在客戶(hù)端不進(jìn)行bind而直接連接服務(wù)器時(shí),且客戶(hù)端需要知道當(dāng)前使用哪一個(gè)IP地址
// 進(jìn)行通訊時(shí)比較有用(如多網(wǎng)卡的情況)
if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == ⑴)
error_die("getsockname");
*port = ntohs(name.sin_port);
}
// 開(kāi)始監(jiān)聽(tīng)
if (listen(httpd, 5) < 0)
error_die("listen");
// 返回socket id
return(httpd);
}
3)accept_request()函數(shù)
// 線(xiàn)程處理函數(shù)
void accept_request(void *arg)
{
int client = *(int*)arg;
char buf[1024]; // 讀取行數(shù)據(jù)時(shí)的緩沖區(qū)
size_t numchars; // 讀取了多少字符
char method[255]; // 存儲(chǔ)HTTP要求名稱(chēng)(字符串)
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
* program */
char *query_string = NULL;
// 1個(gè)HTTP要求報(bào)文由要求行(requestline)、要求頭部(header)、空行和要求數(shù)據(jù)4個(gè)部份
// 組成,要求行由要求方法字段(get或post)、URL字段和HTTP協(xié)議版本字段3個(gè)字段組成,它們
// 用空格分隔。如:GET /index.html HTTP/1.1。
// 解析要求行,把方法字段保存在method變量中。
// 讀取HTTP頭第1行:GET/index.php HTTP 1.1
numchars = get_line(client, buf, sizeof(buf));
i = 0; j = 0;
// 把客戶(hù)真?zhèn)€要求方法存到method數(shù)組
while (!ISspace(buf[i]) && (i < sizeof(method) - 1))
{
method[i] = buf[i];
i++;
}
j=i;
method[i] = '\0';
// 只能辨認(rèn)get和post
if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
{
unimplemented(client);
return;
}
// POST的時(shí)候開(kāi)啟cgi
if (strcasecmp(method, "POST") == 0)
cgi = 1;
// 解析并保存要求的URL(如有問(wèn)號(hào),也包括問(wèn)號(hào)及以后的內(nèi)容)
i = 0;
// 跳過(guò)空白字符
while (ISspace(buf[j]) && (j < numchars))
j++;
// 從緩沖區(qū)中把URL讀取出來(lái)
while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < numchars))
{
// 存在url
url[i] = buf[j];
i++; j++;
}
url[i] = '\0'; // 保存URL
// 先處理如果是GET要求的情況
// 如果是get方法,要求參數(shù)和對(duì)應(yīng)的值附加在URL后面,利用1個(gè)問(wèn)號(hào)(“?”)代表URL的結(jié)
// 尾與要求參數(shù)的開(kāi)始,傳遞參數(shù)長(zhǎng)度受限制。如index.jsp?10023,其中10023就是要傳遞
// 的參數(shù)。這段代碼將參數(shù)保存在query_string中。
if (strcasecmp(method, "GET") == 0)
{
// 待處理要求為url
query_string = url;
// 移動(dòng)指針,去找GET參數(shù),即?后面的部份
while ((*query_string != '?') && (*query_string != '\0'))
query_string++;
// 如果找到了的話(huà),說(shuō)明這個(gè)要求也需要調(diào)用腳本來(lái)處理
// 此時(shí)就把要求字符串單獨(dú)抽取出來(lái)
// GET方法特點(diǎn),?后面為參數(shù)
if (*query_string == '?')
{
// 開(kāi)啟cgi
cgi = 1;
// query_string指針指向的是真實(shí)的要求參數(shù)
*query_string = '\0';
query_string++;
}
}
// 保存有效的url地址并加上要求地址的主頁(yè)索引。默許的根目錄是htdocs下
// 這里是做以下路徑拼接,由于url字符串以'/'開(kāi)頭,所以不用拼接新的分割符
// 格式化url到path數(shù)組,html文件都早htdocs中
sprintf(path, "htdocs%s", url);
// 如果訪(fǎng)問(wèn)路徑的最后1個(gè)字符時(shí)'/',就為其補(bǔ)全,即默許訪(fǎng)問(wèn)index.html
if (path[strlen(path) - 1] == '/')
strcat(path, "index.html");
// 訪(fǎng)問(wèn)要求的文件,如果文件不存在直接返回,如果存在就調(diào)用CGI程序來(lái)處理
// 根據(jù)路徑找到對(duì)應(yīng)文件
if (stat(path, &st) == ⑴) {
// 如果不存在,就把剩下的要求頭從緩沖區(qū)中讀出去
// 把所有headers的信息都拋棄
while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */
numchars = get_line(client, buf, sizeof(buf));
// 然后返回1個(gè)404毛病,即回應(yīng)客戶(hù)端找不到
not_found(client);
}
else
{
// 如果文件存在但卻是個(gè)目錄,則繼續(xù)拼接路徑,默許訪(fǎng)問(wèn)這個(gè)目錄下的index.html
if ((st.st_mode & S_IFMT) == S_IFDIR)
strcat(path, "/index.html");
// 如果文件具有可履行權(quán)限,就履行它
// 如果需要調(diào)用CGI(CGI標(biāo)志位置1)在調(diào)用CGI之前有1段是對(duì)用戶(hù)權(quán)限的判斷,對(duì)應(yīng)
// 含義以下:S_IXUSR:用戶(hù)可以履行
// S_IXGRP:組可以履行
// S_IXOTH:其它人可以履行
if ((st.st_mode & S_IXUSR) ||
(st.st_mode & S_IXGRP) ||
(st.st_mode & S_IXOTH) )
cgi = 1;
// 不是cgi,直接把服務(wù)器文件返回,否則履行cgi
if (!cgi)
serve_file(client, path);
else
execute_cgi(client, path, method, query_string);
}
// 斷開(kāi)與客戶(hù)真?zhèn)€連接(HTTP特點(diǎn):無(wú)連接)
close(client);
}
4)execute_cgi()函數(shù)
此函數(shù)履行流程以下:
void execute_cgi(int client, const char *path,
const char *method, const char *query_string)
{
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 = ⑴;
// 首先需要根據(jù)要求是Get還是Post,來(lái)分別進(jìn)行處理
buf[0] = 'A'; buf[1] = '\0';
// 如果是Get,那末就疏忽剩余的要求頭
if (strcasecmp(method, "GET") == 0)
// 把所有的HTTP header讀取并拋棄
while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */
numchars = get_line(client, buf, sizeof(buf));
// 如果是Post,那末就需要讀出要求長(zhǎng)度即Content-Length
else if (strcasecmp(method, "POST") == 0) /*POST*/
{
// 對(duì)POST的HTTP要求中找出content_length
numchars = get_line(client, buf, sizeof(buf));
while ((numchars > 0) && strcmp("\n", buf))
{
// 使用\0進(jìn)行分割
buf[15] = '\0';
// HTTP要求的特點(diǎn)
if (strcasecmp(buf, "Content-Length:") == 0)
content_length = atoi(&(buf[16]));
numchars = get_line(client, buf, sizeof(buf));
}
// 如果要求長(zhǎng)度不合法(比如根本就不是數(shù)字),那末就報(bào)錯(cuò),即沒(méi)有找到content_length
if (content_length == ⑴) {
// 毛病要求
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;
}
// fork本身,生成兩個(gè)進(jìn)程
if ( (pid = fork()) < 0 ) { // 復(fù)制1個(gè)線(xiàn)程
// 毛病處理
cannot_execute(client);
return;
}
sprintf(buf, "HTTP/1.0 200 OK\r\n");
send(client, buf, strlen(buf), 0);
// 子進(jìn)程要調(diào)用CGI腳本
if (pid == 0) /* child: CGI script */
{
// 環(huán)境變量緩沖區(qū),會(huì)存在溢出風(fēng)險(xiǎn)
char meth_env[255];
char query_env[255];
char length_env[255];
// 重定向管道
// 把父進(jìn)程讀寫(xiě)管道的描寫(xiě)符分別綁定到子進(jìn)程的標(biāo)準(zhǔn)輸入和輸出
// dup2功能與freopen()函數(shù)類(lèi)似
dup2(cgi_output[1], STDOUT); // 把STDOUT重定向到cgi_output的寫(xiě)入端
dup2(cgi_input[0], STDIN); // 把STDIN重定向到cgi_input的讀取端
// 關(guān)閉沒(méi)必要要的描寫(xiě)符
close(cgi_output[0]); // 關(guān)閉cgi_inout的寫(xiě)入端和cgi_output的讀取端
close(cgi_input[1]);
// 服務(wù)器設(shè)置環(huán)境變量,即request_method的環(huán)境變量
// 設(shè)置基本的CGI環(huán)境變量,要求類(lèi)型、參數(shù)、長(zhǎng)度之類(lèi)
sprintf(meth_env, "REQUEST_METHOD=%s", method);
putenv(meth_env);
if (strcasecmp(method, "GET") == 0) {
// 設(shè)置query_string的環(huán)境變量
sprintf(query_env, "QUERY_STRING=%s", query_string);
putenv(query_env);
}
else { /* POST */
// 設(shè)置content_length的環(huán)境變量
sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
putenv(length_env);
}
// 用execl運(yùn)行cgi程序
execl(path, NULL);
exit(0);
} else { /* parent */
// 父進(jìn)程代碼
// 關(guān)閉cgi_input的讀取端和cgi_output的寫(xiě)入端
close(cgi_output[1]);
close(cgi_input[0]);
// 對(duì)Post要求,要直接write()給子進(jìn)程
// 這模樣進(jìn)程所調(diào)用的腳本就能夠從標(biāo)準(zhǔn)輸入獲得Post數(shù)據(jù)
if (strcasecmp(method, "POST") == 0)
// 接收POST過(guò)來(lái)的數(shù)據(jù)
for (i = 0; i < content_length; i++) {
recv(client, &c, 1, 0);
// 把POST數(shù)據(jù)寫(xiě)入cgi_input,現(xiàn)在重定向到STDIN
write(cgi_input[1], &c, 1);
}
// 然后父進(jìn)程再?gòu)妮敵龉艿览锩孀x出所有結(jié)果,返回給客戶(hù)端
while (read(cgi_output[0], &c, 1) > 0)
send(client, &c, 1, 0);
// 關(guān)閉管道
close(cgi_output[0]);
close(cgi_input[1]);
// 最后等待子進(jìn)程結(jié)束,即等待子進(jìn)程
waitpid(pid, &status, 0);
}
}
參考文獻(xiàn)
http://armsword.com/2014/10/29/tinyhttpd-code-analyse/
http://blog.csdn.net/jcjc918/article/details/42129311
http://techlog.cn/article/list/10182680
上一篇 隨機(jī)采樣和隨機(jī)模擬:吉布斯采樣Gibbs Sampling實(shí)現(xiàn)文檔分類(lèi)
下一篇 Bootstrap UI 編輯器哪家強(qiáng)?推薦以下最好的15款 【快速GUI利器,一般人我不告訴他!】