1. 程式人生 > >深入理解計算機系統_3e 第十一章家庭作業 CS:APP3e chapter 11 homework

深入理解計算機系統_3e 第十一章家庭作業 CS:APP3e chapter 11 homework

cep serve 技術分享 apn only class control 相同 法則


註:tiny.c csapp.c csapp.h等示例代碼均可在Code Examples獲取

11.6

A.

書上寫的示例代碼已經完成了大部分工作:doit函數中的printf("%s", buf);語句打印出了請求行;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; }

如果按照這個打印的話,第一個請求抱頭Host將無法輸出,所以我們應該在while循環前輸出一個報頭:

void read_requesthdrs(rio_t *rp)
{
  char buf[MAXLINE];
  
  Rio_readlineb(rp, buf, MAXLINE);
  printf("%s", buf); /* Host: ..... */
  while(strcmp(buf, "\r\n")){
    Rio_readlineb(rp, buf, MAXLINE);
    printf("%s", buf);
  }
  return
; }

B.

我們在A中是將請求行和請求報頭輸出到標準輸出,在命令行啟動的時候用“>”重定向到一個文件即可。

但是要註意一點:由於我們輸出到文件而非一個交互設備,所以流緩沖默認是滿緩沖的,如果我們在一個靜態請求後按下“CRTL+C”終止tiny,那麽由於緩沖內容沒有輸出,則文件內將沒有內容,所以我們應該在read_requesthdrs函數最後加上一個fflush

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

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

關於流緩沖的問題,可以參考我之前寫的這篇文章:文件描述符 流 流緩沖的一些概念與問題

運行輸出:

frank@under:~/tmp$ ./a.out 15213 > line_and_headers
^C
frank@under:~/tmp$ cat line_and_headers 
Accepted connection from (localhost, 45600)
GET /test.c HTTP/1.1
Host: localhost:15213
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.119 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
DNT: 1
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7

frank@under:~/tmp$ 

C.

由B中的請求行:GET /test.c HTTP/1.1可知使用的是HTTP/1.1

D.

註意: 書上說查RFC 2616來確認各個請求報頭的功能,但是有一些報頭我沒有在該文檔中找到,根據這篇文章:

HTTP/1.1協議更新:RFC2616遭廢棄

“原來的RFC 2616拆分為六個單獨的協議說明,並重點對原來語義模糊的部分進行了解釋,新的協議說明更易懂、易讀。新的協議說明包括以下六部分:”

  • RFC7230 - HTTP/1.1: Message Syntax and Routing - low-level message parsing and connection management
  • RFC7231 - HTTP/1.1: Semantics and Content - methods, status codes and headers
  • RFC7232 - HTTP/1.1: Conditional Requests - e.g., If-Modified-Since
  • RFC7233 - HTTP/1.1: Range Requests - getting partial content
  • RFC7234 - HTTP/1.1: Caching - browser and intermediary caches
  • RFC7235 - HTTP/1.1: Authentication - a framework for HTTP authentication

我們應該在RFC7231中查找,而非RFC2616。 具體的介紹我就不列出了,大家可以自己在RFC7231查找詳細介紹。


11.7

我電腦上倒還沒有MPG格式的視頻,拿一個WEBM的視頻做示例——都是要將響應報頭Content-type: 更改為對應的格式(這裏是video/webm ):

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, ".png"))
    strcpy(filetype, "image/png");
    else if (strstr(filename, ".jpg"))
    strcpy(filetype, "image/jpeg");
    else if (strstr(filename, ".webm"))
    strcpy(filetype, "video/webm");
    else
    strcpy(filetype, "text/plain");
}  

效果如下:

技術分享圖片


11.8

根據書上8.5節的內容,我們先寫一個SIGCHLD的信號處理函數handler

void handler(int sig)
{
  int olderrno = errno;
  
  while (waitpid(-1, NULL, 0) > 0)
  {
    continue;
  }
  if (errno != ECHILD)
  {
    Sio_error("waitpid error");
  }
  errno = olderrno;
}

serve_dynamic中使用signal將其安裝 :

