CS144 计算机网络 Lab4:TCP Connection
前言
经过前面几个实验的铺垫,终于到了将他们组合起来的时候了。Lab4 将实现 TCP Connection 功能,内部含有 TCPReceiver
和 TCPSender
,可以与 TCP 连接的另一个端点进行数据交换。
实验要求
简单来说,这次实验就是要在 TCPConnection
类中实现下图所示的有限状态机:
这些状态对应 TCPState
的内部枚举类 State
:
//! \brief Official state names from the [TCP](\ref rfc::rfc793) specification
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
};
除了三次握手和四次挥手外,我们还得处理报文段首部 RST
标志被置位的情况,这时候应该将断开连接,并将内部的输入流和输入流标记为 error
,此时的 TCPState
应该是 RESET
。
代码实现
先在类声明里面加上一些成员:
class TCPConnection {
private:
TCPConfig _cfg;
TCPReceiver _receiver{_cfg.recv_capacity};
TCPSender _sender{_cfg.send_capacity, _cfg.rt_timeout, _cfg.fixed_isn};
//! outbound queue of segments that the TCPConnection wants sent
std::queue<TCPSegment> _segments_out{};
//! Should the TCPConnection stay active (and keep ACKing)
//! for 10 * _cfg.rt_timeout milliseconds after both streams have ended,
//! in case the remote TCPConnection doesn't know we've received its whole stream?
bool _linger_after_streams_finish{true};
bool _is_active{true};
size_t _last_segment_time{0};
/**
* @brief 发送报文段
* @param fill_window 是否填满发送窗口
*/
void send_segments(bool fill_window = false);
// 发送 RST 报文段
void send_rst_segment();
// 中止连接
void abort();
public:
// 省略其余成员
}
接着实现几个最简单的成员函数:
size_t TCPConnection::remaining_outbound_capacity() const { return _sender.stream_in().remaining_capacity(); }
size_t TCPConnection::bytes_in_flight() const { return _sender.bytes_in_flight(); }
size_t TCPConnection::unassembled_bytes() const { return _receiver.unassembled_bytes(); }
size_t TCPConnection::time_since_last_segment_received() const { return _last_segment_time; }
bool TCPConnection::active() const { return _is_active; }
主动连接
客户端可以调用 TCPConnection::connect
函数发送 SYN
报文段请求与服务端建立连接,由于 Lab3 中实现的 TCPSender::fill_window()
函数会根据发送方的状态选择要发送的报文段类型,在还没建立连接的情况下,这里直接调用 fill_window()
就会将一个 SYN
报文段放在队列中,我们只需将其取出放到 TCPConnection
的 _segments_out
队列中即可:
void TCPConnection::connect() {
// 发送 SYN
send_segments(true);
}
void TCPConnection::send_segments(bool fill_window) {
if (fill_window)
_sender.fill_window();
auto &segments = _sender.segments_out();
while (!segments.empty()) {
auto seg = segments.front();
// 设置 ACK、确认应答号和接收窗口大小
if (_receiver.ackno()) {
seg.header().ackno = _receiver.ackno().value();
seg.header().win = _receiver.window_size();
seg.header().ack = true;
}
_segments_out.push(seg);
segments.pop();
}
}
主动关闭
当上层程序没有更多数据需要发送时,将会调用 TCPConnection::end_input_stream()
结束输入,这时候需要发送 FIN
报文段给服务端,告诉他自己没有更多数据要发送了,但是可以继续接收服务端发来的数据。客户端的状态由 ESTABLISHED
转移到 FIN_WAIT_1
,服务端收到 FIN
之后变成 CLOSE_WAIT
状态,并回复 ACK
给客户端,客户端收到之后接着转移到 FIN_WAIT_2
状态。
如果服务端数据传输完成了,会发送 FIN
报文段给客户端,转移到 LAST_ACK
状态,此时客户端会回复最后一个 ACK
给服务端并进入 TIME_WAIT
超时等待状态,如果这个等待时间内没有收到服务端重传的 FIN
,就说明 ACK
顺利到达了服务端且服务端已经变成 CLOSED
状态了,此时客户端也能断开连接变成 CLOSED
了。
void TCPConnection::end_input_stream() {
// 发送 FIN
_sender.stream_in().end_input();
send_segments(true);
}
在上述情景中,客户端是主动关闭(Active Close)的一方,服务端是被动关闭(Passive Close)的一方。
主动重置连接
有两种情况会导致发送 RST
报文段来主动重置连接:
- 当
TCPSender
超时重传的次数过多时,表明通信链路存在故障; TCPConnect
对象被释放但是 TCP 仍然处于连接状态的时候;
和 Lab3 中类似,TCPConnection
通过外部定期调用 tick()
函数来得知过了多长时间,在 tick()
函数里还得处理超时等待的情况:
//! \param[in] ms_since_last_tick number of milliseconds since the last call to this method
void TCPConnection::tick(const size_t ms_since_last_tick) {
_sender.tick(ms_since_last_tick);
// 重传次数太多时需要断开连接
if (_sender.consecutive_retransmissions() > _cfg.MAX_RETX_ATTEMPTS) {
return send_rst_segment();
}
// 重传数据包
send_segments();
_last_segment_time += ms_since_last_tick;
// TIME_WAIT 超时等待状态转移到 CLOSED 状态
if (TCPState::state_summary(_receiver) == TCPReceiverStateSummary::FIN_RECV &&
TCPState::state_summary(_sender) == TCPSenderStateSummary::FIN_ACKED &&
_last_segment_time >= 10 * _cfg.rt_timeout) {
_linger_after_streams_finish = false;
_is_active = false;
}
}
TCPConnection::~TCPConnection() {
try {
if (active()) {
cerr << "Warning: Unclean shutdown of TCPConnection\n";
// Your code here: need to send a RST segment to the peer
send_rst_segment();
}
} catch (const exception &e) {
std::cerr << "Exception destructing TCP FSM: " << e.what() << std::endl;
}
}
void TCPConnection::send_rst_segment() {
abort();
TCPSegment seg;
seg.header().rst = true;
_segments_out.push(seg);
}
void TCPConnection::abort() {
_is_active = false;
_sender.stream_in().set_error();
_receiver.stream_out().set_error();
}
接收报文段
外部通过 TCPConnection::segment_received()
将接收到的报文段传给它,在这个函数内部,需要将确认应答号和接收窗口大小告诉 TCPSender
,好让他接着填满发送窗口。接着还需要把报文段传给 TCPReceiver
来重组数据,并更新确认应答号和自己的接收窗口大小。然后 TCPSender
需要根据收到的包类型进行状态转移,并决定发送含有有效数据的报文段还是空 ACK
给对方。
为什么即使没有新的数据要发送也要回复一个空 ACK
呢?因为如果不这么做,对方会以为刚刚发的包丢掉了而一直重传。
void TCPConnection::segment_received(const TCPSegment &seg) {
if (!active())
return;
_last_segment_time = 0;
// 是否需要发送空包回复 ACK,比如没有数据的时候收到 SYN/ACK 也要回一个 ACK
bool need_empty_ack = seg.length_in_sequence_space();
auto &header = seg.header();
// 处理 RST 标志位
if (header.rst)
return abort();
// 将包交给发送者
if (header.ack) {
need_empty_ack |= !_sender.ack_received(header.ackno, header.win);
// 队列中已经有数据报文段了就不需要专门的空包回复 ACK
if (!_sender.segments_out().empty())
need_empty_ack = false;
}
// 将包交给接受者
need_empty_ack |= !_receiver.segment_received(seg);
// 被动连接
if (TCPState::state_summary(_receiver) == TCPReceiverStateSummary::SYN_RECV &&
TCPState::state_summary(_sender) == TCPSenderStateSummary::CLOSED)
return connect();
// 被动关闭
if (TCPState::state_summary(_receiver) == TCPReceiverStateSummary::FIN_RECV &&
TCPState::state_summary(_sender) == TCPSenderStateSummary::SYN_ACKED)
_linger_after_streams_finish = false;
// LAST_ACK 状态转移到 CLOSED
if (TCPState::state_summary(_receiver) == TCPReceiverStateSummary::FIN_RECV &&
TCPState::state_summary(_sender) == TCPSenderStateSummary::FIN_ACKED && !_linger_after_streams_finish) {
_is_active = false;
return;
}
if (need_empty_ack && TCPState::state_summary(_receiver) != TCPReceiverStateSummary::LISTEN)
_sender.send_empty_segment();
// 发送其余报文段
send_segments();
}
测试
在终端中输入 make check_lab4
就能运行所有测试用例,测试结果如下:
发现有几个 txrx.sh
的测试用例失败了,但是单独运行这些测试用例却又可以通过,就很奇怪:
接着测试一下吞吐量(请确保构建类型是 Release 而不是 Debug),感觉还行, 0.71Gbit/s,超过了实验指导书要求的 0.1Gbit/s。但是实际上还可以优化一下 ByteStream
类,将内部数据类型换成 BufferList
,这样在写入数据的时候就不用一个字符一个字符插入队列了,可以大大提高效率。
最后将 Lab0 中 webget
使用的 TCPSocket
换成 CS144TCPSocket
,重新编译并运行 webegt
,发现能够正确得到响应结果,说明我们实现的这个 CS144TCPSocket
已经能和别的操作系统实现的 Socket
进行交流了:
后记
至此,CS144 的 TCP 实验部分已全部完成,可以说是比较有挑战性的一次实验了,尤其是 Lab4 部分,各种奇奇怪怪的 bug,编码一晚上,调试时长两天半(约等于一坤天),调试的时候断点还总是失效,最后发现是优化搞的鬼,需要将 etc/cflags.cmake
第 18 行改为 set (CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -ggdb3 -O0")
才行。以上~~
CS144 计算机网络 Lab4:TCP Connection的更多相关文章
- CS144 计算机网络 Lab2:TCP Receiver
前言 Lab1 中我们使用双端队列实现了字节流重组器,可以将无序到达的数据重组为有序的字节流.Lab2 将在此基础上实现 TCP Receiver,在收到报文段之后将数据写入重组器中,并回复发送方. ...
- 计算机网络要点---TCP
计算机网络要点---TCP 浏览器在通过域名通过dns服务器找到你的服务器外网ip,将http请求发送到你的服务器,在tcp3次握手之后(http下面是tcp/ip),通过tcp协议开始传输数据,你的 ...
- TCP connection status
A TCP connection progresses through a series of states during its lifetime. The following diagram il ...
- 计算机网络及TCP/IP知识点(全面,慢慢看)
TCP/IP网络知识点总结 一.总述 1.定义:计算机网络是一些互相连接的.自治的计算机的集合.因特网是网络的网络. 2.分类: 根据作用范围分类: 广域网 WAN (Wide Area Networ ...
- linux上TCP connection timeout的原因查找
linux上TCP connection timeout的原因查找 好久没有写文章了, 今天解决了一个网络连接超时的问题, 记录以备查看. 最近在线上nginx经常出现输出connection tim ...
- TCP Connection Establishment and Termination
Three-Way Handshake The following scenario occurs when a TCP connection is established: The server m ...
- RT:How HTTP use TCP connection
In HTTP/0.9 (not used anymore), each request uses a separate TCP connection, and the end of a respon ...
- 计算机网络 之 TCP协议报文结构
前言:上学期实训课,由于要做一个网络通信的应用,期间遇到各种问题,让我深感计算机网络知识的薄弱.于是上网查找大量的资料,期间偶然发现了roc大神的博客,很喜欢他简明易懂的博文风格.本文受roc的< ...
- RabbitMQ问题解决:TCP connection succeeded but Erlang distribution failed
说明 本来是要先把Hystrix 仪表盘更完的,但是出现了Turbine.Dashboard.RabbitMQ整合实现监控. 所以先在学RabbitMq的基本操作,在安装过程中出现了 E:\Rabbi ...
- tcp connection
三次握手与四次挥手的原因 https://yq.aliyun.com/articles/7435?spm=5176.8091938.0.0.N4v33a linux里的backlog详解 tcp co ...
随机推荐
- JavaScript 字符串和正则相关的方法
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...
- wmware桥接模式
配置思路(桥接模式) 准备一个与PC机同网段且未被占用的IP地址 将虚拟机的网络模式修改为桥接模式(默认为NAT模式) 修改网卡配置文件,配置为准备好的IP地址,并重启网络服务. 配置DNS解析服务器 ...
- Git在使用过程中遇到的一些问题
git默认对文件中的大小写不敏感. 方案1: 通过配置git来达到识别文件大小写的问题.命令如下: git config core.ignorcecase false 缺点:每个仓库都需要修改. 方案 ...
- Python 跳动的小球
一.实验内容:跳动的小球游戏介绍 二.实验对象:<零基础学Python>第13章 Pygame游戏编程 实例01 用以下代码创建一个游戏弹窗: 导入pygame模块并且用init()方法初 ...
- 后端008_配置Security登录授权过滤器
------------恢复内容开始------------ 现在我们就可以去进行springscurity的配置了.首先我们新建一个配置类.然后该类需要添加@Configuration注解,然后还要 ...
- week4题解
1.深度优先搜索 思路:以固定的移动顺序走迷宫,若能到终点则记一次 到终点后回溯到前一个有分岔的地方,走另一条路线 若走到死路也同样回溯到前一个有分叉的地方. 最终遍历所有路线 #include &l ...
- WLAN的二层通信
WLAN的二层通信中,无线接口收发的报文有4个地址: 发送地址(Transimission address),接收地址(Recevie address),源地址(Source address),目的地 ...
- Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Table 'hive.DELETEME1643159643943' doesn't exist
Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Table 'hive.DELETEME1643159643 ...
- 一条随手的Arduino sketch优化 以Examples-02.Digital-Debounce为例
1 const int buttonPin = 2; 2 const int ledPin = 13; 3 4 int ledState = HIGH; 5 int buttonState; 6 in ...
- 前端小白启动开源框架vue-element
开发java的我按耐不住想学前端的冲动不想看培训机构的视频,决定自学遇到那种"前端知识图谱"的知识架构,看一眼就完了,不能拿来做入门用入门就得是先把工作环境搭起来,能出活就ok了 ...