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: getnameinfo: Temporary failure in name resolution
client
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\n Host: " ;
request += host;
request += " \r\n Connection: 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
测试用例
实际上运行了 tests/webget_t.sh
An in-memory reliable byte stream
发现高程 exam 的抄袭行为……
使用一个 deque 模拟即可
请熟悉其中的接口,后面的实验会用到
测试用例
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
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 下标处有一个子串
实验指南里写的很清楚
需要注意,绿色部分的开始下标即为 ByteStream 的 read_bytes,而结束下标即为 ByteStream 的 written_bytes
push_substring 的实现中首先需要通过 capacity 截取子串,并忽略超出 capacity 的部分
对于 unassembled substrings 的处理
在 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_t
和 uint64_t
似乎有所混用
目前全部改为 uint64_t
Lab Checkpoint 2: the TCP receiver
Translating between 64-bit indexes and 32-bit seqnos
测试用例的前缀为 wrapping_integers
可以在 build/Testing/Temporary/LastTest.log
中查看测试的详情
vi Testing/Temporary/LastTest.log
另外 build/tests
中的可执行文件可以直接运行
主要区分几个概念
SYN c a t FIN seq-no (uint32_t) 2^32-2 (ISN) 2^32-1 0 1 2 absolute seq-no (uint64_t) 0 1 2 3 4 stream index (uint64_t) 0 1 2
理解转换过程后,注意编码细节
Implementing the TCP receiver
test
测试用例的前缀为 recv
测试框架为 receiver_harness.hh
,其结构与 fsm_stream_reassembler_harness.hh
类似,不再赘述
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
很容易推出
w i n d o w s i z e = c a p a c i t y − b u f f e r s i z e windowsize = capacity - buffersize w in d o w s i ze = c a p a c i t y − b u ff ers i ze
测试用例中有很多的临界情况,列举如下
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.
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
为零时,视为一,不过需要记录下来,在重传中会有用
通过给出的 ackno
和 window 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
为零的情形
区别 segments_out
和 outstanding_segments
outstanding_segments
为内部数据结构,使用 list,约定从尾部插入,从而保证其中的 TCPSegment 中 seqno 是从小到大的,在重传中会有用
segments_out
的内容会暴露给外界,如测试框架中的 collect_output,会清空 segments_out
中的内容,并转移到测试框架内部的 outbound_segments
中,从而在 ExpectSegment 时进行测试
当新构造出一个 TCPSegment 时,需要同时添加到 outstanding_segments
和 segments_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
注意以下的行为会 fill_window
WriteBytes
AckReceived
Close
TCPSenderTestHarness::ctor
每执行一步都会 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
关闭 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
test
测试单个用例
framework
tcp_expectation_forward.hh
tcp_expectation.hh
tcp_fsm_test_harness.cc
tcp_fsm_test_harness.hh
TCPTestStep
TCPExpectation
TCPAction
SendSegment - segment_received
Write - write
Tick - tick
Connect - connect
Listen - do nothing
Close - end_input_stream
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 ());
注意这里 _flt
为 TestFdAdapter
类的实例
继承了 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;
send_xxx(seqno, ackno) - SendSegment
相当于模拟 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
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 是先关闭连接的一方,理应要等待才对……
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()
了……