需要注意的几个问题
- 顺序问题
- 丢包问题
- 连接维护
- 流量控制
- 拥塞控制
1、TCP 包序号问题
TCP 每次连接的起始序列号都是不一样的,这个起始序号是随着时间的变化而变化的,可以看成一个 32 位的计数器,每 4 微妙 加一,如果要重复的话,需要等四个小时,保证之前发送的相同序号的包已经挂掉。
2、TCP 三次握手两端的状态变化
这里借用极客时间上刘超老师的图:
从服务器与客户端的 closed 状态开始:
- 服务器主动监听某个端口,进入 listen 状态
- 客户端发送第一次连接请求
SYN,seq=x
,客户端进入 syn_sent 状态 - 服务器监听到第一次连接请求,向客户端返回
ACK,seq=y,ack=x+1
,服务器进入 syn_rcvd 状态 - 客户端收到服务器来的应答请求,发送其对应的应答请求
ACK,seq=x+1,ack=y+1
,并且进入 established 状态,此状态是可持续传输数据的状态 - 服务器收到客户端发来的应答请求,同样进入 established 状态
3、TCP 的四次挥手以及状态的变迁
这里首先来看报文的发送与状态的变迁:
- 首先是客户端发送
FIN,seq=p
的报文,客户端将从 established 状态转变成 fin_wait_1 状态 - 服务器收到报文后回复
ACK,ack=p+1
报文,服务器将从 established 状态转变为 closed_wait 状态 - 客户端收到服务器发送的确认报文后将自己的状态转变为 fin_wait_2 状态,并且开启等待状态,此时服务器已知客户端要结束,而客户端还不知道服务器是什么态度,所以需要等待
- 服务器发送
FIN,ACK,seq=q,ack=p+1
报文,并且进入 last_ack 状态 - 客户端收到服务器的 FIN 报文后发送
ACK,ack=q+1
报文,并且进入 time_wait 状态,进入这个状态之后就会等待2MSL(Maximum Segment Lifetime,报文最大生存时间)
时长后关闭客户端连接 - 服务器收到 ACK 应答报文之后就会进入 closed 状态
再来看下如果其步骤出现异常的情况:
- 在第三步的时候(服务器第一次应答后)如果服务器进行跑路,那么单纯的从 TCP 的角度来说,是一直卡死在这个位置了,因为TCP 没有处理这种情况的机制,但是 Linux 设置有超时时间
tcp_fin_timeout
。 - 如果服务器发送完 FIN 请求之后,客户端发送 ACK 后进行了跑路,如果服务器收不到 ACK 应答,就会重发一次 FIN ,如果客户端跑路则服务器将永远也收不到 ACK ,所以设置客户端要等待 2MSL 时长来确保可响应。
- 如果在客户端发送完最后一次 ACK 之后就跑路的话,其占用的端口将被空闲出来,但是服务器却不知道,服务器将报文发送给新的应用,可能会出现问题,所以需要等待服务器发送的报文全部失效才能将端口空闲出来。
- 在客户端等待完 2MSL 时长之后,将拒绝服务器端的请求,并且发送RST报文,收到RST报文的服务器就知道客户端已经跑路了,以此断开连接。
MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,
超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 域,
是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,
当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。
协议规定 MSL 为 2 分钟,实际应用中常用的是 30 秒,1 分钟和 2 分钟等。
4、TCP累计应答与滑动窗口的引入 rwnd
TCP 不能一个报文一个报文的应答,需要每次 ACK 的时候都表示前面所有的报文都已经被处理,这种方式叫做 累计确认 或 累计应答,为了实现这部分的功能,并且可以控制传输的速度,所以有了滑动窗口来保证这种机制的实施,发送方结构图如下(借用极客时间刘超老师的图):
接收方结构图如下:
5、顺序问题与丢包问题
对于中间有未 ACK 的包来说,有几种确认与重发的机制:
- 超时重试:对于没有ACK的包,都设置有一个定时器,超过了一定的时间就会尝试重新发送,这个超时的时间不能太短,时间必须大于往返时间的 RTT,也不能过长,否则会导致延迟。
- 自适应重传算法:估算往返 RTT 时间,对 TCP 中的 RTT 进行采样,然后进行加权平均,算出一个值,并且这个值是根据网路情况不断变化的。每次需要重传的时候时间都是上一次重传时间的两倍,当每一次重传的时候回将下一次重传的时间设置为先前的两倍。两次超时,说明网络状态不好,不宜频繁反复发送。
- 快速重传机制:当接收方接收到一个大于自己期望值的报文段时,就会检测到数据流中的一个间隔,会发送冗余的 ACK,发送的 ACK 是期望接受到的值,当服务器收到三个冗余的 ACK 的时候,就会在定时器过期之前,重传丢失的报文段。
- SACK(Selective Acknowledgment):这种方式需要在 TCP 头里加一个 SACK 的东西,可以将缓存的地图发送给发送方。例如可以发送 ACK6、SACK8、SACK9,有了地图,发送方一下子就能看出来是 7 丢了。
6、流量控制
在对包的确认的时候,都会携带一个窗口的大小。如果接收方只是去接受数据,但是却不去处理的话,或者处理的速度很慢的话,发送方的发送数据的窗口就会不断的减小,直至减为 0,停止发送数据。当发送方的滑动窗口大小变为 0 的时候,就会定时发送窗口探测数据包,用来查看是否有机会调整窗口的大小。
当探测到接收方窗口大小不为零的时候,不能立马将发送发的滑动窗口从零变为其他的值,要防止低能窗口综合征 ,以免刚空出来一部分,立马又被发送方给填满了。当接收方窗口太小时,不更新发送方窗口,要达到一定的大小的时候(或者缓冲区一半为空)才回去更新发送方的滑动窗口的值。
7、拥塞控制 cwnd
TCP 的拥塞控制主要来避免两种现象,包丢失
和超时重传
。
- 拥塞控制也是用滑动窗口控制的,叫做拥塞窗口,是为了防止把网络塞满的。
- 原来发送一个包,从一端到达另一端,假设一共经过四个设备,每个设备处理一个包时间耗费 1s,所以到达另一端需要耗费 4s,如果发送的更加快速,则单位时间内,会有更多的包到达这些中间设备,这些设备还是只能每秒处理一个包的话,多出来的包就会被丢弃。如果在这些设备上加入缓存的话,四个设备本来每秒处理一个包,处理不过来的在队列里面排着,这样包就不会丢失,但是缺点是会增加时延,这个缓存的包,4s 肯定到达不了接收端了,如果时延达到一定程度,就会超时重传,也是我们不想看到的。
为了解决上述的问题,引入以下几个概念:
-
慢启动: 一条 TCP 连接开始,cwnd 设置为只能发送一个报文段,当收到 ACK 应答之后,cwnd + 1,于是一次性能发两个;当这两个的ACK应答接收后,一次能发四个,以后每次都按二倍来算,是呈
指数型增长
的。 -
ssthresh机制:这个值默认是 65535个字节 ,当超过这个值的时候,cwnd 就会将原本指数型的增长方式改为
线性增长
,每次增加 1/cwnd 的大小,如果 cwnd 为 8 的话,每次增长 1/8, ACK 八次之后 cwnd 才会变成 9。 - 丢包降级:TCP发生丢包的时候需要重传,这时候将 ssthresh 设置为 cwnd / 2,将 cwnd 设置为 1,重新开始慢启动。这种方式属实是太激进了,一旦发生丢包就要将高速的传输速度停了下来,容易造成网络卡顿。所以引出了快速重传的一些保底方案。
- 快速重传算法保活:当接收端发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速地重传,不必等待超时再重传。TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,cwnd 减半为 cwnd/2,然后 sshthresh = cwnd,当三个包返回的时候,cwnd = sshthresh + 3,也就是没有一夜回到解放前,而是还在比较高的值,呈线性增长。
8、TCP 拥塞控制的优化方案
TCP 的拥塞控制主要来避免的两个现象都是有问题的:
- 第一个问题是丢包并不代表着通道满了,也可能是管子本来就漏水。例如公网上带宽不满也会丢包,这个时候就认为拥塞了,退缩了,其实是不对的。
- 第二个问题是 TCP 的拥塞控制要等到将中间设备都填充满了,才发生丢包,从而降低速度,这时候已经晚了。其实 TCP 只要填满管道就可以了,不应该接着填,直到连缓存也填满。
为了优化这两个问题,后来有了 TCP BBR 拥塞算法
。它企图找到一个平衡点,就是通过不断地加快发送速度,将管道填满,但是不要填满中间设备的缓存,因为这样时延会增加,在这个平衡点可以很好的达到高带宽和低时延的平衡。
9、BBR 算法
标准的 TCP 丢包的时候,会快速的降低发送的速度,因为算法假定丢包都是因为过程设备缓存满了导致的,结果会导致很大一部分带宽被浪费掉。
BBR 算法是如何解决延时的?
- 慢启动开始,以前期的延迟时间为延迟最小值 Tmin,然后监控延迟值是否达到 Tmin 的 n 倍,达到这个阈值之后,判断带宽是否消耗尽并且使用到了一定的缓存,进入排空阶段。
- 指数降低发送速率,直至延迟不再降低。这个过程原理同第一步。
- 协议进入稳定运行状态。交替探测带宽和延迟,且大多数情况下都是处于带宽探测阶段。
10、epoll 的 windows 替代策略 IOCP
- epoll 是异步通知,当事件发生的时候,通知应用去调用 IO 函数获取数据。
- IOCP 是异步传输,当事件发生时,IOCP 机制会将数据直接拷贝到缓冲区里,应用可以直接使用。