写在前面:
计划写一个Web 服务器,在小组的群博上没有找到相关的文章,自己打算从开始记录下这个过程,一是整理清楚我的构建过程,二是也能让后面的同学做一下参考。
CSAPP上网络编程那一章最后实现了一个小但是功能较齐全的Web 服务器,叫做TINY。因为只是知道HTTP协议的一些概念,还不太清楚一个Web服务器的工作流程和代码组织结构,而书上给出了 Tiny Server 的完整实现,代码非常短,只有几百行,所以自己模仿着手撸了一遍,并试着分析了代码,运行了一下,给自己一个直观的认识。源代码放在 这里,加注释的代码放在这里。接下来分析下这个Tiny Web服务器。
PS:WEB基础就不写了,自己了解下基本的概念,那么看起代码来就足够了。
CSAPP上面的例子用到的一些通用的函数都放在csapp.h
头文件中,并在csapp.c
中给出实现。我们看到的大写首字母开头的函数,是在原功能函数上面加上了错误处理,比如
pid_t Fork(void)
{
pid_t pid;
if ((pid = fork()) < 0)
unix_error("Fork error");
return pid;
}
(一) main 函数
监听命令行中传来的端口上的连接请求,通过 Open_listenfd 函数打开一个监听套接字,执行无限循环,不断接受连接请求,执行HTTP事务,执行完毕后关掉连接。
Tiny是个单线程的,Server在处理一个客户请求的时候无法接受别的客户,这在实际应用中是肯定不允许的。解决方法有
多进程:accept 之后 fork,父进程继续 accept,子进程来处理这 connfd。这样在高并发下,存在几个问题:
问题1:每次来一个连接都 fork 开销太大。可以查一下调用 fork 时系统具体做了什么,注意一下复制父进程页表的操作。
问题2:并发量上来后,进程调度器压力太大,进程切换开销非常大。
问题3:高负载下,消耗太多内存,此外高并发下,进程间通信带来的开销也不能忽略。多线程:accept 之后开线程来处理连接。这样解决了 fork 的问题,但是问题2和3还是无法解决。
- 线程池:线程数量固定。线程池简介和C++11实现 。这样可以解决以上几个问题。
int main(int argc, char **argv)
{
int listenfd, connfd, clientlen;
struct sockaddr_in clientaddr;
if(argc != 2){
fprintf(stderr, "Usage: %s <port>\n",argv[0]);
exit(1);
}
//port = atoi(argv[1]);
listenfd = Open_listenfd(argv[1]);
while(1){
clientlen = sizeof(clientaddr);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
doit(connfd);
Close(connfd);
}
}
(二)doit 函数
doit 函数处理一个 HTTP 事务。首先读取并解析请求行,用到 rio_readlineb
函数,请参考 用RIO包健壮地读写 。接下来分别解析出 method 、uri 、version,TINY只支持 GET 方法,如果是其他的方法,则调用 clienterror
函数 返回一个错误信息。
TINY不使用请求报头中的任何信息,接下来读取并忽略这些报头。
接下来解析 uri ,将 uri 解析为 文件名 和CGI 参数字符串。并得到请求的是静态内容还是动态内容。
如果没找到这个文件,那么发送一个错误信息给客户端并返回。
如果是请求静态内容,那么首先确认是普通文件并判断是否有读的权限,如果都OK,那么调用 serve_static
函数提供静态内容。类似,调用 serve_dynamic
函数提供动态内容。
/* $begin doit */
void doit(int fd)
{
int is_static;
struct stat sbuf;
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
char filename[MAXLINE], cgiarg