Skip to content

Commit c4b766c

Browse files
authoredAug 6, 2022
Add files via upload
0 parents  commit c4b766c

22 files changed

+3294
-0
lines changed
 

‎Makefile

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Makefile for Proxy Lab
2+
#
3+
# You may modify this file any way you like (except for the handin
4+
# rule). You instructor will type "make" on your specific Makefile to
5+
# build your proxy from sources.
6+
7+
CC = gcc
8+
CFLAGS = -g -Wall
9+
LDFLAGS = -lpthread
10+
11+
all: proxy
12+
13+
csapp.o: csapp.c csapp.h
14+
$(CC) $(CFLAGS) -c csapp.c
15+
16+
cache.o: cache.c cache.h
17+
$(CC) $(CFLAGS) -c cache.c
18+
19+
sbuf.o: sbuf.c sbuf.h
20+
$(CC) $(CFLAGS) -c sbuf.c
21+
22+
proxy.o: proxy.c csapp.h
23+
$(CC) $(CFLAGS) -c proxy.c
24+
25+
proxy: proxy.o csapp.o cache.o sbuf.o
26+
$(CC) $(CFLAGS) proxy.o csapp.o cache.o sbuf.o -o proxy $(LDFLAGS)
27+
28+
# Creates a tarball in ../proxylab-handin.tar that you can then
29+
# hand in. DO NOT MODIFY THIS!
30+
handin:
31+
(make clean; cd ..; tar cvf $(USER)-proxylab-handin.tar proxylab-handout --exclude tiny --exclude nop-server.py --exclude proxy --exclude driver.sh --exclude port-for-user.pl --exclude free-port.sh --exclude ".*")
32+
33+
clean:
34+
rm -f *~ *.o proxy core *.tar *.zip *.gzip *.bzip *.gz
35+

