Skip to content

CS 144 攻略

Posted on:2022.03.08

TOC

Open TOC

Info

https://cs144.github.io/

https://zhuanlan.zhihu.com/p/382380361

StanfordCS144 计算机网络

QQ Group 485077457

Setting up VM

https://stanford.edu/class/cs144/vm_howto/vm-howto-byo.html

Lab Checkpoint 0: networking warmup

Networking by hand

vgalaxy@vgalaxy-VirtualBox:~/Desktop$ telnet cs144.keithw.org http
Trying 104.196.238.229...
Connected to cs144.keithw.org.
Escape character is '^]'.
GET /lab0/sunetid HTTP/1.1
Host: cs144.keithw.org
Connection: close
HTTP/1.1 200 OK
Date: Sun, 20 Feb 2022 13:11:17 GMT
Server: Apache
X-You-Said-Your-SunetID-Was: sunetid
X-Your-Code-Is: 477223
Content-length: 111
Vary: Accept-Encoding
Connection: close
Content-Type: text/plain
Hello! You told us that your SUNet ID was "sunetid". Please see the HTTP headers (above) for your secret code.
Connection closed by foreign host.

没有 SUNet ID,会显示 550 5.1.1 User Unknown

server

netcat -v -l -p 9090

netcat: getnameinfo: Temporary failure in name resolution

client

telnet localhost 9090

Writing a network program using an OS stream socket

It’s normally the job of the operating systems on either end of the connection to turn “best-effort datagrams” (the abstraction the Internet provides) into “reliable byte streams” (the abstraction that applications usually want).

In this lab, you will simply use the operating system’s pre-existing support for the Transmission Control Protocol. You’ll write a program called “webget” that creates a TCP stream socket, connects to a Web server, and fetches a page—much as you did earlier in this lab.

仓库地址

git clone https://github.com/cs144/sponge

文档地址

https://cs144.github.io/doc/lab0/

修改 apps/webget.cc 中的 get_URL 函数即可

void get_URL(const string &host, const string &path) {
// Your code here.
Address address(host, "http");
TCPSocket socket;
socket.connect(address);
string request("GET ");
request += path;
request += " HTTP/1.1\r\nHost: ";
request += host;
request += "\r\nConnection: close\r\n\r\n";
socket.write(request);
string res;
socket.read(res);
while (res != "") {
cout << res;
socket.read(res);
}
socket.close();
// You will need to connect to the "http" service on
// the computer whose name is in the "host" string,
// then request the URL path given in the "path" string.
// Then you'll need to print out everything the server sends back,
// (not just one call to read() -- everything) until you reach
// the "eof" (end of file).
// cerr << "Function called: get_URL(" << host << ", " << path << ").\n";
// cerr << "Warning: get_URL() has not been implemented yet.\n";
}

主要是熟悉一下 API

然后在 build 下键入

$ ./apps/webget cs144.keithw.org /hello

测试用例

$ make check_webget

实际上运行了 tests/webget_t.sh

An in-memory reliable byte stream

发现高程 exam 的抄袭行为……

使用一个 deque 模拟即可

请熟悉其中的接口,后面的实验会用到

测试用例

$ make check_lab0

What’s next? Over the next four weeks, you’ll implement a system to provide the same inter-face, no longer in memory, but instead over an unreliable network. This is the Transmission Control Protocol.

Lab Checkpoint 1: stitching substrings into a byte stream

overview

e9c668fd0cda4edab37d353642aaccd1.png

StreamReassembler

TCPReceiver

TCPSender

TCPConnection

build

cmake .. -DCMAKE_BUILD_TYPE=RelASan
cmake .. -DCMAKE_BUILD_TYPE=Debug
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j8

test

测试用例的前缀为 fsm_stream_reassembler

make check_lab1

解读一下测试框架 fsm_stream_reassembler_harness.hh

类之间的关系如下

classDiagram ReassemblerTestStep <|-- ReassemblerAction ReassemblerTestStep <|-- ReassemblerExpectation ReassemblerExpectation <|-- BytesAvailable ReassemblerExpectation <|-- BytesAssembled ReassemblerExpectation <|-- UnassembledBytes ReassemblerExpectation <|-- AtEof ReassemblerExpectation <|-- NotAtEof ReassemblerAction <|-- SubmitSegment class ReassemblerTestStep { + execute(StreamReassembler) } class ReassemblerTestHarness { - StreamReassembler reassembler + execute(ReassemblerTestStep) }

