CSAPP-Lab-7 实验报告: proxylab

摘要

本文介绍了笔者在做 proxylab 一节的实验报告。该 lab 旨在使用 C 语言实现简易的网络代理,通过实践 socket 编程来更好地从程序员的角度理解网络交互。

这一部分的实验其实难度不大,本篇更多的是梳理一些理论知识。

理论知识

socket 通信

网络套接字(英语:Network socket;又译网络套接字网络接口网络插槽)在计算机科学中是电脑网络中进程间收发数据的端点。socket 中往往包含协议族(IPV4、IPV6 等)、socket 类型(有无连接等)、传输层协议(TCP 或 UDP)等信息,与远端主机建立连接并进行通信。在 linux 上,一切 IO 都被抽象成了文件操作,网络 IO 也不例外,也是通过一系列函数调用得到代表网络连接的文件描述符(fd),接着就可以进行读写操作。从客户端和服务端的角度,一次 socket 通信的流程如下图所示:

客户端侧:

  1. getaddrinfo 获取服务端信息
  2. socket 创建 socket 对象,完全本地操作,不涉及网络通信
  3. connect 与远端服务端建立连接,获取文件描述符
  4. 通过 read/write 读写网络读写,read 阻塞,write 不阻塞
  5. close 关闭文件描述符

服务端侧:

  1. getaddrinfo 获取服务端信息
  2. socket 创建 socket 对象,完全本地操作,不涉及网络通信
  3. bind 将 socket 与本地端口绑定
  4. listen 进入被动监听状态
  5. accept 阻塞等待新的连接到来
  6. 通过 read/write 读写网络读写,read 阻塞,write 不阻塞
  7. close 关闭文件描述符

按照这个流程,可以写出如下的服务端程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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);
// 与客户端交互
doit(connfd);
Close(connfd);
}
}

这样的代码是有效的,但是缺点在于,每次只能处理一个客户端连接,其余的连接只能排队等到这个用户关闭连接后才能被受理。假设没有自动关闭连接的机制,连接中的用户离开电脑几个小时,服务端对外表现完全不可用,这是显然无法接受的。因此,服务端需要并行处理多个连接的能力。

进程并发

fork 创建子进程,用于处理客户端的读写,主进程只负责进行连接的建立,代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int main(int argc, char **argv)
{
int listenfd, connfd;
socklen_t clientlen;
struct sockaddr_storage clientaddr;

if (argc != 2)
{
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(0);
}

Signal(SIGCHLD, sigchld_handler);
listenfd = Open_listenfd(argv[1]);
while (1)
{
clientlen = sizeof(struct sockaddr_storage);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
if (Fork() == 0)
{
Close(listenfd); /* Child closes its listening socket */
echo(connfd); /* Child services client */
Close(connfd); /* Child closes connection with client */
exit(0); /* Child exits */
}
Close(connfd); /* Parent closes connected socket (important!) */
}
}

这种方式有以下优点:

  • 安全:各个子进程有独立的地址空间,不会意外修改掉其他进程的内存。

缺点在于:

  • 不便利:难以在父子进程或其他进程间共享状态,需要通过进程通信(IPC)的方式。
  • 开销大:进程的创建、销毁、切换都需要很大的开销。

IO 多路复用

也称为事件驱动的并发模型。利用 select/epoll 的系统调用,实现在一个进程内监听多个 IO 事件,并进行处理,核心代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
#include "csapp.h"

typedef struct
{ /* Represents a pool of connected descriptors */
int maxfd; /* Largest descriptor in read_set */
fd_set read_set; /* Set of all active descriptors */
fd_set ready_set; /* Subset of descriptors ready for reading */
int nready; /* Number of ready descriptors from select */
int maxi; /* High water index into client array */
int clientfd[FD_SETSIZE]; /* Set of active descriptors */
rio_t clientrio[FD_SETSIZE]; /* Set of active read buffers */
} pool;

int byte_cnt = 0; /* Counts total bytes received by server */

