大家好,欢迎来到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
)。 - 如果成功,就调用
listen
将listenfd
转化为监听描述符。此时,服务器已经开始在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,得到
hostname
,port
,filename
。以上面的请求为例,hostname=“http://pku.edu.cn”,port=“80”(默认),filename=”/”(默认)。实现在parse_uri
中。 - 调用
open_clientfd
建立与服务器hostname
的port
端口的连接。 - 转发请求行: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