CSAPP Tiny web server源代码分析及搭建执行

1. Web基础

webclient和server之间的交互使用的是一个基于文本的应用级协议HTTP(超文本传输协议)。

一个webclient(即浏览器)打开一个到server的因特网连接,而且请求某些内容。server响应所请求的内容,然后关闭连接。浏览器读取这些内容。并把它显示在屏幕上。

对于webclient和server而言。内容是与一个MIME类型相关的字节序列。

常见的MIME类型:

MIME类型 描写叙述
text/html HTML页面
text/plain 无格式文本
image/gif GIF格式编码的二进制图像
image/jpeg JPEG格式编码的二进制图像

webserver以两种不同的方式向客服端提供内容:
1. 静态内容:取一个磁盘文件。并将它的内容返回给client
2. 动态内容:执行一个可执行文件,并将它的输出返回给client

统一资源定位符:URL

http://www.google.com:80/index.html

表示因特网主机 www.google.com 上一个称为 index.html 的HTML文件。它是由一个监听port80的Webserver所管理的。

HTTP默认port号为80

可执行文件的URL能够在文件名称后包含程序參数, “?”字符分隔文件名称和參数,而且每一个參数都用“&”字符分隔开。如:

http://www.ics.cs.cmu.edu:8000/cgi-bin/adder?

123&456

表示一个 /cgi-bin/adder 的可执行文件,带两个參数字符串为 123 和 456

确定一个URL指向的是静态内容还是动态内容没有标准的规则,常见的方法就是把全部的可执行文件都放在 cgi-bin 文件夹中

2. HTTP

HTTP标准要求每一个文本行都由一对回车和换行符来结束

(1)HTTP请求

一个HTTP请求:一个请求行(request line) 后面尾随0个或多个请求报头(request header), 再尾随一个空的文本行来终止报头

请求行: <method> <uri> <version>
HTTP支持很多方法。包含 GET,POST,PUT,DELETE,OPTIONS,HEAD,TRACE。


URI是对应URL的后缀,包含文件名称和可选參数
version 字段表示该请求所遵循的HTTP版本号

请求报头:<header name> : <header data> 为server提供了额外的信息。比如浏览器的版本号类型
HTTP 1.1中 一个IP地址的server能够是 多宿主主机。比如 www.host1.com www.host2.com 能够存在于同一server上。


HTTP 1.1 中必须有 host 请求报头,如 host:www.google.com:80 假设没有这个host请求报头,每一个主机名都仅仅有唯一IP,IP地址非常快将用尽。

(2)HTTP响应

一个HTTP响应:一个响应行(response line) 后面尾随0个或多个响应报头(response header)。再尾随一个空的文本行来终止报头,最后尾随一个响应主体(response body)

响应行:<version> <status code> <status message>
status code 是一个三位的正整数

状态代码 状态消息 描写叙述
200 成功 处理请求无误
301 永久移动 内容移动到位置头中指明的主机上
400 错误请求 server不能理解请求
403 禁止 server无权訪问所请求的文件
404 未发现 server不能找到所请求的文件
501 未实现 server不支持请求的方法
505 HTTP版本号不支持 server不支持请求的版本号

两个最重要的响应报头:
Content-Type 告诉client响应主体中内容的MIME类型
Content-Length 指示响应主体的字节大小
响应主体中包含着被请求的内容。

3.服务动态内容

(1) client怎样将程序參数传递给server

GET请求的參数在URI中传递, “?”字符分隔了文件名称和參数,每一个參数都用一个”&”分隔开,參数中不同意有空格,必须用字符串“%20”来表示
HTTP POST请求的參数是在请求主体中而不是 URI中传递的

(2)server怎样将參数传递给子进程

GET /cgi-bin/adder?123&456 HTTP/1.1

它调用 fork 来创建一个子进程。并调用 execve 在子进程的上下文中执行 /cgi-bin/adder 程序

在调用 execve 之前,子进程将CGI环境变量 QUERY_STRING 设置为”123&456”, adder 程序在执行时能够用unix getenv 函数来引用它

(3)server怎样将其它信息传递给子进程

环境变量 描写叙述
QUERY_STRING 程序參数
SERVER_PORT 父进程侦听的port
REQUEST_METHOD GET 或 POST
REMOTE_HOST client的域名
REMOTE_ADDR client的点分十进制IP地址
CONTENT_TYPE 仅仅对POST而言。请求体的MIME类型
CONTENT_LENGTH 仅仅对POST而言,请求体的字节大小

(4) 子进程将它的输出发送到哪里

一个CGI程序将它的动态内容发送到标准输出。在子进程载入并执行CGI程序之前,它使用UNIX dup2 函数将它标准输出重定向到和client相关连的已连接描写叙述符
因此,不论什么CGI程序写到标准输出的东西都会直接到达client

4. 综合: Tiny web server源代码及分析

(1) main程序

Tiny是一个迭代server,监听在命令行中传递来的port上的连接请求,在通过调用 open_listenfd 函数打开一个监听套接字以后。执行无限server循环,不断接受连接请求(第16行)。执行事务(第17行),并关闭连接它的那一端(第18行)

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(1);
        }
        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函数

doit函数处理一个HTTP事物,首先读和解析请求行(request line)(第11-12行),注意,我们使用rio_readlineb函数读取请求行。
Tiny仅仅支持GET方法,假设client请求其它方法,发送一个错误信息。