下面是各子类的行为

read buffer_size

bytes_written

unassembled_bytes

eof

not eof

push_substring

note

可以将传输的数据视为一个字符串

push_substring 中的 index 参数表明了在字符串的 index 下标处有一个子串

实验指南里写的很清楚

0e4d7c1b98ce44168673e7a291ba93ce.png

需要注意,绿色部分的开始下标即为 ByteStream 的 read_bytes,而结束下标即为 ByteStream 的 written_bytes

push_substring 的实现中首先需要通过 capacity 截取子串,并忽略超出 capacity 的部分

在 push_substring 中,通过比较 index 和绿色部分的结束下标来进行具体的处理

若 index 比绿色部分的结束下标,代表这是一个 unassembled substring

置入如下的容器中

mutable std::forward_list<std::pair<std::string, uint64_t>> _unassembled_substrings{};

使用 forward_list 是因为 C++11 只支持了 forward_list 的 remove_if

使用 mutable 是因为在计算 unassembled_bytes 时需要对其排序,而 unassembled_bytes 为 const 成员函数

设计似乎不太好

在单独的私有成员函数 push_unassembled_substring 中,对 unassembled_substrings 进行与 push_substring 中几乎相同的处理

需要注意 push_unassembled_substring 的调用位置,还需要预先根据 index 排序,因为可能出现连锁的现象

框架代码对于 size_tuint64_t 似乎有所混用

目前全部改为 uint64_t

Lab Checkpoint 2: the TCP receiver

Translating between 64-bit indexes and 32-bit seqnos

测试用例的前缀为 wrapping_integers

ctest -R wrap

可以在 build/Testing/Temporary/LastTest.log 中查看测试的详情

vi Testing/Temporary/LastTest.log

另外 build/tests 中的可执行文件可以直接运行

主要区分几个概念

SYNcatFIN
seq-no (uint32_t)2^32-2 (ISN)2^32-1012
absolute seq-no (uint64_t)01234
stream index (uint64_t)012

理解转换过程后,注意编码细节

Implementing the TCP receiver

test

测试用例的前缀为 recv

测试框架为 receiver_harness.hh,其结构与 fsm_stream_reassembler_harness.hh 类似,不再赘述

make check_lab2

note

实验指南里是这样写的

In your TCP implementation, you’ll use the index of the last reassembled byte as the checkpoint.

这里的 index 就很值得玩味,如果代表的是 stream index,那么当 reassembled 的部分为空时就无法解释了

所以解读为 absolute seq-no,即使 reassembled 的部分为空,checkpoint 就为 0

注意是在 seq-no 和 absolute seq-no 之间转换

还需要手动在 absolute seq-no 和 stream index 之间转换

目前 TCPHeader 中的 ackno 并没有使用

实验指南的定义如下

the index of the first unassembled byte

也就是绿色部分的结束下标

end_input 后,ackno 定义为 fin + 1,这需要额外注意

实验指南的定义如下

the distance between the first unassembled index and the first unacceptable index

很容易推出

windowsize=capacitybuffersizewindowsize = capacity - buffersize

测试用例中有很多的临界情况,列举如下

payload 为空且设置了 fin
payload 不为空且设置了 syn
payload 给出的 seqno 无效
...

发现框架代码使用了 C++17 的特性

optional/string_view

还发现了一些关键字,如 not/and/or

state

string TCPState::state_summary(const TCPReceiver &receiver) {
if (receiver.stream_out().error()) {
return TCPReceiverStateSummary::ERROR;
} else if (not receiver.ackno().has_value()) {
return TCPReceiverStateSummary::LISTEN;
} else if (receiver.stream_out().input_ended()) {
return TCPReceiverStateSummary::FIN_RECV;
} else {
return TCPReceiverStateSummary::SYN_RECV;
}
}
namespace TCPReceiverStateSummary {
const std::string ERROR = "error (connection was reset)";
const std::string LISTEN = "waiting for SYN: ackno is empty";
const std::string SYN_RECV = "SYN received (ackno exists), and input to stream hasn't ended";
const std::string FIN_RECV = "input to stream has ended";
} // namespace TCPReceiverStateSummary

