CSAPP Lab -- Proxy Lab

CSAPP Lab -- Proxy Lab

來自專欄 Computer Science學習筆記4 人贊了文章

準備

這是最後一個lab,要求實現一個並發Web代理。相比本書前半部分的lab,這個任務很容易完成,不需要苦思冥想。讀過最後三章的內容,就可以開始了。需要的知識點有:

  • client-server模型
  • 簡單的socket編程
  • Web伺服器基礎和HTTP協議基礎
  • 多線程並發編程,讀者-寫者模型
  • Cache相關知識(可選)

先去官網下載所需的材料,一定要下載Writeup,它會提供很大的幫助。閱讀Writeup,可以了解到一些實驗相關的信息:

  • Web代理是一個Web伺服器和瀏覽器之間的中間程序。瀏覽器和代理連接,然後代理再將請求轉發給伺服器。伺服器的相應,也由代理返回給瀏覽器。
  • port-for-user.pl用來自動生成可用埠(沒啥用,可以自己隨便選個比較大的,基本都能用)
  • 附贈一個tiny web伺服器,用來進行測試
  • 實驗分為三步。第一步實現基本的轉發,第二步實現並發,第三步加入cache功能
  • 可以用curl這個工具來進行測試,例如:curl -v --proxy http://localhost:15214 http://localhost:15213/home.html就是向代理http://localhost:15214發送請求,得到後面那個uri的資源

弄清實驗要求後,就可以構思大致的思路了。類似於web伺服器,只是接受client的request後,解析一下,然後將信息轉發給對應的伺服器,等伺服器響應後,將responce信息從伺服器對應的描述符中讀取出來,再寫入和client對應的描述符就完成了。

完整代碼:

xiebei1108/csapp_code?

github.com圖標

Part1: 簡單實現

HTTP request中包含請求行和請求頭,所以我們可以設計對應的數據結構,方便存儲解析後的數據:

typedef struct { char host[HOSTNAME_MAX_LEN]; char port[PORT_MAX_LEN]; char path[MAXLINE];} ReqLine;typedef struct { char name[HEADER_NAME_MAX_LEN]; char value[HEADER_VALUE_MAX_LEN];} ReqHeader;

請求行實際上是一個map,但是C語言實現比較麻煩,就直接用了一個數組來保存。

回想一下web伺服器的實現,main()函數裡面bind套接字、listen一個埠、accept等待連接,然後再處理這個描述符。這個過程完成就和代理一樣,可以直接套用,不同的只是處理函數:

