<深刻理解計算機系統> CSAPP Tiny web 服務器

本文是我學習<深刻理解計算機系統>中網絡編程部分的學習筆記。

1. Web基礎html

      web客戶端和服務器之間的交互使用的是一個基於文本的應用級協議HTTP(超文本傳輸協議)。一個 web客戶端(即瀏覽器)打開一個到服務器的因特網鏈接,而且請求某些內容。服務器響應所請求的內容,而後關閉鏈接。瀏覽器讀取這些內容,並把它顯示在屏幕上。
 
      對於web客戶端和服務器而言,內容是與一個MIME類型相關的字節序列。常見的MIME類型:       
 
MIME類型   描述
text/html        HTML頁面
text/plain  無格式文本
image/gif     GIF格式編碼的二進制圖像
image/jpeg     JPEG格式編碼的二進制圖像
     
web服務器以兩種不一樣的方式向客服端提供內容:
(1)靜態內容:取一個 磁盤文件,並將它的內容返回給客戶端
(2)動態內容:執行一個 可執行文件,並將它的輸出返回給客戶端

 

統一資源定位符:URL
http://www.google.com:80/index.html

表示因特網主機 www.google.com 上一個稱爲 index.html 的HTML文件,它是由一個監聽端口80的Web服務器所管理的。 HTTP默認端口號爲80web

可執行文件的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> 爲服務器提供了額外的信息,例如瀏覽器的版本類型
HTTP 1.1中 一個IP地址的服務器能夠是 多宿主主機,例如 www.host1.com  www.host2.com 能夠存在於同一服務器上。
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 錯誤請求 服務器不能理解請求
403  禁止  服務器無權訪問所請求的文件
404 未發現  服務器不能找到所請求的文件
501  未實現 服務器不支持請求的方法
505  HTTP版本不支持 服務器不支持請求的版本
 
兩個最重要的響應報頭:
Content-Type 告訴客戶端響應主體中內容的MIME類型
Content-Length 指示響應主體的字節大小
響應主體中包含着被請求的內容。
 
3.服務動態內容
(1) 客戶端如何將程序參數傳遞給服務器
GET請求的參數在URI中傳遞, 「?」字符分隔了文件名和參數,每一個參數都用一個"&"分隔開, 參數中不容許有空格,必須用字符串「%20」來表示
HTTP POST請求的參數是在請求主體中而不是 URI中傳遞的
 
(2)服務器如何將參數傳遞給子進程
GET   /cgi-bin/adder?123&456    HTTP/1.1

它調用 fork 來建立一個子進程,並調用 execve 在子進程的上下文中執行 /cgi-bin/adder 程序瀏覽器

在調用 execve 以前,子進程將CGI環境變量 QUERY_STRING 設置爲"123&456", adder 程序在運行時能夠用unix getenv 函數來引用它
 
(3)服務器如何將其餘信息傳遞給子進程
           
環境變量  描述
QUERY_STRING 程序參數
SERVER_PORT  父進程偵聽的端口
REQUEST_METHOD GET 或 POST
REMOTE_HOST 客戶端的域名
REMOTE_ADDR  客戶端的點分十進制IP地址
CONTENT_TYPE 只對POST而言,請求體的MIME類型
CONTENT_LENGTH 只對POST而言,請求體的字節大小
  
(4) 子進程將它的輸出發送到哪裏
一個CGI程序將它的動態內容發送到標準輸出,在子進程加載並運行CGI程序以前,它使用 UNIX dup2 函數將它標準輸出重定向到和客戶端相關連的已鏈接描述符
所以,任何CGI程序寫到標準輸出的東西都會直接到達客戶端
 
4. 綜合: Tiny web 服務器
 
(1) main程序
Tiny是一個迭代服務器,監聽在命令行中傳遞來的端口上的鏈接請求,在經過調用 open_listenfd 函數打開一個監聽套接字之後,執行無限服務器循環,不斷接受鏈接請求(第16行),執行事務(第17行),並關閉鏈接它的那一端(第18行)
 1 int main(int argc, char **argv) 
 2 {
 3     int listenfd, connfd, port, clientlen;
 4     struct sockaddr_in clientaddr;
 5 
 6     /* Check command line args */
 7     if (argc != 2) {
 8     fprintf(stderr, "usage: %s <port>\n", argv[0]);
 9     exit(1);
10     }
11     port = atoi(argv[1]);
12 
13     listenfd = Open_listenfd(port);
14     while (1) {
15     clientlen = sizeof(clientaddr);
16     connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen); //line:netp:tiny:accept
17     doit(connfd);                                             //line:netp:tiny:doit
18     Close(connfd);                                            //line:netp:tiny:close
19     }
20 }
 
