写在前面:
计划写一个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在处理一个客户请求的时候无法接受别的客户,这在实际应用中是肯定不允许的。解决方法有
- 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包健壮地读写 。接下来分别解析出 method 、uri 、version,TINY只支持 GET 方法,如果是其他的方法,则调用
- rio_readlineb
函数 返回一个错误信息。
- 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 的 uri 都认为是对动态内容的请求。默认的文件名是 ./home.index。
- ./cgi-bin/
如果是静态内容,会清楚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格式的图片。
函数发送一个HTTP响应,主体是所请求的本地文件的内容。首先根据文件名后缀来判断文件类型,并且发送响应行和响应报头,用空行来终止报头。
- serve_static
接下来是发送响应主体。这里用到了
函数,将被请求文件映射到一个虚拟存储空间,此后就可以通过指针来操作这个文件,最后释放映射的虚拟存储器区域。关于 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
,当有数据可读时返回数据,如果没有数据可读就返回-1并设置 error 为 EAGAIN。那如何知道 fd 上什么时候有数据可读呢? 总不能一直在用户态做轮询吧……因此要用到 I/O多路复用,即事件驱动的方式,所以推荐方式,非阻塞和IO复用联合起来。
- non-blocking I/O
所以目前计划实现是这样的:IO多路复用 + non-blocking + threadpool的设计方案。
来源: http://blog.csdn.net/tanswer_/article/details/78574402