Getting started
首先是版本控制操作,从lab3分支创建用于lab4开发的分支,我是在Clion的Git图形操作界面设定的。然后按照文档中的的介绍,从远程仓库拉取lab4的实验内容
//在新分支dev-lab4下
git fetch
git merge origin/lab4-startercode
make -j4
会弹出一个询问界面,让你提交此次合并到你本地仓库分支的说明commit,可以不管,直接ctrl+X键。
如果你的receiver和sender设计地足够鲁棒性,将有助于此Lab实现
前面任务完成的越多,现在需要做的越少。文档建议实现整个Lab4的代码函数大概在100~150行。本次实验就是将receiver和sender组合实现TCPConnection,其是一个全双工的协议,意思是作为端点的双方都是发送方和接收方。下图一目了然:
Lab4 : The TCP connection
TCPConnection 需要遵循如下基本规则:
接收数据段 :TCPConnection 将调用 segment_received() 函数从互联网上接收 TCPSegment ,且将查看该segment和报文段标志:
- 如果设置了RST 标志位,则立即将 inbound 和 outbound 流置于 error state,且关闭连接。否则······
- 将segment交给TCPReceiver,以便于检查其关心的数据域:seqno、SYN、payload、FIN
- 如果设置了SYN标志位,则TCPSender只关心segment的数据域是:ackno、window_size
- 如果接收到的TCPsegment包含一个有效的seqno,则TCPConnection必须至少返回一个segment作为告知远程端点此时自己的ackno和window size
- 如果远端发送一个包含无效seqno的segment,则必须回复一个包含无效seqno的包,来确认双方是否有效。同时可以查看彼此的ackno和window size,这称为“keep-alive”机制。实现代码如下:
if(_receiver.ackno().has_value() and (seg.length_in_sequence_sapce()==0)and seg.header().seqno==_receiver.ackno().value()-1){
_sender.send_empty_segment();
}
发送数据段:TCPConnection 将会通过互联网发送 TCPSegment:
- TCPSender 将一个 segment 添加到发送队列中时,TCPConnection 需要设置标志位:seqno、SYN、payload、FIN
- 在发送segment前,TCPConnection 将会向自己 TCPReceiver 取得 ackno 和 wind_size ,并设置 ACK 标志
操作系统需要调用一个 tick() 函数来检测时间,当 TCPConnection 的tick()函数被调用后,它需要:
- 告知 TCPSender 时间流逝,以便于重发没有被确认的包
- 在条件满足情况下关闭TCP连接(处于 TIME_WAIT 状态时)
- 当连续重传次数超过 TCPConfig::MAX_RETX_ATTEMPTS 时,发送 RST包
TCP连接的关闭稍微麻烦一些,需要满足以下几种情况:
- 对于 unclean shutdown :既 TCPConnection 收到或者发出带有 RST 标志的segment,发生此情况,对于 inbound 或 outbound 的ByteStreams都会置于 ** error state**,可能会导致还在传输中的正常包被丢弃,无法接收到/发送完整数据
- 对于 clean shutdown :既双方都确认发送完数据后退出,则比较麻烦,这就是著名的 Two Generals Problem , 证明了很难实现通过不可靠信道交换消息并达成共识。但TCP的设计近乎完美的实现了,就是需要满足 四个前提条件 :
- 输入流(inbound stream)报文必须已经结束且完成组装
- 输出流(outbound stream)报文必须已经结束且全部发送给对方(这里的全部包括 FIN 标志)
- 接收到远程端对自己输出流报文的 结束确认ACK
- 自己的 结束确认ACK 被对方收到
这里的 结束确认ACK 均指对双方 FIN 的ACK
对于前面三个条件的检测是比较容易的,但是对于第四个条件是困难的(TCP协议不会对ack包进行ack),所以有两种情况:
- 主动关闭 : 在输入、输出流都结束后等待一段时间后结束连接。首先要满足前面3个前提条件,再继续等待通常是2倍MSL(Maximum Segment Lifetime),本实验中为10倍 initial retransmission timeout
- 被动关闭 :对方先结束流,则已经满足前提条件1、2、3,即可无需等待结束连接。(自己可以确认对方满足前提1~3,则自己满足条件4,所以可以直接结束,有点绕·········)
个人理解:理论上无法完美满足前提条件4,所以先发送完报文的一方(本地端)发送了FIN,肯定能收到对方(远程端)确认的ACK,远程端已经收到本地端的FIN,且发送了过了ACK,则只需要再发送自己FIN给本地端就可以直接结束了(本地端比你先结束,你最后发FIN表明自己也结束了,发完就可以结束连接了),本地端再接收到远端这个FIN后会回复一个确认ACK过去(但远端已经结束或者因为本身不会确认这种ack包,收不到对应的ack),然后等待2MSL后结束
结合TCP连接的经典图来理解吧:
这就是TCP连接中的三次握手和四次挥手。还有对应此过程的FSM图:
图上箭头连线上的 FIN/ACK,左边只事件,右边指相应动作。注意区分实线箭头和虚线箭头。
Implementation
对于TCPConnection类需要额外添加一些数据成员和函数:
bool connection_active = true;
//! count how much milliseconds since the last TCPSegment was received
size_t _time_since_last_segment_received = 0;
//! RST状态标志,特殊处理,如果此标志为true,则发送RST包
void _set_rst_state(const bool send_rst);
//! 由于全双工,对于发送出的包都需要设置ackno和window size
void _add_ackno_and_window_to_send();
然后就是在对应源文件中实现:
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 _time_since_last_segment_received; }
void TCPConnection::segment_received(const TCPSegment &seg) {
//根据头文件中声明,其接收来自网络的segment
//那么就首先要重新统计一个 _time_since_last_segment_received
//即重新置零
_time_since_last_segment_received = 0;
//定义一个TCPHeader变量来序列化传入的TCPSegment参数
const TCPHeader &header = seg.header();
//对于是否是RST包
//对于RST包,需要传入false参数
if (header.rst) {
_set_rst_state(false);
return;
}
//处理其他状态的包,直接交给TCPReceiver,(鲁棒性很强)
_receiver.segment_received(seg);
//对于特殊ACK包,即不占序列号空间的
//任何序列号有效的包都要ACK,keep-alive机制也需要空ACK包
//设置一个bool标志,省的多次调用 length_in_sequence_space()函数
bool _empty_ack_seg = seg.length_in_sequence_space() > 0;
//对于收到的包需要查看是否设置了ACK标志
//如果为真,则需要TCPSender处理
if (header.ack) {
//调用sender专门处理ack的函数
_sender.ack_received(header.ackno, header.win);
//由于该函数会自己fill_window(),所以不需要再发送空ack包
//_empty_ack_seg变量需要置为false
if (_empty_ack_seg && !_segments_out.empty())
_empty_ack_seg = false;
}
//由closed到Listen状态,对于收到SYN包后处于FSM中SYN RECEIVED状态处理
if (TCPState::state_summary(_receiver) == TCPReceiverStateSummary::SYN_RECV &&
TCPState::state_summary(_sender) == TCPSenderStateSummary::CLOSED) {
//则建立连接
connect();
return;
}
//当接收到先FIN包,则要判断是否为被动关闭,并进入FSM的CLOSE WAIT状态
if (TCPState::state_summary(_receiver) == TCPReceiverStateSummary::FIN_RECV &&
TCPState::state_summary(_sender) == TCPSenderStateSummary::SYN_ACKED) {
//将流结束后等待标志置为false
_linger_after_streams_finish = false;
}
//当被动关闭中完成上面的动作,继续判断是否进入FSM的CLOSE状态
if (!_linger_after_streams_finish && TCPState::state_summary(_receiver) == TCPReceiverStateSummary::FIN_RECV &&
TCPState::state_summary(_sender) == TCPSenderStateSummary::FIN_ACKED) {
connection_active = false;
return;
}
// keep-alive机制判断
if (_receiver.ackno().has_value() && (seg.length_in_sequence_space() == 0) &&
seg.header().seqno == (_receiver.ackno().value() - 1)) {
_empty_ack_seg = true;
}
//对于空ACK包调用send_empty_segment()
if (_empty_ack_seg) {
_sender.send_empty_segment();
}
//对于每一个待发送的包都需要添加上 ackno 和 wind_size 再发送
_add_ackno_and_window_to_send();
}
bool TCPConnection::active() const { return connection_active; }
size_t TCPConnection::write(const string &data) {
size_t ret = _sender.stream_in().write(data);
_sender.fill_window();
_add_ackno_and_window_to_send();
return ret;
}
//! \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) {
_time_since_last_segment_received += ms_since_last_tick;
//调用sender的tick()函数
_sender.tick(ms_since_last_tick);
//处理超过连续重传次数阈值的情况,发送RST包
if (_sender.consecutive_retransmissions() > TCPConfig::MAX_RETX_ATTEMPTS) {
//本地可能还有包在发送队列中,直接清除
while (!_sender.segments_out().empty()) {
_sender.segments_out().pop();
}
_set_rst_state(true);
return;
}
//主动关闭检查,判断是否进入CLOSE状态
if (_linger_after_streams_finish && TCPState::state_summary(_receiver) == TCPReceiverStateSummary::FIN_RECV &&
TCPState::state_summary(_sender) == TCPSenderStateSummary::FIN_ACKED &&
_time_since_last_segment_received >= (10 * _cfg.rt_timeout)) {
connection_active = false;
_linger_after_streams_finish = false;
}
//可能会有新的包发送
_add_ackno_and_window_to_send();
}
void TCPConnection::end_input_stream() {
_sender.stream_in().end_input();
//结束了,可能还需要发送FIN包
_sender.fill_window();
_add_ackno_and_window_to_send();
}
void TCPConnection::connect() {
//建立连接首先要发送SYN包
_sender.fill_window();
_add_ackno_and_window_to_send();
}
TCPConnection::~TCPConnection() {
try {
if (active()) {
cerr << "Warning: Unclean shutdown of TCPConnection\n";
// Your code here: need to send a RST segment to the peer
_set_rst_state(true);
}
} catch (const exception &e) {
std::cerr << "Exception destructing TCP FSM: " << e.what() << std::endl;
}
}
void TCPConnection::_set_rst_state(const bool send_rst) {
if (send_rst) {
TCPSegment seg;
seg.header().seqno = _sender.next_seqno();
seg.header().rst = true;
//由于是RST包,不必要添加ackno和wind_size
_segments_out.emplace(std::move(seg));
}
//将输入/输出流置于error state
_sender.stream_in().set_error();
_receiver.stream_out().set_error();
//将等待和连接状态全部置为false
_linger_after_streams_finish = false;
connection_active = false;
}
void TCPConnection::_add_ackno_and_window_to_send() {
while (!_sender.segments_out().empty()) {
TCPSegment seg = std::move(_sender.segments_out().front());
_sender.segments_out().pop();
if (_receiver.ackno().has_value()) {
seg.header().ack = true;
seg.header().ackno = _receiver.ackno().value();
}
seg.header().win = min(static_cast<size_t>(numeric_limits<uint16_t>::max()), _receiver.window_size());
_segments_out.emplace(std::move(seg));
}
然后接着在build目录下打开terminal:
make -j4
make check_lab4
这个会检测所提供的162个测试案例。结果如下:
然后来测试按照文档要求使用wireshark抓包检测。
注意这个测试,你需要先执行make_lab4检测开启虚拟网卡(虚拟部分代码也很有趣),然后才能在wireshark中看到虚拟网卡
首先在一个ternimal界面中的build目录下:
sudo ./apps/tcp_ipv4 -l 169.254.144.9 9090
此作为server端,然后如果成功,就能看到下图:
接着可以打开wireshark在图形控制界面操作,也可以直接按照文档操作。
//如果没有安装tshark,则需要先安装,可能有点慢
sudo apt install tshark
安装完成后在另外一个terminal的build目录下执行:
sudo tshark -Pw /tmp/debug.raw -i tun144
这个命令会抓取tun144虚拟网卡的数据包记录到debug.raw文件中,你可以用wireshark打开此文件后分析详细内容。
接着你在第三个ternimal中的build目录下继续执行:
sudo ./apps/tcp_ipv4 -d tun145 -a 169.254.145.9 169.254.144.9 9090
此作为client端,成功后你将看到:
然后你在server/client端的ternimal窗口可以互相发送字符消息,结束流操作为 ctrl + D,现在服务器端结束流,然后再到客户端结束流。就能看到如下结果:
最后是性能测试:
实验要求是都超过0.10 Gbit/s,我这算是压线吧。好垃圾的实现呀,有点丢脸,所以去找了优秀的实现方案,等做完整个实验再来优化。