lab3
个人实验留档:cs144-2021
Overview
在之前的实验中,我们已经实现了TCPReceiver以及其内部的模块,在这个实验中,我们将要完成的是上图的TCPSender。
Lab 3: The TCP Sender
TCPsender主要负责:
- 追踪发送方的window size,接收并处理tcp报文中的ackno和window size
- 发送tcp报文,合理设置标记位(SYN、FIN),将从ByteStream中读到的数据填充到报文中的payload中
- 追踪已经发送的报文但是没有被ack的报文
- 超时重传没有被ack的报文
检测超时的报文
当接收到接收方发送来的ack时,只有seqno+payload szie≤ack的数据报文才算是被正确ack。
- 设置超时时间(RTO)
- 一旦有报文被发送,就可以开始计时
- 当已经发送的报文中没有未被ack的报文时,就可以停止计时
- 当计时器超时:
- 重传最早没有被ack的报文
- 将超时时间加倍 → exponential backoff(指数退避)
- 重置计数器
- 记录连续重传次数
- 当已经发送的报文中有数据被ack时:
- 重设RTO到初始值
- 如果已经发送的报文中还存在没有被ack的值,则重启计数器
- 将连续重传次数值为0
Implementing the TCP sender
- 私有变量设置:添加下面的私有变量 选择使用std::queue<std::pair<uint64_t, TCPSegment>>来存储已经发送的报文。
// 超时时间上限
int _timeout;
// 计数器
int _timecounter;
// 追踪发出去的但是还没有收到ack的tcpsegment
std::queue<std::pair<uint64_t, TCPSegment>> _outstanding_queue;
// How many sequence numbers are occupied by segments sent but not yet acknowledged
size_t _bytes_in_flight;
// 对方的window_size
size_t _remote_window_sz;
// 是否发送了含有syn标记位的报文
bool _sent_syn;
// 是否发送了含有fin标记位的报文
bool _sent_fin;
// 连续重传次数
size_t _consecutive_retransmissions_count;
void fill window()
这个函数的主要作用从ByteStream中读取数据、填充tcp报文并发送。 此外,还需要合理设置标记位。 对于窗口大小的处理: 当对方窗口大小为0时,当作对方窗口大小为1; 窗口大小应该是要包括在已经发送的报文中没有被ack的数据报文,也就是说,新发送的数据报文大小+已经发送的报文中数据报文大小不能超过window size
函数实现如下:
void TCPSender::fill_window() {
// 如果已经发送了FIN报文,则不再填充窗口
if (_sent_fin) {
return;
}
// 对方的window size为0时,需要将其看作1
size_t window_sz = _remote_window_sz == 0 ? 1 : _remote_window_sz;
// 填充窗口
while (window_sz > _bytes_in_flight) {
TCPSegment seg;
// 如果没有发送过syn报文,则需要设置SYN标记位。第一个发送的一定是SYN报文
if (!_sent_syn) {
seg.header().syn = true;
_sent_syn = true;
}
seg.header().seqno = next_seqno();
// 将需要发送的数据转入payload
size_t len = min(window_sz - _bytes_in_flight - seg.header().syn,
min(TCPConfig::MAX_PAYLOAD_SIZE, _stream.buffer_size()));
seg.payload() = move(_stream.read(len));
// 如果还有空间且输入结束,则设置fin标志位
if (!_sent_fin && _stream.eof() && window_sz > seg.length_in_sequence_space() + _bytes_in_flight) {
seg.header().fin = true;
_sent_fin = true;
}
// 没有数据需要发送,则跳出填充窗口的过程
if (seg.length_in_sequence_space() == 0) {
break;
}
// 如果没有正在等待的包,则需要重启计数器
// if the timer is not running, start it running
if (_outstanding_queue.empty()) {
_timeout = _initial_retransmission_timeout;
_timecounter = 0;
}
// 发送报文
_segments_out.push(seg);
_outstanding_queue.push(std::make_pair(_next_seqno, seg));
_next_seqno += seg.length_in_sequence_space();
_bytes_in_flight += seg.length_in_sequence_space();
// 如果发送了FIN报文,则跳出循环
if (_sent_fin) {
break;
}
}
}
-
void ack received( const WrappingInt32 ackno, const uint16 t window size)
收到了接收方发来的ack以及window size,则需要:- 记录对方的window size
- 检查已经发送的报文中有没有被ack的报文,如果有,则需要将该报文从已经发送的报文中移除
- 如果有报文被确认了,就可以重置RTO、计数器以及连续重传次数
- 由于对方更新了window size,则最后需要调用 fill_window()发送数据
实现如下:
//! \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) { uint64_t abs_ackno = unwrap(ackno, _isn, _next_seqno); _remote_window_sz = window_size; // 如果接收到的非法的ackno,直接返回 if (abs_ackno > _next_seqno) return; // 否则,查看 outstanding_queue 中还没有被确认的 segment bool reset_flag = false; while(! _outstanding_queue.empty()){ auto pair = _outstanding_queue.front(); // 如果abs_ackno >= abs_seq(pair.first) + length_in_sequence_space(),表明被正确接收 if(abs_ackno >= pair.first + pair.second.length_in_sequence_space()){ _bytes_in_flight -= pair.second.length_in_sequence_space(); _outstanding_queue.pop(); if (! reset_flag){ reset_flag = true; } } else { break; } } // 如果有报文被确认了,就可以重置RTO、计数器以及连续重传次数 if (reset_flag){ _timeout = _initial_retransmission_timeout; _timecounter = 0; _consecutive_retransmissions_count = 0; } fill_window(); }
-
void tick( const size t ms since last tick )
改函数被调用时,其参数为距离上一次该函数被调用过去了多长时间。 这个函数主要负责超时重传以及记录连续重传时间 实现如下://! \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) { // 这个函数会被定时调用 _timecounter += ms_since_last_tick; // 当超时了并且有数据需要重传时 if (_timecounter >= _timeout && !_outstanding_queue.empty()){ auto pair = _outstanding_queue.front(); _segments_out.push(pair.second); if (_remote_window_sz > 0){ _timeout *= 2; } // 记录连续重传次数并且将计数器重置 _consecutive_retransmissions_count ++; _timecounter = 0; } }
-
void send empty segment()
生成并发送一个在seq空间长度为0的数据报文(即不设置标记位以及payload)的报文,主要用途是发送一个空的ack报文。void TCPSender::send_empty_segment() { // 发送一个空的segment TCPSegment seg; seg.header().seqno = next_seqno(); _segments_out.push(seg); }