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
→ host = 127.0.0.1, port = 43172, resource = /images/a17.bin?version=1

发给源站的请求必须: - 保留路径和 query string; - 恰好一个 Host 头,值为 host:port(来自请求目标); - 包含 Connection: close。

GET /images/a17.bin?version=1 HTTP/1.1
Host: 127.0.0.1:43172
Connection: close

不能把客户端的 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)不加新规则,重点是:
    1. 流处理纪律:TCP 分片、不依赖特定的 recv 边界;
    2. 缓存身份与可缓存性;
    3. 没见过的请求序列下的缓存管理(字节计数、LRU、容量、淘汰);
    4. 并发下的共享缓存行为。
  • 最终成绩看最后一次提交。

提交要求

  • 语言:强烈推荐 Python 3,也可以用 Ed 支持的其他语言。

  • 入口:根目录下的 proxy.sh(大小写敏感),运行方式 ./proxy.sh <Proxy-Port>,例如 ./proxy.sh 52000 监听 52000;一直运行到被外部终止。Python 可以这样写:

    #!/bin/bash
    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。

  1. 读请求头:循环 recv 累积到 bytes 缓冲区,直到出现 b"\r\n\r\n";对方提前关闭 → 400。
  2. 解析:只看请求行,split(b" ") 必须正好 3 段:GET、http://...、HTTP/1.1;去掉 http:// 后第一个 / 之前是 host:port,之后是 path(保持原始 bytes,直接拿来当缓存键)。任何一步不对 → 400。
  3. 查缓存(加锁,命中就 move_to_end 变 MRU,解锁后再发送)。
  4. 未命中:socket.create_connection((host, port)),发 origin-form 请求,循环 recv 到返回空字节(EOF),每块立刻转发给客户端,同时最多攒 51200 字节用于缓存——一旦超过就不再攒(反正不能缓存)。
  5. 插入缓存:状态行第二段等于 b"200" 且大小 ≤ 51200 时,加锁 → 如果键已存在先删掉旧的(同时 miss 的情况)→ 从 OrderedDict 头部 popitem(last=False) 淘汰到放得下 → 插到尾部。
  6. 关连接:先 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 时延]]