int main(int argc, char **argv)
{
int listenfd, connfd;
socklen_t clientlen;
struct sockaddr_storage clientaddr;
static pool pool;

if (argc != 2)
{
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(0);
}
listenfd = Open_listenfd(argv[1]);
init_pool(listenfd, &pool);

while (1)
{
// 等待 新连接/已经建立连接的事件到来
pool.ready_set = pool.read_set;
pool.nready = Select(pool.maxfd + 1, &pool.ready_set, NULL, NULL, NULL);

// 有新连接到来
if (FD_ISSET(listenfd, &pool.ready_set))
{
clientlen = sizeof(struct sockaddr_storage);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
// 添加并开始监听新的客户端连接
add_client(connfd, &pool);
}

// 检查已经建立连接的客户端事件,并进行处理
check_clients(&pool);
}
}

void add_client(int connfd, pool *p)
{
int i;
p->nready--;
for (i = 0; i < FD_SETSIZE; i++) /* Find an available slot */
if (p->clientfd[i] < 0)
{
/* Add connected descriptor to the pool */
p->clientfd[i] = connfd;
Rio_readinitb(&p->clientrio[i], connfd);

/* Add the descriptor to descriptor set */
FD_SET(connfd, &p->read_set);

/* Update max descriptor and pool high water mark */
if (connfd > p->maxfd)
p->maxfd = connfd;
if (i > p->maxi)
p->maxi = i;
break;
}
if (i == FD_SETSIZE) /* Couldn’t find an empty slot */
app_error("add_client error: Too many clients");
}

