COMP5416 Assignment 作业要求梳理:HTTP Caching Proxy
COMP5416 Assignment 作业要求梳理:HTTP Caching Proxy
课程:COMP5416/COMP4416 — Advanced Network Technologies,Semester 2 2026 规格文件:
COMP5416_Assignment_Specifications-2.pdf(13 页)+ Ed 上的 Submission 说明 截止:2026 年 10 月 16 日(周五,Week 10)23:59 分值:100 分(公开测试 80 + 隐藏测试 20);迟交每天扣总分 5%,超过 5 天 0 分 提交:Ed,根目录必须有proxy.sh;还要README.md和源代码⚠️ 本文只整理作业要求、设计思路与易错点,不放完整解题代码——学校会把作业送查重,代码公开出去被同学抄了,自己也会被牵连。
一句话概括
用原生 TCP socket 写一个 HTTP 代理:对客户端它是 TCP 服务器,对源站它是 TCP 客户端。缓存未命中时去源站取完整响应并转发;命中时直接把之前存下的原始字节还给客户端,不碰源站。四个部分在同一个程序上逐步叠加:
| Part | 要做到什么 | 分值(公开 + 隐藏) |
|---|---|---|
| 1 — HTTP Forwarding | 解析请求、连对源站、完整且逐字节地返回响应 | 24 + 6 = 30 |
| 2 — Response Caching | 精确识别资源、跨连接缓存可缓存响应、区分命中/未命中 | 19 + 6 = 25 |
| 3 — Cache Capacity & LRU | 单对象 ≤ 51200 B、总量 ≤ 102400 B,LRU 替换 | 20 + 4 = 24 |
| 4 — Concurrent Clients | 最多 8 个客户端并发,共享同一个缓存 | 17 + 4 = 21 |
Part 1–3 可以一个一个顺序处理客户端,Part 4 才要求并发。
协议范围(全程适用)
| 方面 | 要求 |
|---|---|
| 传输 | 客户端↔︎代理、代理↔︎源站都是 TCP socket |
| HTTP 版本 | HTTP/1.1 |
| 方法 | 只有 GET,没有请求体 |
| 客户端请求目标 | absolute-form,带显式端口:GET http://127.0.0.1:43172/path?q=1 HTTP/1.1 |
| 发给源站的目标 | origin-form:GET /path?q=1 HTTP/1.1,保留路径和
query string |
| 源站地址 | IPv4
127.0.0.1:<port>,端口每次可能不同;不需要
DNS |
| 每个客户端连接 | 只有一个请求 |
| 源站响应何时结束 | 读到源站关闭连接(EOF)为止——响应不一定有
Content-Length |
| 客户端响应何时结束 | 发完完整响应后,代理关闭客户端连接 |
| 响应数据 | 文本和任意二进制都要逐字节保留 |
| 状态码 | 所有合法响应都要转发(404、500 也一样) |
| 非法请求 | 回 HTTP/1.1 400 Bad Request,然后关连接 |
不需要做:持久连接、pipelining、chunked
编码、Cache-Control / Expires /
ETag / If-None-Match /
If-Modified-Since / 304、缺失或重复的
Host 头、空路径、请求体、超出规格的错误处理。
Part 1 — HTTP 转发
3.1 接收请求
- 一直
recv到出现\r\n\r\n。TCP 是字节流,请求可能在请求行中间、头部中间、甚至\r\n\r\n中间被切开;不能假设一次 recv 就是一个请求。
3.2 转发给源站
从 absolute-form 目标里拆出 host、port、resource(含 query):
GET http://127.0.0.1:43172/images/a17.bin?version=1 HTTP/1.1 |
发给源站的请求必须: - 保留路径和 query string; -
恰好一个 Host 头,值为
host:port(来自请求目标); - 包含
Connection: close。
GET /images/a17.bin?version=1 |
不能把客户端的 absolute-form 请求行原样转发;每个请求都要独立解析,上一个连接的状态不能影响下一个。
3.3 转发响应
- 一直读到源站 EOF;缓冲区边界或某次 recv 成功都不代表结束。
- 逐字节保留:状态行、所有头、空行、完整 body。不要解码成字符串再拼回去(body 可能不是合法 UTF-8)。
- 可以边收边发,也可以收完再发。
3.4 非法请求
不是合法 absolute-form HTTP/1.1
GET(格式错、POST 等其他方法)→
HTTP/1.1 400 Bad Request 后关连接。
3.5 持续服务
处理完一个请求就关这个客户端连接,继续 accept
下一个;进程不能退出。
Part 2 — 响应缓存
- 进程内内存缓存:跨客户端连接保留;重启进程就清空,不需要落盘。
- 缓存键:
<origin-host, origin-port, resource-path>,resource-path 含 query,逐字节比较、不做任何规范化:
| 请求 | 是否同一资源 |
|---|---|
/images/a.bin vs /images/b.bin |
不同 |
| 同路径,端口 41000 vs 42000 | 不同 |
/search?q=cats vs /search?q=dogs |
不同 |
/file%2Fname vs /file/name |
不同(不要 URL 解码) |
| host、port、path/query 字节完全一样 | 相同 |
- 命中:直接返回存储的响应,不联系源站,发完关连接。
- 未命中:按 Part 1 转发,返回完整响应,符合条件就存。
- 缓存的是完整 HTTP 响应(状态行 + 头 + 空行 + body);命中时回放的必须是当初从源站收到的同一串字节,只存 body 再重新拼头是不允许的。
- 可缓存条件:只看数字状态码是不是
200,不看原因短语。
HTTP/1.1 200 OK和HTTP/1.1 200都可以缓存;404、500 等照样转发但不缓存,下次还要找源站。 - 条目不过期、不需要重新验证,直到被 Part 3 的规则淘汰或进程结束。
Part 3 — 容量与 LRU
| 限制 | 值(都是包含边界) |
|---|---|
| 单个缓存响应最大 | 51200 字节(正好 51200 可以存) |
| 缓存总大小最大 | 102400 字节(总量正好 102400 可以) |
- 字节计数:条目大小 = 完整 HTTP 响应的字节数(状态行 + 头 + 空行 + body)。键、LRU 链表等元数据不算。
- 准入:状态码 200 且 大小 ≤ 51200。不满足的照样完整转发,但不能改变已有条目、总字节数或 LRU 顺序。
- LRU:一个条目在「命中返回」或「新插入」时变成 MRU。插入会超过 102400 时,从 LRU 端开始淘汰,直到放得下(可能一次淘汰多个)。
规格里的例子:缓存里有 A(40000, LRU)、B(30000)、C(20000, MRU),共 90000。新来 D(40000),90000 + 40000 > 102400,淘汰 A → 50000 + 40000 = 90000,放下 D。
- 被淘汰的资源再次请求就是普通 miss,要重新去源站取。
Part 4 — 并发客户端
- 至少支持 8 个同时活跃的客户端连接,共享一个缓存。
- 慢客户端/慢源站不能卡住别人:
- 一个客户端请求头发了一半停住时,另一个客户端要能完成请求;
- 一个请求在等慢源站时,别的客户端要能拿到缓存命中、或者从快源站拿到响应。
- 线程或其他并发机制都可以。
- 共享缓存要同步:查找、插入/淘汰、总字节数更新、LRU 更新。
- 等源站响应时不能持有缓存的独占锁。
- LRU 在并发下:操作重叠时,任何与实际执行一致的顺序都可以;但已经完成的操作必须反映在之后开始的操作里。例:一个请求在等源站,期间另一个请求命中了 X,那么前者插入时的淘汰顺序必须考虑 X 已经是 MRU——「开始 fetch」时不能把淘汰顺序定死。
- 同时 miss 同一资源:不要求合并请求,两个都可以去源站取(假设内容相同)。后完成的那个可以丢弃,也可以替换并标为 MRU;但只能占一个逻辑条目、只算一次大小,两个客户端都要收到完整响应。
- 并发时响应字节不能串到别的客户端,缓存状态不能被破坏;一波并发结束后进程继续服务。
测试与评分
- 自动化黑盒测试:评分系统启动你的代理,自己开源站服务器和客户端,观察网络行为。代码结构不评分。
- 可以往 stdout/stderr 打日志,响应只走 TCP。
- 公开测试(Ed 上,输入输出可见)覆盖:
| Part | 公开测试覆盖 |
|---|---|
| 1 | 转发、分片的请求和响应、二进制响应、非 200 响应、非法请求、顺序客户端 |
| 2 | 命中/未命中、资源身份、query string、端口区分、二进制缓存、可缓存性 |
| 3 | 对象和总容量上限、字节计数、LRU 更新、淘汰、多次淘汰、精确容量边界 |
| 4 | 并发客户端、共享缓存一致性、并发命中/未命中、并发下的 LRU、持续服务 |
- 隐藏测试(只看到 pass/fail)不加新规则,重点是:
- 流处理纪律:TCP 分片、不依赖特定的 recv 边界;
- 缓存身份与可缓存性;
- 没见过的请求序列下的缓存管理(字节计数、LRU、容量、淘汰);
- 并发下的共享缓存行为。
- 最终成绩看最后一次提交。
提交要求
语言:强烈推荐 Python 3,也可以用 Ed 支持的其他语言。
入口:根目录下的
proxy.sh(大小写敏感),运行方式./proxy.sh <Proxy-Port>,例如./proxy.sh 52000监听 52000;一直运行到被外部终止。Python 可以这样写:
python3 proxy.py "$@"允许的库:只能用标准库;socket、并发、数据结构、字符串/字节处理都可以。禁止替你做 HTTP 解析/请求构造/转发的库,例如
requests、urllib.request、http.client。不确定就去 Ed 问。README.md(不评分,但没有它就不能对环境导致的测试失败申诉),要写:
- 实现语言和版本;
- 需要的运行/开发环境;
- 构建步骤或依赖包信息;
- 运行方法;
- AI 使用声明:用了哪些工具、怎么用的。
提交内容:
proxy.sh+ 全部源码 +README.md;要在 Ed 环境里能跑,截止前在 Ed 上实际测一遍。
学术诚信与 AI
- 本作业允许使用 AI,但必须在
README.md里声明工具和用途;正确性仍由你负责。 - 必须能讲清楚提交的每一部分代码:会有一次 demonstration,问你实现细节来确认你真的理解。
- 其他任何非本人的工作要立刻报告给 tutor/讲师;作业可能送去查重。
实现思路(不含完整代码)
整体结构:主线程 accept,每个客户端开一个线程
handle_client。
- 读请求头:循环
recv累积到bytes缓冲区,直到出现b"\r\n\r\n";对方提前关闭 → 400。 - 解析:只看请求行,
split(b" ")必须正好 3 段:GET、http://...、HTTP/1.1;去掉http://后第一个/之前是host:port,之后是 path(保持原始 bytes,直接拿来当缓存键)。任何一步不对 → 400。 - 查缓存(加锁,命中就
move_to_end变 MRU,解锁后再发送)。 - 未命中:
socket.create_connection((host, port)),发 origin-form 请求,循环recv到返回空字节(EOF),每块立刻转发给客户端,同时最多攒 51200 字节用于缓存——一旦超过就不再攒(反正不能缓存)。 - 插入缓存:状态行第二段等于
b"200"且大小 ≤ 51200 时,加锁 → 如果键已存在先删掉旧的(同时 miss 的情况)→ 从OrderedDict头部popitem(last=False)淘汰到放得下 → 插到尾部。 - 关连接:先
shutdown(SHUT_WR)再close。
缓存用 collections.OrderedDict:头部是 LRU,尾部是
MRU,move_to_end / popitem(last=False) 都是
O(1)。一把 threading.Lock 包住所有缓存操作,网络
I/O 全部在锁外。
易错点清单
| 坑 | 后果 | 正确做法 |
|---|---|---|
一次 recv 就当成完整请求/响应 |
分片测试和大响应全挂 | 请求读到 \r\n\r\n,响应读到 EOF |
用 .decode() 处理响应 |
二进制 body 报错或被改 | 全程 bytes,只解析状态行 |
| 把 absolute-form 请求行原样发给源站 | 违反 3.2 | 重新构造 GET <path> HTTP/1.1 |
| 缓存键只用 path / 先 URL 解码 | 端口区分、%2F 测试挂 |
(host, port, 原始 path bytes) |
| 只存 body,命中时重新拼头 | 字节不一致 | 存完整原始响应 |
用 "200 OK" in line 判断 |
HTTP/1.1 200(无原因短语)不被缓存 |
只比较第二段是否等于 200 |
| 超大响应先把旧条目淘汰了再发现放不下 | 违反「不改变缓存」 | 先判断大小再动缓存 |
边界写成 < |
51200 / 102400 精确边界挂 | 两个上限都是 <= |
| 同时 miss 同一资源插两次 | 总字节数算重 | 插入前先删掉同键旧条目 |
| 在锁里等源站响应 | 并发测试挂 | 锁只包内存操作 |
| 在 fetch 开始时就算好要淘汰谁 | 违反 6.3 | 插入那一刻再按当前 LRU 顺序淘汰 |
客户端发来的数据没读完就 close |
可能发 RST,客户端丢失尾部数据 | shutdown(SHUT_WR) 后再短暂读空 |
自测清单
本地写一个测试脚本:在 127.0.0.1 随机端口开几个「原始
socket 源站」(能计数被访问次数、能分小块慢慢发、能延迟),再用原始
socket 当客户端,对照下面这些情况:
相关笔记
- [[26-S2-Course/COMP5416/COMP5416-Week03-Applications-Summary|COMP5416 Week 03 应用层讲课总结]](HTTP 报文格式、Web 缓存/代理)
- [[26-S2-Course/COMP5416/COMP5416-Week05-Transport-Summary|COMP5416 Week 05 传输层讲课总结]](TCP 字节流)
- [[26-S2-Course/COMP5416/COMP5416-Week05-Tutorial-DNS-HTTP|COMP5416 Week 05 Tutorial:DNS 与 HTTP 时延]]