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