CS 144: Introduction to Computer Networking, Fall 2025

本篇文章对应 check0.pdf 的内容

3 Network by hand

3.1 Fetch a Web page

这个介绍怎么发送一个 GET 请求

  1. telent cs144.keithw.org http

    该命令告诉 telnet 打开一个可靠字节流(reliable byte stream),并在我电脑上运行一个 http 服务

    预计会收到这样的内容

    TEXT
    Trying 104.196.238.229...
    Connected to cs144.keithw.org.
    Escape character is '^]'.
    ^]
    telnet> close
    Connection closed.
    Click to expand and view more

    该命令不能使用常见的 Ctrl-C 之类方法退出,而是 Ctrl-] 然后输入 close

  2. GET /hello HTTP/1.1

    该命令告诉服务器 URL 的 path 部分

  3. Host: cs144.keithw.org

    该命令告诉服务器 URL 的 host 部分

  4. Connection: close

    该命令告诉服务器这是最后一个 HTTP 请求,服务器在发送完最后一字节的响应数据后应主动关闭 TCP 连接

    再按一次回车,会发送一个空行给服务器,表明发送的 HTTP 请求完成了,这会看到和浏览器一样的响应内容 这不是关闭连接,它只是 HTTP 请求报文语法的结束符

    TEXT
    HTTP/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.
    Click to expand and view more

    Connection: close 属于 HTTP 语义层,告诉服务器:“响应完就关闭连接”

    空行 CRLF 属于 HTTP 报文语法,告诉服务器:“我的请求已经完整,可以开始处理”

3.2 Send yourself an email

这节介绍怎么通过 SMTP 协议发送邮件

首先看课程文档里面的步骤:

ssh sunetid@cardinal.stanford.edu 这里要用 Stanford 的学生 id 去登陆

  1. telnet 67.231.149.169 smtp

    使用 SMTP (Simple Mail Transfer Protocol) 服务连接到服务器,应该会显示

    TEXT
    Trying 67.231.149.169...
    Connected to 67.231.149.169.
    Escape character is '^]'.
    220 mx0a-00000d07.pphosted.com ESMTP mfa-m0342459
    Click to expand and view more
  2. HELO mycomputer.stanford.edu

    该命令会建立会话,一般后是个域名或者 [IP] 这种形式,也可以自定义字段,但可能会因此被归类为垃圾邮件

    实际上现代的 SMTP 一般都支持 EHLO 扩展命令,这里就不详细说了

    会返回类似下面内容

    TEXT
    250 ... Hello cardinal3.stanford.edu [171.67.24.75], pleased to meet you
    Click to expand and view more
  3. MAIL FROM: sunetid@stanford.edu

    这里是明确发送方的 email,由于早期互联网是一个信任网络,因此这里可以随便写

    但现在一般服务器都会去检测,如果是乱写的会被拒绝

    返回如下

    TEXT
    250 2.1.0 Sender ok
    Click to expand and view more
  4. RCPT TO: sunetid@stanford.edu

    这里指定接收人的地址,可以直接指定自己,这样就是自己给自己发邮件

    也可以指定其他任意地址,不过要注意可能会被当成垃圾邮件

    返回内容如下

    TEXT
    250 2.1.5 Recipient ok.
    Click to expand and view more
  5. DATA

    输入该命令后,下面就是信封的文本内容了,如果正常会看到下面输出:

    TEXT
    354 End data with <CR><LF>.<CR><LF>
    Click to expand and view more

    现在需要先写好发件人、收件人、主题

    TEXT
    From: sunetid@stanford.edu
    To: sunetid@stanford.edu
    Subject: Hello from CS144 Lab 0!
    
    This is an email sent via SMTP manually! ...
    .
    Click to expand and view more

    这些内容是给人类看的,当然可以随便写,但有些比较严格的服务器会检测这些字段内容。 如果和上面的 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.
    Click to expand and view more

    在 Subject 后面的邮件正文,必须要隔一行空格才行,否则解析会出错,最后邮件看不到正文信息。

  6. .

    单行的点代表结束,会收到下面类似返回

    TEXT
    250 2.0.0 33h24dpdsr-1 Message accepted for delivery
    Click to expand and view more
  7. QUIT

    该命令结束邮件服务器的会话,一般邮件会加入队列,过一会儿才会收到。

好了,上面介绍的是课程文档里面写的,但由于没有 sunetid,也就无法 ssh 到 Standard 去完成该课程实验,但是可以可以通过 126 邮箱完成。

注释一个 126 邮箱,去设置里面找到 POP3/SMTP/IMAP 相关项,把服务给打开,然后弄个授权码,等下可以使用这个连接到 126 服务器(QQ 不行)。

得到授权码后,要对邮件和授权吗进行编码

TEXT
% echo -n "xxx@126.com" | base64
a********************=

% echo -n "授权码" | base64
T*********************==
Click to expand and view more

