深入理解计算机系统(CSAPP):Proxy Lab

深入理解计算机系统(CSAPP):Proxy Lab本文档介绍了如何实现一个代理服务器 涉及网络和并发编程的知识

大家好,欢迎来到IT知识分享网。

在Proxy Lab中,我们会自己实现一个代理服务器。这涉及到网络(教材第十一章)、并发(教材第十二章)的相当多概念。建议大家在阅读Lab指导的时候,反复回顾教材上的相关内容。

proxy究竟是个啥?

前置知识回顾

网络
/ 伪码 / int open_clientfd(char* hostname, char* port) { 
    listp <- getaddrinfo(hostname, port); for each p in listp { 
    clientfd = socket(); connect(clientfd); if (success)return clientfd; } } 
  • 客户端调用open_clienfd,尝试与主机名为hostname,监听端口为port的服务器建立连接。
  • 首先使用Linux提供的getaddrinfo函数,生成套接字地址(socket address)的链表,即listp
  • 遍历listp,依次尝试socket(创建套接字描述符),connect(建立连接)。如果成功了,那么clientfd就是一个成功打开的网络套接字。对于客户端来说,这就相当于一个普通的文件,可以用系统IO函数读写(在这里,Linux“万物皆文件”的思想很大程度上简化了网络编程)。
/ 伪码 / int open_listenfd(char* port) { 
    listp <- getaddrinfo(port); for each p in listp { 
    listenfd = socket(); bind(listenfd); if (success){ 
    listen(listenfd); return listenfd; } } } 
  • 服务器调用open_listenfd,尝试在端口port上进行监听。
  • 同上使用getaddrinfo生成listp
  • 遍历listp,依次尝试socket(创建套接字描述符),bind(将套接字地址绑定到套接字描述符listenfd)。
  • 如果成功,就调用listenlistenfd转化为监听描述符。此时,服务器已经开始在listenfd上监听客户端的请求,调用accept成功后便可得到已连接描述符。

现在我们可以写出proxy的基本框架了。以简单的迭代(非并发)服务器为例:

/ 伪码 / int main() { 
    listenfd = open_listenfd(port); while(1) { 
    connfd = accept(listenfd); doit(connfd); close(connfd); } } 

以上代理服务器与客户端建立了连接。

/ 伪码 / void doit(int connfd){ 
    read http request from connfd; clientfd = open_clientfd(); generate and send http request to clientfd; while (read http response from clientfd) { 
    cache http response (optional); write http response back to connfd; } } 

以上代理服务器与真正的服务器建立了连接并转发请求,随后将相应传回客户端。

并发

迭代服务器的显著缺点是,一次只服务一个客户端。对此我们有许多改进方案。教材上详细讲述了三种:基于进程(process)、基于线程(thread)、IO多路复用。这里我们使用多线程的方法。

/ 伪码 / int main() { 
    listenfd = open_listenfd(port); while(1) { 
    connfd = accept(listenfd); pthread_create(thread, connfd); } } void* thread(void* vargp) { 
    pthread_detach(pthread_self()); doit(connfd); close(connfd); } 
  • 当代理服务器与一个客户端建立连接后,它并不直接与客户端交互,而是创建一个新的线程去服务这个客户端。
  • 创建新的线程后,内核调度新线程时会执行线程例程(thread routine)thread
  • thread首先将自己从主线程“分离”出去,即主线程不需要显式地回收它,内核会在它结束时自动回收。
  • 代理服务器在这个线程中像之前一样执行各种操作。
  • 定义读写锁:pthread_rwlock_t p;
  • 初始化:pthread_rwlock_init(&p, NULL);
  • 销毁:pthread_rwlock_destroy(&p);
  • 加读锁:pthread_rwlock_rdlock(&p);
  • 加写锁:pthread_rwlock_wrlock(&p);
  • 释放锁:pthread_rwlock_unlock(&p);

像书上用信号量和计数变量实现也可,稍麻烦一些。

