在Lab2和Lab3实现了TCP的Sender和Receiver,但是在建立TCP连接时,每个TCP实体既是Sender,又是Receiver,因此在Lab4的主要工作就是在一个TCP实体中统筹Receiver和Sender,同时完成建立连接和关闭连接的工作。
发送
在Lab3的Sender中,通过将报文段放入_segments_out
队列,来表示此报文段已经发送给对等实体的Receiver了。
但是其实Lab3中实现的Sender是孤立的,在发送报文时,报文首部只有seqno SYN FIN
,但并无所在的TCP实体的ackno
和win
,即Sender的_segments_out
的报文其实都不完整的。因此TCPConnection将TCP实体中接收方的ackno
和win
一起合并到报文段中,同时创建一个新队列来放置有完整首部的报文段。
void TCPConnection::transform_segments_out(){
while(!_sender.segments_out().empty()){
_sender.segments_out().front().header().ack = _receiver.ackno().has_value();
if (_receiver.ackno().has_value())
_sender.segments_out().front().header().ackno = _receiver.ackno().value();
_sender.segments_out().front().header().win = min(_receiver.window_size(), static_cast<size_t>((1 << 16) - 1));
_segments_out.push(_sender.segments_out().front());
_sender.segments_out().pop();
}
}
接收
- 强制连接关闭
先介绍一下报文段首部的rst
,这是连接reset
的重置标志位,用于强制终止一条TCP连接。当一个TCP报文段被标记为RST时,它表示着TCP连接的一个异常终止。因此TCP实体若接收到一个rst
为1的报文段,就进入关闭连接的状态:将Receiver和Sender的字节流均设error
,字节流无法继续读取和接收。 - 对Receiver和Sender进行相应动作
若1不满足,则说明连接正常。- TCPConnection将接收到的报文段通过Receiver的
segment_received(TCPSegment)
接口给Receiver处理,将报文段按序合并到交付上层的字节流。 - 若报文段的
ack
标志位为1,则说明报文段的ackno
有效,此时需要通过Sender的ack_received(ackno, win)
给Sender处理,进行流量控制。
- TCPConnection将接收到的报文段通过Receiver的
- 确保至少发送1个
ack
报文段
如果收到的报文段占有序列号,即是有用的报文段,那么尽管当前实体的Sender没有需要发送的有效载荷,也不需要设置syn fin
标志位,还是需要发送1条空报文,这个空报文会带有ackno
,以对收到的报文进行确认。 - 发送保活报文段
若TCP两端较长时间没有进行通信,那么服务器端需要向客户端发送1个检测报文段(有特定格式),来检测连接是否仍然进行,那么客户端就需要发送1个空的保活报文段,来表示这个连接还没有断开。 - 判断本地和远程关闭顺序
如果在收到这个报文段之后,Sender的字节流已经结束接收,即远程TCP实体已经将所有数据发送完毕,但Sender的字节流还没有发送完毕,那么远程TCP实体必然会更早结束TCP连接,而本地是更晚结束的那个。因此在本地TCP实体发送完数据后,就可以直接关闭连接,而不需要等待远程实体关闭再关闭。此时可将变量_linger_after_streams_finish
设为false
。 - 将Sender的所有报文段队列添加
ackno
和win
以上六点中,第1点为特殊判断,第2点针对本地TCP实体的Sender和Receiver,第3 4 5点针对远程实体的交互,第6点保证报文段的完整性。
void TCPConnection::segment_received(const TCPSegment &seg) {
_time_since_last_segment_received = 0;
TCPHeader header = seg.header();
//! if the rst flag is set, sets both the inbound and outbound streams to the error state
//! and kills the connection permanently
if(header.rst){
inbound_stream().set_error();
outbound_stream().set_error();
_is_rst_set = true;
return;
}
//! gives the segment to the TCPReceiver
_receiver.segment_received(seg);
//! if the ack flag is set, tells the TCPSender about
//! the fields it cares about on incoming segments: ackno and window size.
if(header.ack){
_sender.ack_received(header.ackno, header.win);
}
//! if the incoming segment occupied any sequence numbers,
//!the TCPConnection makes sure that at least one segment is sent in reply.
if(seg.length_in_sequence_space()){
_sender.fill_window();
if(_sender.segments_out().empty()){
_sender.send_empty_segment();
}
}
//! If the inbound stream ends before the TCPConnection has reached EOF on its outbound stream, this variable needs to be set to false.
if(inbound_stream().input_ended() && !outbound_stream().eof()) _linger_after_streams_finish = false;
//! responding to a “keep-alive” segment.
if (_receiver.ackno().has_value() && (seg.length_in_sequence_space() == 0)
&& header.seqno == _receiver.ackno().value() - 1) {
_sender.send_empty_segment();
}
//! reflect an update in the ackno and window size.
transform_segments_out();
}
关闭
TCP连接的关闭有2种可能,若是异常关闭,即之前收到或发出了1个rst
报文段,则TCP已关闭。否则就是TCP双方的数据已传输完毕,因此需要考虑实体的Sender和Receiver状态:
- 条件1. Receiver的接收字节流已经接收到了所有数据(但远程可能还不知道这件事情)
- 条件2. Sender已经确认将所有从上层应用层接收到的字节流数据封装成的报文段发送给远程TCP实体
在以上2个条件都满足后,只需要再发生一件事情就可以关闭连接:*远程确认1条件,即只要远程确认本地已经接收了远程的所有数据。现在假设本地可能经过的2种状态:
- 状态1:条件1满足,条件2不满足
- 状态2:条件1和2均满足
本地在状态1,fin
报文段必然还未发送,从状态1到状态2的转移过程中,至少会给远程发送1个报文段,而这个报文必然会携带本地给远程的ackno
。只要本地从状态1到状态2,远程必然可以确认1条件(满足*条件)。因此,在segment_received(TCPSegment)
中用_linger_after_streams_finish
标记状态1,将其置为假,这样只要达到过状态1且当前处于状态2且_linger_after_streams_finish
为假,则可立刻关闭连接。
另一种关闭连接的情况是,本地没有达到过状态1,直接到状态2,即本地是更早结束流发送的一方。此时远程的状态会是状态1,未达到状态2,发给远程的FINACK
报文段可能因为网络拥塞等问题丢失,需要等待一段时间,看需不需要重传FINACK
报文段,但经过足够长的时间,远程没有发来任何回应,则可仍为远程已达到状态2,可安心地关闭连接了。
总而言之,早发完数据的TCP实体需要等待,而晚发送完的不需要等待,因为晚结束的知道对方已经结束了,没必要等待对方了。
bool TCPConnection::active() const {
if(_is_rst_set) return false;
//Prereq #1 The inbound stream has been fully assembled and has ended
if(inbound_stream().input_ended() && _receiver.unassembled_bytes() == 0
//Prereq #2 The outbound stream has been ended by the local application and fully sent
&& outbound_stream().eof() && _sender.next_seqno_absolute() == outbound_stream().bytes_written() + 2
//Prereq #3 The outbound stream has been fully acknowledged by the remote peer
&& bytes_in_flight() == 0
// At any point where prerequisites #1 through #3 are satisfied, the connection is “done” (and
// active() should return false) if linger after streams finish is false.
&& (!_linger_after_streams_finish
// Otherwise you need to linger: the connection is only done after enough time (10 × cfg.rt timeout) has
// elapsed since the last segment was received.
|| time_since_last_segment_received() >= 10 * _cfg.rt_timeout)) return false;
return true;
}