void serve_dynamic(int fd, char *filename, char *cgiargs) 
{
    if (signal(SIGCHLD, handler) == SIG_ERR)
    {
        unix_error("signal error");
    }
  
    char buf[MAXLINE], *emptylist[] = { NULL };

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

經多次測試運行cgi程序未發現僵屍進程。


11.9

別忘記釋放內存。

void serve_static(int fd, char *filename, int filesize) 
{
    int srcfd;
    char *srcp, filetype[MAXLINE], buf[MAXBUF];
 
    get_filetype(filename, filetype);       
    sprintf(buf, "HTTP/1.0 200 OK\r\n");   
    sprintf(buf, "%sServer: Tiny Web Server\r\n", buf);
    sprintf(buf, "%sConnection: close\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));       
    printf("Response headers:\n");
    printf("%s", buf);

    srcfd = Open(filename, O_RDONLY, 0);
    srcp = Malloc(filesize);
    Rio_readn(srcfd, srcp, filesize);
    Rio_writen(fd, srcp, filesize);            
    Free(srcp);
    //srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
    //Close(srcfd);
    //Munmap(srcp, filesize);              
}


11.10

關於HTML表單的知識參考:HTML 表單

index.html:

<form action="cgi-bin/a.out"  method="GET">
First Number:<br>
<input type="text" name="FirstNumber" value="">
<br><br>
Second Number:<br>
<input type="text" name="SecondNumber" value="">
<br><br>
<input type="submit" value="Submit">
</form> 

由於我們不需要默認值,所以value為空。

由於我們此時的GET請求是“name=value”的形式,所以應該將adder.c中取參數的部分修改為:

    /* Extract the two arguments */
if ((buf = getenv("QUERY_STRING")) != NULL) 
{
    p = strchr(buf, ‘&‘);
    *p = ‘\0‘;
    strcpy(arg1, buf);
    strcpy(arg2, p+1);
  
    /* 取等號後面的參數 */
    p = strchr(arg1, ‘=‘);
    n1 = atoi(p+1);
    p = strchr(arg2, ‘=‘);
    n2 = atoi(p+1);
}

運行效果:

技術分享圖片


11.11

根據RFC 7231, section 4.3.2: HEAD中的描述,HEAD方法大致描述如下:

HEAD方法與GET相同,但是HEAD並不返回消息體。在一個HEAD請求的消息響應中,HTTP投中包含的元信息應該和一個GET請求的響應消息相同。這種方法可以用來獲取請求中隱含的元信息,而無需傳輸實體本身。這個方法經常用來測試超鏈接的有效性,可用性和最近修改。
一個HEAD請求響應可以被緩存,也就是說,響應中的信息可能用來更新之前緩存的實體。如果當前實體緩存實體閾值不同(可通過Content_Length、Content-MD5、ETag或Last-Modified的變化來表明),那麽這個緩存被視為過期了。

所以我們只需要將處理GET方法的兩個函數serve_staticserve_dynamic中增加一個如果是HEAD方法則不返回消息體的判斷。而方法是在doit中判斷的,所以我們設置一個標誌unsigned methods = 0; 並在判斷方法的語句中加上HEAD方法的判斷,如果為GET,其值為0,如果為HEAD,其值為1:

if (strcasecmp(method, "GET") == 0)
{
    methods = 0;
}
else if (strcasecmp(method, "HEAD") == 0)
{
    methods = 1;
}
else
{
    clienterror(fd, method, "501", "Not Implemented",
                "Tiny does not implement this method");
    return;
}   

最後將函數serve_staticserve_dynamic的參數增加一個unsigned methods

void serve_static(int fd, char *filename, int filesize, unsigned methods);
void serve_dynamic(int fd, char *filename, char *cgiargs, unsigned methods);

在其中返回消息體之前加上這樣一條語句:

/* HEAD method doesn‘t need to send response body to client */
if (methods == 1)
    return;

Telnet測試效果:

frank@under:~/tmp$ telnet localhost 15213
Trying 127.0.0.1...
Connected to localhost.
Escape character is ‘^]‘.
HEAD / HTTP/1.0

HTTP/1.0 200 OK
Server: Tiny Web Server
Connection: close
Content-length: 241
Content-type: text/html

Connection closed by foreign host.
frank@under:~/tmp$ 


11.12

如果我們用POST方法傳遞參數的話,URI中將不含參數,而是在消息體中傳遞參數,例如:

index.html(將method改為POST):

<form action="cgi-bin/a.out"  method="POST">
First Number:<br>
<input type="text" name="FirstNumber" value="">
<br><br>
Second Number:<br>
<input type="text" name="SecondNumber" value="">
<br><br>
<input type="submit" value="Submit">
</form> 

抓包request如下:

POST /cgi-bin/a.out HTTP/1.1
Host: localhost:15213
Connection: keep-alive
Content-Length: 32
Cache-Control: max-age=0
Origin: http://localhost:15213
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.119 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
DNT: 1
Referer: http://localhost:15213/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7

FirstNumber=123&SecondNumber=321

可以看到FirstNumber=123&SecondNumber=321放在了消息體內傳遞。

為了將FirstNumber=123&SecondNumber=321存入cgiargs ,我們需要在read_requesthdrs中判斷是否是POST然後將請求中最後的消息體存入cgiargs

首先,在doit中增加一個POST的method判斷:

if (strcasecmp(method, "GET") == 0)
    {
        methods = 0;
    }
    else if (strcasecmp(method, "HEAD") == 0)
    {
        methods = 1;
    }
    else if (strcasecmp(method, "POST") == 0)
    {
        methods = 2; /* POST */
    }
    else
    {
        clienterror(fd, method, "501", "Not Implemented",
                    "Tiny does not implement this method");
        return;
    } 

然後為read_requesthdrs增加兩個參數:

void read_requesthdrs(rio_t *rp, char *cgiargs, unsigned methods);

在其中更據是否是POST方法來讀取消息體:

void read_requesthdrs(rio_t *rp, char *cgiargs, unsigned methods) 
{
    char buf[MAXLINE];

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

    if (methods == 2){  /* POST */
        Rio_readnb(rp, buf, rp->rio_cnt);
        strcpy(cgiargs, buf);
    }

    return;
}

這裏要特別註意,消息體不是以\r\n作為結尾的,抓包如下:

技術分享圖片

可以看到最後不是以換行符作為結尾,所以我們這裏不能像之前那樣使用MAXLINE作為第三個參數調用Rio這樣的函數(讀取函數read無法判斷是否已經到達了結尾,因為緩沖區很長,表現為一直等待輸入,服務器無響應),而是應該根據rp->rio_cnt讀取所緩沖器剩下的所有字符:

if (methods == 2){  /* POST */
    Rio_readnb(rp, buf, rp->rio_cnt);
    strcpy(cgiargs, buf);
}

同時我們要更改一下parse_uri函數,因為如果是POST方法的話,read_requesthdrs就已經更新了cgiargs了,所有需要在parse_uri對方法做一個判斷(也要增加一個method參數),如果是GET方法的話才對cgiargs進行更新:

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

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

最終效果:

技術分享圖片

關於傳遞方法詳細的介紹可以參考:Sending form data


11.13

這裏我們直接忽略EPIPE信號(在主函數中安裝):

if (Signal(SIGPIPE, SIG_IGN) == SIG_ERR)
{
    unix_error("mask signal pipe error");
}

然後更換以前使用的Rio_writen ,使之能夠處理EPIPE:

void Rio_writen(int fd, void *usrbuf, size_t n) 
{
    if (rio_writen(fd, usrbuf, n) != n)
    {
      unix_error("Rio_writen error");
      if(errno == EPIPE)
      {
        unix_error("EPIPE error\nConnection ended");
      }
    }
}

最後要註意cgi程序運行的時候也可能遇到EPIPE信號,我們要交給它自己處理:

if (Fork() == 0) 
{ /* Child */
  /* Real server would set all CGI vars here */
    setenv("QUERY_STRING", cgiargs, 1); 
    Dup2(fd, STDOUT_FILENO);         /* Redirect stdout to client */
    if (Signal(SIGPIPE, SIG_DFL) == SIG_ERR)
    {
        unix_error("mask signal pipe error");
    }
    Execve(filename, emptylist, environ); /* Run CGI program */ 
}

深入理解計算機系統_3e 第十一章家庭作業 CS:APP3e chapter 11 homework