CSAPP Tiny web server源代碼分析及搭建執行

1. Web基礎

webclient和server之間的交互使用的是一個基於文本的應用級協議HTTP(超文本傳輸協議)。css

一個webclient(即瀏覽器)打開一個到server的因特網鏈接,並且請求某些內容。server響應所請求的內容,而後關閉鏈接。瀏覽器讀取這些內容。並把它顯示在屏幕上。 html

對於webclient和server而言。內容是與一個MIME類型相關的字節序列。web

常見的MIME類型: shell

MIME類型 描寫敘述
text/html HTML頁面
text/plain 無格式文本
image/gif GIF格式編碼的二進制圖像
image/jpeg JPEG格式編碼的二進制圖像

webserver以兩種不一樣的方式向客服端提供內容:
1. 靜態內容:取一個磁盤文件。並將它的內容返回給client
2. 動態內容:執行一個可執行文件,並將它的輸出返回給client瀏覽器

統一資源定位符:URLruby

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

表示因特網主機 www.google.com 上一個稱爲 index.html 的HTML文件。它是由一個監聽port80的Webserver所管理的。多線程

HTTP默認port號爲80app

可執行文件的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 可以獲得數據,則可以推斷爲少了一個空行。

相關文章
相關標籤/搜索