void check_clients(pool *p)
{
int i, connfd, n;
char buf[MAXLINE];
rio_t rio;

for (i = 0; (i <= p->maxi) && (p->nready > 0); i++)
{
connfd = p->clientfd[i];
rio = p->clientrio[i];

/* If the descriptor is ready, echo a text line from it */
if ((connfd > 0) && (FD_ISSET(connfd, &p->ready_set)))
{
p->nready--;
if ((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0)
{
byte_cnt += n;
printf("Server received %d (%d total) bytes on fd %d\n",
n, byte_cnt, connfd);
Rio_writen(connfd, buf, n);
}

/* EOF detected, remove descriptor from pool */
else
{
Close(connfd);
FD_CLR(connfd, &p->read_set);
p->clientfd[i] = -1;
}
}
}
}

可以看出来,核心的逻辑在于维护要监听的 fd 列表,在一个循环中,使用 Select 反复监听 IO 事件到来,并进行处理:

  • 来自 listenfd 的新连接:通过 Accept 获取新的连接 connfd,添加到客户端列表中,并开始监听
  • 来自已经建立连接的 connfd 的读写:读取并处理,如果收到 EOF,关闭连接移除监听

这种方式有以下优点:

  • 灵活:程序员可以更好地控制程序的行为,比如为某些客户端优先提供服务。
  • 单线程:只有一个线程,可以方便地在不同函数内共享全局变量,没有并发问题,也更利于调试。

也有着以下缺点:

  • 编程复杂度:事件驱动的编程模型的复杂度是基于进程的方法的数倍,而且并发粒度越细,模型越复杂。对比之下,基于进程的方法容易设计出干净的代码结构。
  • 无法利用多核优势:只使用了单线程。

线程并发

与进程的思想类似,使用线程来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include "csapp.h"

void echo(int connfd);
void *thread(void *vargp);

int main(int argc, char **argv)
{
int listenfd, *connfdp;
socklen_t clientlen;
struct sockaddr_storage clientaddr;
pthread_t tid;

if (argc != 2)
{
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(0);
}
listenfd = Open_listenfd(argv[1]);

while (1)
{
clientlen = sizeof(struct sockaddr_storage);
connfdp = Malloc(sizeof(int));
*connfdp = Accept(listenfd, (SA *)&clientaddr, &clientlen);
// 创建新线程处理客户端的连接
Pthread_create(&tid, NULL, thread, connfdp);
}
}

/* Thread routine */
void *thread(void *vargp)
{
int connfd = *((int *)vargp);
Pthread_detach(pthread_self());
Free(vargp);
echo(connfd);
Close(connfd);
return NULL;
}

还可以使用线程池的方法,通过复用线程进一步减少线程创建和销毁的开销,代码如下。这是一个典型的生产者 - 消费者模型,主线程作为生产者,通过 Accept 获取新连接,工作线程为消费者,处理新连接的读写逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 线程安全的阻塞队列
sbuf_t sbuf; /* Shared buffer of connected descriptors */

int main(int argc, char **argv)
{
int i, listenfd, connfd;
socklen_t clientlen;
struct sockaddr_storage clientaddr;
pthread_t tid;

if (argc != 2)
{
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(0);
}
listenfd = Open_listenfd(argv[1]);

sbuf_init(&sbuf, SBUFSIZE);
// 创建工作线程
for (i = 0; i < NTHREADS; i++)
Pthread_create(&tid, NULL, thread, NULL);

while (1)
{
clientlen = sizeof(struct sockaddr_storage);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
// 将客户端连接存放在阻塞队列中
sbuf_insert(&sbuf, connfd); /* Insert connfd in buffer */
}
}

void *thread(void *vargp)
{
Pthread_detach(pthread_self());
while (1)
{
// 阻塞等到获取客户端fd
int connfd = sbuf_remove(&sbuf);
// 为客户端服务
echo_cnt(connfd);
Close(connfd);
}
}

线程的方法有如下优点:

  • 方便共享状态:线程间共享地址空间
  • 开销小:创建、销毁、切换的开销更小,且可以通过线程池进一步减少开销

缺点在于:

  • 并发问题:无意识的共享可能会使得线程状态被污染,进而出错。还可能出现竞态条件、死锁等问题。

Part 1

实现基础的 http 代理功能,需要做的事情很清晰:

  1. 接受客户端连接,解析 http 请求行(端口与 uri)与请求头
  2. 与目标服务器建立连接,初始化 http 请求行,设置代理请求头,转发额外请求头
  3. 将目标服务器的响应转发给客户端

Part 2

实现代理服务器的并发。这里可以使用上面提到的线程池的方法,通过生产者 - 消费者的模式交互。先实现一个阻塞队列,sbuf.h 代码如下,队列中包含三个信号量,用于同步。mutex 控制了循环队列 buf 的互斥读写,slots,items 记录了队列中的空槽和物品的数量,控制生产者与消费者的等待逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <semaphore.h>

typedef struct
{
int *buf; /* Buffer array */
int n; /* Maximum number of slots */
int front; /* buf[(front+1)%n] is first item */
int rear; /* buf[rear%n] is last item */
sem_t mutex; /* Protects accesses to buf */
sem_t slots; /* Counts available slots */
sem_t items; /* Counts available items */
} sbuf_t;

void sbuf_init(sbuf_t *sp, int n);
void sbuf_deinit(sbuf_t *sp);
void sbuf_insert(sbuf_t *sp, int item);
int sbuf_remove(sbuf_t *sp);

sbuf.c 代码如下。初始化时,队列为空,slotsn,物品数量为 0。在插入新元素时,需要先阻塞等到有空槽 P(slots),再获取队列的互斥锁 P(mutex) 进行插入。在读取新元素时,需要先阻塞等到有元素 P(items),再获取队列的互斥锁 P(mutex) 进行读取并删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include "sbuf.h"
#include "csapp.h"


void sbuf_init(sbuf_t *sp, int n)
{
sp->buf = Calloc(n, sizeof(int));
sp->n = n; /* Buffer holds max of n items */
sp->front = sp->rear = 0; /* Empty buffer iff front == rear */
Sem_init(&sp->mutex, 0, 1); /* Binary semaphore for locking */
Sem_init(&sp->slots, 0, n); /* Initially, buf has n empty slots */
Sem_init(&sp->items, 0, 0); /* Initially, buf has zero data items */
}

/* Clean up buffer sp */
void sbuf_deinit(sbuf_t *sp)
{
Free(sp->buf);
}

/* Insert item onto the rear of shared buffer sp */
void sbuf_insert(sbuf_t *sp, int item)
{
P(&sp->slots); /* Wait for available slot */
P(&sp->mutex); /* Lock the buffer */
sp->buf[(++sp->rear) % (sp->n)] = item; /* Insert the item */
V(&sp->mutex); /* Unlock the buffer */
V(&sp->items); /* Announce available item */
}

/* Remove and return the first item from buffer sp */
int sbuf_remove(sbuf_t *sp)
{
int item;
P(&sp->items); /* Wait for available item */
P(&sp->mutex); /* Lock the buffer */
item = sp->buf[(++sp->front) % (sp->n)]; /* Remove the item */
V(&sp->mutex); /* Unlock the buffer */
V(&sp->slots); /* Announce available slot */
return item;
}

Java 中的阻塞队列,也是类似的实现逻辑。区别在于,只使用了一个可重入锁,搭配两个条件变量 notempty, notfull 标识队列是否非满与非空。

Part 3

实现缓存功能,缓存客户端请求的资源,减少与服务端交互。在服务端并行的设置下,cache 的读写显然必须有锁保护。然而,一把大锁会使得所有请求变为串行执行,是不可行的,可以从两个角度改进:

  • 分段锁。划分多个 cache,根据 hash 路由,每次读写只在段内加锁,jdk 1.7 之前的 ConcurrentHashMap 就是这种思想。
  • 优先读。考虑到 cache 的读多于写,而读取时可以不需要加锁,写入时才需要加锁。因此,可以优先保证读取,来提高性能。可以通过读者写者问题的解决方法实现。

读者写者问题(Readers-Writes Problem)是互斥问题的通用描述,具体为:

  • 读者线程只读取对象
  • 写者线程修改对象
  • 写者对于对象的访问是互斥的
  • 多个读者可以同时读取对象

常见的应用场景是:

  • 在线订票系统:所有顾客都在查看座位,正在订票的顾客必须对数据库有互斥访问权限。
  • 多线程缓存 web 代理:多个线程可以读取缓存,但要写入的线程必须有互斥访问权限。

根据不同的读写策略,可以分为两类读者写者问题,需要注意的是,这两种情况都可能出现 starvation(饥饿)。

第一类读者写者问题(读者优先)

  • 如果写者没有获取到使用对象的权限,不应该让读者等待
  • 在等待的写者之后到来的读者应该在写者之前处理
  • 也就是说,只有没有读者的情况下,写者才能工作

第二类读者写者问题(写者优先)

  • 一旦写者可以处理的时候,就不应该进行等待
  • 在等待的写者之后到来的读者应该在写者之后处理

读者优先的代码如下。第一个读者到来时,获取写锁 P(w) 来阻塞后续写入,最后一个读取完毕的读者释放写锁 V(w) 允许后续写入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* Global variables */
int readcnt; /* Initially = 0 */
sem_t mutex, w; /* Both initially = 1 */
void reader(void)
{
while (1)
{
P(&mutex);
readcnt++;
if (readcnt == 1) /* First in */
P(&w);
V(&mutex);
/* Critical section */
/* Reading happens */
P(&mutex);
readcnt--;
if (readcnt == 0) /* Last out */
V(&w);
V(&mutex);
}
}
void writer(void)
{
while (1)
{
P(&w);
/* Critical section */
/* Writing happens */
V(&w);
}
}

代码与结果

cache 部分我复用了之前 cachelab 的代码,修改成了分段锁的版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
#include <stdio.h>
#include "csapp.h"
#include <stdlib.h>
#include <string.h>
#include "sbuf.h"
#include "cache.h"

void doit(int fd);
int parse_porturi(char *url, char *dest, char **uri);
void *thread(void *vargp);
void *thread_once(void *vargp);

/* Recommended max cache and object sizes */
#define MAX_CACHE_SIZE 1049000
#define MAX_OBJECT_SIZE 102400
#define NTHREADS 3
#define SBUFSIZE 16
#define N_CACHE_SETS 10

/* 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";
static const char *(kept_headers[5]) = {"Host", "User-Agent", "Connection", "Proxy-Connection", NULL};
sbuf_t sbuf; /* Shared buffer of connected descriptors */
static Cache *cache;

int main(int argc, char **argv)
{
printf("%s", user_agent_hdr);
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);
}
pthread_t tid;
// 初始化阻塞队列
sbuf_init(&sbuf, SBUFSIZE);
// 屏蔽SIGPIPE信号
Signal(SIGPIPE, SIG_IGN);
cache = newCache(N_CACHE_SETS, MAX_CACHE_SIZE, MAX_OBJECT_SIZE);
// 线程池
for (int i = 0; i < NTHREADS; i++)
{
Pthread_create(&tid, NULL, thread, NULL);
}

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);
sbuf_insert(&sbuf, connfd);
printf("Accepted connection from (%s, %s) at fd %d\n", hostname, port, connfd);
// int *vargp = malloc(sizeof(int));
// *vargp = connfd;
// Pthread_create(&tid, NULL, thread_once, vargp);
}
return 0;
}

