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
表示因特網主機 www.google.com 上一個稱為 index.html 的HTML文件。它是由一個監聽port80的Webserver所管理的。
HTTP默認port號為80
可執行文件的URL能夠在文件名稱后包含程序參數, “?”字符分隔文件名稱和參數,而且每一個參數都用“&”字符分隔開。如:
表示一個 /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) 執行前准備
- 將被訪問的文件放在tiny同級文件夾下(home.html、photo.jpg)
<html>
<head>
<title>Hello World</title>
</head>
<body>
<h1>Welcome to Tiny Web Server</h1>
</body>
</html>
- 將測試用CGI程序放到cgi-bin文件夾下。並編譯成可執行程序
$ gcc -o adder adder.c
(4) 執行流程及其結果
- 執行Tiny程序,並指定port號(1024–49151可用,其它為知名port)
$ ./tiny 1024
瀏覽器訪問靜態內容(home.html)
瀏覽器訪問不存在的內容
瀏覽器訪問動態內容
還能夠訪問圖片哦
(5) Telnet 測試
- 連接到Tinyserver
$ telnet localhost 1024
- 輸入請求頭(注意空行)
GET /home.html HTTP/1.0
- 驗證結果(注意空行)
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.
- 錯誤的返回
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 能夠得到數據,則能夠推斷為少了一個空行。