# proxylab **Repository Path**: tjuics/proxylab ## Basic Information - **Project Name**: proxylab - **Description**: No description available - **Primary Language**: C - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 228 - **Created**: 2024-05-13 - **Last Updated**: 2025-12-14 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 代理实验 ## 1. 简介 Web 代理是一个程序,作为 Web 浏览器和终端服务器之间的中间人。浏览器不直接连接终端服务器获取网页,而是连接代理,代理将请求转发给终端服务器。当终端服务器回复代理时,代理将回复转发给浏览器。 代理有许多应用场景。代理可以用于防火墙中,以便防火墙后面的浏览器只能通过代理联系防火墙之外的服务器。代理还可以充当匿名器:通过剥离所有标识信息的请求,代理可以使浏览器对 Web 服务器匿名。代理甚至可以用于缓存 Web 对象,通过存储服务器对象的本地副本,然后通过读取缓存中的对象来响应将来的请求,而不是再次与远程服务器通信。 在本次实验中,你将编写一个简单的 HTTP 代理,用于缓存 Web 对象。 * 在实验的第一部分中,你将设置代理来接受传入连接,读取和解析请求,转发请求到 Web 服务器,读取服务器的响应,并将这些响应转发给相应的客户端。这第一部分将涉及学习基本的 HTTP 操作以及如何使用套接字编写通过网络连接进行通信的程序。 * 在第二部分中,你将升级您的代理以处理多个并发连接。这将介绍您处理并发的重要系统概念。 * 在第三部分,您将向您的代理添加缓存,使用最近访问的 Web 内容的简单主存储器缓存。 ## 2. 准备 需要安装netstat工具,在ubuntu下的安装方法为 ``` sudo apt install net-tools ``` 注意:代码所在目录名称必须为proxylab!!!! ## 3. 实验任务 ### 第一部分:实现一个顺序的代理服务器 实现一个基础的顺序代理服务,用于处理 HTTP/1.0 的 GET 请求。其他类型的请求,如 POST,是选做的。 启动时,你的代理应该监听在命令行指定的端口上接收传入连接。一旦建立了连接,代理应该从客户端读取整个请求并解析请求。它应该确定客户端是否发送了有效的 HTTP 请求;如果是,则可以建立自己的连接到适当的 Web 服务器,然后请求客户端指定的对象。最后,代理应该读取服务器的响应并将其转发给客户端。 #### HTTP/1.0 GET请求 当最终用户在 Web 浏览器的地址栏中输入类似于 http://www.tju.edu.cn/tdgk/xxjj.htm 的 URL 时,浏览器将向代理发送一个 HTTP 请求,其开头可能类似于以下内容: ``` GET http://www.tju.edu.cn/tdgk/xxjj.htm HTTP/1.1 ``` 在这种情况下,代理应该将请求解析为至少以下字段:主机名www.tju.edu.cn和路径或查询以及其后的所有内容 /tdgk/xxjj.htm。这样,代理就可以确定它应该打开到 www.tju.edu.cn 的连接,并发送自己的 HTTP 请求,其开头类似于以下形式: ``` GET /tdgk/xxjj.htm HTTP/1.0 ``` 请注意,HTTP 请求中的每一行都以回车符(\r)和换行符(\n)结尾。另外,重要的是每个 HTTP 请求都以空行 "\r\n" 终止。 在上面的示例中,应该注意到浏览器的请求行以 HTTP/1.1 结尾,而代理的请求行以 HTTP/1.0 结尾。现代 Web 浏览器会生成 HTTP/1.1 请求,但是你所实现的代理应该能够处理并转发它们作为 HTTP/1.0 请求。 需要注意的是,即使是 HTTP/1.0 的 GET 请求,也可能非常复杂。教材中描述了 HTTP 传输的某些细节,但您应该参考 RFC 1945 获取完整的 HTTP/1.0 规范。理想情况下,你的 HTTP 请求解析器应该根据 RFC 1945 的相关部分完全健壮,但有一个细节可以放宽要求:虽然规范允许多行请求字段,但你的代理不需要正确处理它们。当然,你的代理不应因为请求格式不正确而提前终止(崩溃)。 #### 请求头 本实验中重要的请求头包括 Host、User-Agent、Connection 和 Proxy-Connection 头部: * 总是发送 Host 头部。虽然这种行为在 HTTP/1.0 规范中没有明确规定,但对于从某些 Web 服务器中获取合理响应是必要的,特别是那些使用虚拟主机的服务器。 Host 头部描述了终端服务器的主机名。例如,要访问 http://www.tju.edu.cn/tdgk/xxjj.htm,您的代理将发送以下头部: ``` Host: www.tju.edu.cn ``` Web 浏览器很可能会在其 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 服务器,代理打算在第一个请求/响应交换后关闭连接。 为了方便起见,所描述的 User-Agent 头部的值已经作为字符串常量在 proxy.c 中提供。 最后,如果浏览器在 HTTP 请求中发送了任何额外的请求头部,您的代理应该将它们原样转发。这意味着您的代理应该检查接收到的请求中是否有其他自定义的请求头部,如果有,则应该将它们一并包含在代理发送的请求中,保持原样传递给目标服务器。这样可以确保代理在转发请求时不会丢失任何额外的重要信息或配置。 #### 端口号 在这个实验中,有两个重要的端口号:HTTP 请求端口和代理监听端口。 在HTTP 请求的 URL 中的端口号是可选字段。也就是说,URL 可以是这种形式:http://www.tju.edu.cn:8080/tdgk/xxjj.htm,在这种情况下,您的代理应该连接到主机 www.tju.edu.cn 的端口 8080,而不是默认的 HTTP 端口 80。你的代理必须能够在 URL 中是否包含端口号的情况下正常工作。 监听端口是您的代理应该用于接收传入连接的端口。你的代理在运行时应该接受一个命令行参数,用于指定代理的监听端口号。例如,使用以下命令,你的代理应该在 15213 端口上监听连接: ``` linux> ./proxy 15213 ``` 你可以选择任何非特权监听端口(大于1024且小于65536),只要该端口未被其他进程使用。由于每个代理都必须使用唯一的监听端口,并且许多人将同时在同一台机器上进行工作,因此实验中提供了脚本 port-for-user.pl 来帮助您选择自己的个人端口号。可以使用它根据你的用户 ID 生成端口号: ``` linux> ./port-for-user.pl pcname: 14850 ``` 该脚本将基于你的用户 ID 生成一个唯一的端口号。根据生成的端口号来启动你的代理,避免端口冲突的问题。 port-for-user.pl 返回的端口号 p 总是偶数。因此,如果您需要额外的端口号(例如Tiny Web服务),您可以安全地使用端口号 p 和 p+1。 请不要随意选择自己的随机端口。如果这样做,可能会干扰其他进程的工作。使用 port-for-user.pl 提供的端口号,以确保你的端口与其他进程的端口不冲突。 ### 第二部分:处理多个并发请求 当你实现了个工作正常的顺序代理后,你应该优化它以实现同时处理多个请求的能力。 实现并发服务器的最简单方式是针对每个新连接请求生成一个新线程来处理。也可以采用其他设计,例如教材中第12.5.5节中描述的预线程化服务器。 注意事项: * 确保您的线程以分离模式运行,以避免内存泄漏。 * open_clientfd 和 open_listenfd 函数在 CS:APP3e 教材中描述的是基于现代且与协议无关的 getaddrinfo 函数,因此是线程安全的。 ### 第三部分:缓存Web对象 在实验的最后部分,你将为你所实现的代理添加一个缓存,用于将最近使用的 Web 对象存储在内存中。HTTP 实际上定义了一个相当复杂的模型,通过该模型,Web 服务器可以指示它们提供的对象应如何被缓存,客户端可以指定代理应如何代表它们使用缓存。但是,你的代理可以采用更加简单的方法。 当你的代理从服务器接收到一个 Web 对象时,它应该在将对象传输给客户端的同时将其缓存到内存中。如果另一个客户端从相同的服务器请求相同的对象,您的代理不需要重新连接到服务器;它可以简单地重新发送缓存的对象。 显然,如果您的代理缓存每一个被请求的对象,那么它将需要无限量的内存。此外,由于某些 Web 对象比其他对象大,可能会出现一个巨大的对象占用整个缓存的情况,导致其他对象根本无法缓存。为了避免这些问题,您的代理应该有一个最大缓存容量和一个最大缓存对象大小。 #### 最大缓存容量 你的代理的总缓存容量应为: ``` MAX_CACHE_SIZE = 1 MiB ``` 在计算代理缓存大小时,代理应仅计算用于存储实际 Web 对象的字节数;任何额外的字节,包括元数据,都应该被忽略。 #### 最大缓存对象大小 你的代理应仅缓存不超过以下最大大小的 Web 对象: ``` MAX_OBJECT_SIZE = 100 KiB ``` 为了方便起见,这两个大小限制都作为宏定义在 proxy.c 中提供。 实现正确缓存的最简单方法是为每个活动连接分配一个缓冲区,并在从服务器接收数据时累积数据。如果缓冲区的大小超过了最大缓冲对象大小,那么可以丢弃缓冲区。如果在超过最大对象大小之前读取完整的 Web 服务器响应,则可以缓存该对象。使用这种方案,你的代理用于 Web 对象的最大数据量将是以下形式,其中 T 是活动连接的最大数量: ``` MAX_CACHE_SIZE + T * MAX_OBJECT_SIZE ``` #### 缓存替换策略 你的代理缓存应采用一种近似最近最少使用(LRU)驱逐策略的驱逐策略。它不必严格符合 LRU,但应该是比较接近的策略。请注意,读取对象和写入对象都算作使用对象。 #### 并发同步 对缓存的访问必须是线程安全的,并确保缓存访问没有竞争条件。需要满足以下条件:多个线程能够同时读取缓存,只允许一个线程写缓存。 因此,使用一个大的独占锁来保护对缓存的访问不是一个可接受的解决方案。您可以探索一些选项,比如对缓存进行分区、使用 Pthreads 读写锁,或者使用信号量来实现自己的读写解决方案。无论哪种情况,你不必实现严格的 LRU 替换策略将为你提供一些灵活性,以支持多个读者。 ## 4. 评价方法 此作业的评分总分为70分: * 基本正确性(Basic Correctness):40分,用于基本代理操作的评分(自动评分) * 并发性(Concurrency):15分,用于处理并发请求的评分(自动评分) * 缓存(Cache):15分,用于实现工作中的缓存的评分(自动评分) ### 自动评分 实验代码中包括一个名为 driver.sh 的自动评分程序,教师将使用该程序为基本正确性(Basic Correctness)、并发性(Concurrency)和缓存(Cache)分配分数。实验目录中执行以下操作: ``` ./driver.sh ``` 您可以运行该程序评测你的代码。 ### 程序的健壮性 你必须编写一个对错误、甚至是格式错误或恶意输入都具有健壮性的程序。服务器通常是长时间运行的进程,Web 代理也不例外。仔细考虑长时间运行的进程应如何应对不同类型的错误。对于许多种类型的错误,你的代理立即退出是不合适的。 健壮性还意味着其他要求,包括对诸如段错误等错误情况的免疫性,以及没有内存泄漏和文件描述符泄漏等错误情况。 ## 5. 测试和调试 除了简单的自动评分程序外,没有任何样本输入或测试程序来测试你的实现。这需要自己设计测试并可能甚至需要自己的测试工具来帮助调试代码并确定何时实现了正确的解决方案。这是现实世界中一项宝贵的技能,因为很少有确切的操作条件可知,参考解决方案通常也无法获得。 幸运的是,有许多工具可以帮助你调试和测试你的代理。请确保测试所有代码路径,并测试一组代表性输入,包括基本情况、典型情况和边界情况。 ### Tiny Web服务 实验代码中包含了教材中提到的Tiny Web 服务器的源代码。虽然不像 thttpd 那样强大,Tiny Web 服务器很容易根据你的需要进行修改。它也是你代理服务器代码的一个合理起点。而且,它是driver.sh用来获取页面的服务器。 ### telnet 你可以使用 telnet 打开到您的代理的连接,并发送 HTTP 请求。 ### curl 您可以使用 curl 生成针对任何服务器(包括你自己的代理)的 HTTP 请求,这是一种非常有用的调试工具。 例如,如果你的代理和 Tiny 服务器都在本地机器上运行,Tiny 服务器正在监听端口15215,而代理正在监听端口15214,则可以通过以下 curl 命令通过代理从 Tiny 服务器请求页面: ``` linux> curl -v --proxy http://localhost:15214 http://localhost:15215/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 <
Dave O’Hallaron
* Closing connection #0
```
### netcat
netcat(也称为 nc)是一个多功能的网络实用工具。你可以像 telnet 一样使用 netcat 打开到服务器的连接。因此,假设你的代理使用端口 12345 运行,你可以像下面这样手动测试您的代理:
```
nc 127.0.0.1 12345
GET http://www.cmu.edu/hub/index.html HTTP/1.0
```
除了能够连接到 Web 服务器外,netcat 还可以自己作为服务器运行。通过以下命令,你可以将 netcat 作为服务器运行在端口 12345 上:
```
nc -l 12345
```
设置了 netcat 服务器后,您可以通过您的代理向它发送一个伪对象的请求,并检查您的代理发送给 netcat 的确切请求。
### Web浏览器
最终,您应该使用最新版本的 Mozilla Firefox 来测试您的代理。访问“关于 Firefox”将自动更新您的浏览器到最新版本。
要配置 Firefox 使用代理,请访问
```
Preferences>Advanced>Network>Settings
```
通过真正的 Web 浏览器看到你的代理正常工作将是非常激动人心的。虽然你的代理的功能可能会受到限制,但您会注意到通过你的代理,可以浏览绝大多数的网站。
需要注意的是,在使用 Web 浏览器测试缓存时,您必须非常小心。所有现代 Web 浏览器都有自己的缓存,在尝试测试代理的缓存之前应该将其禁用。
## 6. 小提示
* 正如教材中所讨论的那样,对于套接字输入和输出,使用标准的I/O函数是有问题的。相反,我们建议您使用在手册目录中的 csapp.c 文件中提供的Robust I/O(RIO)包。
* csapp.c 中提供的错误处理函数对于您的代理可能不太适用,因为一旦服务器开始接受连接,它就不应该终止。您需要修改它们或编写自己的错误处理逻辑。
* 您可以自由修改手册目录中的文件。例如,出于良好的模块化考虑,您可以将缓存函数实现为名为 cache.c 和 cache.h 的库文件。当然,添加新文件你需要更新 Makefile。
* 正如教材第677页的旁注中所讨论的,你的代理必须忽略 SIGPIPE 信号,并且应该优雅地处理返回 EPIPE 错误的写操作。
* 有时,调用 read 从已经提前关闭的套接字接收字节会导致 read 返回 -1,同时 errno 设置为 ECONNRESET。您的代理也不应因此错误而终止。
* 请记住,网络上的所有内容都不是 ASCII 文本。网络上的许多内容都是二进制数据,例如图像和视频。确保在选择和使用网络 I/O 函数时考虑到二进制数据。
* 所有请求都将以HTTP/1.0协议转发,即使原始请求是 HTTP/1.1。
## 7. 提交
运行以下命令
```
make handin
```
将会将proxylab目录打包,在proxylab的父目录下生成proxylab.zip文件,将proxylab.zip文件提交至相应位置。