在前篇文章中介绍了TCP协议的三大特性,其中可靠性是依赖一系列的机制,如:校验和,分组发送,超时重传,流量控制得到保证。
一.数据交互
TCP在交互数据时,采用多种机制保证可靠性,同时也保证TCP的性能,主要是分组、延迟ACK等等。
1.分组确认
对于连续的数据传输有三种方式:
- 单个单个字节发送
- 将整个连续数据发送
- 将整个连续数据拆分成一个个的分组包,然后逐个发送
显然前两种方式都是比较极端,单个单个字节发送对于成块连续数据而言效率非常低,整块连续数据发送对于比较大的数据而言更不现实,TCP缓冲区有限,网络带框也是有限,对于过大数据不可能这样发送。
在TCP协议栈中,有发送缓冲区和接收缓冲区用于缓冲存储即将发送的数据和收到的数据。当应用需要发送连续数据时,TCP将应用的数据存储到缓冲区中,TCP会根据一定的机制将缓冲区数据发送出去,应用同时也将数据写入缓冲区。接收方TCP在收到数据后,存入接收缓冲区中,TCP根据一定机制再将缓冲区中数据提交给应用处理。
TCP将成块的数据发送分成一个个的分组报文发送出去,其中分组报文大小不会超过MSS(Max Segment Size,最大的报文段大小)。TCP对发送的每个字节都采用序号的方式进行标识追踪,序号在建立TCP连接时即已经确定。发送端发送第一个字节数据的序号为建立TCP连接时的SEQ + 1。序号在这里有以下几种作用:
- 序号用于标识追踪发送数据的每个字节
- 接收端根据序号进行ACK确认
- 序号保证数据的有序性,接收端根据序号可以进行排序
标识发送数据的起始序号为TCP报文中的序号,没增加一个字节序号就增加1,所以发送的下个TCP报文的序号为上一个序号加上个报文的大小。通过这种方式TCP可以表示每个已经发送的每个字节。
对成块连续数据分组发送的方式,会带来很多问题,比如发送方如何确认接收方已经收到数据?
TCP采用ACK确认机制解决该问题,接收方在接受到数据后,必须要回复一个ACK的确认包告诉发送方已经收到该报文。其中有个确认序号,标识期待下次接收到的数据的起始序号。这个ACK的确认序号就是TCP报文段中的确认序号。
看到这个机制,对于了解消息MQ读者应该能联想到MQ是如何解决消息丢失的问题。可以说MQ的消息回执确认机制也正式来源于TCP的这个机制。上层很多设计都是来源于底层的设计思想。
通过确认机制可以保证报文丢失时,发送方能够感知到,这一点也是TCP的可靠性保证之一。
从以上图中,可以看到客户端发送的报文都有相应的确认报文用于通知客户端服务器端已经接收到。分组发送,ACK确认机制是TCP可靠性保证的机制之一。
2.延迟的ACK
大多数客户服务器之间的通信都是双方向的数据流动,在这种情况下如果是客户端应用请求TCP发送数据,然后服务端的TCP回复ACK确认,然后服务端TCP再发送应用层的响应值客户端,客户端再发送ACK。这样来回需要四次,无疑增加网络负担,使得网络拥塞。为了提升TCP的性能,尽可能的减小网络负担,延迟ACK策略决定接收端TCP在接手到发送方的数据后,不立即回复ACK,而是经过一个ACK的延迟时间段后再回复ACK。如果这个时间段内有数据需要发送,则放在缓冲区,然后将ACK和这个数据发送作为一个报文段发给发送方。
一般这个延迟的时间是200ms,可以看出,TCP是重复利用网络资源,重复利用TCP数据报,达到最大化的网络传输。
从上图可以看出,每个PSH包都会使捎带ACK的,这样将原本可能需要四次交互减低到只要两次。
3.Nagle算法
前面提到TCP有发送缓冲区,在发送时,应用的数据被内核写入该缓冲区后,TCP再发出去。分组发送时,如果是非常多的微小的分组数据包被不断发出去,则有可能造成网络拥塞,特别是在广域网上。
Nagle算法要求在一个TCP连接上最多只有一个未确认的分组,在该分组的确认包到达之前,发送将不能在发送其他的分组。应用层需要发的数据都被存在缓冲区,待收到确认包后,将缓冲区中的数据一块发送出去。该算法旨在解决大量的微小分组在低速带宽上频繁发送可能造成网络拥塞的问题。
如果更加直接的说,Nagle算法即是将发送的积累沉淀,知道达到一定的条件后再作为大的分组发送,避免大量微小分组造成网络拥塞。这个条件是:
- 如果缓冲区中数据大于MSS
- 禁用Nagle算法或则TCP连接上没有未确认的分组
- 有紧急数据需要发送
满足以上条件之一,将才会发送缓冲区的数据。
4.延迟ACK和Nagle共同作用
Nagle算法旨在解决大量微小分组造成的拥塞问题,但是如果在带宽较大且网络负载不大的局域网上,且应用对延迟非常敏感的时,Nagle算法则不合适。因为应用需要发送的数据被缓冲,未被发出而得到响应,产生延迟。
特别当Nagle算法遇到ACK延迟,两者共同作用时,该情况尤为明显。因为一个TCP连接上只存在一个未确认的分组,且该分组又被ACK延迟。两者共同作用,带来更大的延迟。对于延迟敏感应用则加剧这种情况。
对于大多数局域网内的应用交互,可以通过设置TCP连接套接字,禁用Nagle算法。让一个连接上可以存在多个未确认分组,可以连续发多次分组,从而降低延迟。但是对于在广域网上由于流量较大和RRT较长原因,禁用Nagle算法不仅可能会造成网络拥塞,而且产生的网络延迟可能更严重。
Socket套接字提供应用层Socket参数TCP_NODELAY参数用于开启和关闭Nagle算法。可以通过Java的API了解:
4.ACK的累积
发送端通过Nagle算法累积发送数据,从而避免大量微小分组出现在网络中。从而发送一个较大分组,解决拥塞和一定的延迟问题。接收端如果对接收到的每个报文段都进行ACK,那么网络中将存在大量的ACK包,从而加重网络负载。
TCP设计ACK累积策略,该策略并不是针对每个TCP报文都需要进行ACK确认。接收端的TCP创建一个针对发送端IP的队列,接收到的报文都进入该队列。接收到TCP报文段后,启动延迟ACK定时器。在该定时时间段内,仍然会接收发送过来的数据进入队列。在一定条件下,然后回复已经收到的最大的数据SEQ + 1作为ACK回复确认序号。如下通信过程:
从上图可以看出,当svr和bsdi在建立连接后,svr连续发送了三个TCP报文,但是bsdi并不是对每个报文都进行了ACK,当序号为1的PSH包到达后,bsdi的TCP启动延迟ACK,然后又接收到了1025序号的TCP报文,此时有两个未确认的报文,所以发送了一个2049的ACK。当接收方收到2049的ACK后,即知道前面一个序号为1的报文也已经接收到了,不然接收端是不会回复2049的ACK的。
通过ACK累积,可以减少ACK包,从而提升网络利用率,降低拥塞的可能。
二.超时重传
TCP的可靠性另一方面的保证机制即超时重传。上节说到通过ACK的方式让发送方知道接收方是否收到了数据。但是数据和ACK包都有可能会丢,即使接收方收到了,但是ACK却丢失。那样对于发送方而言,仍然不知道接受方是否接受到了数据。
TCP协议设计超时重传机制。发送方的TCP在发送TCP报文后,针对该报文启动重传定时器,如果当定时器溢出超时后,仍然没有收到该报文的ACK,则重新发送该报文。然后再启动相应的定时器等待ACK,直到发送成功或者重发一定次数失败后。
以下客户单通过telnet程序演示TCP的超时重传机制的过程。
发送端发送"hello2."时由于网络连接断开,然后重复发送。通过tcpdump可以看出
参考
TCP的交互与成块数据流