处理HTTP事务

其实我们回顾前置知识的时候已经搭好了proxy的框架。现在我们来关注doit函数。

  • 从客户端读取请求行(request line)。例如:
    GET http://pku.edu.cn HTTP/1.0
  • 分割URL,得到hostnameportfilename。以上面的请求为例,hostname=“http://pku.edu.cn”,port=“80”(默认),filename=”/”(默认)。实现在parse_uri中。
  • 调用open_clientfd建立与服务器hostnameport端口的连接。
  • 转发请求行:GET <filename> HTTP/1.0
  • 转发请求报头(request header)。按照要求来即可(需要仔细阅读实验指导)。实现在generate_requesthdrs中。
  • 接收并写回HTTP响应。

代码如下。其中实现缓存功能的语句请参考下文。

void generate_requesthdrs(rio_t *rp, char *req_buf, char *hostname); void parse_uri(char *uri, char *hostname, char *port, char *filename); /* * doit - handle one HTTP request/response transaction */ void doit(int fd, size_t t) { 
    char buf[MAXLINE]; rio_t rio_client, rio_server; /* Read request line and headers */ Rio_readinitb(&rio_client, fd); /* read http request line */ /* eg: GET http: //www.cmu.edu:8080/hub/index.html HTTP/1.1 */ if (!Rio_readlineb(&rio_client, buf, MAXLINE)) return; char method[MAXLINE], url[MAXLINE], version[MAXLINE]; if (3 != sscanf(buf, "%s %s %s", method, url, version)) return; if (strcasecmp(method, "GET")) { 
    clienterror(fd, method, "501", "Not Implemented", "do not implement this method"); return; } /* eg: hostname: http: //www.cmu.edu port: 8080 filename: /hub/index.html */ char hostname[MAXLINE], port[MAXLINE], filename[MAXLINE]; /* Parse URI from GET request */ parse_uri(url, hostname, port, filename); /* find the source in cache */ if (read_cache(url, fd, t)) return; /* connect with the server */ int connfd; if ((connfd = Open_clientfd(hostname, port)) < 0) return; Rio_readinitb(&rio_server, connfd); char req_buf[MAXLINE << 1]; /* request line */ sprintf(req_buf, "GET %s HTTP/1.0\r\n", filename); /* request header */ generate_requesthdrs(&rio_client, req_buf, hostname); Rio_writen(connfd, req_buf, MAXLINE); ssize_t len, cachelen = 0; char *cachebuf = malloc(sizeof(char) * MAX_OBJECT_SIZE); while ((len = Rio_readlineb(&rio_server, buf, MAXLINE)) > 0) { 
    if (cachelen + len < MAX_OBJECT_SIZE) memcpy(cachebuf + cachelen, buf, len); cachelen += len; Rio_writen(fd, buf, len); } /* try to cache */ if (cachelen < MAX_OBJECT_SIZE) write_cache(url, cachebuf, cachelen, t); free(cachebuf); Close(connfd); } 

添加缓存功能

cache数据结构
/* Recommended max cache and object sizes */ #define MAX_CACHE_SIZE  #define MAX_OBJECT_SIZE  #define CACHE_BLOCK_NUM 10 typedef struct { 
    int empty; size_t lru; size_t size; pthread_rwlock_t rwlock; char *obj; char *url; } cache_t; cache_t cache[CACHE_BLOCK_NUM]; 

推荐的单个缓存大小为MAX_OBJECT_SIZE(100KB),总缓存大小为MAX_CACHE_SIZE(约1MB)。方便起见,就设10个缓存位置(当然可以像MallocLab一样采用更节省空间的方式,但相信做过Malloc的同学都不会想再做一次)。
cache包括索引url和内容obj(大小为size),读写锁rwlock,空闲标记empty(当然这个可以用size来判断),LRU(Least Recently Used)策略的计数lru

