# Proxy Lab:编写带缓存的 Web 代理
原文:官方实验说明 (opens new window)
实验包:proxylab-handout.tar
自学包说明:运行配置与原说明的差异
15-213,20XX 年秋季
布置日期:11 月 19 日,星期四
截止日期:12 月 8 日,星期二,23:59
最晚提交时间:12 月 11 日,星期五,23:59
译注:上述课程日期以及下文的
SITE-SPECIFIC均来自官方说明模板,不是本仓库的截止日期或提交要求。
# 1 引言
Web 代理是在 Web 浏览器与最终服务器之间充当中间人的程序。浏览器不直接联系最终服务器来获取网页,而是联系代理,由代理把请求转发给最终服务器。当最终服务器回复代理时,代理再把回复发送给浏览器。
代理有许多用途。有时代理用于防火墙,使防火墙后的浏览器只能通过代理联系防火墙外的服务器。代理也可以用作匿名工具:通过去除请求中所有可识别身份的信息,代理可以让浏览器对 Web 服务器保持匿名。代理甚至可以缓存 Web 对象:保存服务器对象的本地副本,并在以后的请求到来时从缓存读取对象进行响应,而不必再次与远程服务器通信。
在本实验中,你将编写一个缓存 Web 对象的简单 HTTP 代理。第一部分中,你将让代理接受传入连接,读取并解析请求,将请求转发给 Web 服务器,读取服务器响应,再把响应转发给相应的客户端。这一部分涉及学习 HTTP 的基本工作方式,以及如何利用套接字编写通过网络连接通信的程序。第二部分中,你将升级代理,使其能够处理多个并发连接。这会引导你接触并发这一重要的系统概念。在第三部分,也就是最后一部分,你将使用简单的主存缓存来保存最近访问的 Web 内容,为代理添加缓存功能。
# 2 实验安排
这是一个个人项目。
# 3 获取实验材料
SITE-SPECIFIC:在这里插入一段说明,解释教师将如何向学生发放proxylab-handout.tar文件。
将实验材料文件复制到你准备开展实验的 Linux 机器上的一个受保护目录,然后执行:
linux> tar xvf proxylab-handout.tar
这会生成名为 proxylab-handout 的实验目录。README 文件介绍了其中的各种文件。
# 4 第一部分:实现顺序 Web 代理
第一步是实现一个处理 HTTP/1.0 GET 请求的基本顺序代理。对 POST 等其他请求类型的支持完全是可选的。
启动时,代理应在命令行指定的端口上监听传入连接。连接建立后,代理应读取客户端的完整请求,并对请求进行解析。它应判断客户端是否发送了有效的 HTTP 请求;如果有效,就自行建立到相应 Web 服务器的连接,请求客户端指定的对象。最后,代理应读取服务器的响应,并将其转发给客户端。
# 4.1 HTTP/1.0 GET 请求
当用户在 Web 浏览器地址栏中输入 http://www.cmu.edu/hub/index.html 这样的 URL 时,浏览器会向代理发送 HTTP 请求,请求的第一行可能如下:
GET http://www.cmu.edu/hub/index.html HTTP/1.1
在这种情况下,代理至少应从请求中解析出以下字段:主机名 www.cmu.edu,以及路径或查询及其后的所有内容 /hub/index.html。这样,代理就能确定应建立到 www.cmu.edu 的连接,并发送自己的 HTTP 请求,其第一行如下:
GET /hub/index.html HTTP/1.0
注意,HTTP 请求中的所有行都以回车符 \r 加换行符 \n 结束。另一个要点是,每个 HTTP 请求都以一个空行 "\r\n" 结束。
你应注意到,上例中浏览器的请求行以 HTTP/1.1 结束,而代理的请求行以 HTTP/1.0 结束。现代 Web 浏览器会生成 HTTP/1.1 请求,但代理应能处理它们,并将它们作为 HTTP/1.0 请求转发。
需要考虑的是,HTTP 请求即使只限于 HTTP/1.0 GET 请求这个子集,也可能非常复杂。教材介绍了 HTTP 事务的一些细节,但完整的 HTTP/1.0 规范应参阅 RFC 1945。理想情况下,你的 HTTP 请求解析器应按照 RFC 1945 的有关章节实现充分的健壮性,只有一个例外:虽然规范允许多行请求字段,但本实验不要求代理正确处理它们。当然,代理绝不能因为格式错误的请求而提前终止。
# 4.2 请求头
本实验中的重要请求头是 Host、User-Agent、Connection 和 Proxy-Connection。
- 始终发送
Host请求头。虽然从严格意义上说,HTTP/1.0 规范并未认可这一行为,但对于某些 Web 服务器,尤其是使用虚拟主机的服务器,这对于获得合理响应是必要的。
Host 请求头描述最终服务器的主机名。例如,要访问 http://www.cmu.edu/hub/index.html,代理应发送:
Host: www.cmu.edu
浏览器可能在 HTTP 请求中附带自己的 Host 请求头。如果如此,代理应使用与浏览器相同的 Host 请求头。
- 你可以选择始终发送如下
User-Agent请求头:
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:10.0.3) Gecko/20120305 Firefox/10.0.3
原说明中因一行放不下而将该请求头分成两行展示,但代理发送时应将它作为一行。
User-Agent 请求头通过操作系统和浏览器等参数标识客户端,Web 服务器经常利用这些身份信息调整所提供的内容。发送这个特定的 User-Agent: 字符串,可能会改善简单的 telnet 式测试所返回材料的内容和多样性。
- 始终发送如下
Connection请求头:
Connection: close
- 始终发送如下
Proxy-Connection请求头:
Proxy-Connection: close
Connection 和 Proxy-Connection 请求头用于指定第一次请求/响应交换完成后是否保持连接。让代理为每个请求建立新连接是完全可以接受的,而且也是推荐的。将这两个请求头的值指定为 close,就是告诉 Web 服务器,代理打算在第一次请求/响应交换后关闭连接。
为方便使用,proxy.c 已将上述 User-Agent 请求头的值作为字符串常量提供给你。
最后,如果浏览器在 HTTP 请求中发送了其他请求头,代理应将它们原样转发。
# 4.3 端口号
本实验有两类重要的端口号:HTTP 请求端口和代理的监听端口。
HTTP 请求端口是 HTTP 请求 URL 中的可选字段。也就是说,URL 可以采用 http://www.cmu.edu:8080/hub/index.html 这样的形式;此时代理应连接主机 www.cmu.edu 的 8080 端口,而不是默认的 HTTP 端口 80。无论 URL 是否包含端口号,代理都必须正确工作。
监听端口是代理监听传入连接的端口。代理应接受一个命令行参数,用于指定监听端口号。例如,执行以下命令后,代理应在 15213 端口监听连接:
linux> ./proxy 15213
你可以选择任何未被其他进程使用的非特权监听端口(大于 1024 且小于 65536)。由于每个代理都必须使用不同的监听端口,而且每台机器上会有许多人同时开展实验,我们提供了脚本 port-for-user.pl,帮助你选择个人端口号。用它根据用户 ID 生成端口号:
linux> ./port-for-user.pl droh
droh: 45806
port-for-user.pl 返回的端口号 p 总是偶数。因此,如果你还需要一个额外端口,例如供 Tiny 服务器使用,就可以安全地使用端口 p 和 p + 1。
请不要自行随机选择端口,否则可能干扰其他用户。
# 5 第二部分:处理多个并发请求
当顺序代理能够正常工作后,你应修改它,使它能同时处理多个请求。实现并发服务器最简单的方法,是为每个新的连接请求创建一个新线程。也可以采用其他设计,例如教材第 12.5.5 节介绍的预线程化服务器。
- 注意,线程应以分离模式运行,以避免内存泄漏。
- CS:APP3e 教材介绍的
open_clientfd和open_listenfd函数基于现代的、与协议无关的getaddrinfo函数,因此是线程安全的。
# 6 第三部分:缓存 Web 对象
本实验的最后一部分是为代理添加缓存,在内存中保存最近使用过的 Web 对象。实际上,HTTP 定义了一套相当复杂的模型:Web 服务器可以说明其提供的对象应如何缓存,客户端也可以指定应如何为其使用缓存。不过,你的代理将采用一种简化的方法。
当代理从服务器接收到 Web 对象时,应在向客户端传输对象的同时将其缓存到内存中。如果另一个客户端向同一服务器请求同一对象,代理就不必重新连接服务器,而可以直接重发缓存的对象。
显然,如果代理缓存所有曾被请求过的对象,就需要无限大的内存。此外,Web 对象的大小不一,一个特别大的对象可能占满整个缓存,导致其他对象完全无法缓存。为避免这些问题,代理既应限制缓存的最大大小,也应限制可缓存对象的最大大小。
# 6.1 最大缓存大小
代理整个缓存的最大大小应为:
MAX_CACHE_SIZE = 1 MiB
计算缓存大小时,代理只应统计保存实际 Web 对象所用的字节;包括元数据在内的其他字节都应忽略。
# 6.2 最大对象大小
代理只应缓存不超过如下最大大小的 Web 对象:
MAX_OBJECT_SIZE = 100 KiB
为方便使用,这两个大小限制已经作为宏在 proxy.c 中提供。
实现正确缓存最简单的方法,是为每个活动连接分配一个缓冲区,并在从服务器接收数据时将其累积起来。如果缓冲区大小超过了最大对象大小,就可以丢弃该缓冲区。如果在超过最大对象大小之前就已读完 Web 服务器的整个响应,那么就可以缓存该对象。采用这种方案,代理用于 Web 对象的数据量最多为下式,其中 T 是活动连接的最大数量:
MAX_CACHE_SIZE + T * MAX_OBJECT_SIZE
# 6.3 驱逐策略
代理缓存应采用近似最近最少使用(LRU)的驱逐策略。不必严格实现 LRU,但应合理地接近它。注意,读取对象和写入对象都算使用对象。
# 6.4 同步
缓存访问必须是线程安全的,而保证缓存访问不存在竞争条件,很可能是这一部分最有意思的地方。事实上,还有一项特殊要求:多个线程必须能够同时读取缓存。当然,每次只应允许一个线程写缓存,但读者不应受到这种限制。
因此,用一把大的互斥锁保护所有缓存访问不是可接受的方案。你可以探索将缓存分区、使用 Pthreads 读者—写者锁,或者用信号量自行实现读者—写者方案等选择。无论采用哪种方案,不要求严格实现 LRU 驱逐策略这一点,都会为支持多个读者提供一些灵活性。
# 7 评分
本次作业总分为 70 分:
| 项目 | 分值 | 要求 |
|---|---|---|
| BasicCorrectness | 40 | 代理基本功能(自动评分) |
| Concurrency | 15 | 处理并发请求(自动评分) |
| Cache | 15 | 可正常工作的缓存(自动评分) |
# 7.1 自动评分
实验材料包含名为 driver.sh 的自动评分程序,教师将用它评定 BasicCorrectness、Concurrency 和 Cache 三项成绩。在 proxylab-handout 目录中运行:
linux> ./driver.sh
必须在 Linux 机器上运行该驱动程序。
# 7.2 健壮性
和往常一样,你必须交付一个能够应对错误、甚至格式错误或恶意输入的健壮程序。服务器通常是长时间运行的进程,Web 代理也不例外。应仔细考虑长时间运行的进程面对不同类型的错误时应如何反应。对许多错误而言,让代理立即退出显然并不合适。
健壮性还意味着其他要求,包括不会因段错误之类的错误情形而失效,并且没有内存泄漏和文件描述符泄漏。
# 8 测试和调试
除简单的自动评分程序外,你不会得到用于测试实现的样例输入或测试程序。你必须自行设计测试,甚至自行编写测试框架,来帮助调试代码并判断实现何时正确。这是一项在现实中非常有价值的技能,因为实际运行条件很少能准确预知,而参考实现往往也不可获得。
幸运的是,你可以使用许多工具来调试和测试代理。务必覆盖所有代码路径,并测试有代表性的输入集合,包括基本情形、典型情形和边界情形。
# 8.1 Tiny Web 服务器
实验目录包含 CS:APP Tiny Web 服务器的源代码。虽然它不如 thttpd 强大,但你可以很方便地按需修改。它也是编写代理代码的一个合理起点,而且驱动程序正是使用它来获取页面。
# 8.2 telnet
如教材第 11.5.3 节所述,你可以使用 telnet 建立到代理的连接,并向它发送 HTTP 请求。
# 8.3 curl
你可以用 curl 向任何服务器发送 HTTP 请求,包括自己的代理。它是非常有用的调试工具。例如,假设代理和 Tiny 都运行在本机,Tiny 监听 15213 端口,代理监听 15214 端口,那么可用如下 curl 命令通过代理向 Tiny 请求页面:
linux> curl -v --proxy http://localhost:15214 http://localhost:15213/home.html
* About to connect() to proxy localhost port 15214 (#0)
* Trying 127.0.0.1... connected
* Connected to localhost (127.0.0.1) port 15214 (#0)
> GET http://localhost:15213/home.html HTTP/1.1
> User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu)...
> Host: localhost:15213
> Accept: */*
> Proxy-Connection: Keep-Alive
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Server: Tiny Web Server
< Content-length: 120
< Content-type: text/html
<
<html>
<head><title>test</title></head>
<body>
<img align="middle" src="godzilla.gif">
Dave O’Hallaron
</body>
</html>
* Closing connection #0
# 8.4 netcat
netcat 也称为 nc,是一个用途广泛的网络工具。你可以像使用 telnet 一样,用 netcat 建立到服务器的连接。因此,假设代理在 catshark 上运行,使用端口 12345,就可以像下面这样手工测试代理:
sh> nc catshark.ics.cs.cmu.edu 12345
GET http://www.cmu.edu/hub/index.html HTTP/1.0
HTTP/1.1 200 OK
...
除了连接 Web 服务器外,netcat 自己也能充当服务器。执行下列命令,可以让 netcat 作为服务器在 12345 端口监听:
sh> nc -l 12345
建立 netcat 服务器后,你可以通过代理请求该服务器上的一个虚构对象,从而检查代理发送给 netcat 的准确请求内容。
# 8.5 Web 浏览器
最终,你应使用最新版本的 Mozilla Firefox 测试代理。打开“关于 Firefox”会自动将浏览器更新到最新版本。
要配置 Firefox 使用代理,请进入:
Preferences > Advanced > Network > Settings
译注:这里保留的是原说明中的 Firefox 菜单路径。
看到代理在真正的 Web 浏览器中工作,会令人非常兴奋。虽然代理功能有限,但你会发现,通过它仍能浏览绝大多数网站。
一个重要注意事项是:使用 Web 浏览器测试缓存时必须非常小心。所有现代 Web 浏览器都有自己的缓存,在尝试测试代理缓存之前,应先禁用浏览器缓存。
# 9 提交说明
提供的 Makefile 包含构建最终提交文件的功能。在工作目录中执行:
linux> make handin
输出文件是 ../proxylab-handin.tar,然后就可以提交它。
SITE-SPECIFIC:在这里插入一段说明,告诉每位学生如何提交其proxylab-handin.tar解答文件。
- 教材第 10—12 章包含有关系统级 I/O、网络编程、HTTP 协议和并发编程的有用信息。
- RFC 1945 (opens new window) 是 HTTP/1.0 协议的完整规范。
# 10 提示
- 如教材第 10.11 节所述,用标准 I/O 函数进行套接字输入和输出会有问题。因此,建议使用实验目录中
csapp.c文件提供的健壮 I/O(RIO)包。 csapp.c提供的错误处理函数不适合代理,因为服务器一旦开始接受连接,就不应终止。你需要修改它们,或者自行编写错误处理函数。- 你可以按需要修改实验目录中的文件。例如,为了保持良好的模块化,可以将缓存函数实现为一个库,放在
cache.c和cache.h中。当然,增加新文件时也需要更新所提供的Makefile。 - 如 CS:APP3e 教材第 964 页的旁注所述,代理必须忽略
SIGPIPE信号,并妥善处理返回EPIPE错误的写操作。 - 有时,对一个被提前关闭的套接字调用
read来接收字节,会使read返回 -1,并将errno设置为ECONNRESET。代理也不应因为这个错误而终止。 - 请记住,Web 上的内容并不全是 ASCII 文本。大量内容是二进制数据,例如图像和视频。在选择和使用网络 I/O 函数时,应考虑二进制数据。
- 所有请求都应以 HTTP/1.0 转发,即使原始请求使用的是 HTTP/1.1。
祝你好运!