本节进行 TCP 协议中发送端 TCPSender 的实现。Sender 拥有一个输入的 ByteStream,代表用户提供的待发送的数据,而 Sender 要负责将其组装成 TCP 数据包并发送出去。所谓发送,就是将数据包 push 到一个队列 _segments_out
中即可,下一节实现的 TCPConnection 类(也就是 Sender 和 Receiver 的所有者)会负责从该队列中取走数据包并实际发出。
除了保证 TCPSegment 中数据、序号以及 SYN,FIN 等标识设置正确,Sender 主要还需要考虑两个问题。一是接收窗口,Receiver 一次性能够接收的数据量是有限的,会通过 Header 中 Window Size 字段向 Sender 动态反馈这一信息。Sender 应保证发送途中(即已发送且未被确认接收)的数据量小于等于该窗口。
二是数据重传,由于数据包在发送途中可能丢失,Sender 在发送一个包后不能立即将其丢弃,而是放在暂存区中(称为 outstanding data),如果一段时间内没有收到 Receiver 的确认信号(通过 ackno)则需要重传。讲义中规定了将真实 TCP 协议略简化后的一系列规则如下:
tick
函数感知时间的经过,该函数的参数为距离上次调用 tick
后经过的毫秒数。tick
函数中触发。tick
中触发时,将没有被完全 ack 的最早的(即序列号最小的)暂存数据包重传。同时,如果窗口大小非0,记录连续重传的次数,该信息将供下节实现的 TCPConnection 用于判断是否要结束连接,并使当前 RTO 翻倍(指数退避)。 然后重新根据当前 RTO(可能刚更新过)启动 Timer。实现上,主要是 Timer + 三个主要函数。参考网上很多同学的代码 Timer 功能是放在了 Sender 类中实现,我这里还是单独做了一个类如下:
// Helper class to determine whether a given timeout has reached (i.e., expired) since started.
// It won't emit any signal but provide accessor for caller to check its state
// Also the class won't call any system time function but rely on its update() called to
// know time elapsed and whether timeout is reached.
class Timer {
public:
// start the timer with given timeout.
// call start() on a started timer acts like reset() called first
void start(unsigned int timeout);
// update the timer with info of time elapsed since start/last update
void update(unsigned int time_elapsed);
// reset(stop) the timer
void reset();
// check whether the timer is active
bool active() const { return _active; }
// check whether the timer has timed out (if the timer is inactive, return value is false)
bool expired() const { return _active && _expired; }
private:
bool _active{false};
bool _expired{false};
unsigned int _current_time{0};
unsigned int _timeout{0};
};
主要函数之一: void ack_received(const WrappingInt32 ackno, const uint16_t window_size)
。窗口大小用一个类的成员 _window_size
记录更新,该参数在后面的发送函数中用到。ackno 经 unwrap 后与暂存区中的数据包序号一一比较,将完全被确认的数据包移除(注意可以利用暂存区中数据包序号是从小到大有序的性质)。如果能够移除,按规则重置 RTO 和重传计数。如果移除后暂存区为空,将 Timer 停止。由于暂存区可能腾出了空位,最后调用 fill_window
(见下文)尝试发送新数据包。代码如下:
void TCPSender::ack_received(const WrappingInt32 ackno, const uint16_t window_size) {
_window_size = window_size;
// use next seqno as checkpoint
uint64_t ack_seqno = unwrap(ackno, _isn, _next_seqno);
// it's impossible that ackno > _next_seqno, because that byte hasn't been sent yet!
if (ack_seqno > _next_seqno) {
return;
}
// remove completely ack-ed segments from the retransmission buffer
// because the segment in retrans buffer is ordered by seqno,
// it's ok to break once current seg can't be erased (subsequent seg has larger seqno)
for (auto it = _retrans_buf.begin(); it != _retrans_buf.end();) {
if (unwrap((*it).header().seqno, _isn, _next_seqno) + (*it).length_in_sequence_space() <= ack_seqno) {
it = _retrans_buf.erase(it);
_retrans_timeout = _initial_retransmission_timeout;
_timer.start(_retrans_timeout);
_consec_retrans_count = 0;
} else
break;
}
// stop the timer if retransmission buffer is clear
if (_retrans_buf.empty())
_timer.reset();
// refill the window
fill_window();
}
主要函数之二:void tick(const size_t ms_since_last_tick)
。如果 Timer 已启动,则根据参数进行时间的更新,然后检查是否触发。如果触发,则按规则进行重传,增加重传计数,使 RTO 翻倍,并重启 Timer。代码如下:
void TCPSender::tick(const size_t ms_since_last_tick) {
if (_timer.active())
_timer.update(ms_since_last_tick);
if (_timer.expired()) {
_segments_out.emplace(_retrans_buf.front());
if (_window_size > 0) {
_consec_retrans_count++;
_retrans_timeout *= 2;
}
_timer.start(_retrans_timeout);
}
}
主要函数之三:void fill_window()
。即开头一笔带过的 “发送数据、序号以及 SYN,FIN 等标识设置正确的数据包”。概括来说其流程就是:判断窗口是否未被填满 → 如果未被填满,造一个 TCPSegment,具体又分为判断放 SIN 标识,放数据,判断放 FIN 标识三步 → 将 TCPSegment 加入到 _segments_out
和暂存区中,更新序列号和剩余空间大小。循环直至窗口被填满。具体细节见下面的源文件,就不粘贴了。
完整代码链接:
tcp_sender.hh
tcp_sender.cc