写在前面
此次实验是写一个TCP sender。主要的难点在于填充window,接受ack,以及设计定时器实现重传机制。其基本的思想是根据receiver的窗口大小,封装TCP segment,未收到ack确认的数据报作为outstanding segments存储在额外的数据结构中,也属于window中的一部分。一旦接受到ack,即可将相应的数据报从窗口中移出。另外,还需要一个定时器来表明网络中的拥塞情况,一旦定时器超时,则认为相应的数据报丢失,需要进行重传。数据报丢失有两种情况,一是网络拥塞所致,此时需要将定时器的rto时间翻倍;另一种情况是由于receiver的窗口大小已满所致,即发送速率快过接受速率,此时只需要重启定时器即可,不需要再改动rto。
实验部分
一、定时器RTOtimer类
定时器主要用于判定传送的数据报是否丢失需要重传,需要设计一个定时器类,类成员对象和成员函数如下:
- _rto:timer重传所需要的时间
- _start_time:定时器启动的时刻
- _start:表明定时器是否启动
- reset():当_window_size不等于0时,翻倍_rto
- reset(const size_t rto):收到新的ack时,用初始rto重置定时器
- stop():当收到fin的ack时,关闭定时器
- has_expired:根据当前时间判定定时器是否到期了
class RTOtimer{
private:
size_t _rto;
size_t _start_time;
bool _start;
public:
RTOtimer(const size_t rto):_rto(rto),_start_time(0),_start(false){}
void start(size_t time){
_start_time=time;
_start=true;
}
void reset() {_rto=_rto*2;}
void reset(const size_t rto){_rto=rto;}
void stop(){_start=false;}
bool has_expired(size_t time)const{
return _start&&(time-_start_time)>=_rto;
}
};
二、TCPSender
2.1 TCPSender所需的额外的类成员变量
- _bytes_in_flight:用于记录已发送但还未被确认的字节数
- _consecutive_retransmission:当window_size不为0时,连续重传次数
- _outstanding_segments:已发送但还未被确认的数据报
- _time:当前时间
- _window_size:窗口大小,初始化为1,用于发送syn
- _timer:定时器
- _ackno:确认的字节序列
- finish:表明fin是否已经被发送了
class TCPSender {
private:
//! our initial sequence number, the number for our SYN.
WrappingInt32 _isn;
//! outbound queue of segments that the TCPSender wants sent
std::queue<TCPSegment> _segments_out{};
//! retransmission timer for the connection
unsigned int _initial_retransmission_timeout;
//! outgoing stream of bytes that have not yet been sent
ByteStream _stream;
//! the (absolute) sequence number for the next byte to be sent
uint64_t _next_seqno{0};
size_t _bytes_in_flight=0;
size_t _consecutive_retransmission=0;
std::queue<TCPSegment> _outstanding_segments{};
size_t _time=0;
uint16_t _window_size=1;
RTOtimer _timer;
WrappingInt32 _ackno;
bool finish=false;
... ...
2.2 fill_window()
该函数的设计逻辑如下:
- 计算发送空间:_window_size由两部分组成,一部分是outstanding_segments,另一部分是可以用于发送的空间,因此需要根据还未被确认的字节数,即_bytes_in_flight,来计算可以发送的空间大小space。另外,当_window_size为0时,需要将space置为1,因为需要用一个数据报来获得receiver的窗口大小,否则发送方将永远无法发送数据。
- 生成数据报:若还有空间且!finish(因为每次收到ack都会调用fill_window,避免已经收到了fin后再次调用fill_window,再次生成fin),那么可以生产数据报。先判断是否需要生产syn,是产生syn数据报,并启动定时器;否则生成普通的数据报。这里需要注意,若还有space且字节数已经到达了eof,需要生成fin数据报,fin可以是单独占用一个数据报,也可以携带payload,取决于payload是否为0,生成完fin后需要将finish置true。当payload为0时,退出循环,避免生成无效数据。
该函数的代码实现如下:
void TCPSender::fill_window() {
size_t space=_window_size-(next_seqno()-_ackno);
if(_window_size==0) space=1-(next_seqno()-_ackno);
while(space&&!finish){
TCPSegment seg;
//发送syn
if(_next_seqno==0){
seg.header().syn=true;
seg.header().seqno=_isn;
_next_seqno++;
_bytes_in_flight++;
_segments_out.push(seg);
_outstanding_segments.push(seg);
space--;
_timer.start(_time);
continue;
}
//发送数据和fin
size_t payload=stream_in().buffer_size()<space?
stream_in().buffer_size():space;
payload=payload<TCPConfig::MAX_PAYLOAD_SIZE?
payload:TCPConfig::MAX_PAYLOAD_SIZE;
if(payload!=0){
seg.payload()=stream_in().read(payload);
seg.header().seqno=next_seqno();
_bytes_in_flight+=payload;
_next_seqno+=payload;
space-=payload;
}
if(payload==0 && !stream_in().eof()) break;
//若还有空间且到达eof,置fin
if(space && stream_in().eof()){
//如果没有payload,要置header.seqno
if(!payload){
seg.header().seqno=next_seqno();
}
seg.header().fin=true;
space--;
_next_seqno++;
_bytes_in_flight++;
finish=true;
}
//入队
_segments_out.push(seg);
_outstanding_segments.push(seg);
if(payload==0) break;
}
}
2.3 ack_received
该函数接受两个参数,分别是ackno和window_size,具体的实现逻辑如下:
- 更新window_size和ackno:前者直接将window_size赋给_window_size即可。后者需要考虑三种情况。一是当_ackno=_isn,说明此时发送的是syn,网络中可能存在垃圾的数据报,因此若收到的ackno不等于_isn+1,说明收到了垃圾,不再进行处理;二是收到的ackno的绝对序列号小于等于_ackno的绝对序列号,说明收到的ack重复了,直接忽视;三若不属于前两种情况,那么说明收到的ack序列号有效,更新序列号,_bytes_in_flight以及_consecutive_retransmission即可。
- 处理outstanding_segments:当收到的ackno超过了outstanding_segment的最大序列号,那么就要把相应的数据报从队列中pop出来。
- 关闭定时器:如果收到了fin ack,那么需要将定时器关闭。
- 重置定时器:由于收到了新的ack,需要对定时器重置。用初始的rto重置定时器,并启动。
代码实现如下:
void TCPSender::ack_received(const WrappingInt32 ackno, const uint16_t window_size) {
//更新window_size
_window_size=window_size;
//网络中存在垃圾segments
if(_ackno==_isn && ackno!=_ackno+1) return;
//如果收到了重复的ack,那么就不理会
if(unwrap(ackno,_isn,_next_seqno)<=unwrap(_ackno,_isn,_next_seqno)) return;
//更新bytes_in_flight和ackno以及连续重传数
_bytes_in_flight-=ackno-_ackno;
_ackno=ackno;
_consecutive_retransmission=0;
//更新outstanding segments,确认过的出队
while(!_outstanding_segments.empty()){
TCPSegment front=_outstanding_segments.front();
if(static_cast<size_t>(_ackno-
front.header().seqno)>=front.length_in_sequence_space())
_outstanding_segments.pop();
else break;
}
//确认fin
if(finish && _outstanding_segments.empty()){
_timer.stop();
return;
}
//重置定时器
_timer.reset(_initial_retransmission_timeout);
_timer.start(_time);
}
2.4 tick
该函数传入一个参数,表明与上一次调用该函数的时间间隔,函数设计思路如下:
- 根据参数,获得当前的时间
- 判断定时器是否超时,若超时,则重传,若window_size不为0,还需要对重传次数加1并且将定时器的rto值翻倍。重传结束后需要再次启动定时器。
代码实现如下:
void TCPSender::tick(const size_t ms_since_last_tick) {
//更新时间
_time+=ms_since_last_tick;
//超时重传
if(_timer.has_expired(_time)){
_segments_out.push(_outstanding_segments.front());
if(_window_size){
_consecutive_retransmission++;
_timer.reset();
}
_timer.start(_time);
}
}
2.5 其他函数
TCPSender::TCPSender(const size_t capacity, const uint16_t retx_timeout, const std::optional<WrappingInt32> fixed_isn)
: _isn(fixed_isn.value_or(WrappingInt32{random_device()()}))
, _initial_retransmission_timeout{retx_timeout}
, _stream(capacity),_timer(retx_timeout),_ackno(_isn) {}
uint64_t TCPSender::bytes_in_flight() const { return _bytes_in_flight; }
unsigned int TCPSender::consecutive_retransmissions() const { return _consecutive_retransmission; }
void TCPSender::send_empty_segment() {
TCPSegment seg;
seg.header().seqno=next_seqno();
_segments_out.push(seg);
}
2.6 实验结果