很多人都说TCP协议是一个十分复杂的协议,在学习当中,我对协议每一个问题都分解学习后,每一个分解我都能体会和理解它的要点,并不难理解。但我把这些拆分的细节合并后,确认感觉这样一个协议相对“臃肿”但又好像不得不这样做的感觉。也写过那么多年代码,我也十分理解这种“分布”和“一致”的协调,就好像CAP理论一样,更关键的是许多的CAP选择都是依赖于TCP这样可靠的协议之上,可想而知它“可靠性”的重中之中,我也看到了根基扎实稳重的重要性。当然技术还在不断进步,协议的完善和优化从没有停止,无论如何,学习还得继续。
TCP的交互数据流
1、交互式输入
有些场景比如聊天,对信息交互的时效性非常敏感,又还比如远程操作,我们在本地点击一下鼠标的远程操作,需要被操作设备非常及时的响应,这样一些场景就是交互式输入。
数据交互式输入交互式输入是场景需要之一,它是牺牲了网络利用率而满足了时间的一种选择,试想一下如果客户端每一个小小的动作(如点击ENTER键产生1个字节)就要带着20个字节的TCP头部以及20个自己的IP头部,一共41个字节在网络上跑,如果每种交互都是这样的话,网络的利用率大大降低,所以这种情况更多适用于局域网内。而且在这种交互式交互时还可以看到TCP头部的PSH标识被设置了,它的意思是赶紧把数据交给应用程序,而不是先放缓冲区。
客户端的实时推送请求以及服务端的及时响应确实能保证了“实时交互”的效果,但这里会隐藏了一个客户端对服务端数据回显确认报文(ACK)的一个延迟发送。我们都知道TCP交互是一个可靠的协议,所以对数据的接收和确认是必然的要做的事情,我觉得既然客户端已经得到了服务端的及时响应,对服务端数据的确认响应已经不需要那么及时了。好好地在这一个点上的优化可以节省了网络的不必要浪费。
经受时延的确认通常TCP在接收到数据时并不会立刻发送ACK,相反,它推迟发送,以便将ACK与需要沿该方向发送的数据一起发送(有时这种现象为数据捎带ACK)。绝大多数实现采用的时延为200ms,也就是说,TCP将以最大200ms的时延等待是否有数据一起发送,这样做就节省了不必要的网络开销,从这一点看,TCP协议也是想绝了,毕竟资源珍贵,容不得半丁点的浪费。
2、Nagle算法
虽然交互式输入更多适用在局域网,但广域网就不能用这是不可能的,实时交互式这种场景会产生许多的(微)小分组,例如41字节中的真正数据才1字节。这确实会增加广域网拥塞的可能性。但是“道高一尺,魔高一丈”,一种简单和好的方法应运而生,那就是Nagle算法。该算法要求一个TCP连接上最多只有一个未被确认的未完成的小分组,在该分组的确认到达之前不能发送其它的小分组,相反,TCP收集这些少量的分组,并在确认到来时以一个分组的方式发出去。该算法的优越之处在于它是自适应的:确认到达得越快,数据也就发送得越快。而在希望减少微小分组数目的低速广域网上则会发送更少的分组。
Nagle算法从上图可以看到,因为Nagle算法的生效,所以在“按键”输出后没有等到服务端响应确认前的后续4次数据输入都无法发送,而是等到服务端确认ACK到达后一起打包发送。有一点需要注意的是,这里的输入假设都是小分组输入,并没有很大的数据输入(不超过MSS)。Nagle算法算是在广域网上做了一个这种的选择,对数据交互实时性的影响不会太大,而又对互联网做了一层较好的保护,让交互的效果自适应网络的状态。
TCP的成块数据流
1、滑动窗口
交互式输入我觉得从综合场景来看,相对少数。大部分情况下,大块的数据流交互才是“王道”,这里并不是说我们平时的应用交互就不需要实时,只不过这个“实时”是相对的,在数据量大的情况下如何才是最佳的交互体验,具体问题具体分析才是王道。在TCP头部中我们知道有一个叫做“16位窗口大小”的属性,这个窗口在上一节也介绍过相当于数据接收的“缓存区”,数据交互的双方(无论主动还是被动)都会维护着自己的一个窗口,应用程序没有消费数据之前都是停留在这个缓冲区中。所以,窗口可以看做TCP交互限流的一个关键所在,如果窗口爆满的一方是无法接收数据了,发送方也只能暂停发送,等待对方窗口的空闲才继续发送。毕竟双方应用程序处理数据的速度不一或者网络网速的客观影响,很难确认完美的状态,所以很需要一个像窗口一样的概念去维护双方之间的一个传送速度。
TCP窗口样例通过上图可以看到,这次大块数据交互不像“交互式输入”那样小分组发送,而是每次发送都会充满整个报文段允许的最大值(MSS)从而达到更加的效率,当然这已经是另一次场景了,而这个场景才是我们平常使用到的场景,例如你看个新闻,刷个微博等。从图中也可以看得到窗口的真实作用,服务端一开始就告诉客户端它的窗口大小为4096,客户端要发送的数据远远大于4096(例如一张图片可以就不止了),客户端可以连续以最大报文段的容量连续发送,直到服务端的窗口(缓冲区)被沾满时,才会停止发送,等待服务端的窗口大小变更通知,再继续发送。其实这里的成块数据传送隐藏了许多的规则细节,例如发送的数据不够MSS怎么办,需要等么?又或者服务端的窗口可以腾出了1个字节的空间也要告诉客户端的话就会引发“糊涂窗口综合症”的问题。这些细节后续会慢慢介绍,这里更多先总结窗口的基本作用。
窗口边沿的移动窗口(缓冲区)的总大小是固定的,只不过因为窗口的“空闲”是随着接收方的确认而变动,从而让窗口看起来是动态变化的,这个动态(左右收缩)其实是站在双方的视觉看窗口的空闲状态而言,双方都会计算和维护当前窗口的大小, 例如发送方会计算它到底还有多少数据可以发送,而接收方会计算它到底还有多少窗口空间可以接收数据:
1)、称窗口左边沿向右边沿靠近为窗口靠拢,这种现象发生在数据被发送和确实时;
2)、当窗口右边沿向右移动时将允许发送更多的数据,我们称之为窗口张开。这种现象发生在另一端的接收进程读取已经确认的数据并释放了TCP的接收缓存时;
3)、当右边沿向左移动时,我们称之为窗口收缩。RPC强烈建议不要使用这种方式。
如果左边沿到达右边沿,则称其为一个零窗口,此时发送方不能够发送任何数据。就像“TCP窗口样例”显示的那样。不同系统默认的窗口大小不一样,例如有些是2048字节,有些是4096、8192或更大。但重要是“插口API允许进程设置发送和接收缓存的大小,接收缓存的大小是该连接上所能通告的最大窗口大小,有一些应用程序通过修改插口缓存大小来增加性能”。
2、慢启动
“接收窗口”并非万能,虽然可以通过增加缓存大小提高性能,但影响性能的远不止“接收窗口”的大小,还有网速、路由器等。迄今为止,发送方一开始便向网络发送多个报文段,直至达到接收方通告窗口大小为止。当发送方和接收方处于同一个局域网时这种方式是可以的,但是如果在发送方和接收方之间存在多个路由器时,就有可能出现一些问题。一些中间路由器必须缓存分组,并有可能耗尽存储器的空间。
现在TCP需要支持一种被称为“慢启动(slow start)”的算法。该算法通过观察到新分组进入网络的速率应该与另一端返回确认的速率相同而进行工作。慢启动为发送方的TCP增加了另一个窗口叫“拥塞窗口(congestion window)”,记为cwnd。拥塞窗口是发送方使用的流量控制,而通告窗口则是接收方使用的流量控制。
慢启动例子拥塞窗口被初始化为1个报文段(即另一端通告的报文段大小),收到一个ACK后,拥塞窗口从1个报文段增加为2,即可发送两个报文段,当继续收到这两个报文段的ACK时,拥塞窗口就增加为4,这是一种指数增加关系。发送方去拥塞窗口与通告窗口中最小值作为发送上限。其实这宗一种渐进循环的策略,当吞吐量在某些点上达到了互联网的容量时,于是中间路由器开始丢弃分组,而通知发送方它的拥塞窗口开得过大了。这里又隐藏了许多的实现细节,如丢失重传问题以及如何避免一下子又从慢启动起步这种慢效率的传输过程。这些后续会学习到。这里更多先对慢启动算法的一个初步认识。
窗口的大小应该设置为多大呢?也就是我们的TCP缓存应该设置多大呢?按理论计算,这个应该跟“带宽时延乘积”有关,具体公式如下:
capacity(bit)=bandwidth(b/s)* round-trip time(s)
这个值依赖于网络速度和两端的RTT,可以有很大的变动。例如一条穿越美国(RTT约为60ms)的T1的电话线路(1544000b/s)的带宽延迟乘积为11580字节。这是没有问题的,但对于一条穿越美国的T3电话线路(45000000b/s)的带宽时延乘积则为337500字节,这个值远远超过了最大所允许的TCP通告窗口大小(16字节窗口大小=65535字节)。别忘记了TCP头部还有一各选项可以补充,后续会介绍如果通过选项解决这种超大型窗口问题。“带宽时延乘积”是一个理论值,就是我们能往发送到到接收端之间的网络最多塞满多少数据。
学习总结
本此总结主要是学习了基于TCP协议交互的一些流程与细节问题,如“交互式输入”以及“成块数交互”的场景介绍,在“成块式传送”中是如何利用“通告窗口”作为接收方的限流以及发送方如何利用“拥塞窗口”进行限流并借助“慢启动”的方式渐进循环的摸索互联网的最大极限。
网友评论