Lab Checkpoint 3: the TCP sender

review

translate from segments carried in unreliable datagrams to an incoming byte stream

translate from an outgoing byte stream to segments that will become the payloads of unreliable datagrams

TCPSegment

TCP sender writes all of the fields of the TCPSegment that were relevant to the TCPReceiver in Lab 2: namely, the sequence number, the SYN flag, the payload, and the FIN flag.

However, the TCP sender only reads the fields in the segment that are written by the receiver: the ackno and the window size.

0ba107314f1d4f68a8c321faf7d2b169.png

interface

初始时目前剩余的 window_size 为一

ackno 似乎isn

所以可以构造设置了 syn 的 TCPSegment

从 ByteStream 中读入数据

读入的最大长度取决于 MAX_PAYLOAD_SIZE 和目前剩余的 window_size

可能读取到数据,也可能在 stream eof 的时候添加 fin

当读取的长度为零时,需要特殊处理

这一部分的逻辑比较复杂,要小心

A segment is received from the receiver, conveying the new left (= ackno) and right (= ackno + window size) edges of the window.

应忽略无效的 ackno

当给出的 window size 为零时,视为一,不过需要记录下来,在重传中会有用

通过给出的 acknowindow size,逐步扩展 fill_window 中需要用到的目前剩余的 window_size

然后检查 outstanding_segments 中的所有 TCPSegment,若满足

the ackno is greater than all of the sequence numbers in the segment

则删除,并相应减少 bytes_in_flight

通过对两个 WrappingInt32 作差来判断大小,不确定正确性

使用 length_in_sequence_space 获得 segment 的长度

其中的参数意味着从上一次调用该方法到现在,已经过了多少毫秒

相当于一个虚拟的时钟

重传的核心逻辑

未实现

TCPConnection 中可能会用到

note

请仔细阅读实验指南

尤其是 window_size 为零的情形

outstanding_segments 为内部数据结构,使用 list,约定从尾部插入,从而保证其中的 TCPSegment 中 seqno 是从小到大的,在重传中会有用

segments_out 的内容会暴露给外界,如测试框架中的 collect_output,会清空 segments_out 中的内容,并转移到测试框架内部的 outbound_segments 中,从而在 ExpectSegment 时进行测试

当新构造出一个 TCPSegment 时,需要同时添加到 outstanding_segmentssegments_out

而当测试框架进行 AckReceived 时,在 outstanding_segments 中的 TCPSegment 若满足条件,则不需要添加到 segments_out

而重传时会将 seqno 最小的 TCPSegment,也就是 outstanding_segments 的头元素添加到 segments_out

不理解这样的设计

state

string TCPState::state_summary(const TCPSender &sender) {
if (sender.stream_in().error()) {
return TCPSenderStateSummary::ERROR;
} else if (sender.next_seqno_absolute() == 0) {
return TCPSenderStateSummary::CLOSED;
} else if (sender.next_seqno_absolute() == sender.bytes_in_flight()) {
return TCPSenderStateSummary::SYN_SENT;
} else if (not sender.stream_in().eof()) {
return TCPSenderStateSummary::SYN_ACKED;
} else if (sender.next_seqno_absolute() < sender.stream_in().bytes_written() + 2) {
return TCPSenderStateSummary::SYN_ACKED;
} else if (sender.bytes_in_flight()) {
return TCPSenderStateSummary::FIN_SENT;
} else {
return TCPSenderStateSummary::FIN_ACKED;
}
}
namespace TCPSenderStateSummary {
const std::string ERROR = "error (connection was reset)";
const std::string CLOSED = "waiting for stream to begin (no SYN sent)";
const std::string SYN_SENT = "stream started but nothing acknowledged";
const std::string SYN_ACKED = "stream ongoing";
const std::string FIN_SENT = "stream finished (FIN sent) but not fully acknowledged";
const std::string FIN_ACKED = "stream finished and fully acknowledged";
} // namespace TCPSenderStateSummary

test

测试框架为 sender_harness.hh

测试用例的前缀为 send

make check_lab3

注意以下的行为会 fill_window

每执行一步都会 collect_output

Lab Checkpoint 4: the summit (TCP in full)

doc

