## 一、問題引入 通過 **Tinyhttpd:運行測試【1】 和 抓包分析【2】**,基本完成了對程式的功能測試和通信原理。此時可以進一步對源碼進行分析,本文不考慮代碼一行一行的分析,僅對關鍵部分代碼解析。 ## 二、解決過程 ### 2-1 main()函數 主函數主要創建http的監聽套接字 ...
一、問題引入
通過 Tinyhttpd:運行測試【1】 和 抓包分析【2】,基本完成了對程式的功能測試和通信原理。此時可以進一步對源碼進行分析,本文不考慮代碼一行一行的分析,僅對關鍵部分代碼解析。
二、解決過程
2-1 main()函數
主函數主要創建http的監聽套接字,等待客戶端的連接。一旦有新客戶端連接http,則創建一個新線程與客戶端通信,而主線程(即main函數)繼續等待客戶端的連接。
int main(void)
{
int server_sock = -1;
u_short port = 10080;
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)
{
client_sock = accept(server_sock,(struct sockaddr *) &client_name, &client_name_len);
if (client_sock == -1)
error_die("accept");
/* accept_request(&client_sock); */
if (pthread_create(&newthread , NULL, (void *)accept_request, (void *)(intptr_t)client_sock) != 0)
perror("pthread_create");
}
close(server_sock);
return(0);
}
2-2 startup()函數
如果熟悉套接字編程,那麼肯定對上面的代碼肯定很眼熟。該函數的功能就是伺服器在指定埠創建監聽套接字。但同時對監聽的埠做了冗餘處理,若指定的埠為0,則動態的申請一個埠號。
int startup(u_short *port)
{
int httpd = 0;
int on = 1;
struct sockaddr_in name;
httpd = socket(PF_INET, SOCK_STREAM, 0);
if (httpd == -1)
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);
if ((setsockopt(httpd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))) < 0) /* set port reuse */
{
error_die("setsockopt failed");
}
if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
error_die("bind");
if (*port == 0) /* if dynamically allocating a port */
{
socklen_t namelen = sizeof(name);
if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1)
error_die("getsockname");
*port = ntohs(name.sin_port);
}
if (listen(httpd, 5) < 0)
error_die("listen");
return(httpd);
}
2-3 accept_request()函數
該線程回調函數是整個程式最核心、最關鍵的部分,它包含瞭如何解析客戶端發送的http request和服務區根據客戶端的請求如何發送http respond。
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
* program */
char *query_string = NULL;
numchars = get_line(client, buf, sizeof(buf));
i = 0; j = 0;
while (!ISspace(buf[i]) && (i < sizeof(method) - 1))
{
method[i] = buf[i];
i++;
}
j=i;
method[i] = '\0';
if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
{
unimplemented(client);
return;
}
if (strcasecmp(method, "POST") == 0)
cgi = 1;
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';
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(path, "htdocs%s", url);
if (path[strlen(path) - 1] == '/')
strcat(path, "index.html");
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;
if (!cgi)
serve_file(client, path);
else
execute_cgi(client, path, method, query_string);
}
close(client);
}
解析第一行
http 請求行包括三個欄位:請求方法、URL、協議版本。
解析請求方法:僅支持GET和POST。若為POST時,cgi標誌位置1
numchars = get_line(client, buf, sizeof(buf));
i = 0; j = 0;
while (!ISspace(buf[i]) && (i < sizeof(method) - 1))
{
method[i] = buf[i];
i++;
}
j=i;
method[i] = '\0';
if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
{
unimplemented(client);
return;
}
if (strcasecmp(method, "POST") == 0)
cgi = 1;
解析URL,併在GET方法時,將URL中的第一個?
處替換為\0
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';
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++;
}
}
判斷URL是文件還是文件夾,若為文件夾,則拼接相對路徑path htdocs/xxx/.../xxx/index.html
。
判斷path是否存在,若不存在,先清空讀緩存,執行函數 not_found()
;若存在且cgi標誌位為1,執行函數 execute_cgi()
,若存在且cgi不為1,執行函數 serve_file()
sprintf(path, "htdocs%s", url);
if (path[strlen(path) - 1] == '/')
strcat(path, "index.html");
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;
if (!cgi)
serve_file(client, path);
else
execute_cgi(client, path, method, query_string);
}
三、反思總結
整體理解下來,httpd.c可以解析來自客戶端的GET和POST請求。
- GET方法
可以處理請求格式:
GET / HTTP/1.1 \r\n
GET /index.html \r\n
GET /color.cgi \r\n
GET /color.cgi?color=pink \r\n
- POST方法
可以處理請求格式:
POST /color.cgi HTTP/1.1 \r\n
+"color" = "green"