目录
[CS144 TCP协议问题](#CS144 TCP协议问题)
在Lab0-Lab3的基础上实现TCPConnection,其可以等同于TCP协议的一个端点,包含TCPSender和TCPReceiver,基本覆盖了TCP应该有的完整功能。
我说怎么前四个实验难度感觉不是很大,原来是全部堆到这来了。。。
我们要做的工作就是实现TCPSocket的核心组件-TCPConnection。可以看到Lab0实现的ByteStream分别在TCPSender和TCPReceiver内部作为入站流和出战流用来和应用层(socket)交互。此外Lab1实现的排序器作为TCPReceiver的核心组件,用来接收并存储乱序字节,最终Lab2和Lab3实现的TCPReceiver和TCPSender则是TCP Connection用来接收和发送TCP端的核心组件。如下图所示:

在这里我们需要注意入站流和出站流两个概念,入站和出站针对的是应用层而言,这个要和Socket处理网络中的tcp端的方向注意区分。考虑对于应用层而言,入站即进入应用层,所以应该是TCPReceiver中的排好序的可靠字节流ByteStream,即途中的inbound。出站即从应用出来,应该是TCPSender中代发送字节的ByteStream即outbound。
当前这个实验TCPConnection主要作为TCPSender和TCPReceiver的粘合剂,将两者有机的组合并实现TCP的基本功能。如下图所示:

实验手册也很长,我的做法就是根据手册提示,先实现简单函数(能直接调用TCPReceiver/TCPSender中现有的函数),随后不断make check,发现第一个问题,解决第一个问题如此往复,这样最终完成整个测试。
简单函数的实现
刚入手时实现以下函数:
cpp
//返回出站流的容量,直接返回出站流容量即可。
size_t TCPConnection::remaining_outbound_capacity() const {
return _sender.stream_in().remaining_capacity();
}
//已经发送但尚未确认的字节数,即TCPSender中未确认队列中的字节数
size_t TCPConnection::bytes_in_flight() const {
return _sender.bytes_in_flight();
}
//已经收到,但未排序的字节数,即TCPReceiver中未排序字节数
size_t TCPConnection::unassembled_bytes() const {
return _receiver.unassembled_bytes();
}
//自从上次收到tcp段,已经过去的时间,未实现
size_t TCPConnection::time_since_last_segment_received() const {
return _time_since_last_received_counter;
}
其中最后一个函数表示自从上次收到tcp段已经过去的时间,用一个变量表示,每次tick时累计该值,每次收到tcp段后更新为0即可。
connect()函数的实现
随后实现connect函数,该函数用以发送第一个SYN报文,这里由于初始化时TCPSender的窗口大小默认是1,此时只能发送不带数据的SYN报文,所以直接让TCPSender填充窗口,随后将TCPSender的输出队列搬运到TCPConnection的输出队列中(由于此过程经常用到,所以封装成单独函数)。注意搬运过程,要给所有要发送的tcp段加上当前TCPReceiver的ackno和窗口大小。
cpp
//发送一个SYN数据段
//由于TCPSender的初始化时窗口大小只有1,所以直接调用fill window窗口节课。
void TCPConnection::connect() {
//第一步使其填充窗口,由于初始化窗口大小只有1,所以就是发送SYN报文
_sender.fill_window();
//第二步,将TCPSender发送的TCP段转至TCPConnection的发送队列,调用函数即可
push_sender();
}
//将TCPSender发送的TCP段转至TCPConnection的发送队列,并加上必要标识
void TCPConnection::push_sender() {
//sender的发送端不空
while(!_sender.segments_out().empty()) {
//先从TCPSender中取出,再将其从TCPSender中剔除
TCPSegment seg = _sender.segments_out().front();
_sender.segments_out().pop();
//在交给OS前,TCPConnection需要填充TCPSender填充不了的字段,比如ackno和window
//ackno需要有才能赋值
if(_receiver.ackno().has_value()) {
seg.header().ackno = _receiver.ackno().value();
seg.header().ack = true;
}
//window_size是TCPReceiver自身的属性,直接复制即可。
seg.header().win = _receiver.window_size();
_segments_out.push(seg);
}
}
segment_received()函数的实现
发送完第一个SYN报文后,会收到对方回的ACK报文,我们接下来处理segment_received()函数逻辑。
-
首先应该是检查对方是否设置了RST标志,若设置则关闭入站流和出站流(给他们的ByteStream设置set_error即可)。否则往下。
-
之前的time_since_last_segment_received()函数需要统计自从上次收到tcp段经过的时间,所以这里更新该变量的值为0.
-
接下来将TCP段取出给TCPReceiver。
-
这里有个小坑点,如果数据非法我们不能被激活,只有当有效数据来临后我们才能被激活并显示发送消息,所以我这里写了判断当前Receiver是否被激活(通过isn是否有值判断),如果没被激活直接返回,这样不会勿扰后面的操作。
cpp//这里添加一个函数判断当前是否已经激活receiver了 bool active() const {return _isn.has_value() ? true : false;} -
之后如果当前tcp段设置了ACK标志,则应该将其ackno和window_size更新到TCPSender中去。
-
随后,如果对方发来的tcp段占用了序列号(数据、SYN、FIN)根据TCP的可靠性原则,应该显性回复对方。这里的想法是直接让TCPSender填充窗口,看有无数据段能顺带捎上我们的回复信息,如果没有数据段能顺带捎上我们的ackno的话则发送空段,最后显性推一下。
-
根据提示,可能会有"保持活动"的tcp段过来,即发送方不占用任何字节,且发送方序列号=ackno-1即已经发送过的最大字节,对于这种情况,我们只需要发送空段,告知其自身seqno和回复其ackno和window_size即可。
-
最后要加入一点判断,判断当前端点是主动关闭还是被动关闭,这点后面会讲。
最终代码有点冗长,如下:
cpp
//接收一个TCP段
void TCPConnection::segment_received(const TCPSegment &seg) {
//如果设置了重置标记,则将入站和出战设置为错误状态,并终止连接。
if(seg.header().rst) {
//入站流关闭
_receiver.stream_out().set_error();
//出战流关闭
_sender.stream_in().set_error();
return;
}
//否则
//这里先更新一下时间
_time_since_last_received_counter = 0;
//将tcp段给TCPReceiver
_receiver.segment_received(seg);
//这里判断如果是非法数据,应该直接丢弃
if(!_receiver.active()) return;
//如果设置了ACK,同时更新信息到TCPSender
if(seg.header().ack) {
_sender.ack_received(seg.header().ackno, seg.header().win);
}
//如果对方发来的tcp段,占用了序列号,就需要显性回应。
//采取的措施首先是尝试fill window.如果sender没有要发送的数据捎带ackno和当前窗口信息,则发送空段回复
if(seg.length_in_sequence_space() != 0) {
_sender.fill_window();
//判断sender有无数据可以捎带ackno和窗口信息,如果没有发送空段
if(_sender.segments_out().empty()) {
_sender.send_empty_segment();
}
push_sender(); //显性的推一下
}
//收到"保持活动"的tcp段,即发送方不占用任何字节,且当前已经设置了ackno,且发送方序列号=ackno-1
//对方只是想知道当前的seqno和window_size
if(_receiver.ackno().has_value() && seg.length_in_sequence_space() == 0
&& seg.header().seqno == (_receiver.ackno().value() - static_cast<uint32_t>(1))) {
_sender.send_empty_segment(); //先发送一个空的段
push_sender(); //显性推送一下
}
//如果入站流在出战流到达eof前收到结束信号,说明当前是被动关闭放,需要将_linger_after_streams_finish变量置为false
if(_receiver.stream_out().input_ended() && !_sender.stream_in().input_ended()) {
_linger_after_streams_finish = false;
}
}
active()函数的实现
这个函数表示当前连接要需要存续吗?若只需要存续返回true,否则返回false。
这涉及到TCP何时结束的问题,一个就是当网络情况太糟糕,连续重传次数过多时,发送RST报文,主动断开两个端点的连接,一个就是两个端点通信结束,"干净"的结束。
RST的情况比较好判断,我们直接判断当前TCPConnection的入站流和出站流是否关闭即可。接下来判断是否是干净的结束,根据文档中的描述和四次挥手的实际情况,需要在满足两个端点都满足3个先决条件的情况下,主动关闭的一方需要等待一段时间,被动关闭的一方可以直接关闭。具体的先决条件和为什么这样设计详见文档,这里TCPConnection提供了变量帮助我们辨别当前时主动关闭的还是被动关闭的。该变量需要在收到tcp段的函数中判断是主动关闭还是被动的一方,当收到fin数据报后,导致我们入站流关闭,出站流未关闭的是被动的一方,这符合四次挥手中服务器端收到客户端发送的FIN的情况。则具体函数实现如下:
cpp
//TCP连接是否应该存活
//1.如果一旦收到RST或者自己主动发送RST
//2.所有正常流程结束,正常结束。
//否则返回true。
bool TCPConnection::active() const {
//1.首先判断是否接受到RST,不正常断开连接
if(_sender.stream_in().error() || _receiver.stream_out().error()) {
return false;
}
//2.干净的结束
bool prerequisites = _receiver.stream_out().eof() && _receiver.unassembled_bytes() == 0 && //条件1,入站流已经结束且排序器也为空
_sender.stream_in().eof() && _sender.fin_sent() && //条件2 出战流结束且fin报文发送过了
_sender.bytes_in_flight() == 0; //条件3,出战流全部被确认
if(prerequisites) {
//如果自己不是主动关闭的一方,直接关闭即可
if(!_linger_after_streams_finish)
return false;
else { //否则若是主动关闭的一方,需要等待时间。
TCPConfig cfg;
if(time_since_last_segment_received() >= 10 * cfg.rt_timeout)
return false;
else
return true;
}
}
return true;
}
tick()函数的实现
该函数表征的是TCP对时间的流动,逻辑如下:
- 首先累计_time_since_last_received_counter的时间。
- 将时间概念传导给TCPSender,其内部机制有超时重传。
- 如果连续重传次数过多,则说明当前网络堵塞,设置并发送RST报文。这里首先将自身入站流和出站流关闭,随后将原本Sender的输出队列清空(因为已经决定要断开连接了,不需要再发送数据了),在发送空段,在其中设置rst发送。
- 最后将2/3积压的报文推送到TCPConnection的输出队列中去。
最终代码如下所示:
cpp
//! \param[in] ms_since_last_tick number of milliseconds since the last call to this method
//tick函数实现
void TCPConnection::tick(const size_t ms_since_last_tick) {
//累计上次收到received的时间
_time_since_last_received_counter += ms_since_last_tick;
//更新发送起的时间
_sender.tick(ms_since_last_tick);
//如果重传次数过多则终止连接
if(_sender.consecutive_retransmissions() > TCPConfig::MAX_RETX_ATTEMPTS) {
//首先将入站流和出站流关闭
_sender.stream_in().set_error();
_receiver.stream_out().set_error();
//这里直接将sender的输出队列清空,因为不需要了
_sender.clear_sender();
//其次向对方发送带有rst标志的空段
_sender.send_empty_segment();
//这里发送的空段在队尾,设置rst=1
_sender.segments_out().back().header().rst = true;
}
//这里可能导致sender超时重传,所以需要显性的推一下,这里统一推一下
push_sender();
}
剩下函数补全
将以上几个重要函数全部实现以后,剩下简单函数直接补全即可。
cpp
//应用层的数据发送
size_t TCPConnection::write(const string &data) {
//向发送器的可靠字节流中写入
size_t tot_writen=_sender.stream_in().write(data);
_sender.fill_window();
push_sender(); //向前推。
return tot_writen;
}
//关闭入站字节流
void TCPConnection::end_input_stream() {
//首先关闭入站字节流
_sender.stream_in().end_input();
_sender.fill_window(); //填充窗口,将FIN字段送入写方输出字节流
push_sender(); //显性推送
}
TCPConnection::~TCPConnection() {
try {
if (active()) {
cerr << "Warning: Unclean shutdown of TCPConnection\n";
// Your code here: need to send a RST segment to the peer
//这里直接将sender的输出队列清空,因为不需要了
_sender.clear_sender();
//其次向对方发送带有rst标志的空段
_sender.send_empty_segment();
//这里发送的空段在队尾,设置rst=1
_sender.segments_out().back().header().rst = true;
push_sender();
}
} catch (const exception &e) {
std::cerr << "Exception destructing TCP FSM: " << e.what() << std::endl;
}
}
最终测试,只能通过前49个测试,查阅其他人的博客,得知是环境问题在捣鬼。。由于我用的是docker做的,是在没心情在用virtual box再弄一遍了,就直接放着吧。
完结撒花!!!这里就放到49的截图吧。

CS144 TCP协议问题
留坑待填。。