void doit(int client_fd)
{
int server_fd;
char buf[MAXLINE], method[MAXLINE], url[MAXLINE], version[MAXLINE];
rio_t client_rio, server_rio;
int n;

Rio_readinitb(&client_rio, client_fd);
if (!Rio_readlineb(&client_rio, buf, MAXLINE))
return;
// 读取请求行
sscanf(buf, "%s %s %s", method, url, version);

char server_port[10];
char *uri = "";
// 解析uri与端口
parse_porturi(url, server_port, &uri);
CacheVisit visit;
visit.key = uri;
CacheData *cacheData = fetch(cache, &visit);
// cache命中
if (cacheData != NULL)
{
printf("uri %s hit cache, address %p, size %d\n", uri, cacheData->data, cacheData->size);
Rio_writen(client_fd, cacheData->data, cacheData->size);
return;
}
printf("uri %s miss cache\n", uri);
// 与服务端建立连接
server_fd = Open_clientfd("localhost", server_port);
sprintf(buf, "GET %s HTTP/1.0\r\n", uri);
Rio_writen(server_fd, buf, strlen(buf));
// 设置代理header
sprintf(buf, "Host: www.cmu.edu\r\n");
Rio_writen(server_fd, buf, strlen(buf));
sprintf(buf, "User-Agent: %s\r\n", user_agent_hdr);
Rio_writen(server_fd, buf, strlen(buf));
sprintf(buf, "Connection: close\r\n");
Rio_writen(server_fd, buf, strlen(buf));
sprintf(buf, "Proxy-Connection: close\r\n");
Rio_writen(server_fd, buf, strlen(buf));

// 转发其他header
char key_buf[MAXLINE], value_buf[MAXLINE];
Rio_readlineb(&client_rio, buf, MAXLINE);
while (strcmp(buf, "\r\n"))
{
sscanf(buf, "%s: %s", key_buf, value_buf);
int skip = 0;
for (int i = 0; kept_headers[i] != NULL; i++)
{
if (strcasecmp(key_buf, kept_headers[i]) == 0)
{
skip = 1;
break;
}
}
if (!skip)
{
Rio_writen(server_fd, buf, strlen(buf));
}
Rio_readlineb(&client_rio, buf, MAXLINE);
}
// 缓存结果
char *data = malloc(MAX_OBJECT_SIZE);
int size = 0;
Rio_readinitb(&server_rio, server_fd);
while ((n = Rio_readnb(&server_rio, buf, MAXLINE)) > 0)
{
if (size + n < MAX_OBJECT_SIZE)
{
memcpy(data + size, buf, n);
size += n;
}
else
{
size = MAX_OBJECT_SIZE + 1;
}
Rio_writen(client_fd, buf, n);
}
cacheData = malloc(sizeof(CacheData));
cacheData->data = data;
cacheData->size = size;
if (size <= MAX_OBJECT_SIZE)
{
printf("uri %s is stored into cache\n", uri);
store(cache, uri, cacheData);
}
Close(server_fd);

return;
}

