TCP协议头
序号生成规则:
ISN = M + F (localhost, localport, remotehost, remoteport)
M是一个整数,每隔4ms会加1,F一般是HASH函数。
首部长度
因为选项字段是变长的,需要告知对端TCP头的长度是多少。
常用的6个标志位:
• URG 紧急指针( urgent pointer)有效,配合紧急指针使用
• ACK 确认序号有效。
• PSH 接收方应该尽快将这个报文段交给应用层。
• RST 重建连接。
• SYN 同步序号用来发起一个连接。
• FIN 发端完成发送任务。
URG和PUSH的区别:
URG:通过紧急指针告诉接收端该报文中前多少位是紧急数据,该部分数据不存入接收端缓冲区Buffer,直接上送给上层应用,特别的情况是当通告窗口为0时,发送端依然可以发送紧急数据。
PSH:告知接收端将该报文以及当前缓冲区内的数据都尽快上报给应用层,窗口为0时不能发送。
在报文中,不会出现下面不合法的标志位:
- 所有标志位都为0。
- SYN和FIN同时被置1。
- SYN和RST同时被置1。
- FIN和RST同时被置1。
- FIN位被置1,但ACK位没有被置1。
- PSH位被置1,但ACK位没有被置1。
- URG位被置1,但ACK位没有被置1。
TCP选项
EOP:告知选项结束
NOP:没有特殊含义,一般用于将TCP选项的总长度填充为4字节的整数倍。
MSS:TCP连接初始化时,通信双方使用该选项来协商最大报文段长度,防止IP分片。每一方都有用于通告它期望接收的MSS选项(MSS选项只能出现在SYN报文段中)。如果一方不接收来自另一方的MSS值,则MSS就定为默认值536字节(这个默认值允许2 0字节的IP首部和2 0字节的TCP首部以适合576字节IP数据报)。计算方法如下:
WSOPT:TCP连接初始化时,通信双方使用该选项来协商接收通告窗口的扩大因子,只能出现在同步报文段中,否则将被忽略。取值范围是0-14,由于默认窗口大小为16位,最大只能传输65535字节的数据,实际接收方能接收的字节数远大于65535,会出现如下情况,A、B均处于等待状态,65535字节的数据在网络中传输。
通过窗口扩大选项,可将窗口大小通知为256K,可以充分利用网络及A、B两边的缓冲区,如下图所示:
SACK-permitted:如果某个TCP报文段丢失,则TCP模块会重传最后被确认的TCP报文段后续的所有报文段,原先已经正确传输的TCP报文段也可能重复发送,降低了TCP性能,SACK技术使TCP模块只重新发送丢失的TCP报文段,不用发送所有未被确认的TCP报文段。通过修改/proc/sys/net/ipv4/tcp_sack内核变量来启用或关闭选择性确认选项。
SACK:该选项的参数告诉发送方本端已经收到并缓存的不连续的数据块,从而让发送端可以据此检查并重发丢失的数据块。因为一个块信息占用8字节,所以TCP头部选项中实际上最多可以包含4个这样的不连续数据块(考虑选项类型和长度占用的2字节)。
图片来源于网络
TSPOT:时间戳选项。该选项提供了较为准确的计算通信双方之间的回路时间(Round Trip Time,RTT)的方法和解决了编号回绕问题。可以通过修改/proc/sys/net/ipv4/tcp_timestamps内核变量来启用或关闭时间戳选项。
连接建立
又叫三次握手,通过三次握手,指定发送方和接收方收报文和发报文均没有没问题,在三次握手过程中会协商序号以及选项的支持情况。
发送端:发送端发送syn报文后,处于syn-sent状态,等待接收端返回ack,以及syn报文,然后再发送ack响应报文,处于established状态。
接收端:先启动监听,处于listen状态,等待发送端发送连接请求,收到连接请求后,向发送端发送ack、syn报文,处于syn-rcvd状态,待发送端发送ack后建立连接,处于established状态。
发送端和接收端的状态情况如下所示:
连接断开
又叫四次分手,当接收端收到fin报文时,也正好想断开连接,会将ack报文和fin报文一起发送,此时会是三次分手。
接收端:在接收端收到断开连接请求后,会直接向发送端发送ack报文,然后处于closed-wait状态,待上层应用处理完成后欲断开连接时,会向发送端发送fin报文,同时处于last-ack状态,等待发送端回ack报文,如果在超时时间内没有收到ack,会向发送端再发送一次fin报文。
发送端:向接收端发送fin报文,然后处于fin-wait-1 状态,等待接收端返回的ack报文,待收到接收端发送过来的ack报文后处于fin-wait-2状态,收到接收端发送过来的fin报文后,再想接收端发送ack报文,此时处于time-wait状态,最大等待时长一般为2倍的MSL(Maximum Segment Lifetime,最长报文生存时间),用于当出现ack丢包时,还能收到接收端超时重发的fin报文。当发现设备上time-wait状态的连接过多时,一般就要检查是否是存在短连接的反复的连接和断开。
发送端和接收端的状态情况如下所示:
将建立连接和断开连接放在一起,就是下面这个很经典的TCP状态机。
其中:
- 粗线是主要流程,细线为非主要流程
- 实线为发送端流程,虚线为接收端流程
- 数字序号为连接建立流程
- 中文序号为连接断开流程
如何成为一个靠谱的协议
TCP通过解决如下5个问题,尽可能的成为一个靠谱的协议
- 顺序问题
- 丢包问题
- 连接维护
- 流量控制
- 拥塞控制
下面一起看看TCP通过哪些机制解决上面5个问题
报文确认机制
单独确认
将收到的报文一个ID一个ID的确认
累计确认
应答某个 ID,表示该ID之前的所有报文都收到了
SACK
TCP选项中已经描述
Advertised window 通告窗口
指的是接收端向发送端回应的窗口大小
发送端
- 整个加粗黑框内为通告窗口的大小
- LastByteAcked之前的为已经发送且已经收到ack确认的报文
- LastByteSent之前为已经发送但未收到ack的报文
接收端
- MaxRcvBuffer:接收端最大缓存的量
- LastByteRead 之后是已经接收且回了ack,但是还没被应用层读取的报文
- NextByteExpected之后为还没有回ack的报文
- AdvertisedWindow=MaxRcvBuffer-(NextByteExpected-LastByteRead)
还可能出现下面这种情况:
发送端一直在发送报文,接收到的窗口大小一直在减小,一直减小到LastByteAcked和 LastByteSent在一个位置,即窗口大小为0了。如下图所示:
接收端一直在接收报文,但是上层应用一直不读取缓冲区数据,慢慢的缓冲区满了,就会通知发送端当前窗口大小为0。
当出现窗口大小为0时,发送方会定时发送窗口探测数据包,看是否有机会调整窗口的大小。当接收方比较慢的时候,要防止低能窗口综合征(空出一个字节来就赶快告诉发送方,然后马上又填满了,依次反复,浪费网络资源,以及发送端和接收端系统资源-组装与解析报文),可以当窗口太小的时候,不更新窗口,直到达到一定大小,或者缓冲区一半为空,才更新窗口。
Sliding Window 滑动窗口
下面这个图比较简明的介绍了滑动窗口机制,注意告知的rwnd大小包含已经收到但未向发送端回ack的报文。
重传机制
超时重试:对每一个发送了,但是没有 ACK 的包,都有设一个定时器,超过了一定的时间,就重新尝试,该时间要大于往返时延RTT(Round-Trip Time),TCP 通过采样 RTT 的时间,然后进行加权平均,算出一个值,不断变化的,重传时间是不断变化的,我们称为自适应重传算法(Adaptive Retransmission Algorithm)。
假设5、6、7 都超时了,就会重新发送。接收方发现 5 原来接收过,于是丢弃 5;6 收到了,发送 ACK,要求下一个是 7,7 不幸又丢了。当 7 再次超时的时候,有需要重传的时候,TCP 的策略是超时间隔加倍。每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。
快速重传:当接收方收到一个序号大于下一个所期望的报文段时,就检测到了数据流中的一个间格,每收到一个这样的报文,就会发送一起期望的ack,当连续发送三个冗余的 ACK,客户端收到后,就在定时器过期之前,重传丢失的报文段。
假设接收方发现 6、8、9 都已经接收了,就是 7 没来,那肯定是丢了,于是发送三个 6 的 ACK,要求下一个是 7。客户端收到 3 个,就会发现 7 的确又丢了,不会等待定时器超时,马上重发7号报文。
拥塞控制
发送方维持一个叫做拥塞窗口cwnd(congestion window)的状态变量。滑动窗口 的rwnd (接收窗口)是怕发送方把接收方缓存塞满,而拥塞窗口 cwnd,是怕把网络塞满。拥塞窗口和滑动窗口共同控制发送的速度。发送端通过下面公司控制发送报文数量。
LastByteSent - LastByteAcked <= min {cwnd, rwnd}
理想的情况是下面这种情况,发送报文和接收报文正好使网络占用率最高。
丢包恢复机制
一条 TCP 连接开始,cwnd 设置为一个报文段,一次只能发送一个;当收到这一个确认的时候,cwnd 加一,于是一次能够发送两个;当这两个的确认到来的时候,每个确认 cwnd 加一,两个确认 cwnd 加二,于是一次能够发送四个;当这四个的确认到来的时候,每个确认 cwnd 加一,四个确认 cwnd 加四,于是一次能够发送八个,(一直发送到慢启动门限ssthresh(大部分是65535)个时),每收到一个确认后,cwnd 增加 1/cwnd,我们接着上面的过程来,一次发送八个,当八个确认到来的时候,每个确认增加 1/8,八个确认一共 cwnd 增加 1,于是一次能够发送九个,变成了线性增长。
慢启动:sshresh 设为 cwnd/2,将 cwnd 设为 1,容易造成网络卡顿
快速恢复:当接收端发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速的重传,不必等待超时再重传。TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,cwnd 减半为 cwnd/2,然后 sshthresh = cwnd,当三个包返回的时候,cwnd = sshthresh + 3
两种情况对比如下所示:
TCP BBR(Bottleneck Bandwidth and Round-trip propagation time)
当前TCP协议处理丢包存在的问题。
问题一:是丢包并不代表着通道满了,也可能是管子本来就漏水。例如公网上带宽不满也会丢包,这个时候就认为拥塞了。
问题二:TCP 的拥塞控制要等到将中间设备都填充满了,才发生丢包,从而降低速度,这时候已经晚了。其实 TCP 只要填满管道就可以了,不应该接着填,直到连缓存也填满。
解决问题:充分利用带宽;降低buffer占用率。
主要为下面3个步骤:
S1:慢启动开始时,以前期的延迟时间为延迟最小值Tmin。然后监控延迟值是否达到Tmin的n倍,达到这个阀值后,判断带宽已经消耗尽且使用了一定的缓存,进入排空阶段。
S2:指数降低发送速率,直至延迟不再降低。这个过程的原理同S1
S3:协议进入稳定运行状态。交替探测带宽和延迟,且大多数时间下都处于带宽探测阶段。
三种情况对比如下所示: