TCP数据传输
TCP的数据传输分为两种,一种是交互式数据,一种是块数据,交互式数据如Telnet,一般都是小于10个字节的分组,而成块数据如FTP传输文件,基本都是大于512字节的报文,对于这两种数据,TCP的处理机制是不一样的,算法也不相同,下面是一个telnet的抓包,由于此处讨论的主要是交互数据,而不是telnet协议,因此抓包主要集中在交互上
CentOS release 5.3 (Final)
Kernel 2.6.18-128.el5 on an x86_64
login: qa
Password:
Last login: Fri May 13 15:35:27 from 10.103.51.142
[qa@hding ~]$ ls
Desktop hello socat-1.7.3.0.tar.gz
[qa@hding ~]$
#ls
1. IP 10.103.51.142.57545 > 10.8.116.6.telnet: P 73:74(1) ack 199 win 16375 C->S ‘l’
2. IP 10.8.116.6.telnet > 10.103.51.142.57545: P 199:200(1) ack 74 win 46 S->C ‘l’
3. IP 10.103.51.142.57545 > 10.8.116.6.telnet: P 74:75(1) ack 200 win 16375 C->S ‘s’
4. IP 10.8.116.6.telnet > 10.103.51.142.57545: P 200:201(1) ack 75 win 46 S->C ‘s’
5. IP 10.103.51.142.57545 > 10.8.116.6.telnet: .ack 201 win 16375 C->S ack
#endl
6. IP 10.103.51.142.57545 > 10.8.116.6.telnet: P 75:77(2) ack 201 win 16375 C->S ‘/r/n’
7. IP 10.8.116.6.telnet > 10.103.51.142.57545: P 201:203(2) ack 77 win 46 S->C ‘/r/n’
8. IP 10.103.51.142.57545 > 10.8.116.6.telnet: .ack 203 win 16374 C->S ack
9. IP 10.8.116.6.telnet > 10.103.51.142.57545: P 203:302(99) ack 77 win 46 S->C
\033[00m\033[01;34mDesktop\033[00m \033[01;34mhello\033[00m \033[01;32msocat-1.7.3.0.tar.gz\033[00m\033[m[qa@hding ~]$
10. IP 10.103.51.142.57545 > 10.8.116.6.telnet: .ack 302 win 16349 C->S ack
从以上报文来看,交互式数据的报文都非常的小,回应也非常的及时,时效性好,每输入一个字符都会有回显,每次都发送一个字节,两个字节,这就产生了41字节的或都42字节的报文,因为IP头加TCP头总共40字节,在局域网上,会传送这些小分组,当然在以太网中不会有什么麻烦,但是如果在广域网上,带宽小的地方,这些小分组就会产生拥塞,因此就产生了Nagle算法
糊途窗口综合症
当发送端应用进程产生数据很慢,如telnet,每次只产生一字节的数据,或者接收端应用进程处理接收缓冲区数据很慢,这样会导致接收端通告的窗口很小,如一字节,从而使网络中充斥着载荷很小的小包
Nagle算法
Nagle 算法要求一个TCP连接上最多只能有一个未被确认的未完成的小分组,在该分组确认到达之前不能发送其它的小分组,一般实现时都是以MSS为标准,当小分组拼凑成一个MSS大小时,统一发出去,这个算法的优点在于它是自适应的,确认到达的越快,数据也就发送的越快,只要接收到对端的ACK,就可以发数据了,在接收到ACK来之前,先缓存小分组,而在希望小分组数目低的广域网上,则会发送更少的分组,在以太网中由于时延小,总是能够及时的收到对端的ACK,因此也就看不到Nagle算法的优势,但在广域网上,时延较大,收不到ACK时,如果开启Nagle算法,则发送端发送一个小分组后,在收到ACK之前不会再发送第二个小分组,而会缓存这些小分组,直到ACK来之后才会发出去,如果关闭Nagle算法,广域网中充斥着小分组,容易阻塞网络。Nagle算法的核心就是减少小分组在网络中的传输,但是在减少网络中传输的分组同时,是牺牲了时延的代价,如果关闭Nagle算法,可以无需等待ACK发很多小分组在网络中,然而开启Nagle算法,必需等待ACK才能发,显然就慢了很多。
if there is new data to send
if the window size >= MSS and available data is >= MSS
send complete MSS segment now
else
if there is unconfirmed data still in the pipe
enqueue data in the buffer until an acknowledge is received
else
send data immediately
Clark解决方法
Nagle解决的是糊途窗口综合症的发送端,而接收端则需要Clark算法来解决,只要数据到达就发送确认,但宣布的窗口大小为0,直到缓存空间已能放入最大MSS或者MSS/2已经空了,才通告窗口,这样可以避免每收一个字节,通告窗口为1,而使发端发送小包,有效提高利用率。
延时的确认
这表示一个报文段达到时并不立即发送确认,如果对每个数据包都进行确认,则代价较高,所以tcp会延迟一段时间(不超过200ms),如果这段时间内有数据发送到对端,则稍带ACK,如果在延迟ACK定时器触发时,发现ACK尚未发送,则立即发送,延迟ACK避免了糊途窗口综合症,因为如果不延迟,发端收到ACK立马又要发,此时并无缓存多个小包,发的还是小包,第二不必单独发ACK,而是带数据,如果延迟内有多个数据段到达,那么允许协议栈发送一个ACK进行确认
Nagle解决糊途综合症的发端,延迟也可以避免糊途综合症,但是如果发端要发数据给接收端,偏偏这数据小于MSS,此数据到达对端,对端没有数据要发,因此延迟确认,而发端再发数据需要满足MSS,或者等待对端的ACK,导致你不发ACK,我发的数据又到不了MSS,而迟迟不发数据,直至延时定时器超时发ACK,再发数据,Nagle算法是可以通过TCP_NODELAY来关闭,通常在交互性很强的应用程序会关闭Nagle算法,或者对端不经常发数据,也就意味着不能稍带ACK,此时也要关闭延时确认。
TCP滑动窗口
首先来讨论一下最简单的传输方式
这是非常简单的一种模型A发给B数据,B进行确认,A等到B确认后,再发新的数据,如果发的数据丢了,A没有等到B的确认,过了规定的时间,A认为数据丢了,进行重传,这样可以保证数据能够到达B,但这种方式太耗时间,效率太低,显然是不可行的,由于TCP是有序号的,且每个包都是独立的,ACK也是可以累积的,所以接收方不必确认每一个收到的分组,因此可以多个包进行一次确认,但是这也是有限制的,不可能A一直发送数据导致B接收不过来,此时就会用到通告窗口,B接收了A的数据并进行确认时,会给B一个通告窗口,让B根据这个窗口来控制发送数据的速度。
A发送Message #1, Message2
B确认#1,并通知窗口为2,意味着A可以发送两个包
A收到ACK #1 窗口为2,计算可以发送的数据为1,即#3,因为#2未被确认,所以#2属于已发送未确认,窗口大小=已发送未确认(发送途中)+可以发送的数据包
B收到#2后,发ACK#2,并通知窗口为2
A收到ACK#2后,只有#3在途中,于是可以发送#4
B收到#3后,发ACK#3, 通知窗口为1,窗口变小,意味着B缓存里有一个包没有被应用程序处理,滞留在缓存中,此时#4在传输中被丢弃
A收到ACk#3后,照理可以发#5,但由于窗口是1,而又没有收到#4的ACK,所以不能发数据包
B一直收不到#4的数据包,所以不发ACK
A一直等不到ACK#4,过了一段时间后,确认丢失,#4数据包重发
B 收到#4后,发回ACK#4,并通告窗口为2,此时A接到ACK#4后,没有数据在途中,连发#5,#6
显然这种方式大大提高了效率,同时也保证了传输的可靠性,TCP将独立的字节数据当作流来处理,如果一个字节,一个字节的确认,再加上重传,这显然是不行的,所以TCP把字节放在一个段中一起发送,一起接收,一起确认,每条消息都有序号,每一条消息都能独立的确认,因此同一时刻发多条消息,不会影响确认,至于同时发多少条消息,这里面用到了滑动窗口的概念。
假设一次TCP连接发送端需要传送这一串数据
第一部分已发送,已确认,31个字节已确认
第二部分发送未确认,也就是在发送的途中,32~45
第三部分是还未发送,但接收方说有足够的空间可以容纳的字节数,46~51
第四部分超过接收方接受的字节>52
发送窗口指的是第二部分和第三部分之和,统称为允许发送方传送的字节数,包括在途中的和未发送的,总共32~51,20个字节
可用窗口指的是发送方还可以发送的字节数,即第三部分,窗口减掉在途中未被确认的字节数,如下图
对端的确认只确认已收到的最大字节,如经过一段时间,接收端接收到了之前在途中的报文段#1:32~34,#2:35~36,#4:42~45,但是#3:37~41丢失了,对端只确认#1,#2, ACK:36,当发送端收到确认信息后,原来32~36的在途中字节被确认,变成第一部分的,窗口大小不变的话,看上去就是窗口向右移动,又有了新的字节可以传送,这就是滑动窗口的机制,随着字节不断的被确认,窗口不断的向右滑,直到数据全部确认完毕,如下图
当有报文丢失,#3丢失了,ACK只能回#2中的最后一个字节,TCP处理丢失的片断有两种方式:
仅重传超时片断:发送端一直等不到ACK:#3,当重传计时器超时,TCP只重传报文#3,这种情况如果报文#4传到了,那么接收端在收到报文3后,可以与报文4一起给予确认,但如果报文4也丢了,则不得不再等一个超时,来重传4
重传所有片断:发送端一直等不到ACK:#3,则认为他之前传的数据全部丢了,即全部重传,这种方式的问题可能重传了不必要重传的数据,TCP只能知道哪些数据确认了,但未确认的非连续片断则无法确认,解决的方式是对TCP滑动窗口算法进行扩展,添加SACK(selective acknowledgment)选择确认
首先双方必需都支持这个功能,可以在连接建立时进行协商,是否允许SACK,则是TCP的头选项,这一选项包括一个关于已接收但未确认的片断数据sequence number范围的列表,如储存了#4:42~45这个片断,当接收端回ACK时附带一个SACK选项指明,“已接收42~45,但尚未确认”,并为#4打开SACK位,发送端重传#3时,看到#4的SACK为1,则不会重传#4,下图会更直观
服务器连续发送4个片断给客户端:#1:80, #2:120, #3:160, #4:140,并被告知客户端的窗口大小为560,其中#3丢失
客户端收到#1,#2,没有收到#3,因此只确认前两个片断201,但又收到了#4,于是把#4 SACK置1
服务器收到ACK:#2后,把窗口向右滑动200字节,等待ACK:#3,超过重传定时器后,由于#4的SACK是1,于是仅重传#3,而不重传#4
客户端收到#3后,#3,#4一起确认ACK:501给服务器
当然窗口也会变化,如在接收端发生拥挤时,缓存不够时,接收端会通告窗口变小,使得发送端发送的字节数减少
TCP超时重传
TCP之所以被称之为是可靠的连接,就在于它会知道报文有没有到达对端,并且有超时重传机制保证数据丢了也没有关系,超时重传的方法概念是简单的,它是基于对端的ACK,从而得以实现。每一次发一个片断,就开启一个重传计时器,计时器有一个初始值并随时间递减,如果在片断接收到确认之前超时,则重传片断,难就难在这个超时的时间如何设置:
设长了,重发就慢,丢了老半天才重发,没有效率,性能差
设短了,会导致可能并没有丢就重发了,于是增加网络拥塞,导致更多的超时,更多的超发
重要的是这个时间不能定死,因为不同的网络,不同的环境时间都是不一样的,TCP引入RTT(Round Trip Time),也就是一个数据包从发出去到回来所需要的时间,这样发送端大约知道一个往返需要的时间,从而估计这个超时的值 RTO(Retransmission TimeOut),以让传输更加高效,但是如何估计也是个问题。这只是一个概念,但要精确却相当的难
经典的算法RFC793
先采样RTT,记下最近几次的RTT值
然后做平滑SRTT(Smoothed RTT). 对RTT加权移动平均: SRTT=(a*SRTT)+(1-a)*RTT 0.8
Karn算法
在一个分组重传时会发生这么一个问题,
发一个分组,ack没回来,重传,计算第一次发送分组到重传后的ACK,显然就大了
发一个分组,ack回来慢了,重传,由于延时的ack再重传后很快就到了,如果主算重传的时间与ACK之间的时间则又短了
因此在发生重传时,RTT的估计会有问题,TCP弄不清楚回来的ACK是重传的,还是第一次发送分组延时的,导致估计RTT有误,Karn算法的最大特点就是忽略重传,不把重传的RTT采样,但是这样一来,如果网络真的时延很大,重传的不算,RTO一直是很小的值,则一定会发生重传,于是又定义如果发生一次重传,RTT的值进行指数退避,下一次传输时使用这个退避后的RTO,而对于一个没有重传的报文段,除非收到一个确认,否则不要计算新的RTO
telnet 到server后,服务器接口down后,发一个数据包,重传
1. 13:49:55.410155 IP 10.8.116.8.41935 > 10.8.116.6.telnet: P 1:2(1) ack 2 win 46
2. 13:49:55.610444 IP 10.8.116.8.41935 > 10.8.116.6.telnet: P 1:2(1) ack 2 win 46
3. 13:49:56.012249 IP 10.8.116.8.41935 > 10.8.116.6.telnet: P 1:2(1) ack 2 win 46
4. 13:49:56.816814 IP 10.8.116.8.41935 > 10.8.116.6.telnet: P 1:2(1) ack 2 win 46
5. 13:49:58.423954 IP 10.8.116.8.41935 > 10.8.116.6.telnet: P 1:2(1) ack 2 win 46
6. 13:50:01.640232 IP 10.8.116.8.41935 > 10.8.116.6.telnet: P 1:2(1) ack 2 win 46
7. 13:55:20.983004 IP 10.8.116.8.41935 > 10.8.116.6.telnet: P 1:2(1) ack 2 win 46
8. 13:55:20.984141 IP 10.8.116.6.telnet > 10.8.116.8.41935: P 2:3(1) ack 2 win 46
9. 13:55:20.984163 IP 10.8.116.8.41935 > 10.8.116.6.telnet: P 2:3(1) ack 3 win 46
10. 13:55:20.984246 IP 10.8.116.6.telnet > 10.8.116.8.41935: P 3:4(1) ack 3 win 46
可以观察到client重传了5 次,每次RTO为0.2,0.4,0.8,1.6,3.2秒,linux只重传5次,第7个包是在服务器正常后,客户端再一次发一个数据包给服务器,此时服务器回应了客户端,后面的数据基本上都是及时响应的
TCP流控与拥塞
TCP不仅可靠,而且具有流控的功能,从TCP的包头中可以看到window窗口大小这个字段,它是接收端通过通告这个窗口来控制发送端的发流速度,其中需要弄清楚当服务器从客户端接收数据,它是将数据放在缓存中,服务器做两个动作:
确认:服务器必需将确认信息发回客户端以表明数据接收到
传输:服务器必需处理数据,将它传递给目标应用程序
当服务器接收到数据会发确认信息给客户端,但并不代表服务器的缓存中的数据都处理掉了,因此在发确认信息时会根据自己的缓存情况发送通告窗口给客户端,以使得自己能够承受,也就意味着当接收数据的速度快于TCP处理速度时,缓存有可能满,因此通告窗口大小来控制流量,窗口大小=缓存-滞留在缓存中未处理掉的数据
通过减小窗口来控制发端速度的例子
服务器通告窗口为360
客户端发140字节的request
服务器收到后发送ACK,并把窗口减小100,因为要处理这140字节
客户端收到服务器的ACK,把窗口调整到了360-100=260,于是开始计算自己可用窗口为260,发了180字节的request
服务器收到这180字节后,回ACK,根据自己的缓存,决定把窗口再减小180,通告窗口为80,意味着让客户端少发点,自己处理不过来
客户端收到ACK后,发现只能发80,于是就发了80
服务器缓存满了之后,通告窗口为0,关闭窗口,不让客户端再发数据,直到服务器认为自己有缓存了,再通告窗口,客户端再发数据,如下图
窗口的大小可大可小,但是窗口的右边沿不能向左移动,原来通告的是360,客户端会准备发360个字节过来,因为发送分组有可能不是同时的,如第一个报文140,第二个报文可以发180,因为窗口大小是360,但如果接收到第一个报文后回的ACK是100,那么客户端此时的180已经发出去了,超过了窗口上限,就出错了,而发出去的180被服务器接收后,由于缓存已满被丢弃,这是因为窗口的右边沿向左移了,导致原来的窗口缩水,窗口可以小,但这个小只是窗口右边沿在收到ACK后向右移的距离,而不能向左边移。如下图
这个流控是通过接收端的通告窗口而实现的,但是TCP觉得这还不够,因为滑动窗口需要依赖于连接的发送端和接收端,其并不知道网络中间发生了什么。TCP设计者觉得,仅仅流控是不够的,还应该知道网络拥塞。如网络时延突然增大,TCP会进行重传,有时会因为路由器的瓶颈导致数据包的丢失拥塞避免算法是一种处理分组丢失的方法,由于分组受到损坏而引起的丢失概率小上,因此TCP判断拥塞依据就是发生超时重传或者接收到重复的确认,如果是超时重传,则依赖于一个精准的RTT,重复的确认则分两种情况,第一种因为网络时延,使得接收端收到了乱序包,即接收到的分组顺序不一致,TCP会发确认,期望发端把中间没有收到顺序的包重新发一遍,这种情况重复的确认通常为1~2个,因为过了一段时间,时延的分组到达接收端,接收端就不会再发这个重复的ACK了,另一种情况则是该中间的分组的真的丢失了,接收端肯定会不断的发重复的ACK,此时会有很多个ACK,则必需得重传了,拥塞避免算法是通过一个拥塞窗口cwnd (congestion window)来控制发端的速率,同时我们知道发端的速率也可以通过接收端的rwnd (receive window)来控制,通常我们认为通过rwnd是流控,因为这个控制被动的,而cwnd才是主动的去避免拥塞,发送窗口应该是min(cwnd, rwnd),这边介绍四种经典算法,慢启动,拥塞避免,拥塞发生,快速恢复
慢启动:
网络连接好后,一开始由于发送端并不知道网络的状况,因此初始发包通常为一个或者两个MSS字节,随后根据网络状况逐渐提速,到达一定的限值后,进入拥塞避免算法,这就避免了连接建立好后,发送端发送过多的数据导致网络拥塞。
慢启动增加了一个拥塞窗口,用来控制发送端的速率cwnd(Congestion Window)
1. 连接建立好后,先初始化cwnd=1,表明可以传一个MSS大小的数据
2. 每当收到一个ACK时,cwnd++,呈线性上升
3. 每当过一个RTT时,cwnd=cwnd*2,呈指数上升,因为第一个RTT只有一个ack,第二个RTT就会有2个ack,此时cwnd=1+1+2,第三个RTT就会有4个ack,因为在第二个RTT时间内,收到了之前发的两个ACK,此时cwnd已经是4了,此时cwnd=1+1+2+4
4. 设置一个ssthresh(slow start threshold)上限,因为cwnd呈指数上升,很快就会使得窗口很大,导致发送数据过多
拥塞避免算法:
当cwnd>=ssthresh时,执行拥塞避免算法,一般ssthresh=65535字节
1. 收到一个ACK时,cwnd=cwnd+1/cwnd
2. 当每过一个RTT时,cwnd=cwnd+1
如第一个RTT,收到一个ACK,此时cwnd=1+1, 第二个RTT,收到2个ACK,cwnd=2+1/2+1/2=3,第三个RTT,收到3个ACK,cwnd=2+1/2+1/2+1/3+1/3+1/3=4,它保证每过一个RTT, cwnd只增加1,而在这个RTT时间内收到多少人ACK并不关心,而慢启动关心的是ACK个数
当网络拥塞状态时,算法:
我们一般依据超时或者收到重复的确认以代表网络拥塞
重传定时器超时没有收到ACK,重传数据包,TCP认为这种情况太差,网络非常拥塞,ACK都发不回来,于是采取重新慢启动,使得网络流量快速降下来
1. 降低慢启动上限sshthresh = cwnd/2
2. cwnd =1
3. 进入慢启动
如果是因为收到了重复的ACK,则TCP需要判断是因为乱序而使对端发重复的ACK,还是因为报文丢失才发的重复的ACK,如果只是一些报文段的重新排序,则在重新排序的报文段被处理并产生一个新的ACK之前,只可能产生1~2个重复的ACK,因为乱序,肯定是后面的包比前面的包先到,前面的包由于延时晚到了,但最终也会到,因此再发了1~2个重复的ACK后,基本也就到了,统一确认,如果一连串收到3个或3个以上的重复的ACK,就非常可能是报文段丢失,而因为收到了重复的ACK,说明收发两端间仍然有流动的数据,因此不想执行慢启动来突然减少数据流,这边就提出了快速重传和快速恢复
快速重传:快速重传的前题是当接收方收到乱序的分组时,要立即发送重复确认,这样可以让发送方及早发现分组丢失,而不是延迟的确认,发送方只要接到三个重复的确认,就立即重传,而不必等待重传计时器超时,但计时器仍然存在,快速重传是相对超时重传而言
快速恢复:当发送端收到三个重复的确认后,sshthresh慢开始门限减半,但接下去不再执行慢开始,即拥塞窗口不设为1,而设置成减半后的sshthresh门限大小,然后使拥塞窗口慢慢加大,这是基于TCP认为既然有重复的ACK能回来,网络可能没有拥塞,或者拥塞并不严重,也没必要直接把cwnd设为最低,
当收到第三个重复的ACK,表明网络拥塞,更新cwnd与sshthresh
1. cwnd = cwnd/2
2. sshthresh = cwnd
3. cwnd = sshthresh
4. 进入拥塞避免
快速恢复的思想是“数据包守恒”原则,如果发送方收到一个重复的ACK意味着网络中有一个包到达了接收端,或者说在网络中走掉了一个“老”包,那么就能再发一个新包,于是把cwnd+1,这样说来快速重传的步骤:
当收到第3个重复的ACK时,将ssthresh设置为当前拥塞窗口cwnd的一半,重传丢失的分组:sshtrensh = cwnd / 2, cwnd = ssthresh+3MSS(因为快速重传,所以不让cwnd直接降为1,而又因为收到了三个确认报文,老包走了三个,新包可以多三个)
每次收到另一个重复的ACK时,cwnd增加1,原因是老包,新包数据守恒
当下一个确认新数据的ACK到达时,设置cwnd为原先的ssthresh的值,因为该ACK确认了新的数据,说明重复的ACK的数据已收到,恢复过程结束,可以恢复到之前的状态,再次进入拥塞避免状态
定时器
TCP传输是可靠的,但只钱对数据,对ACK的传输是不可靠的,TCP不对ACK报文进行确认,只对包含数据的ACK进行确认,这就存在一个问题,如果ACK丢失了,双方就有可能因为等待对方而使连接终止,这个情况发生在0窗口情况下,一开始接收端由于自己的缓存情况发送了0窗口,发送端等待接收端允许自己发送数据的窗口更新,而接收端发送的非0窗口更新的ACK却丢了,这样接收端不知道丢了ACK,发送端又不敢擅自发送数据,为了防止这种死锁的产生,发送方使用一个坚持定时器(persist timer)来周期性的向对端查询,以便发现窗口已增大,这些询问的报文称为窗口探查包(window probe),每次重复的探察遵循指数退避算法
TCP连接后,即使没有数据传输,连接仍然存在,意味着对端如果崩溃了,本端仍然不知情,这分为两种,如果服务器端崩溃,client向服务器发送数据,由于是一个半开放的连接,会收到一个RST复位连接的包,而如果是客户端崩溃,服务器端又无需发数据给客户端,那么这个连接会一直存活下去,至于是否需要一个定 时器轮询,以查看对端是否还存在,这有争论。
使用:希望知道对端的情况
不使用:出现短暂差错,不会使一个非常好的连接断掉,不必要耗费带宽
按分组计费的情况下,会浪费更多的钱
,保活应该是应用程序的事,TCP不管这些
参考:
TCP/IP协议卷一
TCP/IP guide
http://www.kuqin.com/shuoit/20140611/340486.html TCP的那些事儿上
http://www.kuqin.com/shuoit/20140611/340485.html TCP的那些事儿下
来源:oschina
链接:https://my.oschina.net/u/2303535/blog/679517