一.基础概念
1.TCPSender在TCPSocket中的地位与作用
Lab0中实现了基于内存模拟的流控制-字节流(ByteStream),底层使用std::deque实现,根据最大容量Capacity进行容量控制。个人理解它相当于应用层的输入输出缓存区,用户依托Socket发送数据,读取数据。都要先将数据推入缓存区中,例如使用TCP发送数据,用户需要先将数据推入ByteStream中,TCPSender根据滑动窗口当前长度从ByteStream中读取一定数量的数据,之后再进行协议封装,最后推入发送队列中。TCPSender就是执行bytestream到Tcpsegment的工具。值得注意的是这个转换过程应该是自动执行的,且存在滑动窗口流量控制与超时重传机制。LAB4中将会实现TCPConnection他包括TCPSender与TCPReceiver。
从图中知道bytestream需要转换为TCPSegment需要添加协议头,添加序列号,SYN,payload以及FIN标志位。根据接收端接收到的ackno与window_size确定滑动窗口长度进行流量控制。故而要实现TCPSender时,可发送的数据范围由接收端给出的滑动窗口长度决定。协议头的标志位由当前TCPSender的状态决定。
2.何为端到端模型
Lab3原文如下:
TCP is a protocol that reliably conveys a pair of flow-controlled byte streams (one in each
direction) over unreliable datagrams. Two parties participate in the TCP connection, and
each party acts as both “sender” (of its own outgoing byte-stream) and “receiver” (of an
incoming byte-stream) at the same time. The two parties are called the “endpoints” of the
connection, or the “peers.
端到端是网络连接。 网络要通信,必须建立连接,不管有多远,中间有多少机器,都必须在两头(源和目的)间建立连接,一旦连接建立起来,就说已经是端到端连接了,即端到端是逻辑链路,这条路可能经过了很复杂的物理路线,但两端主机不管,只认为是有两端的连接,而且一旦通信完成,这个连接就释放了,物理线路可能又被别的应用用来建立连接了。简而言之,端到端的通信只要要处理好对应层内的协议就好了,且两端需要对等,协议需要具备普适性。一切优化更改都需要在其上一层进行,本层只负责本层的事务。我们要保证两个端点的行为一致性。
It’s important to remember that the receiver can be any implementation of a valid TCP receiver—it won’t necessarily be your own TCPReceiver. One of the valuable things about Internet standards is how they establish a common language between endpoints that may otherwise act very differently.
3.TCP协议头格式
TCPSender负责组装的TCPSegment格式见上图,发送端主要关注其中蓝色部分即可。序列号,FIN以及SYN标志与载荷数据。相对应的接收端需要关注红色部分。
3.ARQ
The basic principle is to send whatever the receiver will allow us to send (filling the window), and keep retransmitting until the receiver acknowledges each segment. This is called “automatic repeat request” (ARQ). The sender divides the byte stream up into segments and sends them, as much as the receiver’s window allows.
4.TCP如何知道消失丢失(超时重传机制)
TCPSender在发送消失时,会对已经发送的Segment进行一个缓存备份,我得TCPSender实现中使用了一个FIFO队列进行管理。只有当确认号大于缓存区中Segment的序列号时才进行出队操作。总而言之,我们追踪了滑动窗口发送的每一个Segment的序列号与其内容。通过确认号与序列号以及超时时间RTO来判断数据是否丢失。是否选择重传。
LAB3原文:
需要注意的是如果超时事件发生且窗口长度非0,需要将RTO时间加倍,这是因为在窗长非0时,出现了数据丢失那么当前网络拥塞较为严重,为了避免快速重传导致网络负载加倍,可以降低重传速率。
超时重传定时器的状态在RFC官方文档中有详细说明如下所示:
这里面有几个坑:
1.重传计时器在窗口发送时如果没有启动则会重新启动。这里注意并不是每发送一个数据包就启动一次定时器,对每一个数据包进行计时开销是极其大的。启动定时器是为了对存入重传缓冲区的数据进行计时,如果重传缓冲区空了就要关闭计时器,发送时将数据压入缓冲区,这时如果定时器未启动则启动定时器。
2.当收到新的ACK序号(必须大于之前的ACK序号),会进行定时器更新,具体为清除重传计数,恢复RTO时间。清除缓存区已经确认的数据。
3.如果重传事件发生且窗长大于0,那么必须使RTO加倍。
总结一下定时器有三种状态,分别为启动、停止、超时。只有在新数据推入且定时器未开启的时候会启动定时器,只有在重传缓冲区没有数据时会关闭定时器,超时发生时如果窗长大于0那么RTO必须加倍。立即定时器的三个状态时Lab3最重要的一点。
二.具体实现
1.TCP发送状态转移
要完成Lab3需要很好的理解下图,理解了发送的状态转移有助于在写发送窗口时,理清楚条件设置,如下所示:
CLOSED:此时未发送SYN同步标志,如果在这个状态那么发送SYN
SYN_SENT:此时已经发送SYN但是没有收到ACK这时要做的就是等待
SYN_ACKED1:此时收到ACK可以正常收发数据,根据窗口大小划分数据包,尽可能向发送队列写入数据
SYN_ACKED2:此时发送stream已经到达EOF但是FIN还未发送,需要发送FIN
发送了FIN后,窗口将不在发送新数据,此时应该保证fill_window()函数不做任何事情。具体代码实现如下:
void TCPSender::fill_window()
{
// CLOSED (waiting for stream to begin no SYN sent)
if (next_seqno_absolute() == 0)
{
// send SYN
_send_segment("", true);
return;
}
// SYN_SEND (stream started but nothing acknowledged)
else if (next_seqno_absolute() > 0
&& next_seqno_absolute() == bytes_in_flight())
{
return;
}
size_t cur_window_size = (_window_size == 0) ? 1 : _window_size;
while (cur_window_size > bytes_in_flight())
{
// SYN_ACKED (stream ongoing)
if (next_seqno_absolute() > bytes_in_flight() && !stream_in().eof())
{
// flag for send success
bool success_send = false;
size_t payload_size = min(TCPConfig::MAX_PAYLOAD_SIZE,
cur_window_size - bytes_in_flight());
string payload = move(_stream.read(payload_size));
// stream reached EOF and remaining window size can insert FIN flag
if (stream_in().eof()
&& cur_window_size - bytes_in_flight() - payload.size() > 0)
success_send = _send_segment(move(payload), false, true);
else
success_send = _send_segment(move(payload));
// Nothing to send cause segment length is zore,break.
if (!success_send) break;
}
else if (stream_in().eof())
{
// SYN_ACK (stream ongoing, stream has reached EOF, but FIN flag hasn't been sent yet)
if (next_seqno_absolute() < stream_in().bytes_written() + 2)
{
_send_segment("", false, true);
}
else
// FIN_SENT
break;
}
}
}
2.ack接收部分
最难的发送部分实现完毕这个较为简单
//! \param ackno The remote receiver's ackno (acknowledgment number)
//! \param window_size The remote receiver's advertised window size
void TCPSender::ack_received(const WrappingInt32 ackno, const uint16_t window_size)
{
// update window size
_window_size = window_size;
// get 64-bit absolute ackno
uint64_t abs_ackno = unwrap(ackno, _isn, _last_ackno);
// if something impossible return
if (abs_ackno > next_seqno_absolute()) return;
// if ackno is new ack, check retrans buffer
if (abs_ackno > _last_ackno)
{
// update new 64-bit ackno
_last_ackno = abs_ackno;
while (!_flight_buffer.empty())
{
const TCPSegment &seg = _flight_buffer.front();
if (seg.header().seqno.raw_value() + seg.length_in_sequence_space()
<= ackno.raw_value())
_flight_buffer.pop();
else
break;
}
// update timer setting
_consecutive_retransmissions_count = 0;
_rto = _initial_retransmission_timeout;
if (!_flight_buffer.empty())
_timer.start(_rto);
else
_timer.stop();
}
fill_window();
}
3.重传部分
//! \param[in] ms_since_last_tick the number of milliseconds since the last call to this method
void TCPSender::tick(const size_t ms_since_last_tick)
{
// timer elapse
_timer.tick(ms_since_last_tick);
// if timer out and retrans buffer is not empty
if (_timer.is_expired() && !_flight_buffer.empty())
{
// retrans
_segments_out.push(_flight_buffer.front());
// window size has odd cause of internet's bad status, double RTO
if (_window_size > 0)
{
_rto *= 2;
++_consecutive_retransmissions_count;
//std::cout << _consecutive_retransmissions_count << std::endl;
}
_timer.start(_rto);
}
else if (_flight_buffer.empty())
{
_timer.stop();
}
}
个人复盘请勿传播,引用。