前情提要,如果看了书本,这个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 path
URI *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 4
typedef 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 8192
typedef 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);
}