int parse_porturi(char *url, char *dest, char **uri)
{
// 默认端口号
char *default_port = "80";

// 在URL中查找协议部分的冒号
const char *protocol_end = strstr(url, "://");
if (protocol_end != NULL)
{
// 找到协议部分的冒号
protocol_end += 3; // 移动到协议部分的末尾
const char *port_start = strchr(protocol_end, ':');

if (port_start != NULL)
{
// 找到冒号,表示有端口号
int port;
if (sscanf(port_start + 1, "%d", &port) == 1)
{
sprintf(dest, "%d", port);
*uri = strchr(protocol_end, '/');
return 1;
}
}
}
*uri = strchr(protocol_end, '/');
sprintf(dest, "%s", default_port);
return 0;
}

void *thread_once(void *vargp)
{
int connfd = *(int *)vargp;
doit(connfd); /* Service client */
Close(connfd);
return NULL;
}

void *thread(void *vargp)
{
Pthread_detach(pthread_self());

while (1)
{
int connfd = sbuf_remove(&sbuf); /* Remove connfd from buffer */
doit(connfd); /* Service client */
Close(connfd);
}
}

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
*** Basic ***
Starting tiny on 15325
Starting proxy on 20457
1: home.html
Fetching ./tiny/home.html into ./.proxy using the proxy
Fetching ./tiny/home.html into ./.noproxy directly from Tiny
Comparing the two files
Success: Files are identical.
2: csapp.c
Fetching ./tiny/csapp.c into ./.proxy using the proxy
Fetching ./tiny/csapp.c into ./.noproxy directly from Tiny
Comparing the two files
Success: Files are identical.
3: tiny.c
Fetching ./tiny/tiny.c into ./.proxy using the proxy
Fetching ./tiny/tiny.c into ./.noproxy directly from Tiny
Comparing the two files
Success: Files are identical.
4: godzilla.jpg
Fetching ./tiny/godzilla.jpg into ./.proxy using the proxy
Fetching ./tiny/godzilla.jpg into ./.noproxy directly from Tiny
Comparing the two files
Success: Files are identical.
5: tiny
Fetching ./tiny/tiny into ./.proxy using the proxy
Fetching ./tiny/tiny into ./.noproxy directly from Tiny
Comparing the two files
Success: Files are identical.
Killing tiny and proxy
basicScore: 40/40

