# 2020q3 Tiny Web Server ###### tags: `sysprog2020` ## 目標 完成 CS:APP 3/e 第 11 章的 Homework 11.6 到 11.13 結果:未完成 [Github](https://github.com/JKChun/csapp_tiny_web_server) ## Homeworks ### 11.6 #### A. Modify Tiny so that it echoes every request line and request header. 此題為讓 Tiny 將從 client 端收到的 request line 和 request header 回傳給 client 端,原本的 Tiny server 是以 doit function 讀取 request line 和 header,再用 parse_uri function 判斷 web content 是 static 還是 dynamic,最後回傳 response headers 和 body 給 client。 將 Tiny server 裡的 doit function 改為 echo function,把 request line 和 header 作為 response body 回傳給 client 端。 ```cpp= /* * echo - replace doit function, transmit request line and header * back to client */ void echo(int fd) { size_t n; rio_t rio; char io_buf[MAXBUF]; /* Send response headers to client */ sprintf(io_buf, "HTTP/1.0 200 OK\r\n"); sprintf(io_buf, "%sServer: Tiny Web Server\r\n", io_buf); sprintf(io_buf, "%sConnection: close\r\n\r\n", io_buf); Rio_writen(fd, io_buf, strlen(io_buf)); printf("** echo server **\n"); printf("Response headers:\n"); printf("%s", io_buf); /* Send response body to client */ Rio_readinitb(&rio, fd); while((n = Rio_readlineb(&rio, io_buf, MAXLINE)) != 0){ if (strcmp(io_buf, "\r\n") == 0) break; Rio_writen(fd, io_buf, n); } } ``` ![](https://i.imgur.com/RWpGFgN.png) ``` GET / HTTP/1.1 Host: 140.116.245.27:5000 Connection: keep-alive DNT: 1 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Accept-Encoding: gzip, deflate Accept-Language: zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7 ``` #### B. Use your favorite browser to make a request to Tiny for static content. Capture the output from Tiny in a file. 修改 `echo` function 取代 `doit`,使用 `open` function 創造一個 `logfile` 檔案,在寄送 response body 時也寫入檔案。 ```cpp= /* * echo - replace doit function, transmit request line and header * back to client */ void echo(int fd) { size_t n; rio_t rio; char io_buf[MAXBUF]; int fp; /* Send response headers to client */ sprintf(io_buf, "HTTP/1.0 200 OK\r\n"); sprintf(io_buf, "%sServer: Tiny Web Server\r\n", io_buf); sprintf(io_buf, "%sConnection: close\r\n\r\n", io_buf); Rio_writen(fd, io_buf, strlen(io_buf)); printf("** echo server **\n"); printf("Response headers:\n"); printf("%s", io_buf); fp = open("./logfile", O_WRONLY | O_CREAT); /* Send response body to client */ Rio_readinitb(&rio, fd); while((n = Rio_readlineb(&rio, io_buf, MAXLINE)) != 0){ if (strcmp(io_buf, "\r\n") == 0) break; Rio_writen(fp, io_buf, n); Rio_writen(fd, io_buf, n); printf("%s", io_buf); } fclose(fp); } ``` #### C. Inspect the output from Tiny to determine the version of HTTP your browser uses. 根據 logfile 的內容可以得知 Chrome 使用的 HTTP version 為 1.1。 下方為 logfile 的內容,URI 為 /favicon.ico 是因為 Chrome 會自動送出第二個 http request,內容就是向 server 要 favicon.ico 檔案,所以 logfile 裡原本的 request line 和 header 被覆蓋掉了,favicon 為 favorites icon 的縮寫,詳情請見 wiki [Favicon](https://zh.wikipedia.org/wiki/Favicon)。 ``` GET /favicon.ico HTTP/1.1 Host: 140.116.245.27:5000 Connection: keep-alive User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36 DNT: 1 Accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8 Referer: http://140.116.245.27:5000/ Accept-Encoding: gzip, deflate Accept-Language: zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7 =0.9 ``` #### D. Consult the HTTP/1.1 standard in RFC 2616 to determine the meaning of each header in the HTTP request from your browser. You can obtain RFC 2616 from www.rfc-editor.org/rfc.html. 在 [RFC2616 Section 14](https://www.rfc-editor.org/rfc/rfc2616.html#section-5.3) 有 `header` 的定義,如:`Accept` 在 [section 14.1](https://www.rfc-editor.org/rfc/rfc2616.html#section-14.1),`Referer` 在 [section 14.36](https://www.rfc-editor.org/rfc/rfc2616.html#section-14.36),`User-Agent` 在 [section 14.36](https://www.rfc-editor.org/rfc/rfc2616.html#section-14.43),但是 `DNT(Do Not Track)` 呢?,我並沒有在 `RFC Editor` 找到有關 `DNT header` 的文件,只有在 W3C 找到 [Tracking Preference Expression (DNT)](https://www.w3.org/TR/tracking-dnt/)。 --- ### 11.7 #### Extend Tiny so that it serves MPG video files. Check your work using a real browser. 瀏覽器會根據 http response 的 MIME type,來判定如何對URL進行處理,所以伺服器要在 http response 的 Content-Type header 裡放入正確的 MIME type,否則瀏覽器很有可能轉譯錯誤或是無法正常運作,造成下載的檔案無法被正常處理。 Tiny server 是用 `get_filetype` function 去判斷檔案的類別,在 function 裡多加處理 MPG 檔的程式碼( line 14~15 ),檢查 URI 裡的檔名裡有沒有`.mpeg`判斷是否為 MPEG video file,讓瀏覽器可以下載 MPG 的影片檔。 [國際網路號碼分配局(Internet Assigned Numbers Authority, IANA)](https://www.iana.org/) 負責所有的MIME類別,可以從他們的 [Media Types](https://www.iana.org/assignments/media-types/media-types.xhtml) 頁面找到最新且完整的類別清單。 ```cpp= /* * 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, ".png")) strcpy(filetype, "image/png"); else if (strstr(filename, ".jpg")) strcpy(filetype, "image/jpeg"); else if (strstr(filename, ".mpeg")) strcpy(filetype, "video/mpeg"); else strcpy(filetype, "text/plain"); } ``` 測試結果: ![](https://i.imgur.com/6gwodNw.jpg) ![](https://i.imgur.com/58ey2du.jpg) Server output: ``` GET /MPEG_sample.mpeg HTTP/1.1 Accept: text/html, application/xhtml+xml, */* Accept-Language: zh-TW User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko Accept-Encoding: gzip, deflate Host: 140.116.245.27:5000 DNT: 1 Connection: Keep-Alive Response headers: HTTP/1.0 200 OK Server: Tiny Web Server Connection: close Content-length: 567296 Content-type: video/mpeg ``` --- ### 11.8 #### Modify Tiny so that it reaps CGI children inside a `SIGCHLD` handler instead of explicitly waiting for them to terminate. 在 CSAPP 第八章第五小節有提到: > A `signal` is a small message that notifies a process that an event of some type has occurred in the system. `SIGCHLD` 為一種 Linux signal,當 child process "stopped" 或 "terminated" 時,parent 就會收到此 signal,通知 parent 有 child 停止或結束了。 CSAPP 第八章的 sigchld_handler: ```cpp= /* * sigchld_handler - reaps CGI children */ void sigchld_handler(int sig) { int olderrno = errno; while (waitpid(-1, NULL, 0) > 0) { Sio_puts("Handler reaped child\n"); } if (errno != ECHILD) Sio_error("waitpid error"); errno = olderrno; } ``` 用 while loop 的原因在於當 signal handler 在執行時,如果又有複數個 child 執行完,系統只會保留一個 SIGCHLD,所以除了第一次的 SIGCHLD 以外,之後的 SIGCHLD 都只代表 **至少** 有一個 child 發出 SIGCHLD signal,所以 while 是處理可能不只一個 child 發出 SIGCHLD signal 的情況。 CSAPP 第八章第五小節就有說明 handler 一次只處理一個 child process 可能發生的情況:  > The first signal is received and caught by the parent.While the handler is still processing the first signal, the second signal is delivered and added to the set of pending signals. However, since SIGCHLD signals are blocked by the SIGCHLD handler, the second signal is not received. Shortly thereafter, while the handler is still processing the first signal, the third signal arrives. Since there is already a pending SIGCHLD, this third SIGCHLD signal is discarded. Sometime later, after the handler has returned, the kernel notices that there is a pending SIGCHLD signal and forces the parent to receive the signal. The parent catches the signal and executes the handler a second time. After the handler finishes processing the second signal, there are no more pending SIGCHLD signals, and there never will be, because all knowledge of the third SIGCHLD has been lost. `The crucial lesson is that signals cannot be used to count the occurrence of events in other processes.` `waitpid` 的第二個參數 `status` (在上面的例子是傳 `NULL` 進去),根據 [man page](https://linux.die.net/man/2/waitpid): > If status is not NULL, wait() and waitpid() store status information in the int to which it points. This integer can be inspected with the following macros (which take the integer itself as an argument, not a pointer to it, as is done in wait() and waitpid()!) - SIGCHLD Handler 測試: - server terminal output after client request for dynamic content: ``` GET /cgi-bin/adder11.10?first=5&second=2 HTTP/1.1 Host: 140.116.245.27:5000 Connection: keep-alive Upgrade-Insecure-Requests: 1 DNT: 1 User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Referer: http://140.116.245.27:5000/ Accept-Encoding: gzip, deflate Accept-Language: zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7 Handler reaped child ``` 以這題而言只是要 `reap child process` 的確不需要 child 的 `exit status`,以 web server 的角度,有哪些情況會需要 child 的 `exit status`? 加上 handler parent 就不需要在 serve dynamic 執行 wait,如果不讓 parent 在 serve dynamic 執行 `wait`,那假如 parent 先回到 main 執行 `Close(connfd)`,cgi child 執行的寫入還有用嗎?  - how open file actually work ? - what close() actually do ? 答案是有用的,這牽扯到 CSAPP `9.8 Memory Mapping` Kernel 如何產生 child process、 `10.8 Sharing Files` 中 Kernel 是如何紀錄 open files (三個 data structure),在 `fork` 後,child 和 process 都有自己的 Descriptor table,而 child 的 Descriptor table 便是從 parent 的 Descriptor table 複製出來的,假設 parent 是 `fd 3` reference 到 socket file (每個 process 一開始預設 `fd 0`、`fd 1`、`fd 2` 分別是 `stdin`、`stdout`、`stderr`),那 child 也是,當 parent 執行 `Close(connfd)`,只有 close parent 的 Descriptor table 裡的 `connfd`,所以 child 的 `connfd` still refenence 到同樣的 socket file,在 child 執行完 `adder` 並執行 `exit` 後,child 所擁有的資源都會被 kernel 釋放,socket file 的 reference count 減為 0,socket file 被 kernel 刪除。 --- ### 11.9 #### Modify Tiny so that when it serves static content, it copies the requested file to the connected descriptor using `malloc`, `rio_readn`, and `rio_writen`, instead of `mmap` and `rio_writen`. --- ### 11.10 #### A. Write an HTML form for the CGI adder function in Figure 11.27.Your form should include two text boxes that users fill in with the two numbers to be added together. Your form should request content using the GET method. HTML form: - action attribute:`form data` 要送往 server 的路徑 - method attribute:使用的 `method` ```htmlembedded= <!DOCTYPE html> <html> <body> <h1>Adder form</h1> <form action="cgi-bin/adder4Web" method="get"> <p>First number:</p> <input type="text" name="first"><br><br> <p>Second number:</p> <input type="text" name="second"><br><br> <input type="submit" value="Submit"> </form> <p>Click on the submit button, and the input will be sent to server.</p> </body> </html> ``` #### B. Check your work by using a real browser to request the form from Tiny, submit the filled-in form to Tiny, and then display the dynamic content generated by adder. CSAPP 提供的 adder.c 預設 URL 後面給程式的參數字串是 `number&number` 的格式 `http://140.116.245.27:5000/cgi-bin/adder?number&number` 但實際執行格式為 `name=number&name=number` `http://140.116.245.27:5000/cgi-bin/adder?name=number&name=number` 所以要修改 adder.c 取參數的部分(line 11~19 ) adder.c: ```cpp= /* * adder.c - a minimal CGI program that adds two numbers together */ #include "../csapp.h" int main(void) { char *buf, *p; char arg1[MAXLINE], arg2[MAXLINE], content[MAXLINE]; int n1=0, n2=0; /* Extract the two arguments */ if ((buf = getenv("QUERY_STRING")) != NULL) { p = strchr(buf, '&'); *p = '\0'; strcpy(arg1, buf); strcpy(arg2, p+1); n1 = atoi(arg1); n2 = atoi(arg2); } /* Make the response body */ sprintf(content, "Welcome to add.com: "); sprintf(content, "%sTHE Internet addition portal.\r\n<p>", content); sprintf(content, "%sThe answer is: %d + %d = %d\r\n<p>", content, n1, n2, n1 + n2); sprintf(content, "%sThanks for visiting!\r\n", content); /* Generate the HTTP response */ printf("Connection: close\r\n"); printf("Content-length: %d\r\n", (int)strlen(content)); printf("Content-type: text/html\r\n\r\n"); printf("%s", content); fflush(stdout); exit(0); } ``` 修改成 `adder4Web.c`: 1. 使用 `strchr` 搜尋 `=` 字元: ```cpp= /* Extract the two arguments */ if ((buf = getenv("QUERY_STRING")) != NULL) { p = strchr(buf, '&'); *p = '\0'; strcpy(arg1, buf); strcpy(arg2, p+1); n1 = atoi(strchr(arg1, '=')+1); n2 = atoi(strchr(arg2, '=')+1); } ``` ```cpp= /* Extract the two arguments */ if ((buf = getenv("QUERY_STRING")) != NULL) { p = strchr(buf, '&'); *p = '\0'; strcpy(arg1, buf); strcpy(arg2, p+1); n1 = strtol(strchr(arg1, '=')+1, NULL, 10); n2 = strtol(strchr(arg2, '=')+1, NULL, 10); } ``` 2. 使用 `sscanf` function: ```cpp= /* Extract the two arguments */ if ((buf = getenv("QUERY_STRING")) != NULL) { p = strchr(buf, '&'); *p = '\0'; sscanf(buf, "first=%d", &n1); sscanf(p+1, "second=%d", &n2); } ``` Test result: ![](https://i.imgur.com/cmc7feZ.png) --- ### 11.11 #### Extend Tiny to support the HTTP HEAD method. Check your work using telnet as a Web client. 在 [RFC7231 Section 4.3 Method Definitions](https://tools.ietf.org/html/rfc7231#page-21) 有寫所有的 method 定義 ([RFC2616 Section 9](https://tools.ietf.org/html/rfc2616#section-9) 也有): > The HEAD method is identical to GET except that the server MUST NOT send a message body in the response (i.e., the response terminates at the end of the header section). The server SHOULD send the same header fields in response to a HEAD request as it would have sent if the request had been a GET, except that the payload header fields MAY be omitted. 1. 在 doit function 裡判斷 method 是否為 HEAD。 ```cpp= if (!(strcasecmp(method, "GET") == 0 || strcasecmp(method, "HEAD") == 0)) { clienterror(fd, method, "501", "Not Implemented", "Tiny does not implement this method"); return; } ``` 2. 傳入 method 參數至 serve_static function,並在送出 response header 後判斷 method 是否為 HEAD,是就直接 return (line 20、21)。 ```cpp= /* * serve_static - copy a file back to the client */ void serve_static(int fd, char *filename, int filesize, char *method) { 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, "%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)); //line:netp:servestatic:endserve printf("Response headers:\n"); printf("%s", buf); if (strcasecmp(method, "HEAD") == 0) return; /* 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 } ``` 3. 傳入 method 參數至 serve_dynamic function,並多設置一個 CGI 變數 `REQUEST_METHOD`(line 13)。 ```cpp= /* * serve_dynamic - run a CGI program on behalf of the client */ void serve_dynamic(int fd, char *filename, char *cgiargs, char *method) { char buf[MAXLINE], *emptylist[] = { NULL }; ... 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 setenv("REQUEST_METHOD", method, 1); 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 } ``` 4. 在 adder.c 增加 method 變數,藉此判斷是否要送出 response body(line 25、26)。 ```cpp= /* * adder_HEAD.c - a minimal CGI program that adds two numbers together */ #include "../csapp.h" int main(void) { char *buf, *p, *method; char arg1[MAXLINE], arg2[MAXLINE], content[MAXLINE]; int n1=0, n2=0; ... method = getenv("REQUEST_METHOD"); /* Make the response body */ sprintf(content, "Welcome to add.com: "); ... /* Generate the HTTP response */ printf("Connection: close\r\n"); printf("Content-length: %d\r\n", (int)strlen(content)); printf("Content-type: text/html\r\n\r\n"); if (strcasecmp(method, "HEAD") != 0) printf("%s", content); fflush(stdout); exit(0); } ``` telnet test result: 1. client request for static content: ``` E94064032@pn1:~/web_server/cgi-bin> telnet 140.116.245.27 5000 Trying 140.116.245.27... Connected to 140.116.245.27. Escape character is '^]'. HEAD / HTTP/1.1 HOST: 140.116.245.27 HTTP/1.0 200 OK Server: Tiny Web Server Connection: close Content-length: 380 Content-type: text/html Connection closed by foreign host. ``` 2. client request for dynamic content: ``` E94064032@pn1:~/web_server/cgi-bin> telnet 140.116.245.27 5000 Trying 140.116.245.27... Connected to 140.116.245.27. Escape character is '^]'. HEAD /cgi-bin/adder_HEAD?first=145&second=655 HTTP/1.1 HOST: 140.116.245.27 HTTP/1.0 200 OK Server: Tiny Web Server Connection: close Content-length: 111 Content-type: text/html Connection closed by foreign host. ``` --- ### 11.12 #### Extend Tiny so that it serves dynamic content requested by the HTTP POST method. Check your work using your favoriteWeb browser. 1. 在 home.html 裡新增 `method` 為 `POST` 的表格 ```htmlembedded= <h1>Adder form use POST method</h1> <form action="cgi-bin/adder_POST" method="post"> <p>First number:</p> <input type="text" name="first"><br> <p>Second number:</p> <input type="text" name="second"><br><br> <input type="submit" value="Submit"> </form> <p>Click on the submit button, and the input will be sent to server using POST method.</p> ``` 2. 修改 `read_requesthdrs`、`doit` function 根據 [RFC7231 section4.3.3](https://tools.ietf.org/html/rfc7231#section-4.3.3) > The POST method requests that the target resource process the representation enclosed in the request according to the resource's own specific semantics. 也就是說原本要傳給 adder 的參數不會放在 URL 裡,會放在 request message body 送給 server。 由於 request message body 最後不是以`'\n'`結尾,又因為 `Rio_readlineb` 的終止條件是讀取到換行符號,所以在 `read_requesthdrs` 裡不能使用 `Rio_readlineb` 讀取 `message body`,否則會卡住,需使用 `Rio_readnb`,這裡的修改是讓 `read_requesthdrs` 只讀取 `header` 與 區隔 `body` 與 `header` 最後結尾的 `CRLF`,由 `doit` 讀取 `body`,`adder_POST` 與 11.10 的 adder11.10 是一樣的不需作修改。 - 修改 `read_requesthdrs`: - 讓 function 利用 `Content-Length:` header 找出 message body 有幾個字元並回傳。 ```cpp= /* * read_requesthdrs - read HTTP request headers */ int read_requesthdrs(rio_t *rp, char *method) { char buf[MAXLINE]; int content_len; do { Rio_readlineb(rp, buf, MAXLINE); printf("%s", buf); if (strcasecmp(method, "POST") == 0 && strncasecmp(buf, "Content-Length:", 15) == 0) sscanf(buf, "Content-Length: %d", &content_len); } while(strcmp(buf, "\r\n")); return content_len; } ``` - 修改 `doit`: - 新增一個 `content_len` 變數存 `message body` 有幾個字元 - 用 `Rio_readnb` 讀取 `message body` 至 `buf array` (用 `buf array` 存參數) - 當 `method` 為 POST 時將 `buf array` 傳入 `serve_dynamic` (因為用 `buf array` 存參數,非 `cgiargs`) ```cpp= /* * doit - handle one HTTP request/response transaction */ void doit(int fd) { ... if (!(strcasecmp(method, "GET") == 0 || strcasecmp(method, "POST") == 0)) { //line:netp:doit:beginrequesterr clienterror(fd, method, "501", "Not Implemented", "Tiny does not implement this method"); return; } //line:netp:doit:endrequesterr int content_len = read_requesthdrs(&rio, method); //line:netp:doit:readrequesthdrs Rio_readnb(&rio, buf, content_len); ... 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; } if (strcasecmp(method, "POST") == 0) serve_dynamic(fd, filename, buf); else serve_dynamic(fd, filename, cgiargs); //line:netp:doit:servedynamic } } ``` 測試結果: ![](https://i.imgur.com/4Pw4xeP.png) ![](https://i.imgur.com/XSqt9kI.png) --- ### 11.13 #### Modify Tiny so that it deals cleanly (without terminating) with the `SIGPIPE` signals and `EPIPE` errors that occur when the write function attempts to write to a prematurely closed connection. #### what is `SIGPIPE` signals ? CSAPP 第11章第1000頁的 `Aside` 有提到 `SIGPIPE` signals 的產生及為什麼要處理它(防止 server 因為與 client 連線中斷而停止): > For example, if a server writes to a connection that has already been closed by the client (say, because you clicked the “Stop” button on your browser), then the first such write returns normally, but the second write causes the delivery of a SIGPIPE signal whose default behavior is to terminate the process. If the SIGPIPE signal is caught or ignored, then the second write operation returns −1 with errno set to EPIPE. The strerr and perror functions report the EPIPE error as a “Broken pipe,” a nonintuitive message that has confused generations of students. The bottom line is that a robust server must catch these SIGPIPE signals and check write function calls for EPIPE errors. :::info 是在 client 斷線後 server 寫入第二次時才會產生 `SIGPIPE` signal ::: Tiny server 都是用 `Rio_writen` 寫入資料,而 `Rio_writen` 是 CSAPP 打包的函式,加上了 error-handling。 `Rio_writen`: - unix_error: 傳入 string 後 write output to the stderr stream 並 print 出 error 訊息,然後 terminate process. - 沒有成功寫入 n bytes 就代表有 error,pritnt 出 error 並 exit ```cpp= void Rio_writen(int fd, void *usrbuf, size_t n) { if (rio_writen(fd, usrbuf, n) != n) unix_error("Rio_writen error"); } ``` 接著看 `rio_writen`: - 如果 `write` 因為 signal handler 中斷或沒寫入 n bytes 至 fd 就繼續呼叫 `write` - 因為其他錯誤而導至 `write` 失敗就 return -1 - 成功寫入 n bytes 就回傳 n ```cpp= ssize_t rio_writen(int fd, void *usrbuf, size_t n) { size_t nleft = n; ssize_t nwritten; char *bufp = usrbuf; while (nleft > 0) { if ((nwritten = write(fd, bufp, nleft)) <= 0) { if (errno == EINTR) /* Interrupted by sig handler return */ nwritten = 0; /* and call write() again */ else return -1; /* errno set by write() */ } nleft -= nwritten; bufp += nwritten; } return n; } ``` 再來是 `write`: ```cpp= ssize_t write(int fd, const void *buf, size_t n); ``` 節錄至 [Linux man page](https://linux.die.net/man/3/write) > The write function copies at most n bytes from memory location buf to the current file position of descriptor fd. - 從記憶體位置 `buf` 複製 "***最多***" n bytes 到 file descriptor `fd`。 > Upon successful completion, write() shall return the number of bytes actually written to the file associated with fd. This number shall never be greater than n byte. Otherwise, -1 shall be returned and errno set to indicate the error. > The write() function shall fail if: > EPIPE > - A write was attempted on a socket that is shut down for writing, or is no longer connected. In the latter case, if the socket is of type SOCK_STREAM, the SIGPIPE signal is generated to the calling process. - 所以 `Rio_writen` 在 - `rio_writen` 失敗後要去檢查 error 是否為 `EPIPE`, - 不是的話就呼叫 `unix_error`, - 是的話與 `rio_writen` 一樣回傳 -1 (採用 [Tiny web server](https://hackmd.io/@ofAlpaca/S1OZ7bGb4) 的方法)。 - `rio_writen` 成功後回傳 0 修改後的 `Rio_writen`: ---