写在前面:
计划写一个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], cgiargs[MAXLINE];
rio_t rio;
/* Read request line and headers */
Rio_readinitb(&rio, fd);
Rio_readlineb(&rio, buf, MAXLINE); //line:netp:doit:readrequest
sscanf(buf, "%s %s %s", method, uri, version); //line:netp:doit:parserequest
if (strcasecmp(method, "GET")) { //line:netp:doit:beginrequesterr
clienterror(fd, method, "501", "Not Implemented",
"Tiny does not implement this method");
return;
} //line:netp:doit:endrequesterr
read_requesthdrs(&rio); //line:netp:doit:readrequesthdrs
/* Parse URI from GET request */
is_static = parse_uri(uri, filename, cgiargs); //line:netp:doit:staticcheck
if (stat(filename, &sbuf) < 0) { //line:netp:doit:beginnotfound
clienterror(fd, filename, "404", "Not found",
"Tiny couldn't find this file");
return;
} //line:netp:doit:endnotfound
if (is_static) { /* Serve static content */
if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) { //line:netp:doit:readable
clienterror(fd, filename, "403", "Forbidden",
"Tiny couldn't read the file");
return;
}
serve_static(fd, filename, sbuf.st_size); //line:netp:doit:servestatic
}
else { /* Serve dynamic content */
if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) { //line:netp:doit:executable
clienterror(fd, filename, "403", "Forbidden",
"Tiny couldn't run the CGI program");
return;
}
serve_dynamic(fd, filename, cgiargs); //line:netp:doit:servedynamic
}
}
/* $end doit */
(三)clienterror 函数
TINY没有完整的错误处理,但是可以检查一些明显的错误,并把它发送到客户端。在响应行中包含了相应的状态码和状态信息,响应主体中包含一个 HTML 文件,向浏览器用户解释错误。
/* $begin clienterror */
void clienterror(int fd, char *cause, char *errnum,
char *shortmsg, char *longmsg)
{
char buf[MAXLINE], body[MAXBUF];
/* Build the HTTP response body */
sprintf(body, "<html><title>Tiny Error</title>");
sprintf(body, "%s<body bgcolor=""ffffff"">\r\n", body);
sprintf(body, "%s%s: %s\r\n", body, errnum, shortmsg);
sprintf(body, "%s<p>%s: %s\r\n", body, longmsg, cause);
sprintf(body, "%s<hr><em>The Tiny Web server</em>\r\n", body);
/* Print the HTTP response */
sprintf(buf, "HTTP/1.0 %s %s\r\n", errnum, shortmsg);
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Content-type: text/html\r\n");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Content-length: %d\r\n\r\n", (int)strlen(body));
Rio_writen(fd, buf, strlen(buf));
Rio_writen(fd, body, strlen(body));
}
/* $end clienterror */
(四)read_requesthdrs 函数
TINY不使用请求报头中的任何信息,此函数读取并在服务器端打印请求报头中的内容。终止请求报头的空文本行是由回车和换行符对组成的。
/* $begin read_requesthdrs */
void read_requesthdrs(rio_t *rp)
{
char buf[MAXLINE];
Rio_readlineb(rp, buf, MAXLINE);
while(strcmp(buf, "\r\n")) { //line:netp:readhdrs:checkterm
Rio_readlineb(rp, buf, MAXLINE);
printf("%s", buf);
}
return;
}
/* $end read_requesthdrs */
(五)parse_uri 函数
Tiny假设静态内容的主目录就是当前目录,可执行文件的主目录是 ./cgi-bin/
。任何包含字符串 cgi-bin 的 uri 都认为是对动态内容的请求。默认的文件名是 ./home.index。
如果是静态内容,会清楚CGI参数串,将 uri 转换为一个相对路径名,如果 uri 是以 / 结尾的,那么补充上默认的文件名。
如果是动态内容,提取出 CGI 参数,并把剩下的部分转换为相对文件名。
/* $begin parse_uri */
int parse_uri(char *uri, char *filename, char *cgiargs)
{
char *ptr;
if (!strstr(uri, "cgi-bin")) { /* Static content */ //line:netp:parseuri:isstatic
strcpy(cgiargs, ""); //line:netp:parseuri:clearcgi
strcpy(filename, "."); //line:netp:parseuri:beginconvert1
strcat(filename, uri); //line:netp:parseuri:endconvert1
if (uri[strlen(uri)-1] == '/') //line:netp:parseuri:slashcheck
strcat(filename, "home.html"); //line:netp:parseuri:appenddefault
return 1;
}
else { /* Dynamic content */ //line:netp:parseuri:isdynamic
ptr = index(uri, '?'); //line:netp:parseuri:beginextract
if (ptr) {
strcpy(cgiargs, ptr+1);
*ptr = '\0';
}
else
strcpy(cgiargs, ""); //line:netp:parseuri:endextract
strcpy(filename, "."); //line:netp:parseuri:beginconvert2
strcat(filename, uri); //line:netp:parseuri:endconvert2
return 0;
}
}
/* $end parse_uri */
(六)serve_static 函数
TINY提供四种不同类型的静态内容:HTML 文件、无格式的文本文件,以及编码为GIF和JPEG格式的图片。
serve_static
函数发送一个HTTP响应,主体是所请求的本地文件的内容。首先根据文件名后缀来判断文件类型,并且发送响应行和响应报头,用空行来终止报头。
接下来是发送响应主体。这里用到了 mmap
函数,将被请求文件映射到一个虚拟存储空间,此后就可以通过指针来操作这个文件,最后释放映射的虚拟存储器区域。关于 mmap 请参考:认真分析mmap:是什么 为什么 怎么用。
/*
* serve_static - copy a file back to the client
*/
/* $begin serve_static */
void serve_static(int fd, char *filename, int filesize)
{
int srcfd;
char *srcp, filetype[MAXLINE], buf[MAXBUF];
/* Send response headers to client */
get_filetype(filename, filetype); //line:netp:servestatic:getfiletype
sprintf(buf, "HTTP/1.0 200 OK\r\n"); //line:netp:servestatic:beginserve
sprintf(buf, "%sServer: Tiny Web Server\r\n", buf);
sprintf(buf, "%sContent-length: %d\r\n", buf, filesize);
sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, filetype);
Rio_writen(fd, buf, strlen(buf)); //line:netp:servestatic:endserve
/* Send response body to client */
srcfd = Open(filename, O_RDONLY, 0); //line:netp:servestatic:open
srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);//line:netp:servestatic:mmap
Close(srcfd); //line:netp:servestatic:close
Rio_writen(fd, srcp, filesize); //line:netp:servestatic:write
Munmap(srcp, filesize); //line:netp:servestatic:munmap
}
/*
* get_filetype - derive file type from file name
*/
void get_filetype(char *filename, char *filetype)
{
if (strstr(filename, ".html"))
strcpy(filetype, "text/html");
else if (strstr(filename, ".gif"))
strcpy(filetype, "image/gif");
else if (strstr(filename, ".jpg"))
strcpy(filetype, "image/jpeg");
else
strcpy(filetype, "text/plain");
}
/* $end serve_static */
(七)serve_dynamic 函数
TINY 派生一个子进程来运行一个 CGI 程序,来提供动态内容。
此函数一开始先向客户端发送表明成功的响应行。
然后子进程用来自请求 uri 的 CGI 参数初始化 QUERY_STRING 环境变量。
一个 CGI 程序将它的动态内容发送到标准输出,在子进程加载并运行 CGI 程序之前,使用 dup2 函数将标准输出重定向到和客户端相关联的已连接描述符。这样任何 CGI 程序写到标准输出的东西都会直接送到客户端。
然后加载并运行 CGI 程序,其间父进程阻塞在对 wait 的调用中,等待当子进程终止的时候,回收操作系统分配给子进程的资源。
/*
* serve_dynamic - run a CGI program on behalf of the client
*/
/* $begin serve_dynamic */
void serve_dynamic(int fd, char *filename, char *cgiargs)
{
char buf[MAXLINE], *emptylist[] = { NULL };
/* Return first part of HTTP response */
sprintf(buf, "HTTP/1.0 200 OK\r\n");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Server: Tiny Web Server\r\n");
Rio_writen(fd, buf, strlen(buf));
if (Fork() == 0) { /* child */ //line:netp:servedynamic:fork
/* Real server would set all CGI vars here */
setenv("QUERY_STRING", cgiargs, 1); //line:netp:servedynamic:setenv
Dup2(fd, STDOUT_FILENO); /* Redirect stdout to client */ //line:netp:servedynamic:dup2
Execve(filename, emptylist, environ); /* Run CGI program */ //line:netp:servedynamic:execve
}
Wait(NULL); /* Parent waits for and reaps child */ //line:netp:servedynamic:wait
}
/* $end serve_dynamic */
上面是对TINY的代码的分析,只用了几百行C代码就实现了一个简单但是有功效的Web服务器,它既可以提供静态内容,又可以提供动态内容。但是构建一个功能齐全并且健壮的 Web 服务器并不是那么简单,所以还有很多细节要考虑。
我们上面已经说过用线程池可以解决几个问题,但是还要考虑一下,在处理长连接的时候,当一个线程处理完一批数据后,会再次 read,但是可能没有数据,因为默认情况下 fd 是阻塞的,所以这个线程就会被阻塞,当阻塞的线程多了,更多任务来之后,还是无法处理。此时可以把 blocking I/O
换成 non-blocking I/O
,当有数据可读时返回数据,如果没有数据可读就返回-1并设置 error 为 EAGAIN。那如何知道 fd 上什么时候有数据可读呢? 总不能一直在用户态做轮询吧……因此要用到 I/O多路复用,即事件驱动的方式,所以推荐方式,非阻塞和IO复用联合起来。
所以目前计划实现是这样的:IO多路复用 + non-blocking + threadpool的设计方案。