(2) doit函數
doit函數處理一個HTTP事物,首先讀和 解析請求行(request line)(第11-12行),注意,咱們使用rio_readlineb函數讀取請求行。
Tiny只支持GET方法,若是客戶端請求其餘方法,發送一個錯誤信息。
而後將URI解析爲一個文件名和一個可能爲空的CGI參數字符串,而且設置一個標誌代表請求的是靜態內容仍是動態內容(第21行)
若是請求的是靜態內容,就驗證是否爲普通文件,有讀權限(第29行) 
若是請求的是動態內容,就驗證是否爲可執行文件(第37行),若是是,就提供動態內容(第42行)
 1 void doit(int fd) 
 2 {
 3     int is_static;
 4     struct stat sbuf;
 5     char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
 6     char filename[MAXLINE], cgiargs[MAXLINE];
 7     rio_t rio;
 8   
 9     /* Read request line and headers */
10     Rio_readinitb(&rio, fd);
11     Rio_readlineb(&rio, buf, MAXLINE);                   //line:netp:doit:readrequest
12     sscanf(buf, "%s %s %s", method, uri, version);       //line:netp:doit:parserequest
13     if (strcasecmp(method, "GET")) {                     //line:netp:doit:beginrequesterr
14        clienterror(fd, method, "501", "Not Implemented",
15                 "Tiny does not implement this method");
16         return;
17     }                                                    //line:netp:doit:endrequesterr
18     read_requesthdrs(&rio);                              //line:netp:doit:readrequesthdrs
19 
20     /* Parse URI from GET request */
21     is_static = parse_uri(uri, filename, cgiargs);       //line:netp:doit:staticcheck
22     if (stat(filename, &sbuf) < 0) {                     //line:netp:doit:beginnotfound
23     clienterror(fd, filename, "404", "Not found",
24             "Tiny couldn't find this file");
25     return;
26     }                                                    //line:netp:doit:endnotfound
27 
28     if (is_static) { /* Serve static content */          
29     if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) { //line:netp:doit:readable
30         clienterror(fd, filename, "403", "Forbidden",
31             "Tiny couldn't read the file");
32         return;
33     }
34     serve_static(fd, filename, sbuf.st_size);        //line:netp:doit:servestatic
35     }
36     else { /* Serve dynamic content */
37     if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) { //line:netp:doit:executable
38         clienterror(fd, filename, "403", "Forbidden",
39             "Tiny couldn't run the CGI program");
40         return;
41     }
42     serve_dynamic(fd, filename, cgiargs);            //line:netp:doit:servedynamic
43     }
44 }
(3)clienterror函數
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));
}

 

(4)read_requesthdrs 函數
Tiny不使用請求報頭中的任何信息,僅僅調用 read_requesthdrs函數來讀取並忽略這些報頭
注意,終止請求報頭的空文本行是由 回車和換行符組成的,在第6行中檢查
 1 void read_requesthdrs(rio_t *rp) 
 2 {
 3     char buf[MAXLINE];
 4 
 5     Rio_readlineb(rp, buf, MAXLINE);
 6     while(strcmp(buf, "\r\n")) {          //line:netp:readhdrs:checkterm
 7     Rio_readlineb(rp, buf, MAXLINE);
 8     printf("%s", buf);
 9     }
10     return;
11 }
 
(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行)
 1 int parse_uri(char *uri, char *filename, char *cgiargs) 
 2 {
 3     char *ptr;
 4 
 5     if (!strstr(uri, "cgi-bin")) {  /* Static content */ //line:netp:parseuri:isstatic
 6     strcpy(cgiargs, "");                             //line:netp:parseuri:clearcgi
 7     strcpy(filename, ".");                           //line:netp:parseuri:beginconvert1
 8     strcat(filename, uri);                           //line:netp:parseuri:endconvert1
 9     if (uri[strlen(uri)-1] == '/')                   //line:netp:parseuri:slashcheck
10         strcat(filename, "home.html");               //line:netp:parseuri:appenddefault
11     return 1;
12     }
13     else {  /* Dynamic content */                     //line:netp:parseuri:isdynamic
14     ptr = index(uri, '?');                           //line:netp:parseuri:beginextract
15     if (ptr) {
16         strcpy(cgiargs, ptr+1);
17         *ptr = '\0';
18     }
19     else 
20         strcpy(cgiargs, "");                         //line:netp:parseuri:endextract
21     strcpy(filename, ".");                           //line:netp:parseuri:beginconvert2
22     strcat(filename, uri);                           //line:netp:parseuri:endconvert2
23     return 0;
24     }
25 }
 
