前情提要,如果看了书本,这个lab难度不高,但是如果不看书,难度还是挺高的,并且这个lab会用到cachelab中学到的东西,需要阅读
- 第十章:系统编程
- 第十一章:网络编程
- 第十二章:并发
实验介绍
- 使用代理完成客户端和服务器的连接(HTTP操作,socket通信)
- 接受客户端的连接,读并分析请求
- 将请求发送给服务器
- 读取服务器的回应,并将回应发送给对应的客户端
- 实现多线程的功能
- 增加cache功能
测试
测试:./driver.sh
50 15 15
第一部分:实现一个顺序的网络代理
任务要求
- 最开始,代理应该监听某个端口来等待连接的请求,这个端口通过命令行给出
- 一旦建立连接,代理应该读取并解析请求。它需要确定这个请求是否发送了一个合法的HTTP请求
- 如果这个请求合法,则发送给服务器,然后将服务器的response返回给客户
具体实现
main
函数打开一个监听的描述符,如果通过这个监听描述符accept
成功了,则打开了一个用于通信的描述符fd
,将fd
作为doit
的函数,调用doitdoit
函数与描述符b
建立通信,读取客户端发来的请求,这个请求一定是以下两种形式之一- 指定端口
GET http://www.cmu.edu:8080/hub/index.html HTTP/1.1
- 固定端口80
GET http://www.cmu.edu/hub/index.html HTTP/1.1
- 指定端口
- 将上面收到的请求分解,主要是得到中间的url,然后将url分解,得到
host
,port
,path
,以指定端口为例,这三个分别是www.cmu.edu
8080
/hub/index.html
- 根据上面得到的三个参数,构建发往服务器的request
- 这个request是HTTP格式(具体实现上就把这个放到一个字符数组就行了,每一行通过
\r\n
隔开,并且最后要多一行\r\n
),由请求头和请求行组成,实验文档要求格式如下:GET /hub/index.html HTTP/1.0
Host: www.cmu.edu
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:10.0.3) Gecko/20120305 Firefox/10.0.3
Connection: close
Proxy-Connection: close
- 与服务器建立连接,得到
server_fd
描述符,将上面已经生成好的request发往服务器 - 不断地读服务器返回的值,写入
fd
文件描述符
#include "csapp.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>/* Recommended max cache and object sizes */
#define MAX_CACHE_SIZE 1049000
#define MAX_OBJECT_SIZE 102400
#define MAXLINE 8192/* You won't lose style points for including this long line in your code */
static const char *user_agent_hdr ="User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:10.0.3) Gecko/20120305 ""Firefox/10.0.3\r\n";typedef struct {char host[MAXLINE];char port[MAXLINE];char path[MAXLINE];
} URI;void cout_uri_format_error() { printf("wrong uri format\n"); }void parse_line(URI *req_uri, char *uri) {char *host_start = strstr(uri, "://");if (host_start == NULL) {cout_uri_format_error();return;}host_start += 3;char *port_start = strstr(host_start, ":");char *path_start = strstr(host_start, "/");if (path_start == NULL) {cout_uri_format_error();return;}strcpy(req_uri->path, path_start);*path_start = '\0';if (port_start != NULL) {strcpy(req_uri->port, port_start + 1);*port_start = '\0';} else {strcpy(req_uri->port, "80");}strcpy(req_uri->host, host_start);return;
}void build_req_server(char *req_server, URI *req_uri) {sprintf(req_server, "GET %s HTTP/1.0\r\n", req_uri->path);sprintf(req_server, "%sHost: %s\r\n", req_server, req_uri->host);sprintf(req_server, "%s%s", req_server, user_agent_hdr);sprintf(req_server, "%sConnection: close\r\n", req_server);sprintf(req_server, "%sProxy-Connection: close\r\n", req_server);sprintf(req_server, "%s\r\n", req_server);
}void doit(int fd) {// 初始化rio类函数的缓冲区rio_t rio;Rio_readinitb(&rio, fd);// 读入这一行http请求char buf[MAXLINE];Rio_readlineb(&rio, buf, MAXLINE);printf("Request headers:\n");printf("%s", buf);char method[MAXLINE], uri[MAXLINE], version[MAXLINE];// 解析这一行http请求,总共三个部分if (sscanf(buf, "%s %s %s", method, uri, version) != 3) {printf("HTTP Requset Format Wrong!\n");return;}// 判断是否是GET请求,这个比较函数忽略大小写,get也行if (strcasecmp(method, "GET")) {printf("method: %s not implemented\n", method);return;}// 至此,已经完成了对客户端请求的解析,接下来要构造出对服务器的请求// 首先解析我们的uri,得到host port pathURI *req_uri = (URI *)malloc(sizeof(URI));parse_line(req_uri, uri);// 根据我们的信息,构造出真正的发往服务器的请求char req_server[MAXLINE];build_req_server(req_server, req_uri);// 开始连接服务器int server_fd = Open_clientfd(req_uri->host, req_uri->port);if (server_fd < 0) {printf("connection failed\n");return;}// 连接成功,设置缓冲区,将request写入rio_t server_rio;Rio_readinitb(&server_rio, server_fd);Rio_writen(server_fd, req_server, strlen(req_server));// 等待服务器的返回,并写入客户端的fd中size_t rec_bytes;while ((rec_bytes = Rio_readlineb(&server_rio, buf, MAXLINE)) != 0) {printf("proxy received %d bytes\n", (int)rec_bytes);Rio_writen(fd, buf, rec_bytes);}Close(server_fd);
}int main(int argc, char **argv) {if (argc != 2) {fprintf(stderr, "usage: %s <port>\n", argv[0]);exit(1);}// 监听请求连接的端口int listenfd = Open_listenfd(argv[1]);// 与客户端进行连接int connfd;char hostname[MAXLINE], port[MAXLINE];socklen_t clientlen;struct sockaddr_storage clientaddr;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);doit(connfd);Close(connfd);}return 0;
}
第二部分:并发
任务要求
- 实现并发即可,没有要求用什么样的方式
具体实现
- 采用生产者消费者的方式,和书上第
12.5.5
节的代码几乎完全一样 - 需要在main函数中加入一个
Signal(SIGPIPE, SIG_IGN);
以屏蔽SIGPIPE信号
。我不太清楚不屏蔽会怎么样,可能是不屏蔽的话,客户端如果意外挂了,会导致代理服务器一起挂了
#define SUBFSIZE 16
#define NTHREADS 4typedef struct {int *buf;int n;int front;int rear;sem_t mutex;sem_t slots;sem_t items;
} sbuf_t;sbuf_t sbuf;void sbuf_init(sbuf_t *sp, int n) {sp->buf = Calloc(n, sizeof(int));sp->n = n;sp->front = sp->rear = 0;Sem_init(&sp->mutex, 0, 1);Sem_init(&sp->slots, 0, n);Sem_init(&sp->items, 0, 0);
}void sbuf_deinit(sbuf_t *sp) { Free(sp->buf); }void sbuf_insert(sbuf_t *sp, int item) {P(&sp->slots);P(&sp->mutex);sp->buf[(++sp->rear) % (sp->n)] = item;V(&sp->mutex);V(&sp->items);
}int sbuf_remove(sbuf_t *sp) {P(&sp->items);P(&sp->mutex);int item = sp->buf[(++sp->front) % (sp->n)];V(&sp->mutex);V(&sp->slots);return item;
}void *thread(void *vargp) {Pthread_detach(Pthread_self());while (1) {int connfd = sbuf_remove(&sbuf);doit(connfd);Close(connfd);}
}int main(int argc, char **argv) {if (argc != 2) {fprintf(stderr, "usage: %s <port>\n", argv[0]);exit(1);}// 监听请求连接的端口Signal(SIGPIPE, SIG_IGN);int listenfd = Open_listenfd(argv[1]);// 线程池sbuf_init(&sbuf, SUBFSIZE);pthread_t pid;for (int i = 0; i < NTHREADS; i++) {Pthread_create(&pid, NULL, thread, NULL);}// 与客户端进行连接int connfd;char hostname[MAXLINE], port[MAXLINE];socklen_t clientlen;struct sockaddr_storage clientaddr;while (1) {clientlen = sizeof(clientaddr);connfd = Accept(listenfd, (SA *)(&clientaddr), &clientlen);sbuf_insert(&sbuf, connfd);}return 0;
}
第三部分:cache
任务要求
- 这里说是cache,还不如说是一个大号的哈希表,以uri为键,以对应的资源为值。然后对这个哈希表的长度有点要求,大概10个表项。因为题目要求
#define MAX_CACHE_SIZE 1049000
,#define MAX_OBJECT_SIZE 102400
,其中object的意思就是一行,差不多就是十倍的样子 - 如果某个uri对应的资源太大了, 那就不考虑加入cache
- 对这个cahce需要实现并发访问,即加上锁,这里加入读写锁
具体实现
- 结合cachelab中cache的结构,还需要额外加上data字段
- 如果要实现真正的LRU,在并发访问的基础上,还需要对timestamp也加锁,否则就要用原子类型的变量
- 这个实现结合代码来看,思路还是比较清晰的,不再赘述
我在这里犯了两个小错,结果导致debug了好久 cacheline
中的tag
和data
的长度是不一样的,我一开始把data
长度弄成了MAXLINE
,结果0分- 在
doit
中我们用uri
去读cache
以及写cache
,但是我们在doit
的parse_line
函数里,是修改了uri
的,因此要给uri
搞一个备份,否则在写cache的时候,就错了
/* Recommended max cache and object sizes */
#define MAX_CACHE_SIZE 1049000
#define MAX_OBJECT_SIZE 102400
#define MAXLINE 8192typedef struct {int is_valid;char tag[MAXLINE];char data[MAX_OBJECT_SIZE];long long access_time;int read_cnt;sem_t read_lock;sem_t write_lock;
} cacheline;#define MAX_CACHE_LINES 10
cacheline Cache[MAX_CACHE_LINES];sem_t time_mutex;
long long time_stamp = 1;void init_cache() {for (int i = 0; i < MAX_CACHE_LINES; i++) {Cache[i].is_valid = 0;Cache[i].access_time = 0;Cache[i].read_cnt = 0;Sem_init(&Cache[i].read_lock, 0, 1);Sem_init(&Cache[i].write_lock, 0, 1);}Sem_init(&time_mutex, 0, 1);
}void read_in(int i) {P(&Cache[i].read_lock);if (Cache[i].read_cnt == 0) {P(&Cache[i].write_lock);}Cache[i].read_cnt++;V(&Cache[i].read_lock);
}void read_out(int i) {P(&Cache[i].read_lock);if (Cache[i].read_cnt == 1) {V(&Cache[i].write_lock);}Cache[i].read_cnt--;V(&Cache[i].read_lock);
}int read_cache(int fd, char *uri) {int flag = 0;for (int i = 0; i < MAX_CACHE_LINES; i++) {read_in(i);if (Cache[i].is_valid && !strcmp(uri, Cache[i].tag)) {flag = 1;P(&time_mutex);Cache[i].access_time = time_stamp++;V(&time_mutex);Rio_writen(fd, Cache[i].data, strlen(Cache[i].data));}read_out(i);if (flag) {return 0;}}return -1;
}void write_cache(char *uri, char *data) {int has_empty = -1;int lru_evict = 0;for (int i = 0; i < MAX_CACHE_LINES; i++) {read_in(i);if (Cache[i].is_valid == 0) {has_empty = i;}if (Cache[i].access_time < Cache[lru_evict].access_time) {lru_evict = i;}read_out(i);if (has_empty != -1) {break;}}int write_index = (has_empty == -1) ? lru_evict : has_empty;P(&Cache[write_index].write_lock);Cache[write_index].is_valid = 1;P(&time_mutex);Cache[write_index].access_time = time_stamp++;V(&time_mutex);strcpy(Cache[write_index].tag, uri);strcpy(Cache[write_index].data, data);V(&Cache[write_index].write_lock);
}