‎READMEmd

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# 带缓存的小型并发代理服务器
2+
3+
## 描述
4+
本项目基于CS:APP Proxylab,实现了一个带缓存的并发代理服务器。该服务器支持将客户的HTTP GET请求通过HTTP/1.0报文转发给目标服务器,并
5+
6+
## 代码布局
7+
8+
`./tiny`包括CS:APP书中提供的小型Web服务器的实现,它可以用于测试我们的代理服务器
9+
10+
`free-port.sh`用于输出一个本机的空闲TCP端口,你可以用这个端口作为运行`proxy`的参数
11+
12+
`driver.sh`用于测试代理服务器的基本功能、并发性与缓存支持
13+
14+
`proxy_seq.c`, `proxy_cunc.c`, `proxy_cunc_poll.c`, `proxy_cache_poll.c`为四种代理服务器的实现,分别是单线程无并发的代理服务器、基于多线程的并发代理服务器,基于预先创建线程池的并发代理服务器,以及线程池并发+缓存的代理服务器,其具体设计见下一节。`proxy.c`存储了第四个代理服务器的实现代码
15+
16+
17+
18+
`cache.c`与`cache.h`包括缓存的实现代码
19+
20+
`sbuf.c`与`sbuf.h`在CS:APP书中提供,包括了实现生产者-消费者模型的代码
21+
22+
`csapp.c`与`csapp.h`在CS:APP书中提供,包括一系列函数:
23+
24+
- 对若干系统调用的包装,失败时自动发送错误信息
25+
- 对若干库函数的包装,失败时自动发送错误信息
26+
- 若干信号安全的I/O函数的实现以及对其的包装
27+
- 若干健壮的I/O函数的实现(能够应对`read`和`write`冗杂的不足只问题)以及对其的包装
28+
29+
需要注意,本项目对`csapp.c`进行了一些修改,以避免客户端的错误请求导致代理服务器崩溃的问题
30+
31+
32+
## 设计
33+
### 1. `proxy_seq.c` 最简单的迭代式代理服务器
34+
35+
代理服务器的工作非常简单:
36+
37+
首先,它监听来自客户端的HTTP request报文并解析,一方面获取目标服务器的地址与端口号,一边将原来的HTTP request报文转换成自己的HTTP/1.0 request报文。
38+
39+
接下来,它尝试连接目标服务器,然后向连接好的socket中写入HTTP/1.0 request报文。之后它循环地从连接目标服务器的socket中读取字节流,并写入与客户端相连的socket
40+
41+
读到EOF时,循环结束,关闭与服务器相连的socket,以及与客户相连的socket,然后重新开始监听新的客户端
42+
43+
### 2. `proxy_cunc.c` 多线程并发代理服务器
44+
45+
相较于第一个服务器,我们要求该服务器在接受客户端的连接请求并创建连接套接字后,创建一个新的线程去处理有关的工作,而主线程可以一直守候在监听套接字处。
46+
47+
主线程需要连接套接字的描述符传递给新的线程,而新的线程需要自己调用`Pthread_detach(pthread_self())`让主线程不必替结束的线程清理资源
48+
49+
### 3. `proxy_cunc_pool.c` 基于预创建线程池的并发代理服务器
50+
51+
相较于第二个服务器,我们要求该服务器预先分配一个线程池,而不是在连接客户端后再创建线程。我们可以使用生产者-消费者模型来理解此时的工作。其中客户端相当于生产者,我们将与客户端相连的socket描述符不断放入缓冲区的槽位;而执行工作的线程相当于消费者,它们不断从槽位中获取描述符,并执行工作
52+
53+
我们使用CS:APP书中提供的SBUF包(`sbuf.c`, `sbuf.h`)来实现这一缓冲区,以及对缓冲区的若干操作。
54+
55+
### 4. `proxy_cache_poll.c` 提供缓存的并发代理服务器
56+
57+
在第三个服务器的基础上,我们希望该服务器能够拥有缓存功能,可以在不需要查询目标服务器的情况下从缓存中获取要返回给客户端的内容。缓存的替换策略应当接近LRU(但不一定严格实现,稍后说明)
58+
59+
根据书中proxy lab的要求,我们限定缓存的数据载荷总大小不超过`1 MiB`,单个缓存块的数据载荷总大小不超过`100 KiB`。我们需要考虑有关缓存实现的下述问题
60+
61+
#### a. 如何组织缓存块?
62+
63+
简单地将`1 MiB`的总空间切分成10个最大的缓存块(和一个小的剩余块)虽然可行,但是会面临严重的内部碎片问题。另一方面,如果对于每一个写缓存请求,我们都精确地临时切出一个大小匹配的缓存块,那么最后会得到若干大小不同的已分配的缓存块,当我们需要踢出缓存块时这就会导致困难——简单按照LRU可能会踢出一些极小的块而无法腾出足够空间,我们必须权衡块的大小,这样的替换策略可能会非常复杂
64+
65+
因此,利用一种类似malloc实现中“隐式空闲链表”的思想,我们也可以把所有的缓存空间划分成若干个列表,每个列表中缓存块的大小相同。当我们需要写入缓存时,只选择与其大小最匹配的一个列表,然后写入其中的空闲块或是替换满足LRU条件的旧块。
66+
67+
在实现中,我将总的缓存分为6个列表,列表中块的大小和个数如下
68+
69+
- 5个`100 KiB`的块,总计`500 KiB`
70+
- 5个`50 KiB`的块,总计`250 KiB`
71+
- 6个`20 KiB`的块,总计`120 KiB`
72+
- 8个`10 KiB`的块,总计`80 KiB`
73+
- 10个`5 KiB`的块,总计`50 KiB`
74+
- 24个`1 KiB`的块,总计`2 KiB`
75+
76+
#### b. 单个缓存块应该包含哪些信息?
77+
78+
首先,缓存块使用请求的URI作为`key`,其`value`自然就是响应报文的内容。除此之外,还应该标记数据载荷的长度、最后更新的时间。
79+
80+
特别地,我们将空闲块的最后更新时间设置为0,这样我们可以将搜索空块和搜索满足LRU条件的最旧块(后者的时间值一定小于其它已分配块)的过程合并起来。
81+
82+
#### c. 如何确保缓存读写的线程安全?
83+
84+
缓存块作为多个工作线程的共享对象,对其的访问恰好符合读者写者模型,我们可以使用`POSIX`读写锁进行控制。
85+
86+
##### 读取缓存
87+
88+
假设我们已经找到了一个匹配块(通过确认该块已分配并匹配URI),接下来的工作有两步:修改时间戳与读取信息。前者需要用写锁控制,后者则用读锁控制。之所以先修改时间戳,是减小其被其他线程用LRU策略踢出的风险
89+
90+
但是需要注意的是,从我们找到匹配块,到上写锁修改时间戳这一时间段内,其它线程完全有可能抢占调度,将我们的匹配块替换成它自己的缓存。因此在获取写锁之后,我们必须检查一下URI字段是否仍然匹配。同理,在释放写锁到获取读锁的过程也有被抢占调度的风险,我们必须在获取读锁后检查URI字段的匹配情况。假如真的被其它线程趁机替换了我们的目标缓存块,就只能返回0表示未能获取缓存
91+
92+
##### 写入缓存
93+
94+
假设我们已经找到了大小合适的块列表(意味着该列表是所有块大小符合条件的列表中,块最小的一个),下面就需要找到空闲块或者按LRU策略最旧的块。
95+
96+
在获取目标块之后,我们只需要用写锁保护,然后进行写入,并在写入完毕后释放写锁。
97+
98+
然而另一个值得注意的地方在于,此时的写入并不严格遵循LRU原则。从我们获取目标块后,到获取写锁之前这段时间,其它线程可能抢占调度,并在同一个块写入它的缓存。之后我们的写入就会把这个刚刚更新的块覆盖掉!
99+
100+
如果我们想做到严格LRU的话:
101+
102+
- 就像在读取缓存时所做的那样,获取写锁之后马上检查该块的时间戳是否变化,如果有变化则放弃写入,释放写锁,重新寻找目标块。然而,只要有线程在我们获取目标块后抢占调度并写入该块,我们就会不断地放弃写入重新寻找,并陷入饥饿
103+
- 我们也可以在搜索目标块的时候就先上锁,然后检查timestamp,倘若不符合条件再解锁并检查下一个块。但是,频繁上锁解锁的开销我们也无法承受
104+
105+
实际上,与其严格遵循LRU,不如设法减小上述情形发生的概率:
106+
107+
- 正如已经实现的,将缓存块分为多个列表。这样,几个不同的写入请求很可能因为数据量不同而被导向至不同的列表,而不至于抢占同一个块
108+
- 我们可以先搜索一个列表中的所有空闲块,然后随机的选取一个,而不是总选取第一个空闲块,从而减小多个请求被导向到同一个块的概率。不过,这会增大搜索的时间开销
109+
110+
由于替换新写入的块并不会导致严重的运行错误,只要其概率足够小不影响缓存性能,我们可以不必强行遵循LRU策略。
111+
112+
#### d. proxy的主体部分应该做哪些修改?
113+
114+
首先,在创建和关闭`listenfd`前后,我们需要初始化和释放缓存。
115+
116+
之后对于每个执行线程,在解析完request line并获取URI后,应当首先检查缓存中是否由以该URI为key的块,倘若有可以直接从中获取内容返回给客户端
117+
118+
如果缓存未命中,那么在从目标服务器响应报文时,我们应当用一个`char`数组把报文内容储存下来,并不断检查内容的大小是否超过了单个缓存块的最大限制。最终,如果没有超过限制,我们才可以放心地将URI、报文的内容及大小传入对应的写入缓存的函数。
119+
120+
121+
122+
## 编译项目与测试
123+
124+
首先通过`make clean`命令将源代码以外的文件清除,然后通过简单的`make`命令进行编译,获取可执行文件
125+
126+
如果希望使用CS:APP书中提供的测试工具,可以直接键入`./driver.sh`,该工具以`./tiny`路径下的服务器作为目标服务器,并检查代理服务器的基本功能、并发性与缓存能力。
127+
128+
如果希望自己测试代理服务器,首先用`free-port.sh`获取一个空闲的TCP端口号。假如该端口号是`4500`,那么我们只需通过`./proxy 4500 &`即可让proxy在后台运行。之后,我们可以通过`curl`,选择较老的http网站进行测试,例如`curl -v http://www.hangzhou.gov.cn/ --proxy http://localhost:4500`。我们也可以在浏览器中设置使用proxy作为代理