https://cs144.github.io/doc/lab4/class_t_c_p_connection.html

note

stream

sender - outbound - stream_in

receiver - inbound - stream_out

需要区分命名

segment_received

根据接收到的 seg 的头部信息进行一些处理

最复杂的一个方法

1、若 remote 发送了 RST,则进行如下三件套

inbound_stream().set_error();
outbound_stream().set_error();
_is_active = false;

注意在 state 中,若 _is_active 为 false,则 _linger_after_streams_finish 自动为 false

另外若 local 由于某些原因发送了 RST,除了上面的三件套,还需要 send_rst_segment,即向 remote 发送 RST

某些原因如 unclean shutdown 或重传次数过多

2、重置 TCPConnection 自身的计时器,receiver 接收该 seg

该计时器服务于 time_since_last_segment_received 方法

同时在 tick 方法和 try_clean_shutdown 方法中也会用到

3、当 seg 的 length_in_sequence_space 非零时,给出回应

这里可能 local 为 server,所以有一个特判

注意将 TCPSender 发送 SYN 的代码从 ctor 转移到 fill_window 中

其余情形简单的发送一个 ack 即可,代表已经收到啦

由于 ack 不占用 seqno,所以可以在高层填充 header

TCPHeader header;
header.ack = true;
header.ackno = _receiver.ackno().value();
header.win = _receiver.window_size();
// not consume seqno
header.seqno = _sender.next_seqno();

4、若 remote 发送了 ACK,则将 ackno 和 win 转发给 sender

注意及时 fill_window

当然若 local 为 server 在 listen 时,remote 在发送 syn 之前的 ack 都是耍流氓

5、若 remote 发送了 FIN,关闭 inbound 流

意味着不再从 remote 中读取,但可能 outbound 流还没有结束

此时就需要置 _linger_after_streams_finish 为 false

因为 local 可以确认 remote 已经成功接收了 outbound 流中的数据

请仔细体会

6、实验指南中提及的特殊情形

responding to a keep-alive segment

具体见实验指南

write

fsm_winsize 中测试了这个方法

关键在于写入的数据量不是取决于 window_size,而是取决于 outbound 的剩余容量

即使目前 indow_size 不够,在后来接收到 remote 的 ack 后,就可以调整 window_size 从而 fill_window

tick

增加自身的计时器,并将参数转发给 sender 的计时器

要么因为重传次数过多而发送 RST,要么就 collect_output

end_input_stream

关闭 outbound 流

注意需要 fill_window,从而向 remote 发送 FIN

对应在 receiver 的 segment_received 中根据从 remote 接收到的 fin 关闭 inbound 流

体会一下全双工

connect

主动向 remote 发起连接

fill_window 即可发送 SYN

try_clean_shutdown

直接贴代码吧,两种情形

void try_clean_shutdown() {
if (_linger_after_streams_finish) {
if (
// stream end
inbound_stream().eof() and outbound_stream().eof() and
// timeout
_timer.has_value() and _timer.value() >= 10 * _cfg.rt_timeout) {
_is_active = false;
}
} else {
if (
// the inbound stream has been fully assembled and has ended
inbound_stream().eof() and _receiver.unassembled_bytes() == 0 and
// the outbound stream has been fully acknowledged by the remote peer
outbound_stream().eof() and _sender.bytes_in_flight() == 0) {
_is_active = false;
}
}
}

关键在于若 _linger_after_streams_finish 为 true,代表 local 不能确认 remote 已经成功接收了 outbound 流中的数据,因为 TCP 不保证 ack 的可靠传输

所以在等待一段时间后才关闭连接

其使用场景一般为某个函数的最后,在这个函数中布尔表达式中各参量可能发生变化

collect_output

一个比较 tricky 的方法

思路是对 sender 的 segments_out 中的每一个 seg 添加 ack 信息

而 ack 信息的正确性则取决于 receiver 实现的正确性

惯用的思路如下

...
segment_received();
...
fill_window();
...
collect_output();
...

state

enumeration

