CS 144: Introduction to Computer Networking, Fall 2025
本篇文章对应 check0.pdf 的内容
3 Network by hand
3.1 Fetch a Web page
这个介绍怎么发送一个 GET 请求
telent cs144.keithw.org http该命令告诉 telnet 打开一个可靠字节流(reliable byte stream),并在我电脑上运行一个 http 服务
预计会收到这样的内容
TEXTTrying 104.196.238.229... Connected to cs144.keithw.org. Escape character is '^]'. ^] telnet> close Connection closed.该命令不能使用常见的
Ctrl-C之类方法退出,而是Ctrl-]然后输入 closeGET /hello HTTP/1.1该命令告诉服务器 URL 的 path 部分
Host: cs144.keithw.org该命令告诉服务器 URL 的 host 部分
Connection: close该命令告诉服务器这是最后一个 HTTP 请求,服务器在发送完最后一字节的响应数据后应主动关闭 TCP 连接
再按一次回车,会发送一个空行给服务器,表明发送的 HTTP 请求完成了,这会看到和浏览器一样的响应内容 这不是关闭连接,它只是 HTTP 请求报文语法的结束符
TEXTHTTP/1.1 200 OK Date: Tue, 09 Dec 2025 07:32:01 GMT Server: Apache Last-Modified: Thu, 13 Dec 2018 15:45:29 GMT ETag: "e-57ce93446cb64" Accept-Ranges: bytes Content-Length: 14 Connection: close Content-Type: text/plain Hello, CS144! Connection closed by foreign host.Connection: close属于 HTTP 语义层,告诉服务器:“响应完就关闭连接”空行 CRLF 属于 HTTP 报文语法,告诉服务器:“我的请求已经完整,可以开始处理”
3.2 Send yourself an email
这节介绍怎么通过 SMTP 协议发送邮件
首先看课程文档里面的步骤:
ssh sunetid@cardinal.stanford.edu 这里要用 Stanford 的学生 id 去登陆
telnet 67.231.149.169 smtp使用 SMTP (Simple Mail Transfer Protocol) 服务连接到服务器,应该会显示
TEXTTrying 67.231.149.169... Connected to 67.231.149.169. Escape character is '^]'. 220 mx0a-00000d07.pphosted.com ESMTP mfa-m0342459HELO mycomputer.stanford.edu该命令会建立会话,一般后是个域名或者
[IP]这种形式,也可以自定义字段,但可能会因此被归类为垃圾邮件实际上现代的 SMTP 一般都支持
EHLO扩展命令,这里就不详细说了会返回类似下面内容
TEXT250 ... Hello cardinal3.stanford.edu [171.67.24.75], pleased to meet youMAIL FROM: sunetid@stanford.edu这里是明确发送方的 email,由于早期互联网是一个信任网络,因此这里可以随便写
但现在一般服务器都会去检测,如果是乱写的会被拒绝
返回如下
TEXT250 2.1.0 Sender okRCPT TO: sunetid@stanford.edu这里指定接收人的地址,可以直接指定自己,这样就是自己给自己发邮件
也可以指定其他任意地址,不过要注意可能会被当成垃圾邮件
返回内容如下
TEXT250 2.1.5 Recipient ok.DATA输入该命令后,下面就是信封的文本内容了,如果正常会看到下面输出:
TEXT354 End data with <CR><LF>.<CR><LF>现在需要先写好发件人、收件人、主题
TEXTFrom: sunetid@stanford.edu To: sunetid@stanford.edu Subject: Hello from CS144 Lab 0! This is an email sent via SMTP manually! ... .这些内容是给人类看的,当然可以随便写,但有些比较严格的服务器会检测这些字段内容。 如果和上面的
MAIL FROM: / RCPT TO:不一致,可能会被拒收。 我试了下,qq 邮件会拒收,并会收到拒收码;而网易 126 邮箱就可以随便写。TEXT抱歉,您的信件被退回 尊敬的用户 **@126.com: 您发送到 **@qq.com 的邮件被系统退回,以下是退信相关信息: 因信头from字段拒收邮件 建议解决方案:建议您检查邮件信头是否包含from字段,from字段中的邮箱地址是否正确,是否存在多个from字段等问题,并进行相应调整。您也可以尝试使用网页版邮箱或邮箱大师发送邮件。如果仍然投递失败,请联系收件方协助调整,或通过网易邮件帮助中心进行反馈问题。 ------------------------------- 原邮件信息 ------------------------------- 发送时间:2025-12-09 22:08:36 邮件主题:CS144 - Test Message 收件地址:**@qq.com 服务器返回码: SMTP error, DOT: Host qq.com(157.255.221.253) DOT said 550 The "From" header is missing or invalid. Please follow RFC5322, RFC2047, RFC822 standard protocol. [MPaFZkPlT3GZDlVMiasbtU2tOYMzMMEOLjt1R3UrOzI24oWss6B56APQJEAznwCF9Q== IP: ***.***.***.*]. https://service.mail.qq.com/detail/124/995.在 Subject 后面的邮件正文,必须要隔一行空格才行,否则解析会出错,最后邮件看不到正文信息。
.单行的点代表结束,会收到下面类似返回
TEXT250 2.0.0 33h24dpdsr-1 Message accepted for deliveryQUIT该命令结束邮件服务器的会话,一般邮件会加入队列,过一会儿才会收到。
好了,上面介绍的是课程文档里面写的,但由于没有 sunetid,也就无法 ssh 到 Standard 去完成该课程实验,但是可以可以通过 126 邮箱完成。
注释一个 126 邮箱,去设置里面找到 POP3/SMTP/IMAP 相关项,把服务给打开,然后弄个授权码,等下可以使用这个连接到 126 服务器(QQ 不行)。
得到授权码后,要对邮件和授权吗进行编码
% echo -n "xxx@126.com" | base64
a********************=
% echo -n "授权码" | base64
T*********************==由于现实中都使用 https,因此无法直接使用 telnet 连接,这里使用 openssl
openssl s_client -connect smtp.126.com:465 -quiet然后通过命令 HELO 或 EHLO 后面加任意内容建立连接
这里使用上面生成的编码进行验证:
AUTH LOGIN输入验证登陆命令后,复制上面 邮件编码 回车,再复制 授权码编码 回车,这样就登陆成功了,接下来和前面的内容就一样了!
下面简单复制粘贴一个示例参考一下
% openssl s_client -connect smtp.126.com:465 -quiet
Connecting to 220.197.33.216
depth=2 C=US, O=DigiCert Inc, OU=www.digicert.com, CN=DigiCert Global Root G2
verify return:1
depth=1 C=US, O=DigiCert, Inc., CN=GeoTrust G2 TLS CN RSA4096 SHA256 2022 CA1
verify return:1
depth=0 C=CN, ST=Zhejiang, L=Hangzhou, O=NetEase (Hangzhou) Network Co., Ltd, CN=*.126.com
verify return:1
220 126.com Anti-spam GT for Coremail System (126com[20140526])
HELO macbook.sx
250 OK
AUTH LOGIN
334 dXNlcm5hbWU6
a**********************=
334 UGFzc3dvcmQ6
T*********************==
235 Authentication successful
MAIL FROM: <**@126.com>
250 Mail OK
MAIL TO: <**@126.com>
503 bad sequence of commands
RCPT TO: <**@126.com>
250 Mail OK
DATA
354 End data with <CR><LF>.<CR><LF>
From: **@126.com
To: **@126.com
Subject: CS144 Test
This is an email sent via SMTP manually!
.
250 Mail OK queued as gzga-smtp-mtada-g1-1,_____wD3f1hH6DdpREvXAQ--.30649S2 1765271741
QUIT
221 Bye
00488F0302000000:error:0A000126:SSL routines::unexpected eof while reading:ssl/record/rec_layer_s3.c:701:
00488F0302000000:error:0A000197:SSL routines:SSL_shutdown:shutdown while in init:ssl/ssl_lib.c:2834:3.3 Listening and Connecting
之前的实验都是通过 telnet 运行的 client 程序,下面来运行一个 server 程序
netcat -v -l -p 9090然后再开一个窗口,连接到该服务器
telnet localhost 9090应该会看到 server 输出 Connection fromlocalhost 53500 received! 这样的内容,在两个窗口里输出任何内容回车,会立刻显示在两个窗口中,在 netcat 窗口按 Ctrl-C 会使两个窗口都被关闭。
4 Writing a network program using an OS system socket
现代 C++ 编程规则:
- 使用文档 https://en.cppreference.com,cplusplus.com 有点过时了
- 永远不要使用
malloc()或free() - 永远不要使用
new或delete - 基本上永远不要使用原始指针
*,只在需要的时候使用智能指针 smart pointersunique_ptr或shared_ptr(本课程用不上) - 避免 templates、threads、locks 和 virtual functions(本课程不会使用)
- 避免 C 风格字符串
char *str或字符串函数strlen(),strcpy(),这些很容易出错,应该使用std::string。 - 永远不要使用 C 风格的类型转换,例如
(FILE *) x,使用 C++static_cast - 使用常量引用传递函数参数,例如
const Address & address - 每个变量都使用 const,除非要修改它
- 每个对象方法都使用 const,除非要修改它
- 避免使用全局变量,并给每个变量尽可能小的作用域
- 提交之前,运行
cmake --build build --target tidy来获取改进建议,并运行cmake --build build --target format来格式化代码
关于使用 Git:
高频率的最小化提交,并解释修改了什么,以及为什么。
进行小规模的“语义化”提交有助于调试(如果每次提交都能编译通过,并且提交信息清晰地描述了该次提交所做的一件事,那么调试起来会容易得多)
为了支持这种编程风格,Minnow 的类将操作系统函数(可通过 C 语言调用)封装在“现代” C++ 中。 并提供了针对 CS 111 课程中可能已广泛熟悉的概念(尤其是套接字和文件描述符)的 C++ 封装类。
请仔细阅读公共接口部分(位于文件 util/socket.hh 和 util/file_descriptor.hh 中 public: 之后的内容)。
(请注意,Socket 是 FileDescriptor 的一种类型,而 TCPSocket 是 Socket 的一种类型。)
4.5 Writing webget
现在要实现 webget 程序,一个利用操作系统 TCP 支持及流套接字抽象,来获取网页的程序,就像上那样。
在
bulid目录下,打开文件../apps/webget.cc在
get_URL函数中,安装文本描述实现简易 Web 客户端,使用前面一样的 HTTP 请求格式。使用TCPSocket和Address类。提示:
注意在 HTTP 每行必须以\r\n结尾不要忘记在请求中包含
Connection: close,这告诉服务器,在这个请求后不应该继续等待更多信息的发送。
相反,服务器将发送一个回复,然后立即结束其输出字节流。
当读取完服务器发送的整个字节流后,套接字会达到 “EOF” 文件结束状态,这意味着传入的字节流已经终止。
这样客户段就能知道服务器已经完成了回复。确保 print 所有来自服务器的输出,直到 sockets 遇到 “EOF” (End Of File),一次读取调用是不够的。
期望编写 8 行左右代码
使用
cmake --build build编译代码使用
./apps/webget cs144.keithw.org /hello测试程序。
对比浏览器中得到的结果,和上面 3.1 节中得到的结果当系统运行正常时,执行
cmake --build biuld --target check webget命令来运行自动化测试。
在实现get_URL函数之前,应该会看到以下输出:SHELL$ cmake --build build --target check_webget Test project /home/cs144/minnow/build Start 1: compile with bug-checkers 1/2 Test #1: compile with bug-checkers ........ Passed 1.02 sec Start 2: t_webget 2/2 Test #2: t_webget .........................***Failed 0.01 sec DEBUG: Function called: get_URL( "cs144.keithw.org", "/nph-hasher/xyzzy" ) DEBUG: get_URL() function not yet implemented ERROR: webget returned output that did not match the test's expectations实现后会看到
SHELL$ cmake --build build --target check_webget Test project /home/cs144/minnow/build Start 1: compile with bug-checkers 1/2 Test #1: compile with bug-checkers ........ Passed 1.09 sec Start 2: t_webget 2/2 Test #2: t_webget ......................... Passed 0.72 sec 100% tests passed, 0 tests failed out of 2
我的实现:
void get_URL( const string& host, const string& path )
{
// 创建 socket
TCPSocket sock;
// 连接服务器
sock.connect( Address( host, "http" ) );
// 发送数据
sock.write( "GET " + path + " HTTP/1.1\r\n" ); // GET: path HTTP/1.1
sock.write( "Host: " + host + "\r\n" ); // Host: host
sock.write( "Connection: close\r\n\r\n" );
// 关闭写端
sock.shutdown( SHUT_WR );
// 读取返回数据
string data;
// 判断是结束
while ( !sock.eof() ) {
// 存储读取内容到 data
sock.read( data );
cout << data;
}
}到 build/ 目录下运行测试命令 ./apps/webget cs144.keithw.org /hello 返回内容:
HTTP/1.1 200 OK
Date: Sat, 10 Jan 2026 16:56:46 GMT
Server: Apache
Last-Modified: Thu, 13 Dec 2018 15:45:29 GMT
ETag: "e-57ce93446cb64"
Accept-Ranges: bytes
Content-Length: 14
Connection: close
Content-Type: text/plain
Hello, CS144!对比之前使用 telnet 的返回内容:
HTTP/1.1 200 OK
Date: Sat, 10 Jan 2026 16:00:48 GMT
Server: Apache
Last-Modified: Thu, 13 Dec 2018 15:45:29 GMT
ETag: "e-57ce93446cb64"
Accept-Ranges: bytes
Content-Length: 14
Connection: close
Content-Type: text/plain
Hello, CS144!
Connection closed by foreign host.运行测试成功通过!
5 An in-memory reliable byte stream
| 类/接口 | 角色/关系 | 说明 |
|---|---|---|
ByteStream | 基类 | 提供共享缓冲区 |
Writer | 继承自 ByteStream | 写端,负责写入数据 |
Reader | 继承自 ByteStream | 读端,负责读出数据 |
关键设计:Writer 和 Reader 都继承自 ByteStream,共享同一个缓冲区。所有成员变量应定义在 ByteStream 类的 protected 部分。
- Writer 端(写入)
| 方法 | 功能 |
|---|---|
push(data) | 写入数据(不超过可用容量) |
close() | 关闭流 |
is_closed() | 流是否已关闭 |
available_capacity() | 还能写多少字节 |
bytes_pushed() | 累计写入了多少字节 |
- Reader 端(读出)
| 方法 | 功能 |
|---|---|
peek() | 查看缓冲区内容(不移除) |
pop(len) | 移除前 len 个字节 |
is_finished() | 流是否结束(已关闭且读完) |
bytes_buffered() | 缓冲区当前有多少字节 |
bytes_popped() | 累计读出了多少字节 |
类实现代码:
这里使用 string 作为数据流存储
// byte_stream.hh
class ByteStream
{
public:
explicit ByteStream( uint64_t capacity );
// Helper functions (provided) to access the ByteStream's Reader and Writer interfaces
Reader& reader();
const Reader& reader() const;
Writer& writer();
const Writer& writer() const;
void set_error() { error_ = true; }; // Signal that the stream suffered an error.
bool has_error() const { return error_; }; // Has the stream had an error?
protected:
// Please add any additional state to the ByteStream here, and not to the Writer and Reader interfaces.
uint64_t capacity_;
bool error_ {};
bool is_closed_ { false }; // 流刚创建时为未关闭状态
std::string buffer_ {}; // 使用字符串存储数据流
uint64_t bytes_pushed_ {};
uint64_t bytes_popped_ {};
};注意流刚创建状态为为关闭。
具体方法实现代码
- Writer
ByteStream::ByteStream( uint64_t capacity ) : capacity_( capacity ) {}
// Push data to stream, but only as much as available capacity allows.
void Writer::push( string data )
{
const uint64_t available = capacity_ - buffer_.size();
// 缓存满了
if ( available == 0 ) {
return;
}
// 对比 剩余空间 和 数据大小
const uint64_t to_write = min( available, data.size() );
// 阶段字符串
data.resize( to_write );
// 拼接缓存结果
buffer_ += data;
// 累计写入字节量
bytes_pushed_ += to_write;
}
// Signal that the stream has reached its ending. Nothing more will be written.
void Writer::close()
{
is_closed_ = true;
}
// Has the stream been closed?
bool Writer::is_closed() const
{
return is_closed_;
}
// How many bytes can be pushed to the stream right now?
uint64_t Writer::available_capacity() const
{
return capacity_ - buffer_.size();
}
// Total number of bytes cumulatively pushed to the stream
uint64_t Writer::bytes_pushed() const
{
return bytes_pushed_;
}- Reader
// Peek at the next bytes in the buffer -- ideally as many as possible.
// It's not required to return a string_view of the *whole* buffer, but
// if the peeked string_view is only one byte at a time, it will probably force
// the caller to do a lot of extra work.
string_view Reader::peek() const
{
return buffer_;
}
// Remove `len` bytes from the buffer.
void Reader::pop( uint64_t len )
{
const uint64_t popped = min( len, buffer_.size() );
buffer_.erase( 0, popped );
bytes_popped_ += popped;
}
// Is the stream finished (closed and fully popped)?
bool Reader::is_finished() const
{
return is_closed_ && buffer_.empty();
}
// Number of bytes currently buffered (pushed and not popped)
uint64_t Reader::bytes_buffered() const
{
return buffer_.size();
}
// Total number of bytes cumulatively popped from stream
uint64_t Reader::bytes_popped() const
{
return bytes_popped_;
}注意使用的变量尽量使用 const,除非要修改它。