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
表示因特網主機 www.google.com 上一個稱爲 index.html 的HTML文件。它是由一個監聽port80的Webserver所管理的。多線程
HTTP默認port號爲80app
可執行文件的URL可以在文件名稱後包含程序參數, 「?」字符分隔文件名稱和參數,並且每個參數都用「&」字符分隔開。如:函數
表示一個 /cgi-bin/adder 的可執行文件,帶兩個參數字符串爲 123 和 456
肯定一個URL指向的是靜態內容仍是動態內容沒有標準的規則,常見的方法就是把所有的可執行文件都放在 cgi-bin 文件夾中
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地址很是快將用盡。
一個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 指示響應主體的字節大小
響應主體中包含着被請求的內容。
GET請求的參數在URI中傳遞, 「?」字符分隔了文件名稱和參數,每個參數都用一個」&」分隔開,參數中不一樣意有空格,必須用字符串「%20」來表示
HTTP POST請求的參數是在請求主體中而不是 URI中傳遞的
GET /cgi-bin/adder?123&456 HTTP/1.1
它調用 fork 來建立一個子進程。並調用 execve 在子進程的上下文中執行 /cgi-bin/adder 程序
在調用 execve 以前,子進程將CGI環境變量 QUERY_STRING 設置爲」123&456」, adder 程序在執行時可以用unix getenv 函數來引用它
環境變量 | 描寫敘述 |
---|---|
QUERY_STRING | 程序參數 |
SERVER_PORT | 父進程偵聽的port |
REQUEST_METHOD | GET 或 POST |
REMOTE_HOST | client的域名 |
REMOTE_ADDR | client的點分十進制IP地址 |
CONTENT_TYPE | 僅僅對POST而言。請求體的MIME類型 |
CONTENT_LENGTH | 僅僅對POST而言,請求體的字節大小 |
一個CGI程序將它的動態內容發送到標準輸出。在子進程載入並執行CGI程序以前,它使用UNIX dup2 函數將它標準輸出重定向到和client相關連的已鏈接描寫敘述符
所以,不論什麼CGI程序寫到標準輸出的東西都會直接到達client
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);
}
}
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
}
}
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;
}
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;
}
}
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");
}
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
}
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
將所有源文件tiny.c、csapp.c和csapp.h放在同一個文件夾下。
$ gcc -o tiny tiny.c csapp.c -lpthread
注:加-lpthread是因爲csapp.c中有些函數用了多線程庫
<html>
<head>
<title>Hello World</title>
</head>
<body>
<h1>Welcome to Tiny Web Server</h1>
</body>
</html>
$ gcc -o adder adder.c
$ ./tiny 1024
瀏覽器訪問靜態內容(home.html)
瀏覽器訪問不存在的內容
瀏覽器訪問動態內容
還可以訪問圖片哦
$ 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.
需要注意的是 HTTP 協議的頭部和數據之間有一個空行,假設瀏覽器沒法查看到內容,而經過 Telnet 可以獲得數據,則可以推斷爲少了一個空行。