(6)serve_static 函數
Tiny提供四種不一樣的靜態內容:HTML文件、無格式的文本文件、GIF編碼格式圖片、JPEG編碼格式圖片
serve_static 函數發送一個HTTP響應,其主體包含一個本地文件的內容。
首先咱們經過檢查文件名的後綴來判斷文件類型(第7行),而且發送響應行和響應報頭給客戶端(第8-12行)。注意用一個空行終止報頭
第16行,咱們使用 unix mmap函數將被請求文件映射到一個虛擬問存儲器空間,調用mmap將文件srcfd的前filesize個字節映射到一個從地址srcp開始的私有隻讀虛擬存儲器區域
一旦文件映射到存儲器,就再也不須要它的描述符了,關閉這個文件(第17行)。
第18行執行的是到客戶端的實際文件傳動。rio_writen 函數拷貝從srcp位置開始的filesize個字節(已經被映射到了所請求的文件) 到客戶端的已鏈接描述符。
第19行 釋放了映射的虛擬存儲器區域,避免潛在的存儲器泄漏

 

 1 void serve_static(int fd, char *filename, int filesize) 
 2 {
 3     int srcfd;
 4     char *srcp, filetype[MAXLINE], buf[MAXBUF];
 5  
 6     /* Send response headers to client */
 7     get_filetype(filename, filetype);       //line:netp:servestatic:getfiletype
 8     sprintf(buf, "HTTP/1.0 200 OK\r\n");    //line:netp:servestatic:beginserve
 9     sprintf(buf, "%sServer: Tiny Web Server\r\n", buf);
10     sprintf(buf, "%sContent-length: %d\r\n", buf, filesize);
11     sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, filetype);
12     Rio_writen(fd, buf, strlen(buf));       //line:netp:servestatic:endserve
13 
14     /* Send response body to client */
15     srcfd = Open(filename, O_RDONLY, 0);    //line:netp:servestatic:open
16     srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);//line:netp:servestatic:mmap
17     Close(srcfd);                           //line:netp:servestatic:close
18     Rio_writen(fd, srcp, filesize);         //line:netp:servestatic:write
19     Munmap(srcp, filesize);                 //line:netp:servestatic:munmap
20 }
21 
22 /*
23  * get_filetype - derive file type from file name
24  */
25 void get_filetype(char *filename, char *filetype) 
26 {
27     if (strstr(filename, ".html"))
28     strcpy(filetype, "text/html");
29     else if (strstr(filename, ".gif"))
30     strcpy(filetype, "image/gif");
31     else if (strstr(filename, ".jpg"))
32     strcpy(filetype, "image/jpeg");
33     else
34     strcpy(filetype, "text/plain");
35 }  
 
(6)serve_dynamic 函數
Tiny經過派生一個子進程並在子進程的上下文中運行一個CGI程序,來提供各類類型的動態內容。
serve_dynamic函數一開始就向客戶端發送一個代表成功的響應行,,同時還包括帶有信息的server報頭。
第13行,子進程用來自請求URI的CGI參數初始化QUERY_STRING環境變量
第14行,子進程 重定向它的標準輸出到已鏈接文件描述符
第15行,加載並運行CGI程序,由於CGI程序運行在子進程的上下文中,它可以訪問全部在調用execve函數以前就存在的打開文件和環境變量
第17行,父進程阻塞在對wait的調用中,等待子進程終止的時候,回收操做系統那個分配給子進程的資源

 

 1 void serve_dynamic(int fd, char *filename, char *cgiargs) 
 2 {
 3     char buf[MAXLINE], *emptylist[] = { NULL };
 4 
 5     /* Return first part of HTTP response */
 6     sprintf(buf, "HTTP/1.0 200 OK\r\n"); 
 7     Rio_writen(fd, buf, strlen(buf));
 8     sprintf(buf, "Server: Tiny Web Server\r\n");
 9     Rio_writen(fd, buf, strlen(buf));
10   
11     if (Fork() == 0) { /* child */ //line:netp:servedynamic:fork
12     /* Real server would set all CGI vars here */
13     setenv("QUERY_STRING", cgiargs, 1); //line:netp:servedynamic:setenv
14     Dup2(fd, STDOUT_FILENO);         /* Redirect stdout to client */ //line:netp:servedynamic:dup2
15     Execve(filename, emptylist, environ); /* Run CGI program */ //line:netp:servedynamic:execve
16     }
17     Wait(NULL); /* Parent waits for and reaps child */ //line:netp:servedynamic:wait
18 }
 
相關文章
相關標籤/搜索