TCP头部介绍
- Source Port 源端口
发送方的端口
- Destination Port 目的端口
接收方的端口
- Sequence Number 序号
如果SYN标志位为1,代表是初始序列号,后续的第一个数据字节的序号为该序号+1
如果SYN标志位为0,代表是本报文段锁发送的数据的第一个字节的序号
- Acknowledgment Number 确认号
期望收到对方的下一个报文段的第一个字节的序号,也就是确认号之前的报文都收到了
- Header Length 首部长度
表明数据起始位置距离TCP报文的起始处的长度,单位是4 bytes
- Resv Reserve保留位
保留给之后使用
- Flags 标志位
- CWR Congestion window reduced 拥塞队列减少
代表收到了一个ECE的报文段,然后进行回应
- ECE Explicit Congestion Notification Echo 显式拥塞通知
如果SYN为1,代表ECN打开
如果SYN不为1,在ip层会有一个congestion experienced字段,代表对sender的拥塞迹象的提醒
- URG 紧急
紧急指针有效
- ACK 确认
确认序号有效,除了第一个SYN,其他所有的tcp报文都应该有这个字段
- PSH 催促功能
要求应用层尽快处理
- RST 重制
TCP连接重置
- SYN 同步序列号
只有双方发送的第一个报文段才会有这个标志
- FIN 结束
发送方发送的最后一个报文段
- Window Size 窗口大小
接收方的接受窗口大小,单位是window unit
- TCP Checksum 校验和
用来进行错误校验,校验的范围包括首部和数据以及IP伪首部,伪首部包含了发送方接收方的ip地址、TCP的协议号以及tcp报文的长度(单位是byte)
- Urgent Pointer 紧急指针
如果URG是1,则表示从序列号到最后一个紧急数据byte的偏移量,单位是byte
- Options 其他选项
每个option由三部分组成:类型(1 byte),长度(1 byte),数据(可变),只有类型是必填的
option中包含的字段如下(from wiki pedia)
- Padding 对齐
全部是0,保证整个头部长度是32 bit的倍数
首部校验和
保持首部和数据的校验和,以检测数据在传输过程中的变化。使用Checksum字段
包编号和累计确认
可靠,对应到网络协议上,就是客户端每发送一个包,服务器端就应该有回复。如果一定时间没有回复,就会重新发送。
在平常的交流或者工作安排中,如果只是收到序号为n的应答,发送序号n+1的包,这样的效率非常慢,一头没有处理完的话,另外一头就要干等着。所以当接收方在处理的时候,发送方也可以交代新的任务
由于任务是按照顺序的,这样对于接收方就可以先来的先处理,在汇报的时候,直接汇报最近处理的任务就行了,就表示之前的都处理完了。
对应到TCP协议中,为了保证顺序性,每一个包都有一个ID,在建立连接的时候就会商量起始的ID,然后后面按照ID一个个发送,接收方在接受完过后,就会应答最近接受的一个ID,表示之前的全部接受并接受了。这就叫做累计确认(cumulative acknowledgment)
发送接收缓冲区
为了记录所有发送的包和接收的包,TCP在发送端和接收端分别有缓存来保存这些记录。
发送端
对于发送端来说,缓存按照包的ID一个个排列,根据处理的情况分成四个部分
- 发送了并且已经确认的
- 发送了但是没有确认,没有收到ack包
- 没有发送,但是等待发送的(只要前面有新确认的,这些就可以发送了)
- 没有发送,并且暂时还不会发送的
为什么要区分第三部分和第四部分?把握分寸,控制节奏
在TCP中,接收端会给发送端一个窗口的大小,叫做Advertised Window,这个窗口的大小等于第二部分加上第三部分。超过了这个大小,接收端就会处理不过来。
发送端需要保持下面的数据结构:
- LastByteAcked:第一部分和第二部分的分界线
- LastByteSent:第二部分和第三部分的分界线
- LastByteAcked + AdvertisedWindow:第三部分和第四部分的分界线
接收端
对于接收端来说,也会根据ID组织接收到的包的情况,内容更简单一点
- (接收并且确认,并且应用层读取了)
- 接收并且确认了的,应用层还没有读取
- 还没有接收,但是马上能接收的,是能接受的最大工作量
- 还没有接收,但是也没办法接受的
> 最左边那条线叫做LastByteRead
- 红色的部分就是MaxRcvBuffer,也就是能接受的最大缓存的量,固定值,
- 等待接收未确认的部分可以根据应用层的处理情况动态调整大小
- LastByteRead到NextByteExpected就是已经发送了ACK,但是应用层没有读取的包
- AdvertisedWindow就是MaxRcvBuffer减去已接收已确认的部分
顺序问题和丢包问题
在上面的图中,可以看出
- 4,5的ACK已经发出了,但是4,5的发送端还没有收到
- 6,7,8,9已经发出了
如果8,9的包先到了接收端,6,7没有到,发送端也只能缓存这8,9没办法ACK
所以在发送过程中,顺序问题和丢包问题都可能发生,这就需要确认与重发的机制
发送端超时重试
对于每一个发送了,但是没有ACK的包,都设置一个定时器,超过了一定时间就重新尝试。
超时的时间需要TCP机制通过采样RTT的时间,然后进行加权平均,算出一个不断变化,不断适应的值,这个值肯定是大于RTT时间的。
计算的算法叫做自适应重传算法
当一个包多次超时丢失的时候,TCP的策略是时间间隔加倍,每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。
快速重传算法
当接收方收到了多个包的序号都比期望序号大的时候,就会发送冗余的ACK,依然是期望那个没有收到的包。例如收到了4,期望收到5,但是6,7,8都到了,就会发送三个重复的4的ACK
当发送方收到重复的ACK后,就会马上重新发送
SACK
在TCP头部加一个SACK的东西,接收方将本地将缓存的地图发送给发送方,发送方就能知道哪些需要立刻重传了
流量控制(照顾接收方,别人收不过来了)
接收方在发送ACK的时候,也会携带一个窗口的大小rwnd,表示接收方的处理能力,在发送方控制AdvertisedWindow的大小。
> 发送方收到了4的确认,窗口大小为9
> 发送方把10,11,12,14都发了,收到了6的确认,窗口大小变为8,所以右边不动
> 接收方的上层应用一直不处理,ACK的窗口一直变小,变成0
> 发送方收到14的确认后,窗口调整为0
如果接收方处理地确实慢,就可以设置为0,发送方将会暂停发送。然后发送方会定时发送窗口探测数据包,看是否有机会调整窗口的大小
当接收方低能的时候,窗口变大了一点,不要太着急告诉发送方,等到窗口达到一定大小或者缓冲区一半为空才更新窗口
拥塞控制(照顾网络环境,网络太堵了)
拥赛控制也是通过窗口大小来控制,cwnd,怕把网络塞满
发送方的LastByteSent-LastByteAcked(发送未确认) <= min{cwnd, rwnd}
注意,AdvertisedWindow的大小由rwnd来控制,当cwnd<rwnd的时候,LastByteSent-LastByteAcked < rwnd,差距的那部分就是发送方未发送可发送的部分
发送方如何判断网络的堵塞程度?对于TCP协议来说,网络环境是黑盒,把整个网络路径当成一个通道,那么通道的容量 = 带宽 x 往返延迟
如果发送方已经把通道塞满了,再发送新的包,中间的设备就可能会把多出的包丢掉。就会出现包丢失,然后超时重传的现象。一旦出现这些现象,也表明发送太快了,需要调整拥塞控制的端口
一条TCP链接开始,cwnd设置为1
- 一次只能够发送一个,当收到一个确认的时候,cwnd++
- 一次能够发送两个,当两个包的确认来的时候,cwnd+2
- 一次就能够发送四个,当四个包的确认到达后,cwnd+4,一次能够发送8个。
cwnd在tcp建立一开始指数增长,一直到达一个阈值ssthresh,一般为65535
当超过这个值的时候,cwnd的增长速度变为线性的,每收到一个确认,cwnd增长1/cwnd,当把一个窗口中的所有包发完并收到确认后才会++
但是这样cwnd还是会增长,后面会出现拥塞,表现为丢包。这个时候就将sshresh设置为cwnd/2,将cwnd设为1,重新开始慢启动。但是这种方式非常激进,会造成严重的网络卡顿
丢包后的快速重传算法中,如果收到了一个包的三次ACK,发送方就会快速重传,不必等待超时。
这种情况下网络环境拥塞问题是不严重的,所以sshresh设置为cwnd/2,cwnd减半为cwnd/2,然后开始cwnd线性增长,当三个包返回的时候,cwnd = sshresh + 3
TCP BBR拥塞算法
从上面可以看出,传统拥塞算法都是有问题的
- 丢包并不意味着通道满,可能是网络环境本来就不好,带宽不满也会丢包,这样降低cwnd是不对的
- 传统拥塞控制会等到中间设备都填满了才会发生丢包,然后降低速度,这时候已经晚了。只要TCP填满管道就可以了,而不是把中间设备的缓存都填满
从而提出了BBR拥塞算法,会不断试探网络环境,把管道填满,但是不填满中间设备的缓存