缓存操作
void init_cache(); void free_cache(); int find_cache(char *url) { 
    int find = -1; for (int i = 0; i < CACHE_BLOCK_NUM; ++i) { 
    pthread_rwlock_rdlock(&cache[i].rwlock); if (!cache[i].empty && strcmp(cache[i].url, url) == 0) { 
    find = i; } pthread_rwlock_unlock(&cache[i].rwlock); if (find != -1) break; } return find; } int read_cache(char *url, int clientfd, size_t t) { 
    int num = find_cache(url); /* not found */ if (num == -1) return 0; pthread_rwlock_rdlock(&cache[num].rwlock); int flag = strcmp(url, cache[num].url); /* covered after find */ if (flag != 0) return 0; pthread_rwlock_unlock(&cache[num].rwlock); /* try updating lru */ /* no reader, add write lock and write url */ if (!pthread_rwlock_trywrlock(&cache[num].rwlock)) { 
    cache[num].lru = t; pthread_rwlock_unlock(&cache[num].rwlock); } /* read obj and send to the client */ pthread_rwlock_rdlock(&cache[num].rwlock); Rio_writen(clientfd, cache[num].obj, cache[num].size); pthread_rwlock_unlock(&cache[num].rwlock); return 1; } int find_evict() { 
    size_t minn = 1 << 30; int idx = -1; for (int i = 0; i < CACHE_BLOCK_NUM; ++i) { 
    pthread_rwlock_rdlock(&cache[i].rwlock); if (cache[i].empty) { 
    pthread_rwlock_unlock(&cache[i].rwlock); return i; } if (cache[i].lru < minn) { 
    minn = cache[i].lru; idx = i; } pthread_rwlock_unlock(&cache[i].rwlock); } return idx; } void write_cache(char *url, char *obj, size_t size, size_t t) { 
    int num = find_evict(); pthread_rwlock_wrlock(&cache[num].rwlock); strcpy(cache[num].url, url); memcpy(cache[num].obj, obj, size); cache[num].lru = t; cache[num].size = size; cache[num].empty = 0; pthread_rwlock_unlock(&cache[num].rwlock); } 
  • 重点关注读写锁操作。原则就是读时加读锁,写时加写锁,中途退出的时候不要忘了解锁。
  • read_cache先调用find_cache找到URL对应的缓存块。但是找到缓存块之后可能下一个拿到锁的是写者,可能会修改这个缓存块的内容。因此要再次确认内容无误。
  • 按照严格的LRU策略,read_cache读缓存命中后要更新lru,但是这是一个写操作,而写者要等到没有读者才能拿到锁。如果每个读命中后都要写,那么大量的写者会饥饿(starvation),更会导致lru更新顺序错乱。所以我们调用pthread_rwlock_trywrlock,只在可以即时拿到写锁的情况下更新lru

提高健壮性

使用tiny服务器进行的测试其实非常水,通过本地测试的proxy可能完全没法在真正的浏览器上运行。所以我们要尽可能地提高proxy的健壮性(robustness)!

  • 使用memcpy代替strcpy。网络内容肯定不止字符串,图片、视频等都是二进制数据,使用strcpy会出错。
  • 调用各种函数时,必须检查返回值。如果返回值指明错误,应当采取恰当的措施。这里直接使用"csapp.h"封装的大写字母开头的函数即可。
  • 修改发生异常的行为。显然代理服务器不能碰到异常就exit,他应该忍辱负重、心平气和地告诉客户和真正的服务器:出错了,再来一遍吧。所以我们要把unix_error等函数里的exit全去掉,如果有兴趣还可以添加错误提示。
  • 对客户端的输入几乎不能有任何假设。在处理输入的时候要加一堆特判。
  • malloc以后别忘了free,打开文件/建立连接以后别忘了close。长时间运行的服务器会把这些小错误无限放大。
  • 反复测试,避免并发错误。创建线程、使用读写锁的时候要特别注意。

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/125993.html

(0)
上一篇 2025-09-22 16:26
下一篇 2025-09-22 16:33

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信