阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

Tiny Web服务器代码分析

102次阅读
没有评论

共计 6460 个字符,预计需要花费 17 分钟才能阅读完成。

《深入理解计算机系统》中开发了一个小但是功能齐全的称为 Tiny 的 web 服务器,这里是 Tiny 服务器的源码解析。

1.Tiny 的 main 程序

Tiny 是一个迭代服务器,通过命令行中传递来的端口值,调用 Open_listenfd()函数打开一个监听套接字,然后 Tiny 执行无限循环:服务器阻塞在 accept,等待监听描述符 listenfd 上的连接请求,当服务器从 accept 返回 connfd,表明已经与客户端建立起了连接,执行事务,并关闭连接它的那一端,进行下一次循环。

#include "csapp.h"

void doit(int fd);
void read_requesthdrs(rio_t *rp);
int parse_uri(char *uri, char *filename, char *cgiargs);
void serve_static(int fd, char *filename, int filesize);
void get_filetype(char *filename, char *filetype);
void serve_dynamic(int fd, char *filename, char *cgiargs);
void clienterror(int fd, char *cause, char *errnum,
                char *shorting,char *longmsg);

int main(int argc,char *argv[])
{int listenfd,connfd,port,clientlen;
    struct sockaddr_in clientaddr;

    if(argc != 2)
    {fprintf(stderr,"usage: %s <port>\n",argv[0]);
        exit(0);
    }

    port = atoi(argv[1]);

    listenfd = Open_listenfd(port);
    while(1)
    {clientlen = sizeof(clientaddr);
        connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
        doit(connfd);
        Close(connfd);
    }
}

2.doit()处理 HTTP 事务

先了解一下 HTTP 请求的组成。一个 HTTP 请求是由一个 请求行 ,后面跟随零个或者更多个 请求报头,再跟随一个空的文本行来终止报头列表。
HTTP 请求行的格式如下。

<method><uri><version>

HTTP 支持许多的方法,包括 GET、POST、OPTIONS、HEAD、PUT、DELETE 和 TRACE。目前 Tiny 只支持 GET 方法,GET 方法指导服务器生成和返回 URI 标识的内容。URI 是 URL 的后缀,包括文件名和参数。版本字段表明了该请求遵循的 HTTP 版本,有 HTTP/1.0 和 HTTP/1.1。
doit()处理 HTTP 事务。
读取并解析请求行 , 代码中使用 Rio_readlineb() 从 fd 读取一行数据到 buf,然后分别写入变量 method,uri 和 version。
Tiny 不使用请求报头中的任何信息,使用 read_requesthdrs()函数忽略掉报头的信息。
从请求中提取 URI 信息 ,使用 parse_uri() 来从 URI 中提取文件名和请求参数,并返回值标识静态内容或者动态内容。使用 stat()获取文件的状态并将状态保存到 sbuf 中,执行成功返回 0,如果执行失败则会返回 - 1 表示该文件在磁盘上不存在。
如果 请求的是静态内容 ,需要验证该文件是一般文件(st_mode == S_ISREG) 并且我们有读权限。如果是我们就向客户端提供静态内容。相似的如果 请求的是动态内容,需要验证该文件是可执行文件,如果是我们就向客户端提供动态内容。

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);
    sscanf(buf, "%s %s %s", method, uri, version);
    if(strcasecmp(method,"GET"))
    {clienterror(fd, method, "501","Not Implemented",
            "Tiny does not implement this method");
        return;
    }
    read_requesthdrs(&rio);

    /*prase URI from GET request*/
    is_static = parse_uri(uri, filename, cgiargs);
    if(stat(filename, &sbuf) < 0)
    {clienterror(fd, filename, "404","Not Found",
            "Tiny couldn't find this file");
        return;
    }
    if(is_static)//server static content
    {if(!(S_ISREG(sbuf.st_mode) || !(S_IRUSR & sbuf.st_mode)))
        {clienterror(fd, filename, "403","Forbidden",
                "Tiny couldn't read the file");
            return;
        }
        serve_static(fd, filename, sbuf.st_size);
    }
    else//server dynamic content
    {if(!(S_ISREG(sbuf.st_mode) || !(S_IXUSR & sbuf.st_mode)))
        {clienterror(fd, filename, "403","Forbidden",
                "Tiny couldn't run the CGI program");
            return;
        }
        serve_dynamic(fd, filename, cgiargs);   
    }
}

3.clienterror()用于检查一些错误

先了解一下 HTTP 响应行的组成。HTTP 响应和 HTTP 请求是相似的。一个 HTTP 响应的组成有:一个响应行,后面跟随零个或者更多的响应报头,再跟随一个终止报头的空行,再跟随响应主体。响应行的格式是:

<version><status code><status message>