*** Concurrency ***
Starting tiny on port 12414
Starting proxy on port 22796
Starting the blocking NOP server on port 32913
Trying to fetch a file from the blocking nop-server
Fetching ./tiny/home.html into ./.noproxy directly from Tiny
Fetching ./tiny/home.html into ./.proxy using the proxy
Checking whether the proxy fetch succeeded
Success: Was able to fetch tiny/home.html from the proxy.
Killing tiny, proxy, and nop-server
concurrencyScore: 15/15

*** Cache ***
Starting tiny on port 23752
Starting proxy on port 26743
Fetching ./tiny/tiny.c into ./.proxy using the proxy
Fetching ./tiny/home.html into ./.proxy using the proxy
Fetching ./tiny/csapp.c into ./.proxy using the proxy
Killing tiny
Fetching a cached copy of ./tiny/home.html into ./.noproxy
Success: Was able to fetch tiny/home.html from the cache.
Killing proxy
cacheScore: 15/15

totalScore: 70/70

总结

完结撒花!终于在 2023 年完成了 CSAPP 这门课的学习!在这门课的学习与实践里,收获还是非常多的。一方面复习了本科学习的专业知识,还从教授的旁征博引中领悟了新的理解;另一方面,在这么多课程学完之后,对计算机系统的认识也有了更清晰的轮廓,有一种计算机大厦落地建成的感觉。细分方向的专业课是计算机系统里小而独立的模块,像计算机组成、操作系统、编译原理等,CSAPP 可以把相关的知识串起来,以程序员的角度,建立起对计算机系统的顶层认识。这也是我觉得这门课的魅力所在。

祝看到这篇博客的朋友们,新年快乐,万事顺意!

参考