[Web Server](一)Tiny Web Server分析

来源:互联网 发布:淘宝代理免费网上加盟 编辑:程序博客网 时间:2024/06/06 08:40

写在前面:

计划写一个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在处理一个客户请求的时候无法接受别的客户,这在实际应用中是肯定不允许的。解决方法有

  1. 多进程:accept 之后 fork,父进程继续 accept,子进程来处理这 connfd。这样在高并发下,存在几个问题:
    问题1:每次来一个连接都 fork 开销太大。可以查一下调用 fork 时系统具体做了什么,注意一下复制父进程页表的操作。
    问题2:并发量上来后,进程调度器压力太大,进程切换开销非常大。
    问题3:高负载下,消耗太多内存,此外高并发下,进程间通信带来的开销也不能忽略。

  2. 多线程:accept 之后开线程来处理连接。这样解决了 fork 的问题,但是问题2和3还是无法解决。

  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的设计方案。

原创粉丝点击