版本字段描述了响应所遵循的 HTTP 版本。状态码是一个三位的正整数,指明对请求的处理。状态消息给出与错误代码等价的英文描述。
clienterror()发送一个 HTTP 响应报文个给客户端,在响应行中包含状态码和状态消息,响应主体包含一个 HTML 文件,来向用户解释错误。

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));
}

4.read_requesthdrs()来跳过请求报头的信息,直到遇见表示报头结束的空文本行。

void read_requesthdrs(rio_t *rp)
{char buf[MAXLINE];

    Rio_readlineb(rp, buf, MAXLINE);
    while(strcmp(buf, "\r\n"))
    {Rio_readlineb(rp, buf, MAXLINE);
        printf("%s", buf);
    }
    return;
}

5.parse_uri()解析 URI 参数

parse_uri()将 URI 解析为一个文件名和一个可选的 CGI 参数字符串。如果是静态内容,我们将清除 CGI 参数串(即将 cgiargs 置空),然后将 URI 转换为一个相对的 UNIX 路径名,例如./home.html。如果 URI 是以 / 结尾的则要在后面添加默认文件名 home.html。如果是动态内容,URI 中的文件名和 CGI 参数是以? 分割,以此可以抽出 CGI 参数和文件名。

int parse_uri(char *uri, char *filename, char *cgiargs)
{char *ptr;

    if(!strstr(uri, "cgi-bin"))//static content
    {strcpy(cgiargs, "");
        strcpy(filename, ".");
        strcat(filename, uri);
        if(uri[strlen(uri)-1] == '/')
            strcat(uri, "home.html");
        return 1;
    }
    else
    {ptr = index(uri, '?');
        if(ptr)
        {strcpy(cgiargs, ptr+1);
            *ptr = '\0';
        }
        else
            strcpy(cgiargs, "");
        strcpy(filename, ".");
        strcat(filename, uri);
        return 0;
    }
}

6.serve_static()处理静态内容

Tiny 提供四种不同的静态内容:HTML 文件、无格式文本文件、编码为 GIF 和 JPEG 格式的图片。
serve_static()函数发送一个 HTTP 响应,响应的主体包括一个本地文件的内容。首先,我们检查文件名的后缀来判断文件类型,并在响应报头的 Content-type 字段中显示出来。随后我们发送响应行和响应报头给客户端,用一个空行来终止报头。
然后我们以只读的方式打开所请求的文件获得文件句柄 srcfd,并用 mmap 函数将文件映射到虚拟存储器空间,代码中是将 srcfd 指向的文件的前 filesize 个字节映射到一个地址从 srcp 开始的只读私有虚拟存储器区域。一旦映射成功我们就可以通过 srcp 来操作文件不再需要文件句柄了,所以我们关闭 srcfd。然后我们使用 Rio_writen()将文件传送到客户端。这时静态内容的处理操作已经完成了,我们释放 srcp 的虚拟存储器映射。

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);
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    sprintf(buf, "%sServer: Tiny Web Server\r\n", buf);
    sprintf(buf, "%sContent-lenght: %d\r\n", buf, filesize);
    sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, filetype);
    Rio_writen(fd, buf, strlen(buf));
    /*Send response body to client*/
    srcfd = Open(filename, O_RDONLY, 0);
    srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE,srcfd,0);
    close(srcfd);
    Rio_writen(fd, srcp, filesize);
    Munmap(srcp, filesize);
}

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");
}

7.serve_dynamic()处理动态内容

Tiny 通过派生一个子进程并在子进程中运行一个 CGI 程序,来提供各种类型的动态内容。
程序中首先发送一个表明成功的响应行,同时还包括带有信息的 Server 报头给客户端。CGI 程序负责发送响应的剩余部分。
然后我们派生一个子进程,子进程用来自请求 URI 的 CGI 参数初始化 QUERY_STRING 环境变量,CGI 程序会通过这个变量获取 CGI 参数值。接下来,子进程重定向标准输出到已连接文件描述符,然后加载并运行 CGI 程序。因为 CGI 程序运行在子进程的上下文中,它能够访问所有在调用 execve 函数之前就存在的打开文件和环境变量。因此 CGI 程序写到标准输出上的数据都会直接送到客户端进程,不会受到来自父进程的干涉。旗舰父进程阻塞在 wait()的调用中,当子进程终止时,回收操作系统分配给子进程的资源。

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)
    {setenv("QUERY_STRING", cgiargs, 1);
        Dup2(fd, STDOUT_FILENO);
        Execve(filename, emptylist,environ);
    }
    Wait(NULL);
}

深入理解计算机系统(原书第 2 版) PDF 清晰中文版  http://www.linuxidc.com/Linux/2015-03/114720.htm

让 Tiny 服务器运行起来  http://www.linuxidc.com/Linux/2015-04/116046.htm

本文永久更新链接地址:http://www.linuxidc.com/Linux/2015-04/116050.htm

正文完
星哥说事-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2022-01-20发表,共计6460字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中