TCP协议
TCP包头格式
首先是源端口号和目标端口号,如果没有这两个端口号,数据就不知道应该发给哪个应用
接下来是包的序号。包的序号是为了解决乱序的问题。
然后是确认序号。发送出去的包应该有确认,用来确定对方有没有收到。如果没有收到就应该重新发,直到送达。这个可以解决丢包问题。
接下来是一些位状态。例如SYN是发起一个连接,ACK是恢复,RST是重新连接,FIN是结束连接等。TCP是面向连接的,因而双方要维护连接的状态,这些状态位包的发送,会引起双发的状态的变更。
还有一个重要的就是窗口大小,TCP要做流量控制,用心双发各什么一个窗口,标识自己当前能够处理的能力,别发送的太快,也别发送的太慢。
TCP的三次握手
TCP的连接建立,我们常常成为三次握手。
- 刚开始,客户端和服务端都处于CLOSED状态。先是服务端主动监听某个端口,处于LISTEN状态。
- 客户端主动发起SYN,之后处于SYN-SENT状态
- 客户端收到发起的连接,返回SYN,并且ACK客户端的SYN,之后处于SYN-RCVD状态
- 客户端收到服务端发送的SYN和ACK之后,发送ACK的ACK,之后处于ESTABLISHED状态,因为它一发一收成功了。
- 服务端收到ACK的ACK之后,处于ESTABLISHED状态,因为它也一发一收成功了。
TCP四次挥手
- 断开的时候,当A向B发送有个FIN,告诉B我要断开,之后A处于FIN-WAIT-1状态
- B收到A的FIN之后,发送给A FIN告诉A它知道了,然后B处于CLOSED-WAIT状态
- A收到B发送的ACK之后,进进入FIN-WAIT-2状态,如果这个时候B跑路,那么A将永远处于在这个状态里面。TCP协议里面并没有对这个状态的处理,但是Linux有,可以调整tcp_fin_timeout参数,设置一个超时时间
- 如果B没有跑路,向A发送FIN到A时,A发送FIN的ACK到B,A结束FIN-WAIT-2的状态。按说A可以直接跑路了,但是最后的这个ACK万一B收不到呢?则B后重新发送一个FIN到A,这个时候A已经跑路的话,B就再也收不到ACK了,因而TCP协议要求A最后等待一段时间TIME_WAIT,这个时间要足够长,长到如果B没收到ACK的话,B的FIN会重新发送,A会重新发送一个ACK并且足够时间到达B
- A直接跑路还有一个问题,A的端口就直接空出来了,但是B不知道,B原来发过的很多包很可能还在路上,如果A的端口被一个新的应用占用了,这个新的应用会受到上个连接中B发过来的包,虽然序列号是重新生成的,但是这里要上一个双保险,防止产生混乱,因而也需要等足够长的时间,等到原来B发送的所有的包都死翘翘,再空出端口来
- 等待时间设为2MSL,MSL是Maximum Segment Lifetime,报文最大生存空间,它是任何报文在网络上存在的最长时间,超过这个时间报文奖杯丢弃。因为TCP报文基于是IP协议的,而IP头中有一个TTL域,是IP数据报可以经过的最大路由数,没经过一个处理他的路由器此值就减1,当此值为0时则数据报将被丢弃,同事发送ICMP报文通知源主机。协议规定MSL为两分钟,实际应用中常用的是30秒,1分钟,2分钟
- 还有一种异常情况就是,B超过了2MSL的时间,依然没有收到它发的FIN的ACK。按照TCP原理,B还会重发FIN,这个时候A再收到这个包之后,A就表示,我已经在这里等了这么久的时间了,之后的我都不认了,于是就直接发送RST,B就知道A早就跑了。
TCP状态机
下面是著名的TCO的状态机。
TCP包头很复杂,但是主要关注五个问题,顺序问题,丢包问题,连接维护,流量控制,拥塞控制
为了记录所有发送的包和接收的包,TCP需要发送端和接收端分别由缓存来保存一些记录,发送端的缓存里是按照包的ID一个一个排列的,根据处理情况分成四个部分。
- 第一部分:发送了并且已经确认的
- 第二部分:发送了并且尚未确认的
- 第三部分:没有发送但是已经等待发送的
- 第四部分:没有发生并且暂时还不会发送的
在TCP里,接收端会给发送端报一个一个窗口的大小,叫Advertised window。这个窗口的大小等于发送了并且尚未确认的加上没有发送但是等待发送的大小,超过这个窗口的,接收端做不过来,就不能发送了。所以,发送端需要把持下面的数据结构。
对于接收端来讲,它的缓存里记录的内容要简单一些
- 第一部分:接受并且确认过的
- 第二部分:还没有接收的,但是马上就能接收的
- 第三部分:还没接收,也没办法接收的
顺序问题和丢包问题
超时重试:即对每一个发送了,但是没有ACK的包,都有设一个定时器,超过了一定的时间,就重新尝试。这个时间不宜果断,时间必须大于往返时间RTT,否则会引起不必要的重传。也不宜过长,这样超时时间边长,访问就变慢了。
估计往返时间,需要TCP通过采样RTT的时间,然后进行加权平均,算出一个值,并且这个值还是要不断的变化的,因为网络不断的变化。除了采样RTT,还要采样RTT的波动范围,计算出一个估计的超时时间。由于重传时间是不断变化的,我们称为自适应重传算法
TCP的策略是超时间隔加倍,每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁发送。
还有一冲方式称为Selective Acknowledgment(SACK)。这种方式需要在TCP头里加一个SACK的东西,可以将缓存的地图发送给发对方。例如可以发送SAKC6、SACK8、SACK9,发送方一下子就能看出来7丢了。
流量控制问题
发送方会定时发送窗口探测数据包,看是否有机会调整窗口的大小。当接收方比较慢的时候,要防止低能窗口综合征,别空出一个字节就赶快告诉发送方,然后马上又填满了,可以当窗口太小的时候,不更新窗口,直到达到一定大小,或者缓冲区一半为空,才更新窗口。
拥塞控制问题
拥塞控制的问题,也是通过窗口的大小来控制的,前面的滑动窗口rwnd是怕发送方把接收方缓存塞满,而拥塞窗口cwnd,是怕把网络塞满。
这里有一个公式LastByteSent - LastByteAcked <= min(cwnd, rwnd),是拥塞窗口和滑动窗口共同控制发送的速度。
TCP的拥塞控制主要来避免两种现象,包丢失和超时重传,一旦出现了这些现象就说嘛,发送速度太快了,要慢一点。但是有两个问题,第一个问题是丢包并不代表着通道满了,也可能是馆子本来就漏水。第二个问题是TCP的拥塞控制要等到将中间设备都填充满了,才发生丢包,从而降低速度,这时候已经晚了。其实TCP只要填满管道就可以了,不应该接着填,知道连缓存也填满。
为了优化这两个问题,后来有了TCP BBR 拥塞算法。它企图找一个平衡点,就是通过不断的加快发送速度,将管道填满,但是不要填满中间设备的缓存,因为这样时延会增加,在这个平衡点可以很好的达到高带宽和低延时的平衡。
更多精彩,请关注微信公众号: