Proxy Lab:编写带缓存的 Web 代理

Proxy Lab:编写带缓存的 Web 代理

原文:官方实验说明
实验包: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 请求头

本实验中的重要请求头是 HostUser-AgentConnectionProxy-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

ConnectionProxy-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_clientfdopen_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 是 HTTP/1.0 协议的完整规范。

10 提示

  • 如教材第 10.11 节所述,用标准 I/O 函数进行套接字输入和输出会有问题。因此,建议使用实验目录中 csapp.c 文件提供的健壮 I/O(RIO)包。
  • csapp.c 提供的错误处理函数不适合代理,因为服务器一旦开始接受连接,就不应终止。你需要修改它们,或者自行编写错误处理函数。
  • 你可以按需要修改实验目录中的文件。例如,为了保持良好的模块化,可以将缓存函数实现为一个库,放在 cache.ccache.h 中。当然,增加新文件时也需要更新所提供的 Makefile
  • 如 CS:APP3e 教材第 964 页的旁注所述,代理必须忽略 SIGPIPE 信号,并妥善处理返回 EPIPE 错误的写操作。
  • 有时,对一个被提前关闭的套接字调用 read 来接收字节,会使 read 返回 -1,并将 errno 设置为 ECONNRESET。代理也不应因为这个错误而终止。
  • 请记住,Web 上的内容并不全是 ASCII 文本。大量内容是二进制数据,例如图像和视频。在选择和使用网络 I/O 函数时,应考虑二进制数据。
  • 所有请求都应以 HTTP/1.0 转发,即使原始请求使用的是 HTTP/1.1。

祝你好运!