enum class State {
LISTEN = 0, //!< Listening for a peer to connect
SYN_RCVD, //!< Got the peer's SYN
SYN_SENT, //!< Sent a SYN to initiate a connection
ESTABLISHED, //!< Three-way handshake complete
CLOSE_WAIT, //!< Remote side has sent a FIN, connection is half-open
LAST_ACK, //!< Local side sent a FIN from CLOSE_WAIT, waiting for ACK
FIN_WAIT_1, //!< Sent a FIN to the remote side, not yet ACK'd
FIN_WAIT_2, //!< Received an ACK for previously-sent FIN
CLOSING, //!< Received a FIN just after we sent one
TIME_WAIT, //!< Both sides have sent FIN and ACK'd, waiting for 2 MSL
CLOSED, //!< A connection that has terminated normally
RESET, //!< A connection that terminated abnormally
};

definition

TCPState::TCPState(const TCPState::State state) {
switch (state) {
case TCPState::State::LISTEN:
_receiver = TCPReceiverStateSummary::LISTEN;
_sender = TCPSenderStateSummary::CLOSED;
break;
case TCPState::State::SYN_RCVD:
_receiver = TCPReceiverStateSummary::SYN_RECV;
_sender = TCPSenderStateSummary::SYN_SENT;
break;
case TCPState::State::SYN_SENT:
_receiver = TCPReceiverStateSummary::LISTEN;
_sender = TCPSenderStateSummary::SYN_SENT;
break;
case TCPState::State::ESTABLISHED:
_receiver = TCPReceiverStateSummary::SYN_RECV;
_sender = TCPSenderStateSummary::SYN_ACKED;
break;
case TCPState::State::CLOSE_WAIT:
_receiver = TCPReceiverStateSummary::FIN_RECV;
_sender = TCPSenderStateSummary::SYN_ACKED;
_linger_after_streams_finish = false;
break;
case TCPState::State::LAST_ACK:
_receiver = TCPReceiverStateSummary::FIN_RECV;
_sender = TCPSenderStateSummary::FIN_SENT;
_linger_after_streams_finish = false;
break;
case TCPState::State::CLOSING:
_receiver = TCPReceiverStateSummary::FIN_RECV;
_sender = TCPSenderStateSummary::FIN_SENT;
break;
case TCPState::State::FIN_WAIT_1:
_receiver = TCPReceiverStateSummary::SYN_RECV;
_sender = TCPSenderStateSummary::FIN_SENT;
break;
case TCPState::State::FIN_WAIT_2:
_receiver = TCPReceiverStateSummary::SYN_RECV;
_sender = TCPSenderStateSummary::FIN_ACKED;
break;
case TCPState::State::TIME_WAIT:
_receiver = TCPReceiverStateSummary::FIN_RECV;
_sender = TCPSenderStateSummary::FIN_ACKED;
break;
case TCPState::State::RESET:
_receiver = TCPReceiverStateSummary::ERROR;
_sender = TCPSenderStateSummary::ERROR;
_linger_after_streams_finish = false;
_active = false;
break;
case TCPState::State::CLOSED:
_receiver = TCPReceiverStateSummary::FIN_RECV;
_sender = TCPSenderStateSummary::FIN_ACKED;
_linger_after_streams_finish = false;
_active = false;
break;
}
}

fsm

谢希仁 yyds

7d95e3d733ca4fa1bfeae0644f8a1151.png

test

make check_lab4

测试单个用例

ctest -R '^test_name$'

framework

tcp_expectation_forward.hh tcp_expectation.hh tcp_fsm_test_harness.cc tcp_fsm_test_harness.hh

TCPTestStep

TCPTestHarness

