在最开始介绍TCP的时候,我们就介绍了TCP的三个特点,分别是面向连接、可靠、字节流式。前面内容我们已经介绍过了TCP的连接管理,接下来的这部分内容将会介绍与TCP可靠性强关联的TCP重传。
很多网络协议都提供了checksum或者CRC手段来检测收到的数据包是否发生错误,但是检测到数据包错误后很多协议都不会进行重传等操作来可靠的修复错误。例如常见的IP和UDP协议完全没有重传,对于链路层的以太网协议,虽然有重传操作但是尝试若干次重传还没有成功会也会放弃(CSMA/CD)
经过N多专家前扑后继的研究,在目前的信息论(information theory)和编码理论(coding theory)中,主要有两种方式用来保证可靠的传输
1、通过传输的数据包中增加冗余的error-correcting codes来修复传出错误的报文,这种方式中接受端在接收到报文的时候,如果报文中有少量的bit传输错误,接收端可以通过冗余数据恢复出正确的数据包。
2、使用ARQ(Automatic Repeat Request)机制来提高数据传输的可靠性,ARQ机制需要发送端重复发送传输错误的数据包直到接收端接收到正确的数据包为止
当前也有把这两种方式结合起来一起使用的协议,比如在LTE通信中,RLC层使用ARQ,MAC层使用HARQ(HARQ就是上面两种方式的综合体,先通过error-correcting codes来尝试修复传输错误的报文,如果修复失败则进行ARQ过程)。我们接下来要讲到的TCP协议则使用ARQ的方式。
来解决丢包和比特错误两类问题最简单的方式就是重新发送出错的数据包,这就需要知道
接收端是否已经接收到对应的数据包。这个可以通过ACK(acknowledgment)来反映接收端接收到数据包的情况。但是这个又带来其他小问题比如发送端应该等待ACK确认包多长时间?如果超过这个时间发送端就认为数据包丢失而重新发送这个数据包。这个时间就叫做RTO(Retransmission Timeout),RTO应该根据环回时间RTT(round-trip-time)来估计。环回时间应该包括三部分:数据包传送过的时间,接收端处理这个数据包并产生ACK的时间,ACK确认包返回的时间。但是RTT这个时间是随着网络状况动态变化的,网络负载较重产生拥塞的时候,RTT就会变大,因此发送端就需要一种方式来动态估计这个RTT时间,这个过程就叫做round-trip-time estimation。这个估计过程是一个统计过程,真实的RTT应该比较接近这个统计平均值。另外一个问题是如果ACK报文丢失怎么办?如果接收端回复的ACK报文丢失,这又可以分为两种场景,一是后面的ACK报文在发送端RTO超时前到达发送端,发送端通过这个ACK报文可以得知之前的报文接收端已经收到。另外一种情况就是RTO超时前,没有收到后续的ACK报文,发送端则可以直接重传没有收到ACK的报文,这样接收端会接收到重复的TCP报文,接收端可以丢弃重复的报文。
接收端接收到的数据包和发送端发送的数据包是否一致。一般来说有两种方式,一种是CRC,另外一种是checksum,在TCP协议中通过checksum机制检查比特错误。当TCP的checksum校验失败的时候,接收端并不会发送ACK给发送端。对于数据完整性要求较高的应用,应该在应用层添加更可靠的校验方式。
当TCP发送端每次发送一个数据包然后等待ACK的时候(即停等式 stop and wait),这种场景下TCP对网络带宽的利用率非常低,因此为了提高带宽利用率,允许TCP在没有收到ACK报文的情况下发送其他数据包。当多个数据包同时在网络中传输的时候,问题会变得更加复杂,比如发送端必须缓存还没有被接收端ACK的报文,当发送端速度低于接收端速度时候发送端需要降低TCP发送速度等等。
TCP主要有两种重传方式,上面我们介绍的是基于定时器的重传(timeout or timer-based retransmission),这种重传方式是发出去的数据在RTO超时后还没有收到对应的ACK就会进行超时重传。另外TCP还有一种基于ACK报文结构顺序的重传,这种重传叫做快速重传(fast retransmission或者fast retransmit),当TCP注意到累计ack(即TCP头中的ack number)不再推进或者接收端通过SACK信息指示发送端接收端存在洞(hole)时候就会触发发送端的重传,通常来说快速重传比超时重传更高效。另外谷歌还对快速重传提出了一种改进的重传机制,即早期重传(ER,Early Retransmit),还记得之前TFO也是谷歌提出来的吧。在这重传子系列内容中我们重点关注TCP如果判断丢包以及重传对应的数据包。至于发送多少数据包则等到我们后面的拥塞控制时候在来讲解。
补充说明:
1、从本章起,wireshark示例中server端的端口为9877,client端的端口为10000。其中client端使用raw socket编程来精确控制TCP的每个报文的,因此不要按照通常的协议要求来看待client的行为,我在/proc下添加了参数tcp_discard_on_port,设置后内核模块可以丢弃指定端口的tcp数据,当把tcp_discard_on_port设置为9877后,server发过来的TCP报文递交给raw socket后,内核TCP模块会直接丢弃这个tcp报文,而不会因为对应的端口没打开而回复RST消息。server端则是linux内核的原始实现,因为server的行为是与linux的实现一致的。我们在查看wireshark抓包图示的时候重点观察server的行为,不要纠结client的行为。