这是我参与「第三届青训营 -后端场」笔记创作活动的第6篇笔记
1、可靠数据传输协议
传输层是在“以网络层IP协议为底层”的基础上传输数据的,而网络层不保证数据的可靠传输。
1、基于“停-等”机制
停等机制的思想是,发送方先发送一个分组,等待接收方的响应,然后才能继续发送分组。这种方式保证分组不会乱序到达。
需要考虑这些问题:
-
分组内部可能出现数据错误
- 产生原因:可能由于各种原因,造成到达的数据流与实际发送的不一致
- 解决方案:使用校验和进行差错检测,接收方接收到数据后,就向发送方反馈信息是否有误(ACK/NCK)
-
接收方可能收到重复分组
-
产生原因:
- 如果出现差错,接收方会向发送方反馈“错误信息”,发送方会重传分组。
- 发送方的控制信息也可能出现错误,所以也有校验和机制。如果控制信息出错,发送方就直接重传,但可能原本的控制信息是ACK,就导致接收方收到了重复分组
-
解决方案:
- 为每个分组添加唯一标识,接收方收到重复的分组就直接丢弃
- 有了唯一标识之后,可以弃用NCK,只需要使用ACK+最后一个有效的分组编号,同样可以让发送方知晓应该发送哪个分组
-
-
分组在传输途中可能丢失
- 产生原因:可能网络发生了拥塞,或者其他各种原因,导致该分组没有正确到达接收方
- 解决方案:在接收方维护一个计时器,在规定时间内没有收到接收方的反馈,就重传该分组,这叫超时重传
2、基于“流水线”机制
流水线机制允许发送方连续发送多个分组,这产生了两个问题:
- 需要更大的序列号范围,才能唯一标识每个分组。因为连续发送的多个分组都有可能出现问题。
- 接收方需要缓存发出的分组,直到它被确认收到
接收方收到一个分组就返回一个ACK,重复和乱序到达的分组直接丢弃
3、基于“滑动窗口”机制
窗口指的是“允许使用的序列号的范围”,等待确认的消息个数不能超过窗口的宽度。
绿色:已经发送成功(接收方响应ACK成功)
黄色:已经发出,还没确认
蓝色:即将发出,但还没发出的
白色:待使用的序列号
滑动窗口协议产生的序列号是连续的,避免序列号重复导致分组混乱。这样接收方就能实现知道下一个分组的序列号,实现对重复分组或乱序分组的处理。
对乱序分组的两种做法
收到了乱序分组,可以直接丢弃,响应正确收到的最后一个分组序列号,让发送方重新发送。这样可以对一群分组进行统一确认,减少控制消息发送次数,减少控制消息发生错误的可能,进而避免额外的分组重传浪费资源。但丢弃掉正确但乱序的分组,本身也浪费资源
也可以选择不丢弃,而是缓存起来。这就需要:
- 接收方对每个分组进行单独确认
- 接收方也维护一个滑动窗口,才能知道哪些分组还没有被正确接收
4、总结
实现可靠数据传输,最好的实践是滑动窗口+流水线机制。
它的分组至少需要包含以下信息:
- 校验和,用于判断分组内部数据是否出现错误
- 序列号,用于标识一个分组,有两个作用:判断是否重复、判断是否乱序到达
- 滑动窗口信息
2、TCP概述
1、TCP的头部格式
-
序列号:
- 在建立连接时,发起连接的一方生成一个随机数作为序列号的初始值,通过SYN报文发给接收方
- 每发送一次数据,序列号就累加一次
- 作用:解决数据包的乱序问题、重复问题
-
确认应答号:
- 接收端会填入它上次收到的序列号+1的值,含义是期望收到的下一个数据包的序列号的值
- 发送端收到这个应答号,含义是这个序列号之前的序列号对应的数据包都被正确接收,接收端需要这个序列号的数据包
- 作用:解决数据包的丢包问题
-
控制位
- ACK:为1时,表示确认应答号字段有效。TCP规定除了建立连接时的SYN包之外,通信过程中这个位必须是1
- RST:为1时,表示TCP连接异常,必须强制断开连接
- SYN:为1时,表示申请建立连接,并且使用这个数据包的序列号作为初始序列号
- FIN:为1时,表示申请断开连接。
2、TCP的特点
TCP是一个面向连接的、可靠的、基于字节流的全双工传输层协议。
- 面向连接:一个发送方、一个接收方,一对一发送消息
- 可靠:能保证一个数据包最终一定能正确到达接收方
- 有序的字节流:不限制消息的大小,而且确保接收方最终收到的消息是有序的
- 全双工:同一个连接中能够双向传输数据流
3、TCP工作在哪一层
网络层的IP协议是不可靠的,它不保证数据包的可靠交付,也不保证数据包交付的顺序,而且不会对数据包做差错检测。
如果要保证可靠交付,就需要利用IP协议上层的传输层TCP协议,由TCP来决定要发送哪个数据包,然后交由IP协议进行发送即可。
3、关于TCP连接
1、什么是TCP连接
建立一个TCP连接,需要通信双方达成三个共识:
- Socket:四元组,包括IP地址和端口号
- 序列号:用来解决重复、乱序问题
- 窗口大小:用来做流量控制
2、如何唯一确定一个TCP连接
TCP连接四元组:
- 源IP地址、源端口号
- 目的IP地址、目的端口号
其中,源IP、目标IP用于封装IP头部,通过IP协议把数据包发给接收方
源端口、目的端口用于封装TCP头部,作用是告诉通信双方应该把数据包交给哪个进程
3、一个端口的最大连接数
服务器通常是固定IP、固定监听某个端口的。
所以理论上,它能建立的连接数 = 客户端IP个数 * 客户端端口个数
- IPV4最多有2的32次方个
- 端口号最多有2的16次方个,因为TCP协议的端口号字段有16位
但是实际上服务器并不能为所有IP地址和所有端口建立TCP连接,原因有两个:
-
操作系统的文件描述符限制
- 每个TCP连接都被看做一个文件,占用一个文件描述符
- 文件描述符是有上限的,如果文件描述符被占满了,会报错说too many open files
-
服务器的内存限制
- 每个TCP连接都会占用一定的内存
- 服务器的内存是有限的,达到上限会OOM
4、TCP与UDP
1、UDP的特点
UDP协议唯一的功能就是,利用IP协议做到数据传输。它是面向无连接的、不保证可靠的。
2、UDP的头部格式
- 源端口、目标端口:用于指定数据包交付给哪个进程
- 校验和:能标识当前UDP包是否出现错误
- 包长度:UDP头部+数据的总长度
1、为什么UDP头部没有“头部长度”字段
因为TCP头部有可变长的选项字段,所以TCP头部的长度不是固定的,需要标识出来才能知道数据从哪开始
UDP头部是固定的8个字节,长度不会变化,所以不用额外记录长度
2、为什么TCP头部没有“包长度”字段
包长度字段主要是为了计算出数据长度。知道了包长度,也知道了头部长度,就能知道数据长度。
知道了数据的起始点,知道了数据的长度,也就知道了具体的数据内容。
TCP计算出数据长度的方式:
- TCP数据长度 = IP数据长度 - IP头部长度 - TCP头部长度
- 其中的IP数据长度 和 IP头部长度,都记录在了IP头部中。
UDP其实也能通过这个公式计算出数据长度,但还有一个考量:
- 网络设备为了处理方便,要求头部长度是4字节的整数倍
- 可能是为了把UDP头部凑成4字节的倍数,引入了一个冗余的包长度字段
3、TCP和UDP的区别
-
连接方面:
- TCP是面向连接的,每次通信之前要先建立连接
- UDP是面向无连接的
-
通信方面:
- TCP是一对一的双方通信
- UDP支持一对一、一对多、多对多
-
可靠性:
- TCP是可靠交付数据的,保证数据不出错、不丢失、不重复、不乱序
- UDP是尽力交付,不保证可靠
-
拥塞控制、流量控制:
- TCP具有拥塞控制、流量控制机制
- UDP没有这些机制,即使网络非常拥堵,UDP也感受不到,继续正常发送数据
-
头部开销:
- TCP首部较长,默认是20字节,如果使用了选项字段,头部会变得更长,开销较大
- UDP首部较短,只有8个字节,固定不变,开销较小
-
传输方式:- TCP是流式传输,没有边界
- UDP是一个包一个包传输,所以有边界
-
数据分段:- TCP的数据如果超过MSS大小,会在传输层分片。如果某个分片丢失,只需要重传丢失的分片
- UDP的数据不会在传输层分片,会直接交给IP层。数据如果超过MTU,会在IP层分片,后续如果丢失就丢失了。
-
扩展性:
- TCP是写死在Linux内核中的,不好修改,它规定了很多功能,应用层也不好扩展,只能去使用它的特性
- UDP可以在应用层去实现一些特性,这是可插拔的,扩展性非常好,因为应用层的限制是很小的,很轻易可以实现跨平台
4、TCP和UDP的使用场景
TCP的特点是面向连接、可靠数据交付,常用于:
- HTTP、HTTPS协议
- FTP文件传输
UDP的特点是轻量化、简单高效、没有流量控制和拥塞控制,常用于:
- 要求延迟低的场景,比如视频、语音
- 包总量较少的通信,因为没必要为了几个包去花费建立连接的开销
- 广播通信
- TCP功能无法满足的场景,需要在应用层去扩展UDP
对于一个特定的场景,有两个选择依据:
-
是否允许少量丢包,如果不允许那一般就使用TCP,也可以在应用层去给UDP实现可靠数据传输。 -
数据包的大小,如果数据包小于MTU,那就可以用UDP,否则建议使用TCP因为TCP会做分段工作,它会保证每个TCP段都小于MTU。而UDP会直接把数据包交给IP协议进行分片,IP对数据分片和组装的效率很低。
5、深入研究三次握手
1、双方需要交换哪些信息
一次连接需要这些信息:
-
Socket:
- 通信双方的IP、端口号,用于基本的通信。也叫TCP四元组,能唯一确定一个连接。
- 源IP和目的IP在IP头部中,用于找到主机。源端口与目的端口在TCP头部中,用于找到进程。
-
序列号:双方需要指定初始序列号,每个分组的序列号自增,用于判断重复分组与乱序分组
-
窗口大小:用于接收方的流量控制
-
另外,三次握手还协商了MSS的大小,取决于较小的一方。
2、三次握手的过程
三次握手(Three-way Handshake)其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。
刚开始客户端处于 Closed 的状态。服务端主动监听某个端口(应用程序中指定的),处于 Listen 状态。
三次握手的过程如下:
-
第一次握手(客户端向服务器发送一个 SYN 报文):
- 客户端构造一个SYN报文,TCP头部的SYN标志位置为1
客户端随机初始化一个序列号(client_isn)作为初始序列号,填入序列号字段- 这个报文不能携带应用层数据,但要消耗掉一个序列号
- 发送报文,此时客户端处于
SYN_SENT状态。
-
第二次握手(服务器回应客户端SYN+ACK报文):
- 服务器收到客户端的 SYN 报文之后,也构造一个SYN报文,把SYN置为1,ACK置为1
服务器随机初始化自己的序列号(server_isn)作为初始序列号,把这个序列号填入序列号字段把确认应答号字段设置为 客户端初始序列号 + 1- 这个报文也不能携带应用层数据
- 发送报文,此时服务器处于
SYN_RCVD的状态。
-
第三次握手(客户端向服务器响应ACK报文):
- 客户端收到 SYN 报文之后,构造一个ACK报文,把ACK标志位置为1
把确认应答号字段设置为 服务器初始序列号 + 1- 客户端向服务器发送这个ACK报文
- 此时客户端处于
ESTABLISHED状态。(ESTABLISHED:已确认的) - 服务器收到 ACK 报文之后,也处于
ESTABLISHED状态。此时双方已经成功建立了连接。 - 第三次握手可以携带应用数据
3、为什么需要三次握手
三次握手有四个原因:
- 确认通信双方的接收能力和发送能力是否正常
- 防止服务器通过历史SYN擅自建立连接(主要原因)
- 保证双方的初始序列号可靠交换
- 避免服务器资源浪费
1、确认双方的通信能力
第一次:客户端发送网络包,服务端收到了。
- 客户端指定自己的序列号(client_isn)
- 服务器已知客户端发送能力没问题,服务器的接收能力没有问题
第二次:服务端发包,客户端收到了。
- 服务器收到客户端的消息,指定自己的序列号(server_isn),把确认应答号字段设置为 client_isn+1
- 客户端已知服务器接收、发送能力没有问题,客户端的接收、发送能力也没有问题
第三次:客户端发包,服务端收到了。
- 客户端检查“确认应答号”字段,发现值是自己的序列号+1
- 服务器这才能确认,服务器的发送能力、客户端的接收能力没有问题
因此,需要三次握手才能确认双方的接收与发送能力是否正常。
2、避免历史SYN发起连接
三次握手的首要原因,是防止旧的SYN报文初始化连接。
比如客户端发送SYN报文尝试连接,由于网络拥塞,这个包没有及时到达。客户端触发超时重传,又发送了一个SYN报文,这两个SYN报文的序列号不一样。
使用两次握手的情况:
- 服务器一旦收到了客户端的SYN报文,就会立刻响应SYN+ACK,建立连接,等待客户端与其通信。
- 发送方收到响应,发现确认应答号不匹配,就发送RST报文终止连接
- 服务器收到RST报文,只好再断开连接。
使用三次握手的情况:
- 旧的SYN报文先抵达服务器,服务器对其做了SYN+ACK响应,但还没有建立起连接
- 客户端收到响应,发现头部的“确认应答号”字段不匹配(因为旧报文的初始序列号和新的不一样)。客户端发送RST报文,终止连接。
- 随后新的SYN报文抵达服务器,服务器对其做了SYN+ACK响应
- 客户端收到响应,发现头部的“确认应答号”字段成功匹配,就发起第三次握手,建立连接
总结,如果使用两次握手的弊端:
-
三次握手的情况,服务器在发送SYN+ACK后会等待,如果客户端说没问题再建立连接。
-
如果只是两次握手,服务器在发送SYN+ACK后会立即建立连接,如果客户端说有问题才能断开连接
这就造成服务器建立起很多无效的连接,浪费了资源。
解决方法就是,每次建立连接之前都先询问一下客户端,这就是第三次握手的思想。
3、交换初始序列号
在可靠数据传输过程中,序列号非常重要,它的作用是:
- 接收方判断分组是否重复
- 接收方判断分组是否按序到达
- 发送方判断哪些数据被成功接收
所以在建立连接时,需要客户端告诉服务器自己的序列号,然后服务器告诉客户端成功收到,服务器也告诉客户端自己的序列号,客户端随后告诉服务器成功接收。
每次发送一个消息,都需要对方回应才能证明对方成功收到。如果是两次握手,那么肯定有一方无从得知对方是否正确收到了自己的序列号
过程:
- 客户端告诉服务器自己的序列号
- 服务器回应收到
- 服务器告诉客户端自己的序列号
- 客户端回应收到
其中的第2、3步可以合并,所以成了三次握手
4、避免服务器资源浪费
如果采用两次握手,客户端恶意连续发送超级多个SYN报文
服务器每收到一个SYN就建立一个连接,就会建立很多冗余的连接,耗尽服务器的资源。
4、为什么不选择更多次握手
三次握手是理论上握手次数最少的可靠连接方式,追加更多次握手也不能使连接更可靠。
世界上不存在完全可靠的通信协议。从通信时间成本空间成本以及可靠度来讲,选择了“三次握手”作为点对点通信的一般规则。
5、关于序列号
1、为什么每次建立连接都随机指定序列号
有两个原因:
-
避免旧数据被一个具有相同四元组的连接接收到- 如果该四元组的上次连接异常关闭,那么网络中可能残留着上次连接的旧数据
- 如果每次都从一个数开始递增序列号,如果旧数据的序列号正好处于接收方的接收窗口内,就会错误接收到不属于本次连接的数据。
- 只要每次都随机指定序列号,那么旧数据的序列号基本不可能在新的接受窗口范围内,也就不会收到旧数据
-
保证安全性,防止黑客模拟出一个符合序列号规则的报文,让接收端接收- 每次连接都随机指定序列号,而不是以一个固定的数字开始,这样黑客就无从得知序列号的数值。
TCP断开连接时四次挥手,设计了TIME_WAIT状态,为什么还会存在历史报文?
按理说TIME_WAIT会等待2MSL,这个时间足够这次连接产生的所有报文都被丢弃了,再建立起新的连接,网络中不可能有旧连接的数据包存在才对。
但是,并不能保证每次连接都正常通过四次挥手关闭,还是要多重保险。
不过服务器进程被强制停止后,内核会自动触发四次挥手的正常流程。
有两种异常断开会残留旧数据的情况:
- 服务器掉电,不过连接不存在了,不会接收旧数据
- 服务器重写了内核的MSL时长,而且设得较小,导致TIME_WAIT时长较小,不足以让所有的报文丢弃
而且如果在TIME_WAIT状态下收到了合法的SYN报文,会复用之前的四元组连接,所以还是可能存在旧数据。
2、为什么发送方和接收方的初始序列号不一样
TCP是全双工的,也就是说通信的任意一方都可以充当发送端,向另一方发送消息。
假设通信双方为A和B,此时就相当于有两条线路:
- A的序列号,对应B的接收窗口
- B的序列号,对应A的接收窗口
它们双方是互不影响的,如果让它们使用相同的序列号规则,比如建立连接时由建立连接的一方随机一个序列号,作为双方的初始序列号,正常情况下也没有问题。
但是TCP的设计中,单边通信也是涉及到双方的通信的,因为接收方要向发送方响应ACK,所以发送方和接收方的序列号初始值绝对不能相同。
3、初始序列号如何产生
随机生成序列号,还是有可能随机成一样的,就还是有收到历史报文的风险。
解决方案是,生成序列号时加上时间戳的逻辑。
早期,初始的序列号(ISN)是基于时钟的,每4毫秒+1,绕一圈需要四个多小时。
后来RFC提出了一个更好的随机生成ISN的算法:
ISN = M + F (localhost, localport, remotehost, remoteport)
- M是一个计时器,每4毫秒+1
- F是一个哈希算法,根据源IP、源端口、目的IP、目的端口,生成一个随机数。
4、通信过程中序列号的回绕
虽然初始序列号已经不可能重复,但是通信过程中,这个序列号要一直递增,它并不是无限递增的。
序列号是一个32位的数据,上限是4G,在到达上限后会回到0继续递增。
在频繁的网络场景中,序列号很快就会回绕,导致序列号重复出现,还是有可能出现历史报文被接收的问题。
5、TCP时间戳
由于序列号存在回绕的问题,所以无法根据序列号的大小关系来判断两个数据包发送的时间关系。
使用TCP时间戳来解决序列号回绕的问题。
tcp_timestamps 参数是默认开启的。开启之后,TCP头部就会启用时间戳选项,它可以避免序列号回绕。
它的思想是:
- 发送每个数据包,都会携带一个递增的时间戳
- 收到数据后,如果时间戳小于最近收到数据的时间戳,就能说明这是一个历史数据,可以丢弃。
6、什么时候消耗序列号
TCP的控制报文,比如SYN和FIN,也是会消耗一个序列号(seq)的。
但是ACK不会消耗序列号
7、通信过程中序列号的增长
建立连接后,双方都有一个初始序列号
后续通信时,会在初始序列号的基础上增加序列号
序列号的含义是,这个数据包的第一个位在整个数据流中的相对位置
6、某次握手丢失了怎么办
1、第一次握手丢失
第一次握手是指,客户端想建立连接,它向服务端发送一个SYN报文,此时客户端处于SYN_SENT状态
如果第一次握手丢失,也就是这个SYN报文丢失,客户端迟迟得不到回应,就会触发超时重传机制,重新发送SYN报文。
这个时间是写死在内核中的,有的是1秒。
如果SYN报文一直丢失,客户端也不会无限重传,而是有一个最大重传上限,可以自己设置,默认是5
而且每次超时重传的时间间隔是上次的2倍。比如第一次等1秒重传,第二次就要等2秒。
如果发生了第五次重传,就会等待32秒,然后就没有重传机会了,客户端就不再尝试发起连接。
总耗时是1+2+4+8+16+32,大概1分钟左右。
2、第二次握手丢失
第二次握手是指,服务端收到客户端发来的SYN报文,它做出SYN+ACK的响应,此时服务端处于SYN_RCVD状态。
这个第二次握手其实包含两个信息:
- 对客户端发来的SYN报文做ACK响应
- 给客户端发送自己的SYN,包含连接信息
那么如果第二次握手丢失,会发生两件事情:
- 客户端收不到ACK回应,就重传SYN报文
- 服务器收不到第三次握手,就重传SYN+ACK报文,到达重传上限就会把该连接信息从半连接队列中删除。
服务器重传第二次握手也是有最大次数的,默认是5
3、第三次握手丢失
第三次握手是指,客户端收到了服务器发来的SYN+ACK,它对服务器做出ACK响应,此时客户端处于ESTABLSH状态。
这个ACK是对服务器发送的第二次握手的响应,如果它丢失了,服务器迟迟没有获得响应,就会重传第二次握手,直到达到最大重传次数。
7、半连接队列与全连接队列
1、概述
半连接队列叫SYN队列,全连接队列叫ACCPET队列
服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立起连接。
服务器会把此种状态下请求连接放在内核的“半连接队列”中,向发送方响应SYN+ACK。
如果服务器收到了该客户端的ACK,此时连接已经建立,服务器将其从”半连接队列“移除,放入”全连接队列“。
应用可以通过Socket接口的accept()函数 ,从“全连接队列”取出连接来使用。
2、全连接队列满了怎么办
如果应用处理连接的速度过慢,就可能导致全连接队列被占满。
这没有办法,因为全连接队列中的都是正常的连接,不能把它们丢弃去维护新的连接。
3、半连接队列满了怎么办
服务器发送完SYN-ACK包,就会把连接放入半连接队列。
如果之后未收到客户端的ACK,服务器就会进行重传。
如果重传次数超过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。
如果半连接队列已满,那新的连接就无法建立。
可以设置这个参数:
net.ipv4.tcp_syncookies = 1
-
当半连接队列满了后,后续服务器收到SYN包,不会进入半连接队列
但也不会直接丢弃,而是给客户端发一个Cookie,包含SYN+ACK的序列号
-
客户端向服务器发起第三次握手,会携带这个Cookie。服务器收到客户端的ACK后,检查cookie的合法性,然后直接放入“全连接队列”。
-
思想是,既然服务器没有地方保存这个连接的状态,就交给客户端来保存,下次检查合法性就能知道它的连接状态。
举个例子:
- 三面过了就可以入职(进入全连接队列),二面过了就会登记在面试官的本本上(半连接队列)
- 如果本子写满了,就给通过二面的候选人发一个短信,之后就算本子上没有他,他也能拿着短信证明自己通过了二面,开始三面。
8、关于SYN攻击
服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的,所以服务器容易受到SYN洪泛攻击。
SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,Server则回复确认包,并等待Client确认,由于源地址不存在,因此Server需要不断重发直至超时,这些伪造的SYN包将长时间占用半连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪。SYN 攻击是一种典型的 DoS/DDoS 攻击。
检测 SYN 攻击非常的方便,当你在服务器上看到大量的半连接状态时,特别是源IP地址是随机的,基本上可以断定这是一次SYN攻击。在 Linux/Unix 上可以使用系统自带的 netstat 命令来检测 SYN 攻击。
netstat -n -p TCP | grep SYN_RECV
常见的防御 SYN 攻击的方法有如下几种:
- 缩短等待第三次握手的超时时间,更快地移除不可用的连接
- 增加最大半连接数
- 过滤网关防护,对请求做限流和分发
- SYN cookies技术
6、TCP的数据分段
一个应用层的数据,会经过TCP加一层头部,再经过IP加一层头部,最后被数据链路层传输。
数据链路层中,一个包的最大长度为MTU,以太网是1500字节。
而除去TCP头部和IP头部后,实际的数据长度称为MSS。
既然IP层会分片,为什么TCP层也要分片?
比如一个TCP报文,包含TCP头部和数据,它超过了MTU大小,IP层就会对其进行分片,分成若干个IP数据报。
发送给目标主机后,由目标主机的IP层进行重新组装,最后交给接收方的TCP层。
但是IP分片是不靠谱的,如果一个TCP报文被分成了多个IP分片,其中一个IP分片丢失,全部的IP分片都要重传。
期间如果发生分片丢失,接收方没有收到完整的TCP报文,IP层就不会把报文交给TCP层。- 接收方的TCP迟迟没有收到数据包,也就不会给发送方响应ACK
- 发送方的TCP就得超时重传整个TCP报文。
- 这是因为TCP的最小单元(报文)被经过了二次切分,只有第一个分片才有TCP头部,后面的分片都只有数据。只能全部重传。
所以,由IP层进行分片效率很低,TCP协议在连接建立时会协商双方的MSS值,由TCP进行数据分片,保证每个数据包在加上IP头部后都不会超过MTU,IP层就不用分片了。
TCP层全权负责分片,每个IP报文都有TCP头部,都会被接收方的IP层交给TCP层,而不用考虑重新组装。如果发生分片丢失,那么TCP是可知该分片的具体细节的,只需要重传该分片即可。
7、深入研究四次挥手
1、四次挥手的过程
连接的双方都可以主动发起断开连接。
刚开始双方都处于 ESTABLISHED 状态,假如是客户端先发起关闭请求。四次挥手的过程如下:
-
第一次挥手(客户端向服务器发FIN):
- 客户端调用close()函数,发送一个 FIN 报文,TCP首部的FIN置为1
- 此时客户端处于
FIN_WAIT_1状态。
-
第二次挥手(服务器向客户端发ACK):
- 服务端收到 FIN 之后,内核会自动发送 ACK 报文,此时服务端处于
CLOSE_WAIT状态。 - 此时的TCP处于半关闭状态,客户端到服务端的连接已经释放了,但服务器到客户端的连接还没有释放
- 客户端收到服务端的确认后进入
FIN_WAIT_2状态,等待服务端发出的FIN报文。
- 服务端收到 FIN 之后,内核会自动发送 ACK 报文,此时服务端处于
-
第三次挥手(服务器向客户端发FIN):
- 服务器处理完数据后,调用close()函数,给客户端发 FIN 报文
- 此时服务端处于
LAST_ACK的状态。
-
第四次挥手(客户端向服务器发ACK):
-
客户端收到 FIN 之后,发送一个 ACK 报文作为应答
-
此时客户端处于
TIME_WAIT状态。此时客户端的TCP未释放掉,需要经过时间等待计时器设置的时间
2MSL后,客户端才进入CLOSED状态。 -
服务端收到 ACK 报文之后,就关闭连接了,处于
CLOSED状态。
-
2、挥手为什么需要四次
假如客户端主动断开连接:
-
客户端发送FIN报文,表示客户端不再发送数据了,但还能接收数据
-
服务器响应一个ACK,表示收到了信息。
但服务器可能还没结束本次连接数据的发送,等本次连接的数据全部发出后,服务端发出FIN报文
-
客户端收到FIN报文,给服务器响应一个ACK
客户端独自等待2MSL,然后断开连接。
每个方向都需要一个FIN和ACK响应,所以挥手需要四次。
与三次握手不同,三次握手中服务器的ACK和SYN是一起发送的。
而断开连接时,服务器通常需要等待数据的发送和处理,所以FIN和ACK需要分开发送。
如果服务器没有什么资源需要发送,也可以将ACK和FIN合并成一次挥手,这样只需要三次挥手。
3、某次挥手丢失了怎么办
1、第一次挥手丢失
第一次挥手,主动断开连接的一方,暂且称为客户端,会发送FIN报文,进入FIN_WAIT_1状态
如果第一次挥手丢失了,客户端就迟迟收不到ACK,就会重传FIN报文
重传达到上限次数,客户端就直接进入CLOSE状态,断开连接。
2、第二次挥手丢失
被关闭的一方,暂且称为服务器,在收到客户端发来的FIN后,会响应一个ACK,进入CLOSE_WAIT状态。
如果这个ACK丢失了,客户端就迟迟收不到服务端的响应,它就会重传FIN报文,重传到上限次数,客户端就直接进入CLOSE状态。
3、第三次挥手丢失
服务器发送完所有的数据,向客户端发一个FIN报文,进入LAST_ACK状态。
-
对服务器来说,如果FIN报文丢失,会迟迟得不到客户端的回应,它会超时重传FIN,直到到达上限次数
-
对客户端来说,它收到服务器的第二次挥手后就已经进入FIN_WAIT_2状态了,这个状态不会持续太久
- 如果等到了服务器的第三次挥手,客户端就会进入TIME_WAIT状态
- 如果60秒内没有等到第三次挥手,客户端的连接就会直接关闭
4、第四次挥手丢失
当客户端收到服务器发来的FIN后,就会响应一个ACK。服务器收到这个ACK后就会进入CLOSE状态。
如果服务器迟迟没有收到ACK,就处于LAST_ACK状态,它会超时重传FIN报文,直到到达上限次数。
4、TIME_WAIT 状态
TIME_WAIT状态也称为2MSL等待状态。
主动断开连接的一方,才有 TIME_WAIT 状态。
1、2MSL的具体值
什么是MSL
MSL是Maximum Segment Lifetime的英文缩写,可译为“最长报文段寿命”,它是一个数据包在网络上存在的最长时间,超过这个时间报文将被丢弃。
因为TCP协议是基于IP协议的,而IP头部有一个“TTL”字段,是IP数据报可以经过的最大路由数,此值为0时该报文段被废弃,同时发送ICMP控制报文通知源主机。所以说,一个数据包在网络中是有寿命的,TTL为0就代表着寿终正寝了。
MSL是时间,而TTL是跳数。不过MSL必须要大于等于TTL消耗为0的时间,来确保报文已经自然消亡。
它的思想就是,TTL有最大跳数,那么在MSL时间内,它差不多已经跳够上限了,要消亡了。
为什么要等待2个MSL
2MSL是从客户端收到服务器的FIN后发出ACK时开始计时的。如果期间又收到了服务器的FIN,会再次发送ACK,然后重新开始计时。
等待2个MSL的原因:
- 客户端向服务器发送最后一个ACK,最慢的情况是,它花费了1MSL到达了服务器,此时服务器关闭
- 如果这个ACK丢失,服务器会超时重传FIN报文,这个动作的时间是1、2、4、8、16,也就是说服务器可以在半分钟内重传5次FIN
- 这些FIN都丢失的概率是非常低的,而一个FIN报文最慢需要1MSL才能到达服务器,此时客户端能够再次发送ACK,然后关闭。这个ACK再次丢失的概率很小。
2、为什么设计TIME_WAIT状态
这样设计有两个原因:
- 延迟新的连接的建立,防止具有相同四元组的旧数据包被接收
- 保证“被动关闭连接”的一方能够正常关闭
防止具有相同四元组的旧数据包被接收
如果没有 TIME_WAIT 状态或者等待时长过短,网络中可能还残留着本次通信的数据包。
如果客户端和服务器立即建立了下一次通信,就有可能错误接收到旧的数据包,产生数据错乱。
所以设计了一个等待状态,这个状态的客户端是无法再次发起连接的。
等待2MSL的时间,足以让两个方向上的旧数据包都被丢弃,此时就能再次发起连接了,再出现的数据包肯定是新连接产生的。
保证“被动关闭连接”的一方能够正常关闭
TIME_WAIT 状态还有一个作用:保证“被动关闭连接的一方”能正确收到最后一个ACK,帮助其正常关闭。
- 如果客户端在发送最后一个ACK后,立马关闭连接,那如果这个ACK发生了丢失,服务端会一直处于LAST_ACK状态。
- 服务端如果没有收到ACK,就会多次超时重传FIN,但此时客户端已经接收不到了。
后果:相当于服务端的连接没有完全关闭,那如果客户端试图与其建立新的连接,服务端就会返回RST报文,无法建立新的连接。
如果有了 TIME_WAIT 状态,那么客户端就能收到服务器重传的FIN,进而重新发送ACK,重新设置2MSL计时器,直到不再收到FIN报文。
不过服务器在重发FIN到达一定次数后,也会主动关闭连接。
3、等待时间过长的危害
服务器也能主动断开连接,所以服务器也会有TIME_WAIT状态。
在Linux系统中,MSL默认是30,2MSL是60。如果想修改,只能直接去修改Linux内核。Linux不允许设置2MSL的值,也说明这个值比较危险。
设置太小没作用,设置太大有两个后果:
-
连接迟迟不断开,占用系统资源
- TCP 连接会占用系统资源,包括文件描述符、内存资源、CPU 资源、线程资源等
-
一堆即将关闭但还没关闭的连接持续占用端口资源。
- 客户端端口被占用过多,就无法发起新的连接
一般来说,尽量不要由服务器主动断开连接,而是让客户端去断开连接,分别独自承受TIME_WAIT,不要让服务器集中承受TIME_WAIT
4、在TIME_WAIT收到SYN
场景是,服务端刚结束连接,处于TIME_WAIT状态,此时收到了和上一次连接相同的四元组发来的SYN报文,希望再次建立连接。
首先会判断SYN的合法性,合法要求:
- 客户端SYN报文的时间戳比服务端上次收到的报文的时间戳大
- 客户端SYN报文的序列号比服务器期望收到的下一个序列号大
如果这个SYN是合法的,服务器会停止TIME_WAIT,转变为SYN_REVD状态,发送SYN+ACK进行第二次握手。
相当于是重用了上次的四元组连接。由于序列号是随机生成的,所以不会收到旧数据。
5、TCP连接异常断开的场景
一个TCP连接,没有开启keepalive,没有数据交互,如果一端的主机突然掉电,或者一端的进程突然崩溃,有什么区别?
1、没有数据传输,主机掉电
主机突然掉电,它来不及告诉客户端。而且没有开启保活机制,客户端也无法探测服务端是否还存活。
所以客户端的TCP连接会一直处于连接状态,直到客户端重启该进程。
所以,在没有数据交互且没有开启保活机制的场景下,一方的TCP连接处于连接状态,并不代表这个连接是可用的
2、没有数据传输,进程崩溃
由于只是进程崩溃,内核还在正常工作,所以内核会自动发起四次挥手的流程,正常断开连接。
3、有数据传输,主机掉电重启
首先,客户端向服务器发数据,服务器还在重启,客户端收不到ACK,会触发超时重传
重传会持续5次,期间如果服务器启动了,内核就会收到重传的数据
内核会检查数据包:
- 如果服务器上没有进程在监听该端口号,就发送RST报文,重置该连接
- 如果服务器上有进程在监听该端口号,但是服务器重启后,之前的连接已经丢失了,内核找不到对应的Socket,就会响应RST,终止连接
总之,服务器重启之后,内核收到之前的连接发来的数据,都会响应RST报文来终止连接
4、有数据传输,主机宕机
主机一直没有重启,客户端在超时重传达到上限后,内核就会判断这个连接出了问题
通过 Socket 接口告诉应用程序该 TCP 连接出问题了,于是客户端的 TCP 连接就会断开
这个上限有两个方面:
超时重传最大次数,默认15超时最大时间,每一轮的超时时间是成倍增长的
在重传报文且一直没有收到对方响应的情况时,先达到「最大重传次数」或者「最大超时时间」这两个的其中一个条件后,就会停止重传
4、有数据传输,进程崩溃重启
内核依然在正常工作。进程崩溃后,内核会主动发起四次挥手
如果是在进程崩溃阶段收到了数据包,内核发现没有进程在监听该端口号,就会响应RST终止连接
6、拔掉网线,连接还在吗
拔掉网线几秒再插回去,或者关闭无线网再开启,TCP连接还能正常运行吗?
首先,物理层断开并不会影响到传输层。
TCP连接底层是一个保存在内核中的Socket结构体,它包含连接的状态信息。
当拔掉网线后,操作系统不会对它做任何修改,所以不会影响到TCP连接,已经存在的连接依然是连接状态。
真正影响连接的并不是拔网线这个动作,而是拔了网线之后双方做了什么。
1、拔掉网线后有数据传输
比如服务器掉线,客户端向服务器发送数据:
-
服务器此时在网络上属于不可达的状态,所以服务器不会收到数据包。
-
客户端得不到ACK响应,就会触发超时重传
-
在超时重传的过程中,如果服务器插上了网线,它就能接收到重传的数据包,正确响应ACK。客户端只会认为是网络波动导致丢包了
-
如果服务端一直没有插网线,客户端达到最大重传上限,内核就会判定出该 TCP 有问题
然后通过 Socket 接口告诉应用程序该 TCP 连接出问题了,于是客户端的 TCP 连接就会断开
之后服务器重新插上网线,如果服务器向客户端发送了数据,由于客户端已经没有该连接的信息,就会响应RST报文
服务端收到RST之后就会释放连接
-
2、拔掉网线后没有数据传输
要看是否开启了keepalive,如果开启的话,客户端还是能主动探测到服务端的状态
- 如果服务器一直没有插上网线,对连续多个探测报文都没有回应,客户端就能知道服务器出现问题,进而主动断开连接
- 如果服务器插上网线了,收到探测报文就会回应,客户端重置保活机制
8、TCP的保活机制
1、建立连接后客户端挂掉
如果已经建立连接,但客户端发生错误挂掉了怎么办?
这个客户端不会发送有用的数据,但也不会主动断开连接,导致服务器的这个连接迟迟释放不掉。
所以TCP设计了保活机制 keepalive:
- 一个时间段之后,如果没有任何连接相关的活动,就会每隔一段时间发送一个探测报文
- 如果连续的几个探测报文都得不到回应,系统内核就将错误信息通知给上层应用程序,认为连接已经死亡。
相关的内核参数:
默认是7200秒内(两个多小时),如果没有活动就开始保活,每隔75秒探测一次,连续9次不回应就认为连接死亡。
一旦保活机制启动,有三种情形:
- 客户端正常,正确响应了探测报文,那么保活时间重置。
- 客户端崩溃后重启了,客户端丢失了连接的相关信息,就会给探测报文响应一个RST报文,表示连接出错
- 客户端崩溃,一直不响应探测报文,达到规定次数后,服务器判定连接已死亡。
应用层实现心跳监测
TCP的默认保活策略间隔时间太长,可以去修改内核参数,也可以在应用层实现心跳监测。
可以设置一个定时器,每次有请求就重置,如果到达了60秒还没有请求,就主动断开连接。
2、建立连接后服务端挂掉
比如建立了TCP连接,之后进程崩溃了,服务端会自动发送FIN报文,与客户端开始正常的四次挥手流程。
3、TCP的keepalive和HTTP的Keep-Alive
这是两个完全不同的东西。
- HTTP是应用层协议,Keep-Alive是在用户态实现的,叫做HTTP的长连接
- TCP是传输层协议,keepalive是在内核态实现的,叫做TCP的保活机制
HTTP的长连接是指:
- 使用同一个TCP连接,进行多个HTTP请求和响应的通信工作,避免频繁建立连接和断开连接带来的开销
- HTTP1.1默认开启Keep-Alive,可以通过请求头来设置
- 长连接也允许HTTP流水线技术,即连续发送多个请求,服务器按序响应
TCP的保活机制:
- 相当于心跳监测,在一个连接长时间没有通信时,会隔段时间发一个探测报文,检测对方是否还存活
- 如果连续多次都没有回应,就认为对方已经死亡,探测方主动断开连接
9、重传机制
TCP实现可靠数据传输的基础,是序列号+确认应答。
TCP的重传机制有四种:
- 超时重传
- 快速重传
- SACK
- D-SACK
1、超时重传
发送方在发出数据后,启动定时器,如果在规定时间内没有收到响应,就重传刚才的数据包。
1、超时重传的情形
有两种触发超时重传的情形:
- 发出的数据包丢失
- 确认应答丢失
2、超时时间的计算
RTT与超时时间
定时器的超时时间对性能影响很大,TCP如何处理?
RTT:包的往返时间。
超时时间必须大于RTT,否则没有意义:
- 如果太大,对丢失的响应过慢,性能很差
- 如果太小,会发生多次不必要的重传,增加网络拥塞,导致更多的超时,恶性循环。
RTT并不是常量,会根据网络的实际情况变化。也就是说,RTT是动态的,那么超时时间也应该设置成动态的,跟随RTT来一起变化。
产生两个问题:
- 如何测量RTT
- RTT和超时时间的具体关系
如何测量RTT
测量从数据包发出,到接收到ACK的时长,称为SampleRTT。
测量多个SampleRTT,求平均值,就可以估计RTT。估值叫做EstimatedRTT,它用来模拟此时的RTT。
EstimatedRTT是逐渐迭代的,而不是每次都计算新的值。使用加权平均算法,这样的算法可以更加均衡,过滤掉一些网络波动。
公式:EstimatedRTT = (1- a)EstimatedRTT + aSampleRTT,此处的a是典型值,通常为0.125
RTT和超时时间的具体关系
最好的情况是,比如当时的RTT是2ms,那么在2ms之后的一瞬间,没有收到ACK,就立马触发重传机制,此时效率是最高的。
所以,超时时间只要比RTT略大即可。
超时时间 = EstimatedRTT + “安全边界”
安全边界:RTT的波动值(DevRTT),即SampleRTT与EstimatedRTT的差值。
使用指数加权移动平均算法:DevRTT = (1- β)*DevRTT + β *|SampleRTT-EstimatedRTT| ,此处的β是典型值,通常为0.25
超时时间:TimeoutInterval = EstimatedRTT + 4*DevRTT
如果再次超时
如果超时重传的数据再次超时,说明此时网路不太好,TCP的策略是将超时等待时间加倍,也就降低了发送数据包的频率。
每当遇到一次超时重传,就会将下一次的超时时间设置为之前的两倍。这样可能导致重传周期较长,所以设计了“快速重传”机制。
2、快速重传
TCP还有另一种触发重传的情况:
由于发送方会间歇性返回ACK,表明当前接收到了哪个数据。如果分组丢失,接收方在乱序接收到其他数据包后,肯定会导致发送多个重复的ACK。
- 比如接收方响应ACK 100,发送方发了一个100的数据
- 但接收方一直响应ACK 100,ACK 100,说明它还没有收到那个报文,报文大概率是丢失了。
TCP规定,如果接收方收到了同一个数据的3个以上ACK,就直接重传。
这就是快速重传机制:在定时器超时之前就进行了重传。
1、快速重传的弊端
如果只是使用上面提到的这种,收到三个连续ACK就触发快速重传,存在一个问题。
- 由于TCP使用滑动窗口,接收方会连续发送多个数据包,比如发出了1、2、3、4、5
- 这些数据包在网络中传输,它们到达接收端的顺序是不一定的
- 1先到了,接收端回ACK2
- 但是2丢失了,接收端收到了4、5、3,但由于缺少2,所以回应三次ACK2
- 发送端收到了三个连续的ACK2,根据刚才的分析,应该重传2
此时就出现了问题:
- 在TCP的不同实现中,多次收到重复的ACK,可能是这个分组没有接收到,后续的分组接收到了;也可能是后续的分组也没接收到。
发送端只能知道2出了问题,不能知道刚才发出去的那么多数据包还有哪些出了问题- 只能等到接收端收到2后,再告诉发送端还缺少哪个数据包
这样就拉低了滑动窗口的效率,如果能触发重传时一次性把所有缺少的数据包发出是最好的。
3、SACK
为了解决到底该重传哪些报文的问题,就设计了SACK。
SACK( Selective Acknowledgment),选择性确认。
具体做法:
- 接收方在TCP头部的“选项”字段加一个SACK,它可以
将接收缓冲区的情况发给发送方 - 这样
发送方就可以具体地知道哪些数据收到了,哪些数据没收到,只重传没收到的数据即可。
可以设置内核参数来开启,Linux2.6之后默认开启
4、D-SACK
1、D-SACK的作用
接收方可以使用D-SACK告诉发送方,哪些数据被重复接收了。用于接收方收到了数据,但响应报文丢失的情形。
比如:
- 接收方收到了发送方的10、11,响应ACK10、ACK11
- 这两个ACK都丢失了,发送方触发了超时重传
- 接收方再次收到了10,它就响应一个D-SACK,告诉发送方10已经收到了。
- 发送方就可以具体地知道,刚才的ACK丢失了
总结D-SACK的好处
- 可以让发送方知道,导致超时重传的是发出的数据包丢了,还是响应的ACK丢了
- 可以让发送方知道,发出的数据是否被网络延迟了
5、选择重传
选择重传是指,接收方会缓存乱序到达的数据包,发送方只需要重传丢失的那个具体的数据包即可,不需要重传很多重复的数据包
好处是提高了效率,坏处是接收方必须缓存乱序到达的数据包,等待全部数据包都收到之后才能把完整的数据交给应用层
10、滑动窗口
1、为什么设计滑动窗口
设计一个最简单的,符合TCP思想的通信协议,可以这样设计:
-
假设只是单边通信
-
发送方维护一个序列号,每次发包就递增。
接收方维护一个期望收到的序列号,用来和收到的数据序列号比对,不一样就丢弃,一样就响应ACK
-
思想是,发出一个包,等待ACK后才能再次发送下一个包。一一确认的思想
这样一一确认的坏处:
-
网络的性能瓶颈在于:一个数据包在网络上的传输速度不确定,如果网络环境很差,它就跑的很慢,这对于数据包和ACK都是一样的
-
所以要提高网络传输效率的思想应该是:
- 尽量频繁地持续占用网络发送有效数据,这样在整场通信中,网络的延迟对通信的整体就越来越小了。
- 相反,如果使用一一确认,相邻的两个数据包之间的发送间隔是RTT,效率非常低
-
举个例子,一个面试官在上海组织一场面试,10个应聘者需要坐飞机前往。
- 最合理的方案是,10个人都提前坐飞机到面试现场,然后面试官很快地筛选,面试就结束了,耗时 = 飞机 + 筛选 * 10
- 一一确认的方案是,1个人坐飞机来了,面试官进行筛选,然后发送ACK告诉下一个候选人,他再坐飞机来,耗时 = (飞机 + 筛选) * 10
- 这里的飞机指的就是网络,面试官筛选指的就是目标主机接收数据包、处理数据包的工作
- 我们假设一段时间内只有一架飞机飞往上海,那么这10个人都坐着同一架飞机去面试,这个飞机就是滑动窗口范围。
滑动窗口机制,使得TCP不用一个数据包一个数据包地发送、响应。
2、滑动窗口的思想
1、窗口的含义
滑动窗口的大小表示:无需等待应答还可以继续发送的字节数。
窗口的实现,是操作系统开辟的一段缓存空间。窗口是以字节长度来工作的,而不是报文的个数。
发送方在收到ACK之前,必须把已经发送的数据缓存起来,因为可能还要重传。如果收到了ACK,就可以把对应的数据从缓存中删除
接收方收到数据,也是先放在缓存中,再拿出来处理。只有应用程序拿到数据包后才会响应ACK,数据到了接收方缓冲区内不会响应ACK。
2、窗口大小怎么确定
TCP头部有一个window字段,窗口大小是由接收端告诉发送端的,表示自己还有多大的缓冲区,用于流量控制。
发送方根据接收方的处理能力来发送数据,避免发送数据太多接收端处理不过来,最后还得重传,没有意义。
发送方发送的数据大小不能超过接收方的窗口大小。
发送方和接收方的窗口大小
由于它们各自维护自己的滑动窗口,接收方读取完数据后,需要通过window字段告诉发送方新的窗口大小,而网络存在延迟。
那么某一时刻,发送方和接收方的窗口大小很可能不同,导致一些由于接收方窗口已满而丢弃数据的丢包情况,造成资源的浪费
操作系统缓冲区与滑动窗口的关系
一端的发送窗口或接收窗口,都是放在操作系统内核缓冲区中的。
由于窗口而丢失数据有两种情形:
- 应用程序繁忙,无法及时读取缓冲区的内容,造成无法及时响应ACK,进而造成缓冲区已满,滑动窗口剩余空间为0
- 系统资源紧张,
操作系统直接减小了缓冲区的大小,就可能导致数据的丢失。
对于后一种情况,发送方的滑动窗口大小是根据接收方上一次报文的window字段来决定的。
如果接收方刚刚响应了一个较大的window,之后又缩小了缓冲区,就可能导致发送端发来一个大于剩余缓冲区大小的报文,就只能将其丢弃。
这样的丢包是由于先减小了缓冲区,再缩小窗口导致的。因此,TCP规定必须先缩小窗口,过段时间再减小缓冲区。
3、发送方的滑动窗口
某一时刻的发送方滑动窗口细节:
发送方的窗口分为四个部分:
-
已经发出并且收到ACK的数据
-
窗口范围内,已经发出但还没收到ACK的数据,有三种可能:
- 数据还在路上
- 数据已经被接收方收到,但放在了缓冲区中,还没来得及读取
- 响应的ACK还在路上
-
窗口范围内,还没发出的数据
-
窗口范围外,还没发出的数据
思想是:
- 接收方还有多大的缓冲区,发送方的窗口就有多大,就能连续发送这么多个数据包,到达接收端后就被缓存起来
- 接收端每次响应ACK时就会带上自己的缓冲区大小,发送方会及时调整自己的窗口大小
- 发送方收到了一个ACK,这个ACK之前的数据都被正确接收了,窗口右移,发送一些新数据
发送方的滑动窗口如何由程序表示
使用三个指针来划分四个区域,其中两个是绝对指针,指向特定的序列号。一个是相对指针,做偏移计算。
- SND.WND:表示发送窗口的大小,由接收方指定
- SND.UNA:绝对指针,指向窗口内已发送但还没收到ACK的第一个数据包的序列号
- SND.NXT:绝对指针,指向窗口内可发送但还没发送的第一个数据包的序列号
- 有这些信息就能计算出来最后一部分,超过窗口范围的还没发送的第一个数据包的序列号
4、接收方的滑动窗口
某一时刻的接收方滑动窗口细节:
- 接收方也需要维护滑动窗口,底层是操作系统的内存缓冲区。
- 窗口内的数据,是还没有被应用程序处理的,暂存在这里。
如何记录接收方窗口的状态?三个区域,需要两个指针:
- RCV.WND:接收窗口的大小,会告诉发送方
- RCV.NXT:“期望接收到的下一个数据包”的第一个字节的序列号
3、工作方式
TCP采用的是“累计确认”机制:ACK一个序列号,表示这个序列号以及之前的数据包都被正确接收到了。
累计确认的方式非常适合滑动窗口机制。
11、流量控制
1、为什么需要流量控制
流量控制主要还是服务于滑动窗口机制,用来规范窗口大小的。
如果数据发送过快,接收方处理不过来,就不会对消息做出响应,那发送方就会一直超时重传,浪费网络资源。
合理的做法是,发送数据时充分利用接收方的资源,但也保证不会超出接收方的处理能力
流量控制:控制数据发送的速度,防止数据发送的速度超过接收方处理的速度。本质是速度匹配机制。
2、具体做法
缓冲区buffer
接收方为TCP连接分配buffer:
绿色:buffer中存储有数据的部分
蓝色:buffer中空闲的部分
上层应用可能处理buffer中数据的速度较慢,如果发送方传输数据过快,就会造成buffer溢出。
流量控制机制
接收方通过报文段的window字段,将“剩余的缓冲区容量”告诉发送方,发送方以此来限制自己的发送行为,不会发送总长度大于此值的数据包。
3、窗口关闭的处理
假如发送方得知,接收方的window=0,那么发送方不会发送新的报文段,接收方也不会接收到新的报文段。
等到接收方处理完数据,就会响应ACK。但是如果这个ACK丢失了,接收方肯定不会重传。
即使此时缓冲区有了空闲,发送方也无法得知,产生了死锁。怎么解决?
TCP的解决方案是:
- 得知接收方的window=0,发送方就启动一个计时器,隔段时间发一个“窗口探测报文”
- 接收方收到这个探测报文,就会响应当前的window值
- 如果依然是0,就重置计时器。如果不是0,就继续发送
探测3次,每次间隔半分钟到一分钟,如果三次的响应结果都为0,有些TCP实现会让发送端发送RST报文来终止连接。
4、小窗口大脑袋问题
如果接收方比较繁忙,接收窗口内积压的数据很多,就会导致空闲缓冲区越来越小,发送方的窗口也越来越小
- 如果接收方腾出了几个字节,响应了ACK
- 那么发送方如果也是挑出几个字节去发送,就得不偿失,因为TCP头部都占20多个字节呢,IP头部也有20个字节,加起来就40多个字节了
- 如果数据只占几个字节,那么这次发数据就非常不划算,所以应该等到数据较大后再发送出去。
举个例子,一架能坐100人的飞机,上了1个人也能起飞,而不是选择先等等其他人,这个开销肯定巨大。
解决思路:
-
接收方等到窗口较大了再通知发送方
- 做法是,当窗口小于MSS和缓冲区大小一半中较小的值时,直接告诉发送方,窗口大小为0。MSS是一个TCP包的最大数据长度
- 等到接收方处理了一部分数据,空闲空间大于MSS或者一半缓冲区后,再通知最新的窗口大小
-
发送方等到窗口较大了再发送数据
-
发送方默认开启了Nagle算法,满足两个条件其中之一才能发数据:
- 窗口大小超过MSS,或者数据大小超过MSS
- 收到之前发出的数据的ACK。这个策略是为了保持连续发送,避免浪费网络资源
-
只要两个条件都不满足,发送方就一直囤积数据,先不发送。
-
对于一些交互性比较强的场景,可以关闭Nagle算法。
-
5、TCP的粘包问题
TCP的粘包问题可能由发送方或者接收方引起。
1、发送方的TCP粘包问题
由于“小窗口大脑袋”问题,发送方默认开启了Nagle算法,它的做法是,把多个较小的数据包合并成一个TCP包发送。
这样导致的后果是,可能该四元组的连接上层存在多个不同的请求,比如开启长连接后的HTTP协议。
这样,来自多个不同请求的数据会紧挨着一起发给接收方,如果不做特殊处理,接收方的应用层是无法正常响应这些请求的。
- HTTP的话没问题,因为每个HTTP请求都有规范的请求头部,可以区分不同的请求。
- 而如果是设计一个基于TCP的应用层协议,就必须考虑这个问题。
2、接收方的TCP粘包问题
TCP在接收到数据包之后,不会立即交给应用层,而是先缓存在接收缓存中,然后由应用层去主动读取数据。
如果接收数据包的速度远大于应用层处理数据的速度,接收方就会缓存多个包,这些包会被去掉TCP头部,而且都是挨着的,也存在粘包问题。
3、如何解决
有两种途径:
- 关闭发送方的Nagle算法。不过这样就会导致小窗口大脑袋问题,浪费资源,而且接收方的粘包问题依然存在
- 在应用层规范数据的格式。
一般会在应用层解决:
- 设计规范的数据格式,比如每个数据设计一个开始符、结束符,但是必须保证数据中不包含规定的开始符和结束符
- 也可以在发送时,把数据的实际长度作为数据的头部,也可以达到区分的目的
4、UDP没有粘包问题
UDP不是流水线连续发送的,而是一个包一个包发送的,所以不存在粘包问题。
12、拥塞控制
1、为什么需要做拥塞控制
如果网络拥塞,就会导致RTT(数据包往返时间)增大,可能会触发TCP的超时重传,进而继续发送数据包,造成网络越来越拥堵。
合理的做法是,如果当前网络拥堵,就少发或不发数据包。
如果网络上的所有通信协议都有拥塞控制机制,网络拥堵的持续时间就会大大减少,这是很好的策略。
2、拥塞控制和流量控制
从结果来看,都是调整了发送方的速率,但它们迎合的目的不一样:
- 流量控制,是发送方在迎合接收方的窗口范围,避免发送的数据包过多导致接收方处理不过来
- 拥塞控制,是发送方在迎合网络的状态,避免发送数据过多加剧网络拥堵
3、如何做拥塞控制
拥塞控制需要解决两件事情:
- 如何知晓当前的网络拥塞程度:Loss事件
- 如何动态调整发送速率:拥塞窗口+拥塞控制算法
1、网络拥塞程度
每次发送方的超时重传、快速重传,都会判定为一次Loss事件,因为超时重传肯定要么是数据丢了,要么是ACK丢了,丢就是因为网络环境差。
发生Loss后,发送方降低发送速率。
2、拥塞窗口
为了限制发送方发送数据,TCP的做法和流量控制类似,设置一个属性:cwnd,用于反映网络拥塞程度。它叫做“拥塞窗口”。
所以TCP发送方的发送窗口大小,等于接收窗口和拥塞窗口中的较小值。
拥塞窗口的变化规则:
- 网络中没有拥塞,拥塞窗口变大,全力运行滑动窗口
- 网络中有拥塞,拥塞窗口变小,限制滑动窗口的运行
4、拥塞控制算法
有四种拥塞控制算法:
- 慢启动
- 拥塞避免
- 拥塞发生
- 快速恢复
1、慢启动
TCP连接刚建立时,有一个慢启动的过程,意思是慢慢地提高数据包的发送数量
TCP连接刚建立时,cwnd初始化为1,表示能发送1个MSS大小的数据
但此时的可用带宽通常远远高于初始的速率,所以如果发送速率很慢,就没有合理利用网络的性能,也属于一种对资源的浪费。
所以TCP的策略是:
- 每收到一个ACK,就让cwnd+1,发包的个数是指数增长的,可以较快地达到可用带宽
- 比如上次发了4个包,收到4个ACK,下次就能发8个包
2、拥塞避免
慢启动阶段,发送的数据包个数也不能无限地指数增长下去,而是碰到一个慢启动门限ssthresh就停止慢启动策略,转而使用拥塞避免策略。
这个慢启动门限的含义就是,它是一个比较稳定不会发生Loss的发包数量,大于它就需要谨慎探测网络带宽了。
一般来说 ssthresh 的大小是 65535 字节。
拥塞避免阶段,cwnd的规则是:
- 每收到1个ACK,cwnd增长1/cwnd,相当于线性增长
- 比如上次发了8个包,下次能发9个
拥塞避免阶段相当于点了一脚刹车,减缓了增长的速度,但还是在增长。
如果很多个TCP连接都在增加发包数量,网络就慢慢变得拥堵,此时就需要对发包的数量进行限制。
3、拥塞发生
当网络发生Loss事件,说明发送方进行了重传。而重传分为超时重传和快速重传,它们的起因不一样,所以处理策略也不一样。
1、超时重传的拥塞发生算法
在有快速重传机制的情况下发生了超时重传,说明要么是数据包丢了,要么是一堆ACK都丢了,此时网络环境非常差,必须大力整治。
做两件事情:
- 当前拥塞窗口重置为1,相当于立即减小当前的发包个数
- 慢启动门限变为原先拥塞窗口的一半,相当于限制最大的发包个数
这种方式相当激进,急刹车
2、快速重传的拥塞发生算法
快速重传情况,还能连续收到3个ACK,说明网络环境还比较好,只是偶尔丢了个包,整治力度会小一些。
做两件事情:
- 当前拥塞窗口大小改为原先的一半
- 慢启动门限变为原先拥塞窗口的一半
之后进入快速恢复算法
4、快速恢复
快速恢复算法用于发生快速重传之后的情况。
在发生快速恢复之前,cwnd和慢启动阈值已经调整了:
- 拥塞窗口大小改为原先的一半
- 慢启动门限变为原先拥塞窗口的一半
快速恢复的策略:
- 拥塞窗口大小变为当前慢启动门限 + 3的大小(+3是因为已经有3个重复ACK离开了网络,网络多出来一些资源,窗口大小可以适当+3)
- 重传丢失的数据包
- 如果再收到重复ACK,cwnd+1
- 如果收到新数据的ACK,就把慢启动门限设置为快速恢复刚开始的拥塞窗口大小,恢复拥塞避免策略,线性增长拥塞窗口大小。
13、TCP的性能分析
TCP的吞吐率 throughput
给定拥塞窗口大小和RTT,TCP的平均吞吐率是多少?
忽略慢启动的过程,假定发生超时时,CongWin大小为w,吞吐率是W/RTT
超时后,CongWin=W/2,吞吐率是W/2RTT
平均吞吐率为0.75W/RTT
TCP的公平性
如果K个TCP连接,共享相同的瓶颈带宽R,那么能保证带宽分配相对公平吗?
TCP具有公平性,因为存在拥塞控制机制
可以看出,在数据传输的过程中,资源会逐渐趋向平均分配。
UDP的公平性
UDP没有拥塞控制机制,不会限制速率,所以多媒体应用经常使用UDP。
同一带宽上,UDP比TCP占用资源更多,因为网络拥堵时TCP会降低发送速率,而UDP一直以恒定速率发送,相当于TCP为它停车让路了。
并发TCP连接
有些应用并不满足与单个TCP连接的资源,它会并发地发起多个TCP连接,一同为自己服务。
比如链路速率为R,已有9个连接。如果新来的应用请求1个TCP,它能获得 R/10的速率。但如果它请求11个TCP,就能获得 R/2的速率。
所以,一个TCP连接是公平的,但使用TCP的应用可以占用多个TCP连接资源。
14、TCP的缺陷
-
升级工作困难
- TCP协议是在内核中实现的,应用程序只能使用而不能修改
- 想要升级协议必须升级内核,而升级内核需要慎重,协议需要双方都支持才行,所以很多新特性得不到迅速推广
-
TCP建立HTTPS连接的延迟
- 大多数网站都是HTTPS的,需要先进行TCP的三次握手之后,再进行TLS的四次握手,才能开始传输数据,延迟较大
- 如果能把TLS内置在TCP中,减少握手的次数,就可以改善。但是TLS是应用层实现的,而TCP是内核实现的,所以很难结合
- 而且,TLS无法对TCP头部进行加密,存在一些安全问题
-
TCP存在队头阻塞问题
- TCP必须保证收到的数据是有序的,如果收到乱序的数据,就需要等待缺失的数据包发过来以后才能把数据交给应用层
- 这就意味着如果多个请求使用同一个TCP连接,如果有一个请求丢了包,那么就会阻塞该连接中的所有请求
-
网络迁移需要重新建立TCP连接
- TCP是通过四元组来确定连接的,如果一方的网络环境发生变化,比如更换了IP地址,原有的连接就必须作废,重新建立连接
- 由于TCP的慢启动特性,所以网络会短暂卡顿一下