void TCPTestHarness::execute(const TCPTestStep &step, std::string note) {
try {
step.execute(*this);
while (not _fsm.segments_out().empty()) {
_flt.write(_fsm.segments_out().front());
_fsm.segments_out().pop();
}
_steps_executed.emplace_back(step.to_string());

注意这里 _fltTestFdAdapter 类的实例

继承了 FdAdapterBase 类和 TestFD

其 write 方法在配置了 seg 的其他信息后,通过序列化的方式写入 FD 中

将对象转化为可传输的字节序列

void TestFdAdapter::write(TCPSegment &seg) {
config_segment(seg);
TestFD::write(seg.serialize());
}
TCPSegment TCPTestHarness::expect_seg(const ExpectSegment &expectation, std::string note) {
try {
auto ret = expectation.expect_seg(*this);
_steps_executed.emplace_back(expectation.to_string());
return ret;

相当于模拟 remote 给 local 发送 seg

详见之前的 TCP FSM

非常重要,具有启发意义

local test

fsm 开头,但不是以 fsm_stream 开头

注意有些是 relaxed 版本

real test name

In the test names, “c” means your code is the client (peer that sends the first syn), and “s” means your code is the server.
The letter “u” means it is testing TCP-over-UDP, and “i” is testing TCP-over-IP (TCP/IP). The letter “n” means it is trying to interoperate with Linux’s TCP implementation.
“S” means your code is sending data; “R” means your code is receiving data, and “D” means data is being sent in both directions.
At the end of a test name, a lowercase “l” means there is packet loss on the receiving (incoming segment) direction, and uppercase “L” means there is packet loss on the sending (outgoing segment) direction.

wireshark

precondition

sudo apt install tshark

server 端口总为 9090

./apps/tcp_ipv4 -l 169.254.144.9 9090

captor

sudo tshark -Pw /tmp/debug.raw -i tun144

client

./apps/tcp_ipv4 -d tun145 -a 169.254.145.9 169.254.144.9 9090

下面考虑连接后 server 首先按下 <C-d> 中断连接,然后 client 按下 <C-d> 中断连接

其捕获的 packets 如下

1 0.000000000 169.254.144.1 → 169.254.144.9 TCP 40 15573 → 9090 [SYN] Seq=0 Win=0 Len=0
2 0.000677060 169.254.144.9 → 169.254.144.1 TCP 40 9090 → 15573 [SYN, ACK] Seq=0 Ack=1 Win=64000 Len=0
3 0.001396230 169.254.144.1 → 169.254.144.9 TCP 40 15573 → 9090 [ACK] Seq=1 Ack=1 Win=64000 Len=0
4 6.934169758 169.254.144.9 → 169.254.144.1 TCP 40 9090 → 15573 [FIN, ACK] Seq=1 Ack=1 Win=64000 Len=0
5 6.934802967 169.254.144.1 → 169.254.144.9 TCP 40 15573 → 9090 [ACK] Seq=1 Ack=2 Win=64000 Len=0
6 10.090191484 169.254.144.1 → 169.254.144.9 TCP 40 15573 → 9090 [FIN, ACK] Seq=1 Ack=2 Win=64000 Len=0
7 10.090765796 169.254.144.9 → 169.254.144.1 TCP 40 9090 → 15573 [ACK] Seq=2 Ack=2 Win=64000 Len=0

client 向 server 发送 SYN

server 向 server 发送 SYN 和 ACK 回应

client 再次向 server 发送 ACK 回应,三次握手完成,连接建立

server 按下 <C-d>

server 向 client 发送 FIN,client 发送 ACK 回应

server DEBUG 如下

DEBUG: Outbound stream to 169.254.144.1:23086 finished (1 byte still in flight).
DEBUG: Outbound stream to 169.254.144.1:23086 has been fully acknowledged.

client DEBUG 如下

DEBUG: Inbound stream from 169.254.144.9:9090 finished cleanly.

client 按下 <C-d>

client 向 server 发送 FIN,server 发送 ACK 回应

server DEBUG 如下

DEBUG: Inbound stream from 169.254.144.1:40103 finished cleanly.
DEBUG: Waiting for lingering segments (e.g. retransmissions of FIN) from peer...
DEBUG: Waiting for clean shutdown...

一段时间后中断连接

DEBUG: TCP connection finished cleanly.
done.

client DEBUG 如下

DEBUG: Waiting for clean shutdown... DEBUG: Outbound stream to 169.254.144.9:9090 finished (1 byte still in flight).
DEBUG: Outbound stream to 169.254.144.9:9090 has been fully acknowledged.
DEBUG: TCP connection finished cleanly.
done.

立刻中断连接

行为和实验指南中似乎有点不一致……

不太理解,因为 server 是先关闭连接的一方,理应要等待才对……

performance

build$ ./apps/tcp_benchmark
CPU-limited throughput : 1.31 Gbit/s
CPU-limited throughput with reordering: 1.19 Gbit/s
build$ date
2022年 03月 08日 星期二 09:52:29 CST

webget revisited

测试可以通过,但是

Warning: unclean shutdown of TCPSpongeSocket

原来是忘记注释掉 socket.close() 了……