然后将URI解析为一个文件名称和一个可能为空的CGI參数字符串。而且设置一个标志表明请求的是静态内容还是动态内容(第21行)
假设请求的是静态内容。就验证是否为普通文件,有读权限(第29行)
假设请求的是动态内容,就验证是否为可执行文件(第37行),假设是,就提供动态内容(第42行)

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

(4)read_requesthdrs 函数

Tiny不使用请求报头中的不论什么信息。仅仅调用 read_requesthdrs函数来读取并忽略这些报头。
注意。终止请求报头的空文本行是由 回车和换行符组成的。在第6行中检查

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

(5)parse_uri 函数

Tiny假设静态内容的主文件夹就是当前文件夹,可执行文件的主文件夹是 ./cgi-bin/ 不论什么包含字符串 cgi-bin 的URI都觉得是对动态内容的请求。
首先将URI解析为一个文件名称和一个可选的CGI參数字符串。
假设请求的是静态内容(第5行)。就清除CGI參数串(第6行)。然后将URI转换为一个相对的unix 路径名,比如 ./index.html
假设URI是用’/’ 结尾的(第9行) ,我们就把默认的文件名称加在后面(第10行)
假设请求的是动态内容(第13行),就会抽取全部的CGI參数(第14-20行),并将URI剩下的部分转换为一个对应的unix文件名称(第21-22行)

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

(6)serve_static 函数

Tiny提供四种不同的静态内容:HTML文件、无格式的文本文件、GIF编码格式图片、JPEG编码格式图片
serve_static 函数发送一个HTTP响应,其主体包含一个本地文件的内容。
首先我们通过检查文件名称的后缀来推断文件类型(第7行)。而且发送响应行和响应报头给client(第8-12行)。

注意用一个空行终止报头
第16行,我们使用 unix mmap函数将被请求文件映射到一个虚拟问存储器空间,调用mmap将文件srcfd的前filesize个字节映射到一个从地址srcp開始的私有仅仅读虚拟存储器区域。
一旦文件映射到存储器,就不再须要它的描写叙述符了,关闭这个文件(第17行)。
第18行执行的是到client的实际文件传动。rio_writen 函数拷贝从srcp位置開始的filesize个字节(已经被映射到了所请求的文件) 到client的已连接描写叙述符。
第19行释放了映射的虚拟存储器区域,避免潜在的存储器泄漏

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

(7)serve_dynamic 函数

Tiny通过派生一个子进程并在子进程的上下文中执行一个CGI程序。来提供各种类型的动态内容。
serve_dynamic函数一開始就向client发送一个表明成功的响应行,,同一时候还包含带有信息的server报头。


第13行,子进程用来自请求URI的CGI參数初始化QUERY_STRING环境变量
第14行,子进程重定向它的标准输出到已连接文件描写叙述符
第15行,载入并执行CGI程序。由于CGI程序执行在子进程的上下文中,它能够訪问全部在调用execve函数之前就存在的打开文件和环境变量
第17行,父进程堵塞在对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) { /* 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
}

5.调试及执行

(1) 下载csapp.h 和 csapp.c

http://csapp.cs.cmu.edu/public/ics2/code/include/csapp.h
http://csapp.cs.cmu.edu/public/ics2/code/src/csapp.c
关于CSAPP代码下载的技巧:比方code/conc/sbuf.c,对应的下载地址在
http://csapp.cs.cmu.edu/public/ics2/code/conc/sbuf.c

(2) 编译

将全部源文件tiny.c、csapp.c和csapp.h放在同一个文件夹下。

$ gcc -o tiny tiny.c csapp.c -lpthread

注:加-lpthread是由于csapp.c中有些函数用了多线程库

(3) 执行前准备

  1. 将被訪问的文件放在tiny同级文件夹下(home.html、photo.jpg)
<html>
<head>
<title>Hello World</title>
</head>
<body>
<h1>Welcome to Tiny Web Server</h1>
</body>
</html>
  1. 将測试用CGI程序放到cgi-bin文件夹下。并编译成可执行程序
$ gcc -o adder adder.c

(4) 执行流程及其结果

  1. 执行Tiny程序,并指定port号(1024–49151可用,其它为知名port)
$ ./tiny 1024
  1. 浏览器訪问静态内容(home.html)
    TWS

  2. 浏览器訪问不存在的内容
    TWS Error

  3. 浏览器訪问动态内容
    TWS adder

  4. 还能够訪问图片哦
    Kali Linux

(5) Telnet 測试

  1. 连接到Tinyserver
$ telnet localhost 1024
  1. 输入请求头(注意空行)
GET /home.html HTTP/1.0

  1. 验证结果(注意空行)
HTTP/1.0 200 OK
Server: Tiny Web Server
Content-length: 108
Content-type: text/html

<html>
<head>
<title>Hello World</title>
</head>
<body>
<h1>Welcome to Tiny Web Server</h1>
</body>
</html>
Connection closed by foreign host.
  1. 错误的返回
HTTP/1.0 404 Not found
Content-type: text/html
Content-length: 143

<html><title>Tiny Error</title><body bgcolor=ffffff>
404: Not found
<p>Tiny couldn't find this file: .kkk
<hr><em>The Tiny Web Server</em>
Connection closed by foreign host.

(6) 提醒

须要注意的是 HTTP 协议的头部和数据之间有一个空行,假设浏览器无法查看到内容,而通过 Telnet 能够得到数据,则能够推断为少了一个空行。

posted @ 2017-08-20 19:35  yfceshi  阅读(3170)  评论(0编辑  收藏  举报