tinyhttpd是一个demo版的服务器。代码几百行。源码分析在http://suo.im/6bkZlt。从中可用一窥服务器的基础原理。他采用的是一个请求新开一个线程处理的方式。里面涉及了多进程、多线程、进程间通信等知识。 我们从main函数开始分析。
int main(void)
{
int server_sock = -1;
u_short port = 0;
int client_sock = -1;
struct sockaddr_in client_name;
int client_name_len = sizeof(client_name);
pthread_t newthread;
// 拿到一个监听的文件描述符
server_sock = startup(&port);
while (1)
{
// 等待连接到来
client_sock = accept(server_sock,
(struct sockaddr *)&client_name,
&client_name_len);
// 每一个连接用一个线程处理,主函数是accept_request
if (pthread_create(&newthread , NULL, accept_request, client_sock) != 0)
perror("pthread_create");
}
// 服务器退出
close(server_sock);
return(0);
}
main函数的逻辑很简单。首先调用startup拿到一个监听型的文件描述符。然后启动服务器。阻塞在accept等待请求的到来。我们看看startup。
// 传统的服务器启动流程
int startup(u_short *port)
{
int httpd = 0;
struct sockaddr_in name;
// 拿到一个用于监听的文件描述符
httpd = socket(PF_INET, SOCK_STREAM, 0);
memset(&name, 0, sizeof(name));
name.sin_family = AF_INET;
name.sin_port = htons(*port);
name.sin_addr.s_addr = htonl(INADDR_ANY);
// 绑定一个地址到socket,没有的话ip是本机地址,端口随机
if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
error_die("bind");
// port等于0,系统会随机分配一个端口(bind函数里实现)。这里通过文件描述符拿到系统随机分配的端口
if (*port == 0) /* if dynamically allocating a port */
{
int namelen = sizeof(name);
// 获取socket绑定的地址信息
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);
}
startup函数是经典的socket编程流程。这时候服务器已经启动。等待请求的到来。我们回忆main函数里的accept函数。他返回的是一个和客户端通信的文件描述符。然后新开一个线程,线程里执行accept_request函数。把这个描述符传给线程,让他处理。accept_request函数的主要逻辑如下。
// 文件路径htdocs下
sprintf(path, "htdocs%s", url);
// 最后一个字符是/说明是个目录,则取该目录下的index.html文件
if (path[strlen(path) - 1] == '/')
strcat(path, "index.html");
// 找不到该文件返回404
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");
// 是可执行文件则说明是cgi程序
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
// 执行cgi程序
execute_cgi(client, path, method, query_string);
}
主要是两个逻辑。 1 静态文件的请求,则直接读取文件内容,然后返回给客户端。线程退出。 2 执行动态脚本。 下面我们只分析2。通过执行execute_cgi函数执行动态脚本。该函数比较长,分开分析。
void execute_cgi(int client, const char *path,
const char *method, const char *query_string)
{
// 一系列简单解析http协议的逻辑
// 获取两个匿名管道
if (pipe(cgi_output) < 0) {
cannot_execute(client);
return;
}
if (pipe(cgi_input) < 0) {
cannot_execute(client);
return;
}
}
申请两个管道。
// fork进程
if ( (pid = fork()) < 0 ) {
cannot_execute(client);
return;
}
创建一个进程。我们看看父进程和子进程都做了什么事情。先看子进程
char meth_env[255];
char query_env[255];
char length_env[255];
// 先断开文件描述符1和标准输出file结构的关联,然后使1指向cgi_ouput[1]指向的file结构
dup2(cgi_output[1], 1);
dup2(cgi_input[0], 0);
// 关闭读端
close(cgi_output[0]);
// 关闭写端
close(cgi_input[1]);
// 输入参数给处理请求的cgi进程使用
sprintf(meth_env, "REQUEST_METHOD=%s", method);
putenv(meth_env);
if (strcasecmp(method, "GET") == 0) {
sprintf(query_env, "QUERY_STRING=%s", query_string);
putenv(query_env);
}
else { /* POST */
sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
putenv(length_env);
}
// 执行cgi进程
execl(path, path, NULL);
exit(0);
子进程输入关闭管道的一端,然后输入环境变量给cgi进程。然后执行cgi程序。再看父进程。
close(cgi_output[1]);
close(cgi_input[0]);
if (strcasecmp(method, "POST") == 0)
for (i = 0; i < content_length; i++) {
/*
读数据然后写入写端cgi_input[1],对端是子进程的cgi_input[0],作为子进程的标准读入,
即子进程可以读到这里写入的数据
*/
recv(client, &c, 1, 0);
write(cgi_input[1], &c, 1);
}
/*
等待子进程写入,然后返回给客户端,cgi_output[1]是子进程的标准输出端,
从cgi_output[1]写入的数据可以从cgi_output[0]读取
*/
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);
父进程同样关闭管道一端。如果是post则把客户端的body输入给子进程。然后在read函数阻塞等待子进程的输入。最后两个进程退出。整个服务器的处理过程是,每次来一个请求(假设是cgi)。新开一个处理线程。主线程继续监听。然后新开的处理线程fork出一个进程执行cgi。这时候就相当于有两个进程。父子进程互相通信完成一个客户端的处理,然后退出。