本文共 9706 字,大约阅读时间需要 32 分钟。
写在前面:
计划写一个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还是无法解决。
int main(int argc, char **argv){ int listenfd, connfd, clientlen; struct sockaddr_in clientaddr; if(argc != 2){ fprintf(stderr, "Usage: %s\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
函数,请参考 。接下来分别解析出 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, "Tiny Error "); sprintf(body, "%s\r\n", body); sprintf(body, "%s%s: %s\r\n", body, errnum, shortmsg); sprintf(body, "%s%s: %s\r\n", body, longmsg, cause); sprintf(body, "%s
The Tiny Web server\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 请参考:。
/* * 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的设计方案。
转载地址:http://hmuoi.baihongyu.com/