‎cache.c

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
#include "cache.h"
2+
3+
#include "csapp.h"
4+
5+
const int block_size[6] = {1024, 5120, 10240, 20480, 51200, 102400};
6+
const int block_cnt[6] = {24, 10, 8, 6, 5, 5};
7+
8+
cache_block *cache_lists[LIST_CNT];
9+
10+
void cache_init() {
11+
for (int i = 0; i < LIST_CNT; ++i) {
12+
/* initialize cache block list */
13+
cache_lists[i] =
14+
(cache_block *)malloc(block_cnt[i] * sizeof(cache_block));
15+
cache_block *this_list = cache_lists[i];
16+
/* initialize every block in this list */
17+
for (int j = 0; j < block_cnt[i]; ++j) {
18+
this_list[j].url = (char *)calloc(MAXLINE, sizeof(char));
19+
this_list[j].data = (char *)calloc(block_size[i], sizeof(char));
20+
this_list[j].datasize = 0;
21+
this_list[j].timestamp = 0;
22+
pthread_rwlock_init(&this_list[j].rwlock, NULL);
23+
}
24+
}
25+
}
26+
27+
void cache_deinit() {
28+
for (int i = 0; i < LIST_CNT; ++i) {
29+
cache_block *this_list = cache_lists[i];
30+
for (int j = 0; j < block_cnt[j]; ++j) {
31+
free(this_list[j].url);
32+
free(this_list[j].data);
33+
pthread_rwlock_destroy(&this_list[j].rwlock);
34+
}
35+
free(this_list);
36+
}
37+
}
38+
39+
int cache_read(char *url, int fd) {
40+
/* search every list */
41+
int cache_hit = 0;
42+
cache_block *target = NULL;
43+
for (int i = 0; i < LIST_CNT; ++i) {
44+
cache_block *this_list = cache_lists[i];
45+
/* search every block in this list */
46+
for (int j = 0; j < block_cnt[i]; ++j) {
47+
/* if uri match, and timestamp not zero(means block valid), then
48+
* hit! */
49+
if (!strcmp(url, this_list[j].url) && this_list[j].timestamp) {
50+
cache_hit = 1;
51+
target = &this_list[j];
52+
break;
53+
}
54+
}
55+
if (cache_hit) break;
56+
}
57+
if (!cache_hit) {
58+
printf("no matched cache block\n");
59+
return 0;
60+
}
61+
62+
/* first update timestamp before block kicked by other thread */
63+
pthread_rwlock_wrlock(&target->rwlock);
64+
/* we have to check target block again incase other thread kiked it */
65+
if (strcmp(url, target->url)) {
66+
printf("oops, the matched block modified by other thread just now\n");
67+
pthread_rwlock_unlock(&target->rwlock);
68+
return 0;
69+
}
70+
/* we can update the timestamp safely */
71+
target->timestamp = get_timestamp();
72+
pthread_rwlock_unlock(&target->rwlock);
73+
74+
/* now we can get cache content */
75+
pthread_rwlock_rdlock(&target->rwlock);
76+
/* double check, just in case */
77+
if (strcmp(url, target->url)) {
78+
printf("oops, the matched block modified by other thread just now\n");
79+
pthread_rwlock_unlock(&target->rwlock);
80+
return 0;
81+
}
82+
Rio_writen(fd, target->data, target->datasize);
83+
pthread_rwlock_unlock(&target->rwlock);
84+
printf("fetch content from cache\n");
85+
return 1;
86+
}
87+
88+
void cache_write(char *url, char *data, int len) {
89+
int list_idx = 0;
90+
cache_block *target = NULL;
91+
/* find target list */
92+
while ((list_idx < LIST_CNT) && (len > block_size[list_idx])) {
93+
++list_idx;
94+
}
95+
if (list_idx == LIST_CNT) {
96+
printf("too much data to cache\n");
97+
return;
98+
}
99+
cache_block *this_list = cache_lists[list_idx];
100+
/* find free block or LRU block as target block */
101+
int64_t min_timestamp = get_timestamp();
102+
for (int j = 0; j < block_cnt[list_idx]; ++j) {
103+
if (this_list[j].timestamp < min_timestamp) {
104+
target = &this_list[j];
105+
min_timestamp = target->timestamp;
106+
if (!min_timestamp) break; /* free block found */
107+
}
108+
}
109+
/* we can write to target block */
110+
pthread_rwlock_wrlock(&target->rwlock);
111+
memcpy(target->url, url, MAXLINE);
112+
memcpy(target->data, data, len);
113+
target->datasize = len;
114+
target->timestamp = get_timestamp();
115+
pthread_rwlock_unlock(&target->rwlock);
116+
printf("write content into cache\n");
117+
}
118+
119+
int64_t get_timestamp() {
120+
struct timeval time;
121+
gettimeofday(&time, NULL);
122+
int64_t s1 = (int64_t)(time.tv_sec) * 1000;
123+
int64_t s2 = (time.tv_usec / 1000);
124+
return s1 + s2;
125+
}

‎cache.h

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// cache.h
2+
#include <sys/time.h>
3+
4+
#include "csapp.h"
5+
6+
#define LIST_CNT 6
7+
#define MAX_OBJECT_SIZE 102400
8+
9+
typedef struct cache_block {
10+
char *url;
11+
char *data;
12+
int datasize;
13+
int64_t timestamp;
14+
pthread_rwlock_t rwlock;
15+
} cache_block;
16+
17+
/* allocate cache memory using calloc */
18+
void cache_init();
19+
/* free cache's memory */
20+
void cache_deinit();
21+
/* try to hit cache block and write content into fd, return 0 if failed */
22+
int cache_read(char *url, int fd);
23+
/* write content into free block or LRU block */
24+
void cache_write(char *url, char *data, int len);
25+
/* return current timestamp */
26+
int64_t get_timestamp();

‎cache.o

13.8 KB
Binary file not shown.

0 commit comments

Comments
 (0)
Please sign in to comment.