一、问题引入
通过 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 %dn", 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)))
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 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
解析URL,并在GET方法时,将URL中的第一个?
处替换为
i = 0;
while (ISspace(buf[j]) && (j
判断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 rn
GET /index.html rn
GET /color.cgi rn
GET /color.cgi?color=pink rn
- POST方法
可以处理请求格式:
-
POST /color.cgi HTTP/1.1 rn
+"color" = "green"
四、参考引用
EZLippi/Tinyhttpd