这次实验要求是实现一个具有缓存功能的代理,它主要有三点评测标准:
- 能正确转发用户的请求给服务器,并将服务器响应报文转发给用户;
- 能够同时处理多个用户的请求;
- 对从服务器获得的对象缓存。
这三点要求可以循序渐进地实现。首先是完成它的基本功能:请求和响应报文的转发。它的逻辑非常简单:创建一个监听描述符,供用户连接;连接上后,从用户处获取请求信息。将请求解析后(主机名、端口号),构建HTTP请求报文,发送给对应服务器。从对应服务器接收响应报文后转发给用户。以下是主线程逻辑,和课本源代码大同小异:
int main(int argc, char **argv) {
int listenfd, connfd;
char hostname[MAXLINE], port[MAXLINE];
socklen_t clientlen;
struct sockaddr_storage clientaddr;
/* Check command line args */
if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(1);
}
listenfd = Open_listenfd(argv[1]);
while (1) {
clientlen = sizeof(clientaddr);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\n", hostname, port);
handle_request(connfd);
Close(connfd);
}
}
handle_request
函数处理用户请求。首先,从用户请求中获取请求行(请求头可忽略),使用parse_uri
方法解析出主机名、文件名、端口号(非必要)。然后,根据用户提供的信息,使用函数construct_request
构建请求报文。通过主机名和端口号连接服务器,完成数据传输。注意在handle_request
有大量系统调用函数,需要不断检测异常。CSAPP提供的包装函数在发现异常后,报告并调用exit(0)
结束进程,显然是不适合服务器的。在网络应用中,发现异常后更适合报告然后忽略之,否则整个应用就这么挂掉了。所以我改成在handle_request
返回,起码进程还继续。解析URI函数parse_uri
和构建报文函数construct_request
没什么好说的,URI、HTTP请求报文长什么样它们就写成什么样。
void handle_request(int fd) {
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE],
host[MAXLINE], msg[MAXLINE], requests[MAXLINE], port[8];
char filename[MAXLINE];
int serverfd;
rio_t rio, server_rio;
/* Read request line and headers */
Rio_readinitb(&rio, fd);
if (rio_readlineb(&rio, buf, MAXLINE) < 0) {
sprintf(msg, "Error: Reading requests from cmdline.\n");
Rio_writen(fd, msg, strlen(msg));
return;
}
printf("%s", buf);
sscanf(buf, "%s %s %s", method, uri, version);
if (strcasecmp(method, "GET")) {
sprintf(msg, "Error: Method %s other than GET not implemented.\n", method);
Rio_writen(fd, msg, strlen(msg));
return;
}
/* Parse URI from GET request, get host, filename and port */
if (!parse_uri(uri, host, filename, port)) {
sprintf(msg, "Error: URI parsing: %s\n", uri);
Rio_writen(fd, msg, strlen(msg));
return;
}
// construct request message
construct_request(requests, host, filename, port, &rio);
// connect end server
if ((serverfd = open_clientfd(host, port)) < 0) {
sprintf(msg, "Error: Connecting end server %s:%s\n", host, port);
Rio_writen(fd, msg, strlen(msg));
return;
}
// send message
Rio_readinitb(&server_rio, serverfd);
if (rio_writen(serverfd, requests, strlen(requests)) < 0) {
sprintf(msg, "Error: Sending message to end server\n");
Rio_writen(fd, msg, strlen(msg));
return;
}
// receive response from server and send to user
size_t n;
while ((n = rio_readlineb(&server_rio, buf, MAXLINE)) != 0) {
if (rio_writen(fd, buf, n) < 0) {
sprintf(msg, "Error: Forwarding message to user: %s\n", buf);
Rio_writen(fd, msg, strlen(msg));
return;
}
}
if (close(serverfd) < 0) {
sprintf(msg, "Error: Closing server FD\n");
Rio_writen(fd, msg, strlen(msg));
return;
}
}
int parse_uri(char *uri, char *host, char *filename, char *port) {
port[0] = '8', port[1] = '0', port[2] = '\0'; // default port
filename[0] = '/', filename[1] = '\0'; // default filename
host[0] = '\0'; // default host
// find // in uri
char *p = strstr(uri, "//");
if (p) p += 2; // skip "http://"
else p = uri; // no "http://"
char *p2 = index(p, ':'); // find port number
if (p2) { // has explicit port number
memcpy(host, p, p2 - p);
char *p3 = index(p2, '/');
memcpy(port, p2 + 1, p3 - p2 - 1); // extract port
p2 = p3; // reach next char after port number
} else {
p2 = index(p, '/');
memcpy(host, p, p2 - p);
}
if (strlen(host) < 1) // error
return 0;
if (*p2) { // has filename
strcpy(filename, p2);
}
return 1;
}
void construct_request(char *requests, char *host, char *filename, char *port, rio_t *rio) {
sprintf(requests, "GET %s HTTP/1.1\r\n", filename);
sprintf(requests, "%sHost: %s\r\n", requests, host);
sprintf(requests, "%s%s\r\n", requests, user_agent_hdr);
sprintf(requests, "%sConnection: close\r\n", requests);
sprintf(requests, "%sProxy-Connection: close\r\n", requests);
sprintf(requests, "%s\r\n", requests);
}
这是满足第一个条件的代理,只接受GET方法的请求。评测脚本跑分得了满分,但是挂在火狐浏览器或者Windows系统代理上,这个代理都是不起作用的。原因是现在的代理接收的都是CONNECT方法的请求。而我的代理只能处理GET请求,所以没有用。只能在curl里做点小实验。另外,在curl下使用代理:
curl -v --proxy http://localhost:12345 http://www.baidu.com
如果把代理转发的请求报文头User-Agent
改成curl/7.58.0
,是可以正常收取网页信息的。然而,换成了火狐或者Chrome的Agent就不行了。由于我不擅长网络,对这点的原理也不清楚。跳过这个疑问,接下来实现第二个作业要求,支持并发的请求。这个要求很简单,只要在主线程监听时为每个请求任务pthread_create
一个线程,并pthread_detach
之就可以了:
while (1) {
clientlen = sizeof(clientaddr);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\n", hostname, port);
if (pthread_create(&tid, NULL, handle_request, &connfd) != 0) {
printf("Error: create thread.\n");
}
if (pthread_detach(tid) != 0) {
printf("Error: Detach thread.\n");
}
}
第三个要求,为代理设计缓存。我的想法是用一个全局的结构体数组来保存缓存。访问这个数组和其相关变量时,要用读者-写者模式(读者优先)同步。这样,就需要读者专用的互斥信号量mutex
,以及读者和写者公用的信号量w
。首先,在从服务器获得一份新数据后,判断其是否可以缓存(大小不大于MAX_OBJECT_SIZE
)。如果可以,便将其缓存在全局数组cache
中。如果缓存后,总缓存大小超过MAX_CACHE_SIZE
,便通过LRU
算法踢出最远访问的缓存数据。由于测试样例比较简单(懒),所以我的临界区和LRU
没有优化,都用了最简单粗暴的办法(不是重点……)。
// 数据对象的大小不超过MAX_OBJECT_SIZE,即可缓存
if (obj_size <= MAX_OBJECT_SIZE) {
// 记录元素的访问时间
obj->time = incre_num++;
// 保存主机名、端口号、文件名
strcpy(obj->host, host), strcpy(obj->port, port);
strcpy(obj->filename, filename);
// lock mutex
P(&w);
if (obj_size + cache_size > MAX_CACHE_SIZE) {
// pick a victim. LRU
int min_time = 0x7fffffff, min_idx = -1, i;
for (i = 0; i < obj_num; ++i) {
if (cache[i]->time < min_time)
min_time = cache[i]->time, min_idx = i;
}
// remove cache[min_idx]
free(cache[min_idx]);
for (i = min_idx + 1; i < obj_num; ++i) {
cache[i-1] = cache[i];
}
--obj_num;
} else { // add obj to cache directly
cache[obj_num++] = obj;
}
V(&w);
}
缓存好了数据以后,在接收到新的请求时,先判断请求数据是否被缓存。如果在缓存中匹配上请求,直接从缓存中复制数据,回传给用户即可,不需要向服务器转发请求。
// if in cache
int flag = 0; // 标记是否在缓存中
P(&mutex); // 读者-写者
++read_cnt;
if (read_cnt == 1) P(&w);
V(&mutex);
for (int i = 0; i < obj_num; ++i) {
// 匹配主机名、端口号、文件名
if (!strcasecmp(cache[i]->host, host) && !strcasecmp(cache[i]->port, port) &&
!strcasecmp(cache[i]->filename, filename)) {
// 从缓存中读取数据
if (rio_writen(fd, cache[i]->cache_content, strlen(cache[i]->cache_content)) < 0) {
sprintf(msg, "Error: Copy cache contents\n");
} else {
flag = 1;
}
break;
}
}
P(&mutex);
--read_cnt;
if (read_cnt == 0) V(&w);
V(&mutex);
if (flag) { // successfully copy from cache
if (close(fd) < 0) { // 关闭连接
sprintf(msg, "Error: Closing client FD\n");
Rio_writen(fd, msg, strlen(msg));
return NULL;
}
return NULL;
}
总得来说,这次实验是比较简单,又比较实用的。这次实验所涉及的套接字通信、多线程,都是准备找工作时重要的技能。书上讲的比较基础,实验的测试样例也很简单,所以这次实验做出来的效果其实也差强人意,想要做出一个功能强大的网络代理还需要学习更多的知识。至此,CSAPP的7个实验就做完了,这是很棒的一门课程,很遗憾我没有早点接触o(╥﹏╥)o。这本书是我读过的计算机专业书籍里最有趣,又最受用的。将来要买一本原版的珍藏……再见吧,我要继续准备找工作了(躺倒……)