由于现实中都使用 https,因此无法直接使用 telnet 连接,这里使用 openssl

TEXT
openssl s_client -connect smtp.126.com:465 -quiet
Click to expand and view more

然后通过命令 HELOEHLO 后面加任意内容建立连接

这里使用上面生成的编码进行验证:

TEXT
AUTH LOGIN
Click to expand and view more

输入验证登陆命令后,复制上面 邮件编码 回车,再复制 授权码编码 回车,这样就登陆成功了,接下来和前面的内容就一样了!

下面简单复制粘贴一个示例参考一下

TEXT
% 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:
Click to expand and view more

3.3 Listening and Connecting

之前的实验都是通过 telnet 运行的 client 程序,下面来运行一个 server 程序

TEXT
netcat -v -l -p 9090
Click to expand and view more

然后再开一个窗口,连接到该服务器

TEXT
telnet localhost 9090
Click to expand and view more

应该会看到 server 输出 Connection fromlocalhost 53500 received! 这样的内容,在两个窗口里输出任何内容回车,会立刻显示在两个窗口中,在 netcat 窗口按 Ctrl-C 会使两个窗口都被关闭。

4 Writing a network program using an OS system socket

现代 C++ 编程规则:

关于使用 Git:

高频率的最小化提交,并解释修改了什么,以及为什么。

进行小规模的“语义化”提交有助于调试(如果每次提交都能编译通过,并且提交信息清晰地描述了该次提交所做的一件事,那么调试起来会容易得多)

为了支持这种编程风格,Minnow 的类将操作系统函数(可通过 C 语言调用)封装在“现代” C++ 中。 并提供了针对 CS 111 课程中可能已广泛熟悉的概念(尤其是套接字和文件描述符)的 C++ 封装类。

请仔细阅读公共接口部分(位于文件 util/socket.hhutil/file_descriptor.hhpublic: 之后的内容)。 (请注意,Socket 是 FileDescriptor 的一种类型,而 TCPSocket 是 Socket 的一种类型。)

4.5 Writing webget

现在要实现 webget 程序,一个利用操作系统 TCP 支持及流套接字抽象,来获取网页的程序,就像上那样。

  1. bulid 目录下,打开文件 ../apps/webget.cc

  2. get_URL 函数中,安装文本描述实现简易 Web 客户端,使用前面一样的 HTTP 请求格式。使用 TCPSocketAddress 类。

  3. 提示:
    注意在 HTTP 每行必须以 \r\n 结尾

    不要忘记在请求中包含 Connection: close,这告诉服务器,在这个请求后不应该继续等待更多信息的发送。
    相反,服务器将发送一个回复,然后立即结束其输出字节流。
    当读取完服务器发送的整个字节流后,套接字会达到 “EOF” 文件结束状态,这意味着传入的字节流已经终止。
    这样客户段就能知道服务器已经完成了回复。

    确保 print 所有来自服务器的输出,直到 sockets 遇到 “EOF” (End Of File),一次读取调用是不够的。

    期望编写 8 行左右代码

  4. 使用 cmake --build build 编译代码

  5. 使用 ./apps/webget cs144.keithw.org /hello 测试程序。
    对比浏览器中得到的结果,和上面 3.1 节中得到的结果

  6. 当系统运行正常时,执行 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
    Click to expand and view more

    实现后会看到

    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
    Click to expand and view more

我的实现:

CPP
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;
  }
}
Click to expand and view more

build/ 目录下运行测试命令 ./apps/webget cs144.keithw.org /hello 返回内容:

TEXT
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!
Click to expand and view more

对比之前使用 telnet 的返回内容:

TEXT
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.
Click to expand and view more

运行测试成功通过!

5 An in-memory reliable byte stream

类/接口角色/关系说明
ByteStream基类提供共享缓冲区
Writer继承自 ByteStream写端,负责写入数据
Reader继承自 ByteStream读端,负责读出数据

关键设计WriterReader 都继承自 ByteStream,共享同一个缓冲区。所有成员变量应定义在 ByteStream 类的 protected 部分。

方法功能
push(data)写入数据(不超过可用容量)
close()关闭流
is_closed()流是否已关闭
available_capacity()还能写多少字节
bytes_pushed()累计写入了多少字节
方法功能
peek()查看缓冲区内容(不移除)
pop(len)移除前 len 个字节
is_finished()流是否结束(已关闭且读完)
bytes_buffered()缓冲区当前有多少字节
bytes_popped()累计读出了多少字节

类实现代码:

这里使用 string 作为数据流存储

CPP
// 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_ {};
};
Click to expand and view more

注意流刚创建状态为为关闭。


具体方法实现代码

CPP
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_;
}
Click to expand and view more
CPP
// 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_;
}
Click to expand and view more

注意使用的变量尽量使用 const,除非要修改它。

Start searching

Enter keywords to search articles

↑↓
ESC
⌘K Shortcut