int main(int argc, char **argv) { int listenfd, *connfd; pthread_t tid; char hostname[MAXLINE], port[MAXLINE]; socklen_t clientlen; struct sockaddr_storage clientaddr; if (argc != 2) { fprintf(stderr, "usage: %s <port>
", argv[0]); exit(1); } init_cache(); listenfd = Open_listenfd(argv[1]); while (1) { clientlen = sizeof(clientaddr); connfd = Malloc(sizeof(int)); *connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen); Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE, 0); printf("Accepted connection from (%s, %s)
", hostname, port); Pthread_create(&tid, NULL, thread, connfd); //處理函數,這個是create了一個新線程 }}

在創建新線程後,它會自動去執行thread()函數,然後thread()函數會去調用主要的處理函數doit()。如果不看並發實現部分的代碼,doit()函數中的邏輯大概就是:parse_request得到解析後的請求行和請求頭,然後send_to_server(),讓他去連接對應的伺服器,向伺服器發送請求。和伺服器建立連接後,返回的信息會在描述符connfd中,也就是send_to_server()函數的返回值。然後再把信息從connfd中讀取出來,直接寫進客戶端對應的描述符fd就行了。

void doit(int fd) { char buf[MAXLINE], uri[MAXLINE], object_buf[MAX_OBJECT_SIZE]; int total_size, connfd; rio_t rio; ReqLine request_line; ReqHeader headers[20]; int num_hds, n; parse_request(fd, &request_line, headers, &num_hds); strcpy(uri, request_line.host); strcpy(uri+strlen(uri), request_line.path); if (reader(fd, uri)) { fprintf(stdout, "%s from cache
", uri); fflush(stdout); return; } total_size = 0; connfd = send_to_server(&request_line, headers, num_hds); Rio_readinitb(&rio, connfd); while ((n = Rio_readlineb(&rio, buf, MAXLINE))) { Rio_writen(fd, buf, n); strcpy(object_buf + total_size, buf); total_size += n; } if (total_size < MAX_OBJECT_SIZE) { writer(uri, object_buf); } Close(connfd);}

解析部分的代碼很簡單,就不解釋了:

void parse_request(int fd, ReqLine *linep, ReqHeader *headers, int *num_hds) { char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE]; rio_t rio; Rio_readinitb(&rio, fd); if (!Rio_readlineb(&rio, buf, MAXLINE)) return; //parse request line sscanf(buf, "%s %s %s", method, uri, version); parse_uri(uri, linep); //parse request headers *num_hds = 0; Rio_readlineb(&rio, buf, MAXLINE); while(strcmp(buf, "
")) { headers[(*num_hds)++] = parse_header(buf); Rio_readlineb(&rio, buf, MAXLINE); }}void parse_uri(char *uri, ReqLine *linep) { if (strstr(uri, "http://") != uri) { fprintf(stderr, "Error: invalid uri!
"); exit(0); } uri += strlen("http://"); char *c = strstr(uri, ":"); *c = ; strcpy(linep->host, uri); uri = c + 1; c = strstr(uri, "/"); *c = ; strcpy(linep->port, uri); *c = /; strcpy(linep->path, c);}ReqHeader parse_header(char *line) { ReqHeader header; char *c = strstr(line, ": "); if (c == NULL) { fprintf(stderr, "Error: invalid header: %s
", line); exit(0); } *c = ; strcpy(header.name, line); strcpy(header.value, c+2); return header;}

解析完後,向伺服器發送請求:

int send_to_server(ReqLine *line, ReqHeader *headers, int num_hds) { int clientfd; char buf[MAXLINE], *buf_head = buf; rio_t rio; clientfd = Open_clientfd(line->host, line->port); Rio_readinitb(&rio, clientfd); sprintf(buf_head, "GET %s HTTP/1.0
", line->path); buf_head = buf + strlen(buf); for (int i = 0; i < num_hds; ++i) { sprintf(buf_head, "%s: %s", headers[i].name, headers[i].value); buf_head = buf + strlen(buf); } sprintf(buf_head, "
"); Rio_writen(clientfd, buf, MAXLINE); return clientfd;}

這樣,就實現web代理的基本功能了。實際上,web代理就是同時擔任客戶端和服務端,然後中間加上個信息的處理就行了。

Part2: 並發

實現並發有很多方法,比如用多進程、IO多路復用、多線程、預線程化(利用生產者-消費者模型)。我採用的是多線程的方法,因為實現起來比較簡單。只需要每次accept之後,創建一個新的線程來處理這個連接就行:

void *thread(void *vargp) { int connfd = *((int*)vargp); Pthread_detach(pthread_self()); //將線程分離,讓這個線程計數結束後自己回收資源 Free(vargp); doit(connfd); Close(connfd); return NULL;}

基本就是將doit()函數包在thread()裡面,然後注意將線程分離出去,避免內存泄露。

Part3: Cache

在server和client之間加入代理的好處之一,就可以實現cache化。因為,經常有很多對同一個資源多次請求的情況,如果每次都從服務端獲取,那樣伺服器會很累。如果可以在代理部分就實現一個cache,將最近客戶端請求過的數據給存儲起來,那樣就不需要每次都要從伺服器請求了,進而提高伺服器的效率。

做到這個部分的時候,最先想到的是cache lab裡面實現的cache模擬器。所以,可以參考那個思路,先定義cache的數據結構:

typedef struct { char *name; char *object;} CacheLine;typedef struct { int used_cnt; CacheLine* objects;} Cache;

這個做法實際不是很好,因為不能充分利用內存空間,但是這樣比較簡單粗暴。這裡有很多選擇,比如cache可以放在硬碟中,也可以放在內存中。我選擇了放在內存中,原因還是實現起來比較簡單。可以實現LRU,但是這裡沒有LRU。原因是因為,我在這裡用了讀者-寫者模型,可以讓多個線程同時來讀。但是,如果用LRU的話,在每次讀之後,都需要更新時間戳,就違背了只讀不寫的原則。

void init_cache() { Sem_init(&mutex, 0, 1); Sem_init(&w, 0, 1); readcnt = 0; cache.objects = (CacheLine*)Malloc(sizeof(CacheLine) * 10); cache.used_cnt = 0; for (int i = 0; i < 10; ++i) { cache.objects[i].name = Malloc(sizeof(char) * MAXLINE); cache.objects[i].object = Malloc(sizeof(char) * MAX_OBJECT_SIZE); }}

所以,最後確定的方案就是,將1MiB內存分為十塊,也就是可以緩存十個web object。然後,採用讀者-寫者模型。再考慮實現,大概就是每次接受請求並解析後,先去cache里看看有沒有對應的web object,如果有就直接返回給客戶端,如果沒有就只能老老實實向服務端請求,然後再加入到緩存,再返回給客戶端了。這個邏輯都在doit()函數裡面實現了。

還沒有提到的就是讀者-寫者模型:

Cache cache;int readcnt; //用來記錄讀者的個數sem_t mutex, w; //mutex用來給readcnt加鎖,w用來給寫操作加鎖int reader(int fd, char *uri) { int in_cache= 0; P(&mutex); readcnt++; if (readcnt == 1) { P(&w); } V(&mutex); for (int i = 0; i < 10; ++i) { if (!strcmp(cache.objects[i].name, uri)) { Rio_writen(fd, cache.objects[i].object, MAX_OBJECT_SIZE); in_cache = 1; break; } } P(&mutex); readcnt--; if (readcnt == 0) { V(&w); } V(&mutex); return in_cache;}void writer(char *uri, char *buf) { P(&w); strcpy(cache.objects[cache.used_cnt].name, uri); strcpy(cache.objects[cache.used_cnt].object, buf); ++cache.used_cnt; V(&w);}

這樣就完成了!

推薦閱讀:

進程與進程管理 | 線程的基本概念
基於 FUSE 的 Bittorrent 文件系統
主存管理 | 段式存儲管理方式
進程與進程管理 | 進程調度
9front 和 cat-v 的來歷

TAG:編程 | 操作系統 | CSAPP |