Writing a network program using an OS stream socket
2026-10-07 · 完成 checkpoint 0 的第一部分:用 OS 提供的 stream socket 写一个最小化的 HTTP 客户端
webget。
实验目标
Checkpoint 0 是整个 CS144 的热身关,目标有两个层次:
- 表层:写一个叫
webget的程序,用操作系统内核现成的 TCP 支持,连上 Web 服务器、抓取一个页面——和之前在 telnet 里手工敲 HTTP 请求做的事一模一样,只是这次用 C++ 代码完成。 - 深层:让学习者亲眼看清楚"可靠字节流"这个抽象。Internet 本身只提供 best-effort 的数据报投递——数据报可能丢失、乱序、内容被篡改、甚至被重复投递;把它变成"可靠、有序的字节流"的是两端操作系统上运行的 TCP。这个热身关用的是现成的 TCP,而从后面的 checkpoint 开始,我们要亲手把这个抽象造出来(ByteStream → Reassembler → TCP Receiver/Sender)。
前置知识
1. stream socket 是什么:长得像文件描述符的字节流
对程序来说,socket 就是一个普通的文件描述符。两个 socket 建立连接后,一方write进去的字节,最终会按同样的顺序从另一方的 socket 里被read出来。传输过程中 TCP 替我们处理了分段、重传、排序、流量控制等一切脏活。
2. HTTP 请求就是一段文本
先用 telnet 手工体验过一遍(telnet cs144.keithw.org http后逐行敲):
GET /hello HTTP/1.1 Host: cs144.keithw.org Connection: close (空行)要点:
- 请求行 + 头字段 + 空行,每一行都以
\r\n结尾(不是\n); - 空行标志请求头结束,服务器据此判断"请求发完了";
Connection: close告诉服务器:回完这个响应就关闭连接——这正是后面客户端能知道"读完了"的关键。
3. 一个贯穿全程的对照:connect()就是三次握手
connect()返回时,TCP 的 SYN / SYN-ACK / ACK 三次握手已经完成。数据可靠性不是免费的,连接建立这一步本身就是协议在"谈条件"(同步序列号、确认双方可达)。
实现思路
get_URL(host, path)分四步走:
- 预约一个 endpoint:
TCPSocket tcpSocket;—— 构造时调用socket(2),拿到一个未连接的 TCP socket(file descriptor)。 - 把名字翻译成地址:
Address address(host, "http");—— 一次搞定两层解析:主机名 → IP 地址,服务名"http"→ 端口号 80。 - 发起连接:
tcpSocket.connect(address);—— 触发三次握手。 - 收发文本:
- 按 HTTP 规定的文本格式拼好请求(
GET path HTTP/1.1+Host:+Connection: close+ 空行,全部\r\n),write()出去——注意write只是把字节交给内核的发送缓冲区,之后的可靠交付由 TCP 负责; - 循环
read()并立刻打印,直到eof()。EOF 的语义:对端(服务器)关闭连接后,本端read返回 0 字节 →eof()为真 → 循环结束。所以Connection: close不仅是对服务器说"回完就关",也是在为客户端自己的读循环安排一个确定的终点。
- 按 HTTP 规定的文本格式拼好请求(
代码实现
/* * apps/webget.cc */voidget_URL(conststring&host,conststring&path){// 预约一个 end-point (file-descriptor)TCPSocket tcpSocket;// 将域名解析为 IP 地址;将服务名解析为端口号Addressaddress(host,"http");// tcp 触发"三次握手"tcpSocket.connect(address);// 请求文本std::string require_str="GET "+path+" HTTP/1.1\r\nHost: "+host+"\r\nConnection: close\r\n\r\n";// write 把请求文本写入内核的发送缓存区,交给 tcp 处理后续工作tcpSocket.write(require_str);// read 读取后立刻打印std::string buffer;while(!tcpSocket.eof()){tcpSocket.read(buffer);cout<<buffer;}}值得注意的两个点:
- RAII:全程没有
close()调用——TCPSocket析构时自动关闭文件描述符,这也是本课程反复强调的现代 C++ 风格(构造获取资源、析构释放资源)。 - 请求串用
\r\n拼接、以\r\n\r\n收尾;这里错一个字符,服务器就不知道该在哪儿停下来。
验证
- 构建:
cmake --build build(或cmake --build build --target webget)。 - 自动测试:
cmake --build build --target check_webget(等价于ctest -R webget)。测试脚本tests/webget_t.sh会让webget去抓cs144.keithw.org/nph-hasher/xyzzy,取输出的最后一行与一个已知哈希比对——所以输出必须和服务器返回的字节流一字不差。 - 手动验证:
./apps/webget cs144.keithw.org /hello,应看到和浏览器、telnet 完全一样的响应(HTTP 头 + 正文)。
复盘
- HTTP 没有魔法。浏览器做的事、telnet 做的事、webget 做的事,本质完全相同:在一条字节流上,按约定格式写一段文本,再读回一段文本。理解了这一点,后面的 lab 就都建立在这条字节流之上。
- EOF 是"对端关闭连接"的协议信号,不是数据的自然属性。
read返回 0 依赖服务器真的把连接关了,而服务器肯关,是因为我们发了Connection: close——客户端和服务器在字节流之上又叠了一层"文本协议"来协调行为。 connect()返回 ≠ 数据到了。它只是完成了三次握手;之后每次write的可靠交付,都是内核里 TCP 状态机在背后工作。从 checkpoint 1 起,这台"状态机"将由我们自己在用户态实现。- 小步提交。handout 强调"频繁、语义化的小 commit"——今天这个 commit 只做了一件事:
get_URL从"未实现"变成"能抓页面",每个 commit 都可编译、都朝测试通过的方向前进。