TCP、UDP、TCP三次握手与四次挥手、TCP如何保证可靠传输、TCP异常分析、拆包和粘包等
作者:互联网
4、OSI模型
4.1、OSI七层模型
4.2、七层模型功能
物理层:利用传输介质为数据链路层提供物理连接,实现比特流的透明传输,如网线;网卡标准。
数据链路层:接收来自物理层的位流形式的数据,并封装成帧,传送到上一层,定义数据的基本格式,如何传输,如何标识,MAC
网络层:将网络地址翻译成对应的物理地址,并通过路由选择算法为分组通过通信子网选择最适当的路径,如不同设备的数据转发。
传输层:端到端传输数据的基本功能;如 TCP、UDP。
会话层:负责在网络中的两节点之间建立、维持和终止通信;如不同软件数据分发给不同软件。
表示层:处理用户信息的表示问题,数据的编码,压缩和解压缩,数据的加密和解密。
应用层:为用户的应用进程提供网络通信服务以及各种应用软件,包括 Web 应用。
说明:
在四层,既传输层数据被称作段(Segments);
三层网络层数据被称做包(Packages);
二层数据链路层时数据被称为帧(Frames);
一层物理层时数据被称为比特流(Bits)。
总结
网络七层模型是一个标准,而非实现,网络四层模型是一个实现的应用模型,网络四层模型由七层模型简化合并而来。
4.3、网络模型相关协议
1、应用层协议
- TCP对应的应用层协议
FTP :定义了文件传输协议,21 (20传输,21连接) |
---|
Telnet:它是一种用于远程登陆的端口,23 |
SMTP:定义了简单邮件传送协议,服务器开放 ,25 |
POP3:它是和SMTP对应,POP3用于接收邮件,110 |
HTTP:超文本传输协议,80 |
HTTPS:超文本安全协议,443 |
- UDP对应的应用层协议
DNS:用于域名解析服务,53 (服务器传输TCP,客户端查询服务器UDP) |
---|
SNMP:简单网络管理协议,161 |
TFTP(Trival File Transfer Protocal):简单文件传输协议,69 |
2、传输层协议
TCP: |
---|
UDP: |
3、网络层协议
ARP:地址解析协议 ,根据IP地址获取物理MAC地址 |
---|
IP:TCP/IP协议簇中的核心协议,也是TCP/IP的载体,IP提供不可靠的,无连接的数据传送服务 |
ICMP:网络控制报文协议,确认IP包是否成功达到目标地址,通知IP包丢失的原因(目标不可达、原点抑制、重定向、改变路由、时间戳) |
IGMP:组管理协议,让一个物理网络上的所有系统知道主机当前所在的多播组 |
DHCP:动态主机配置协议,自动的给子网内新增主机结点分配IP地址,避免手动管理IP |
OSPF:开放式最短路径优先, |
BGP:边界网关协议,用来连接Internet上独立系统的路由选择协议 |
4、数据链路层协议
ARP :地址解析协议 ,根据IP地址获取物理MAC地址 |
---|
RARP:反向地址转换协议,根据物理MAC地址获取IP地址 |
PPP:点对点协议,主要是用来通过拨号或专线方式建立点对点连接发送数据,使其成为各种主 |
5、TCP
5.1、TCP/UDP基本认识
TCP是面向连接的(一对一)、可靠的、基于字节流的传输层通信协议。
TCP连接:用于保证可靠性和流量控制维护的某些状态信息,包括Socket(IP+port)、序列号(解决乱序)和窗口大小(流量控制)
-
⾯向连接:⼀定是「⼀对⼀」才能连接,不能像 UDP 协议可以⼀个主机同时向多个主机发送消息,也就是⼀对多是⽆法做到的;
-
可靠的:⽆论的⽹络链路中出现了怎样的链路变化,TCP 都可以保证⼀个报⽂⼀定能够到达接收端;
-
字节流:消息是「没有边界」的,所以⽆论我们消息有多⼤都可以进⾏传输。并且消息是「有序的」,当「前⼀个」消息没有收到的时候,即使它先收到了后⾯的字节,那么也不能扔给应⽤层去处理,同时对「重复」的报⽂会⾃动丢弃。
UDP是不可靠、基于报文的、可实现多个连接的传输层通信协议。
5.2、TCP/UDP报头格式
1、TCP报头格式
**Seq序列号:**在建⽴连接时由计算机⽣成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送⼀次数据,就「累加」⼀次该「数据字节数」的⼤⼩。⽤来解决⽹络包乱序问题。
**Ack确认应答号:**指下⼀次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。⽤来解决不丢包的问题。
窗口大小:(window size)滑动窗口,用于告知对方(发送方)本方的缓冲还能接收多少字节数据,主要进行流量控制。
校验和:接收端用CRC检验整个报文段有无损坏。
标志位:
- ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建⽴连接时的 SYN 包之外该位必须设置为 1 。
- RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。
- SYN:该位为 1 时,表示希望建⽴连接,并在其「序列号」的字段进⾏序列号初始值的设定(连接报文段)。
- FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双⽅的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。
- PSH:提示接收端立刻从缓冲读走数据。
- RST:表示要求对方重新建立连接(复位报文段)。
2、UDP报头格式
⽬标和源端⼝:主要是告诉 UDP 协议应该把报⽂发给哪个进程。
包⻓度:该字段保存了 UDP ⾸部的⻓度跟数据的⻓度之和。
校验和:校验和是为了提供可靠的 UDP ⾸部和数据⽽设计。
3、IP报头格式
5.3、TCP和UDP区别
1)连接
TCP 是⾯向连接的传输层协议,传输数据前先要建⽴连接。
UDP 是不需要连接,即刻传输数据。
2)服务对象
TCP 是⼀对⼀的两点服务,即⼀条连接只有两个端点。
UDP ⽀持⼀对⼀、⼀对多、多对多的交互通信
3)可靠性
TCP 是可靠交付数据的,数据可以⽆差错、不丢失、不᯿复、按需到达。
UDP 是尽最⼤努⼒交付,不保证可靠交付数据。
4)拥塞控制、流量控制
TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。UDP 则没有,即使⽹络⾮常拥堵了,也不会影响 UDP 的发送速率。
5)⾸部开销
TCP ⾸部⻓度较⻓,会有⼀定的开销,⾸部在没有使⽤「选项」字段时是 20 个字节,如果使⽤了「选项」字段则会变⻓的。
UDP ⾸部只有 8 个字节,并且是固定不变的,开销较⼩。
6)传输⽅式
TCP 是流式传输,没有边界,但保证顺序和可靠。
UDP 是⼀个包⼀个包的发送,是有边界的,但可能会丢包和乱序。
7)分片不同
TCP 的数据⼤⼩如果⼤于 MSS ⼤⼩,则会在传输层进⾏分⽚,⽬标主机收到后,也同样在传输层组装 TCP数据包,如果中途丢失了⼀个分⽚,只需要传输丢失的这个分⽚。
UDP 的数据⼤⼩如果⼤于 MTU ⼤⼩,则会在 IP 层进⾏分⽚,⽬标主机收到后,在 IP 层组装完数据,接着再传给传输层,但是如果中途丢了⼀个分⽚,在实现可靠传输的 UDP 时则就需要重传所有的数据包,这样传输效率⾮常差,所以通常 UDP 的报⽂应该⼩于 MTU。
问题一:为什么 UDP 头部没有「⾸部⻓度」字段,⽽ TCP 头部有「⾸部⻓度」字段呢?
TCP 有可变⻓的「选项」字段,⽽ UDP 头部⻓度则是不会变化的,⽆需多⼀个字段去记录 UDP 的⾸部⻓度
问题二:为什么 UDP 头部有「包⻓度」字段,⽽ TCP 头部则没有「包⻓度」字段呢?
TCP数据长度 = IP总长度 - IP首部长度 - TCP首部长度,因此TCP数据长度是可以计算出来的。虽然UDP头部有包长度字段看起来有点冗余,但是它主要是为了网络硬件设备的设计和处理的方便,首部长度需要是4bity的整数倍。所以,包长度有可能是为了补全UDP首部长度是4bity的整数倍。
问题三:UDP和TCP数据包最大值的确定?
MTU最大传输单元,这个传输单元实际上和数据链路层有密切的关系,由于以太网传输方面的限制,每个以太网帧有最小64bytes和最大1518bytes,超过大小限制(过大或者过小的)都视为错误的数据帧,那以太网转发设备就会丢弃这些数据帧。以太网最大的数据帧是1518bytes,所以刨去以太网的帧头14bytes和帧尾4bytes,剩下1500bytes就是MTU。
- UDP包的大小为:1500 - IP头(20)- UDP头(8) = 1472
- TCP包的大小为:1500 - IP头(20)- TCP头(20) = 1460
5.4、TCP 和 UDP 应用场景:
由于 TCP 是⾯向连接,能保证数据的可靠性交付,因此经常⽤于:
-
FTP ⽂件传输
-
HTTP / HTTPS
由于 UDP ⾯向⽆连接,它可以随时发送数据,再加上UDP本身的处理既简单⼜⾼效,因此经常⽤于:
-
包总量较少的通信,如 DNS 、 SNMP 等
-
视频、⾳频等多媒体通信
-
⼴播通信
5.5、如何唯一确定一个TCP连接?
TCP是四元组(IP+port)
5.6、TCP最大连接数?
一般服务器固定在某个本地端口上监听,等待客户端的链接请求,因此客户端IP和port可变的。
理论上:max(TCP连接数) = 客户端IP * 客户端port
对 IPv4,客户端的 IP 数最多为 2 的 32 次⽅,客户端的端⼝数最多为 2 的 16 次⽅,也就是服务端单机最⼤ TCP 连接数,约为 2 的 48 次⽅。当然,服务端最⼤并发 TCP 连接数远不能达到理论上限。⾸先主要是⽂件描述符限制,Socket 都是⽂件,所以⾸先要通过 ulimit 配置⽂件描述符的数⽬;另⼀个是内存限制,每个 TCP 连接都要占⽤⼀定内存,操作系统的内存是有限的
- ⽂件描述符:Socket 实际上是⼀个⽂件,也就会对应⼀个⽂件描述符。在 Linux 下,单个进程打开的⽂件描述符数是有限制的,没有经过修改的值⼀般都是 1024,不过我们可以通过 ulimit 增⼤⽂件描述符的数⽬;
- 系统内存:每个 TCP 连接在内核中都有对应的数据结构,意味着每个连接都是会占⽤⼀定内存的;
6、TCP三次握手
6.1、TCP三次握手
三次握手(Three-way Handshake)其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。实质上其实就是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号,交换 TCP窗口大小 信息。
重点
TCP连接:用于保证可靠性和流量控制维护的某些状态信息,包括Socket(IP+port)、序列号(解决乱序)和窗口大小(流量控制)
6.2、为什么要三次握手才能初始化Socket、序列号和窗口大小并建立TCP连接?
三次握手的原因(三方面):
- 三次握⼿才可以阻⽌重复历史连接的初始化(主要原因)–⾸要原因是为了防止旧的重复连接初始化造成混乱
- 三次握⼿才可以同步双方的初始序列号
- 三次握⼿才可以避免资源浪费
1、防止旧的重复连接初始化造成混乱
客户端通过上下文比较,发现自己期望收到的Ack num 应该是100 + 1 ,而不是 90 + 1,所以就会向服务器发起RST报文终止连接。
客户端连续发送多次 SYN 建立连接的报⽂,在⽹络拥堵情况下:
-
⼀个「旧 SYN 报⽂」⽐「最新的 SYN 」 报⽂早到达了服务端;
-
那么此时服务端就会回⼀个 SYN + ACK 报⽂给客户端;
-
客户端收到后可以根据⾃身的上下⽂,判断这是⼀个历史连接(序列号过期或超时),那么客户端就会发送RST 报⽂给服务端,表示中⽌这⼀次连接。
注:如果是两次握⼿连接,就不能判断当前连接是否是历史连接,三次握⼿则可以在客户端(发送⽅)准备发送第三次报⽂时,客户端因有足够的上下⽂来判断当前连接是否是历史连接:
- 如果是历史连接(序列号过期或超时),则第三次握⼿发送的报⽂是 RST 报⽂,以此终止历史连接;
- 如果不是历史连接,则第三次发送的报⽂是 ACK 报⽂,通信双⽅就会成功建⽴连接;
2、同步双方初始序列号
TCP 协议的通信双⽅, 都必须维护⼀个「序列号」, 序列号是可靠传输的⼀个关键因素,它的作⽤:
-
接收⽅可以去除重复的数据;
-
接收方可以根据数据包的序列号按序接收;
-
可以标识发送出去的数据包中, 哪些是已经被对⽅收到的;
注:四次握⼿其实也能够可靠的同步双⽅的初始化序号,但由于第⼆步和第三步可以优化成⼀步,所以就成了「三次握⼿」。⽽两次握⼿只保证了⼀⽅的初始序列号能被对⽅成功接收,没办法保证双⽅的初始序列号都能被确认接收。
3、避免资源浪费
如果只有「两次握⼿」,当客户端的 SYN 请求连接在⽹络中阻塞,客户端没有接收到 ACK 报⽂,就会重新发送 SYN ,由于没有第三次握⼿,服务器不清楚客户端是否收到了⾃⼰发送的建⽴连接的 ACK 确认信号,所以每收到⼀个 SYN 就只能先主动建⽴⼀个连接,这会造成什么情况呢?如果客户端的 SYN 阻塞了,重复发送多次 SYN 报⽂,那么服务器在收到请求后就会建⽴多个冗余的⽆效链接,造成不必要的资源浪费。即两次握⼿会造成消息滞留情况下,服务器重复接受⽆⽤的连接请求 SYN 报⽂,⽽造成重复分配资源。
6.3、总结为什么握手是三次?不是两次或者四次?
TCP 建⽴连接时,通过三次握⼿能防⽌历史连接的建⽴,能减少双⽅不必要的资源开销,能帮助双⽅同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。
不使⽤「两次握⼿」和「四次握⼿」的原因:
-
「两次握⼿」:⽆法防⽌历史连接的建⽴,会造成双⽅资源的浪费,也⽆法可靠的同步双⽅序列号;
-
「四次握⼿」:三次握⼿就已经理论上最少可靠连接建⽴,所以不需要使⽤更多的通信次数。
6.4、为什么第三次握手是可以携带数据的,前两次握手是不可以携带数据的?
其实第三次握手的时候,是可以携带数据的。但是,第一次、第二次握手不可以携带数据。
为什么这样呢?大家可以想一个问题,假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据。因为攻击者根本就不理服务器的接收、发送能力是否正常,然后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。
也就是说,**第一次握手不可以放数据,其中一个简单的原因就是会让服务器更加容易受到攻击了。而对于第三次的话,此时客户端已经处于ESTABLISHED状态。对于客户端来说,他已经建立起连接了,**并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据也没啥毛病。
6.5、如何在Linux系统中查看TCP状态?
TCP 的连接状态查看,在 Linux 可以通过 netstat -napt 命令查看。
6.6、为什么客户端和服务端的初始序列号ISN是不同的?
如果⼀个已经失效的连接被重⽤了,但是该旧连接的历史报⽂还残留在⽹络中,如果序列号相同,那么就⽆法分辨出该报⽂是不是历史报⽂,如果历史报⽂被新的连接接收了,则会产⽣数据错乱。所以,每次建⽴连接前重新初始化⼀个序列号主要是为了通信双⽅能够根据序号将不属于本连接的报⽂段丢弃。另⼀⽅⾯是为了安全性,防⽌⿊客伪造的相同序列号的 TCP 报⽂被对⽅接收。
6.7、初始序列号ISN是如何随机产生的?
I S N = M + F ( l o c a l h o s t , l o c a l p o r t , r e m o t e h o s t , r e m o t e p o r t ) ISN = M + F(localhost,localport,remotehost,remoteport) ISN=M+F(localhost,localport,remotehost,remoteport)
M是一个计数器,F是一个hash算法,根据四元组生成一个随意数值,一般使用MD5算法。
6.8、既然IP层会分片,为什么TCP还需要MSS?
MTU :⼀个⽹络包的最⼤⻓度,以太⽹中⼀般为 1500 字节;
MSS :除去 IP 和 TCP 头部之后,⼀个⽹络包所能容纳的 TCP 数据的最⼤⻓度;
当 IP 层有⼀个超过 MTU ⼤⼩的数据(TCP 头部 + TCP 数据)要发送,那么 IP 层就要进⾏分⽚,把数据分⽚成若⼲⽚,保证每⼀个分⽚都⼩于 MTU。把⼀份 IP 数据报进⾏分⽚以后,由⽬标主机的 IP 层来进⾏重新组装后,再交给上⼀层 TCP 传输层。这看起来井然有序,但这存在隐患的,那么当如果⼀个 IP 分⽚丢失,整个 IP 报⽂的所有分⽚都得重传。
因为 IP 层本身没有超时重传机制,它由传输层的 TCP 来负责超时和重传。
当接收⽅发现 TCP 报⽂(头部 + 数据)的某⼀⽚丢失后,则不会响应 ACK 给对⽅,那么发送⽅的 TCP 在超时后,就会重发「整个 TCP 报⽂(头部 + 数据)」。因此,可以得知由 IP 层进⾏分⽚传输,是⾮常没有效率的。所以,为了达到最佳的传输效能 TCP 协议在建⽴连接的时候通常要协商双⽅的 MSS 值,当 TCP 层发现数据超过MSS 时,则就先会进⾏分⽚,当然由它形成的 IP 包的⻓度也就不会⼤于 MTU ,⾃然也就不⽤ IP 分⽚了。经过 TCP 层分⽚后,如果⼀个 TCP 分⽚丢失后,进⾏重发时也是以 MSS 为单位,⽽不⽤重传所有的分⽚,⼤⼤增加了重传的效率。
总结:IP按MTU分⽚,如果某⼀⽚丢失则需要所有分⽚都重传;(2)IP没有重传机制,所以需要等TCP发送⽅超时才能重传;
问题⼀:MSS跟IP的MTU分⽚相⽐,只是多了⼀步协商MSS值的过程,⽽IP的MTU可以看作是默认协商好就是1500字节,所以为什么协商后的MSS可以做到丢失后只发丢失的这⼀⽚来提⾼效率,⽽默认协商好1500字节的IP分⽚就需要所有⽚都重传呢?
-
如果⼀个⼤的 TCP 报⽂是被 MTU 分⽚,那么只有「第⼀个分⽚」才具有 TCP 头部,后⾯的分⽚则没有TCP 头部,接收⽅ IP 层只有重组了这些分⽚,才会认为是⼀个 TCP 报⽂,那么丢失了其中⼀个分⽚,接收⽅ IP 层就不会把 TCP 报⽂丢给 TCP 层,那么就会等待对⽅超时重传这⼀整个 TCP 报⽂。
-
如果⼀个⼤的 TCP 报⽂被 MSS 分⽚,那么所有「分⽚都具有 TCP 头部」,因为每个 MSS 分⽚的是具有TCP 头部的TCP报⽂,那么其中⼀个 MSS 分⽚丢失,就只需要重传这⼀个分⽚就可以。
问题⼆:TCP MSS分⽚如果丢失了⼀⽚,是不是也需要发送⽅等待超时再重传?如果不是,MSS的协商如何能在超时前就直到丢了分⽚从⽽提⾼效率的呢?
- TCP MSS分⽚如果丢失了⼀⽚,发送⽅没收到对⽅ACK应答,也是会触发超时重传的,因为TCP层是会保证数据的可靠交付。
6.9、SYN攻击是什么?如何避免?
服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的,所以服务器容易受到SYN洪泛攻击。SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,Server则回复确认包,并等待Client确认,由于源地址不存在,因此Server需要不断重发直至超时,这些伪造的SYN包将长时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪。SYN 攻击是一种典型的 DoS/DDoS 攻击。
检测 SYN 攻击非常的方便,当你在服务器上看到大量的半连接状态时,特别是源IP地址是随机的,基本上可以断定这是一次SYN攻击。在 Linux/Unix 上可以使用系统自带的 netstats 命令来检测 SYN 攻击。
netstat -n -p TCP | grep SYN_RECV
常见的防御 SYN 攻击的方法有如下几种:
-
缩短超时(SYN Timeout)时间
-
增加最大半连接数
-
过滤网关防护
-
SYN cookies技术
其中⼀种解决⽅式是通过修改 Linux 内核参数,控制队列⼤⼩和当队列满时应做什么处理。
当⽹卡接收数据包的速度⼤于内核处理的速度时,会有⼀个队列保存这些数据包。控制该队列的最⼤值如下参数:
net.core.netdev_max_backlog
SYN_RCVD 状态连接的最⼤个数:
net.ipv4.tcp_max_syn_backlog
超出处理能时,对新的 SYN 直接回报 RST,丢弃连接:
net.ipv4.tcp_abort_on_overflow
当SYN队列占满,重新启动cookies
net.ipv4.tcp_syncookies = 1
-
当 「 SYN 队列」满之后,后续服务器收到 SYN 包,不进⼊「 SYN 队列」;
-
计算出⼀个 cookie 值,再以 SYN + ACK 中的「序列号」返回客户端,服务端接收到客户端的应答报⽂时,服务器会检查这个 ACK 包的合法性。如果合法,直接放⼊到「 Accept 队列」。
-
最后应⽤通过调⽤ accpet() socket 接⼝,从「 Accept 队列」取出的连接。
7、TCP四次挥手
7.1、为什么挥手需要四次?
-
关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。
-
服务器收到客户端的 FIN 报⽂时,先回⼀个 ACK 应答报⽂,⽽服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报⽂给客户端来表示同意现在关闭连接。
另一种回答:
因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,“你发的FIN报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四次挥手。
7.2、为什么Time_wait等待时间是2MSL?
MSL 是 Maximum Segment Lifetime,报⽂最⼤⽣存时间,它是任何报⽂在⽹络上存在的最⻓时间,超过这个时间报⽂将被丢弃。因为 TCP 报⽂基于是 IP 协议的,⽽ IP 头中有⼀个 TTL 字段,是 IP 数据报可以经过的最⼤路由数,每经过⼀个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报⽂通知源主机。
MSL 与 TTL 的区别: MSL 的单位是时间,⽽ TTL 是经过路由跳数。所以 MSL 应该要⼤于等于 TTL 消耗为 0 的时间,以确保报⽂已被⾃然消亡。
2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK没有传输到服务端,客户端⼜接收到了服务端᯿发的 FIN 报⽂,那么 2MSL 时间将重新计时。 在 Linux 系统⾥ 2MSL 默认是 60 秒,那么⼀个 MSL 也就是 30 秒。Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。
#define TCP_TIMEWAIT_LEN (60*HZ) //最长等待时间
7.3、为什么需要Time_wait状态?
-
防⽌具有相同「四元组」的「旧」数据包被收到;
-
保证「被动关闭连接」的一方能被正确的关闭,即保证最后的 ACK 能让被动关闭⽅接收,从而帮助其正常关闭;
1、防止旧连接的数据包
如果发生网络延迟或者存在服务器关闭之前的报文,那么如果有相同端口的TCP连接被复用,该延迟的报文抵达客户端,客户端仍然能够正常接收这个过期的报文,就会导致最后接收到的数据错乱。
2、保证连接正确关闭
客户端四次挥⼿的最后⼀个 ACK 报⽂如果在⽹络中被丢失了,此时如果客户端 TIME-WAIT 过短或没有,则就直接进⼊了 CLOSED 状态了,那么服务端则会⼀直处在 LASE_ACK 状态。当客户端发起建⽴连接的 SYN 请求报⽂后,服务端会发送 RST 报⽂给客户端,连接建⽴的过程就会被终⽌。
7.4、Time_wait过多怎么处理?
过多的Time_wait状态主要危害有两种:
-
内存资源占用
-
对端口资源的占用,一个TCP连接至少会消耗一个本地端口。
端口数量是有限的,一般为32768~61000(max=65535),如果端口资源占用过多的话,会导致无法创建新的连接。
net.ipv4.ip_local_port_range
问题一:如果客户端第四次挥⼿ack丢失,服务端超时重发的fin报⽂也丢失,客户端timewait时间超过了2msl,这个时候会发⽣什么?认为连接已经关闭吗?
- 当客户端 timewait 时间超过了 2MSL,则客户端就直接进⼊关闭状态。服务端超时重发 fin 报⽂的次数如果超过 tcp_orphan_retries 大小后,服务端也会关闭 TCP 连接。
问题二:如果是服务提供方发起的 close ,然后引起过多的 time_wait 状态的 tcp 链接,time_wait 会影响服务端的端⼝吗?
-
不会,如果发起连接⼀⽅(客户端)的 TIME_WAIT 状态过多,占满了所有端⼝资源,则会导致⽆法创建新连接
客户端受端⼝资源限制:
- 客户端TIME_WAIT过多,就会导致端⼝资源被占⽤,因为端⼝就65536个,被占满就会导致⽆法创建新连接。
服务端受系统资源限制:
- 由于⼀个 TCP 四元组表示 TCP 连接,理论上服务端可以建⽴很多连接,服务端只监听⼀个端⼝,但是会把连接扔给处理线程,所以理论上监听的端⼝可以继续监听。但是线程池处理不了那么多⼀直不断的连接了。所以当服务端出现大量 TIMEWAIT 时,系统资源容易被耗尽。
问题三:服务端设置 SO_REUSEADDR 选项,这样服务器程序在重启后,可以⽴刻使⽤。这⾥设置SO_REUSEADDR 是不是就等价于对这个 socket 设置了内核中的net.ipv4.tcp_tw_reuse=1 这个选项?
-
tcp_tw_reuse 是内核选项,主要⽤在连接的发起⽅(客户端)。TIME_WAIT 状态的连接创建时间超过 1 秒后,新的连接才可以被复⽤,注意,这⾥是「连接的发起⽅」;
-
SO_REUSEADDR 是⽤户态的选项,⽤于「连接的服务⽅」,⽤来告诉操作系统内核,如果端⼝已被占⽤,但是 TCP 连接状态位于 TIME_WAIT ,可以重⽤端⼝。如果端⼝忙,⽽ TCP 处于其他状态,重⽤会有“Address already in use” 的错误信息。
tcp_tw_reuse 是为了缩短 time_wait 的时间,避免出现大量的 time_wait 连接⽽占⽤系统资源,解决的是 accept后的问题。
SO_REUSEADDR 是为了解决 time_wait 状态带来的端⼝占⽤问题,以及⽀持同⼀个 port 对应多个 ip,解决的是bind 时的问题。
7.5、如何优化Time_wait?
打开 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 选项;
net.ipv4.tcp_max_tw_buckets
程序中使⽤ SO_LINGER ,应⽤强制使⽤ RST 关闭
1)net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps
作用:可以复⽤处于 TIME_WAIT 的 socket 为新的连接所⽤。但需要注意,tcp_tw_reuse 功能只能⽤客户端(连接发起⽅),因为开启了该功能,在调⽤ **connect()**函数时,内核会随机找⼀个 time_wait 状态超过 1 秒的连接给新的连接复⽤。(但需要对TCP 时间戳的⽀持)
net.ipv4.tcp_timestamps=1(默认即为 1)
2)net.ipv4.tcp_max_tw_buckets
这个值默认为 18000,当系统中处于 TIME_WAIT 的连接⼀旦超过这个值时,系统就会将后⾯的 TIME_WAIT 连接状态重置(但引入的问题会更多)
3)程序中使⽤ SO_LINGER,应⽤强制使⽤ RST 关闭
相当于通过设置socket选项,来设置调用close关闭连接的行为。调用close之后,会立刻发送一个RST标志给对端,直接跳过四次挥手,跳过time_wait,直接关闭。
7.6、如果已经建立了连接,但是client突然发生故障怎么办?
TCP有一个保活机制(keep_alive):
原理:定义⼀个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作⽤,每隔⼀个时间间隔,发送⼀个探测报⽂,该探测报⽂包含的数据⾮常少,如果连续⼏个探测报⽂都没有得到响应,则认为当前的TCP 连接已经死亡,系统内核将错误信息通知给上层应⽤程序。
在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔,以下都为默认值:
net.ipv4.tcp_keepalive_time=7200 //~2小时,两小时内如果没有任何连接相关的活动,则会启动保活机制
net.ipv4.tcp_keepalive_intvl=75 //每次检测间隔75s
net.ipv4.tcp_keepalive_probes=9 //检测响应次数最大9次,如果检测9次无响应,则认为对方不可达,从而中断本次连接
**注意**开启TCP保活机制,需要注意一下情况:
-
第⼀种,对端程序是正常⼯作的。当 TCP 保活的探测报⽂发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下⼀个 TCP 保活时间的到来。
-
第⼆种,对端程序崩溃并重启。当 TCP 保活的探测报⽂发送给对端后,对端是可以响应的,但由于没有该连接的有效信息,会产⽣⼀个 RST 报⽂,这样很快就会发现 TCP 连接已经被重置。
-
第三种,是对端程序崩溃,或对端由于其他原因导致报⽂不可达。当 TCP 保活的探测报⽂发送给对端后,⽯沉⼤海,没有响应,连续⼏次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡。
7.7、服务器出现大量close_wait的连接的原因是什么?有什么解决方法?
close_wait状态是在TCP四次挥手的时候收到FIN但是没有发送自己的FIN时出现的,服务器出现大量close_wait状态的原因有两种:
-
服务器内部业务处理占用了过多时间,都没能处理完业务;或者还有数据需要发送;或者服务器的业务逻辑有问题,没有执行close()方法
-
服务器的父进程派生出子进程,子进程继承了socket,收到FIN的时候子进程处理但父进程没有处理该信号,导致socket的引用不为0无法回收
处理方法:
-
停止应用程序
-
修改程序里的bug
8、TCP如何保证可靠传输?
校验和、确认机制(确认应答+序列号)、重传机制、连接管理(挥手和握手)、流量控制(滑动窗口)、拥塞控制
8.1、TCP可靠的原因?
每个TCP的socket在内核中都有一个发送缓冲区和接收缓冲区,TCP协议要求在对端接收到TCP数据报之后,对其序号进行ACK,只有当接收到一个TCP数据报的ACK之后,才可以把这个TCP数据报从socket的发送缓冲区清除,另外,TCP还有一个流量控制功能,TCP的socket接收缓冲区接收到网络上来的数据缓存后,如果应用程序一直没有读取,那接收缓冲区满了之后,就会通知对端TCP协议中的窗口关闭,就是滑动窗口实现流量控制,保证TCP的接收缓冲区不会溢出,因为对方不允许发送超过所通知窗口大小的数据,要是无视窗口大小而发送了超出窗口大小的数据,则接受发TCP将丢弃它。综上来说,TCP有三次握手、四次挥手,除此之外还有超时重传机制,对于每份报文也存在校验和,保证每份报文可靠性。
9、TCP重传、滑动窗口、拥塞控制、流量控制
9.1、重传机制(丢包)
重传机制主要有四种:超时重传、快速重传、SACK、D-SACK
1、超时重传
在发送数据时,设定⼀个定时器,当超过指定的时间后,没有收到对⽅的 ACK确认应答报⽂,就会重发该数据。
-
应用场景:数据包丢失、确认应答丢失。
-
超时时间的设置:RTO > RTT
超时间隔加倍:每当遇到⼀次超时重传的时候,都会将下⼀次超时时间间隔设为先前值的两倍。两次超时,就说明⽹络环境差,不宜频繁反复发送。
问题一:超时重传、RTO与RTT区别是什么?
-
超时重传:发送端发送报文后若长时间未收到确认的报文则需要重发该报文。可能有以下几种情况:
-
发送的数据没能到达接收端,所以对方没有响应。
-
接收端接收到数据,但是ACK报文在返回过程中丢失。
-
接收端拒绝或丢弃数据。
-
-
RTO(Retransmission Timeout 超时重传时间):从上一次发送数据,因为长期没有收到ACK响应,到下一次重发之间的时间,即就是重传间隔。
- 通常每次重传RTO是前一次重传间隔的两倍,计量单位通常是RTT。例:1RTT,2RTT,4RTT,8RTT…
- 重传次数到达上限之后停止重传。
-
RTT(Round-Trip Time 往返时延):就是数据从网络一端传送到另一端所需的时间,即包的往返时间。
问题二:如果RTO较长或较短时,会发生什么事情呢?
-
RTO较长:网路的空隙时间增大,降低了网络传输效率。
-
RTO较短:产生不必要的重传(可能没有丢包但重发了),会导致网络负荷增大,造成网络拥塞。
问题三:如何计算RTO?
其中 SRTT 是计算平滑的RTT , DevRTR 是计算平滑的RTT 与 最新 RTT 的差距。
在 Linux 下,α = 0.125,β = 0.25, μ = 1,∂ = 4(实验结论)
2、快速重传
不以时间为驱动,⽽是以数据驱动重传(3次同样的ACK触发快速重传机制)
但是,快速重传机制只解决了超时时间的问题,但是重传过程中存在一个问题,是重传之前的一个,还是重传所有?(SACK解决该问题)。
3、SACK
TCP头部[选项]字段里加SACK,就可以将缓存的地图发送给发送方,这样就可以知道哪些数据收到了,哪些数据没有收到,因此只需要重新传丢包的数据。
net.ipv4.tcp_sack
4、D-SACK
Duplicate SACK ,使用了SACK来告诉发送方有哪些数据被重复接收了。
D-SACK优势:
-
可以让「发送⽅」知道,是发出去的包丢了,还是接收方回应的 ACK 包丢了;
-
可以知道是不是「发送⽅」的数据包被⽹络延迟了;
-
可以知道⽹络中是不是把「发送⽅」的数据包给复制了;
net.ipv4.tcp_dsack
9.2、滑动窗口
引言:TCP每发送一次数据,就需要应答一次,然后再发送下一个,这样就存在一个问题,如果应答时间较长的话,就会导致网络的吞吐量较低。所以为了解决数据包的往返时间越长,通信效率越低的问题,引入窗口。
窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等待确认应答返回之前,必须在缓冲区中保留已经发送的数据。如果已经确认应答,则可以删除缓存中的数据。
TCP头部有一个window字段,该字段主要是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。
窗口大小:指不需要等待确认应答,而可以继续发送数据的最大值。
窗口的大小一般都是由接收方的窗口大小来决定。
问题一:程序是如何表示发送方的四个部分呢?
TCP 滑动窗⼝⽅案使⽤三个指针来跟踪在四个传输类别中的每⼀个类别中的字节。其中两个指针是绝对指针(指特定的序列号),⼀个是相对指针(需要做偏移)。
发送窗口:
接收窗口:
9.3、流量控制
为了解决网络浪费的情况,引入流量控制,让发送方根据接收方的实际接收能力控制发送的数据量。
1、操作系统缓存区与滑动窗口的关系
当服务端系统资源⾮常紧张的时候,操⼼系统可能会直接减少了接收缓冲区⼤⼩,这时应⽤程序⼜⽆法及时读取缓存数据,那么这时候就有严重的事情发⽣了,会出现数据包丢失的现象。
- TCP 规定是不允许同时减少缓存⼜收缩窗⼝的,⽽是采⽤先收缩窗⼝,过段时间再减少缓存,这样就可以避免了丢包情况
2、窗口关闭:
窗口关闭:如果窗⼝⼤⼩为 0 时,就会阻⽌发送⽅给接收⽅传递数据,直到窗⼝变为⾮ 0 为⽌,这就是窗⼝关闭。
问题一:窗口关闭存在的问题?
- 接收⽅向发送⽅通告窗⼝⼤⼩时,是通过 ACK 报⽂来通告的,那么,当发⽣窗⼝关闭时,接收⽅处理完数据后,会向发送⽅通告⼀个窗⼝⾮ 0 的 ACK 报⽂,如果这个通告窗⼝的 ACK 报⽂在⽹络中丢失了,那麻烦就⼤了。
- 这会导致发送⽅⼀直等待接收⽅的⾮ 0 窗⼝通知,接收⽅也⼀直等待发送⽅的数据,如不采取措施,这种相互等待的过程,会造成了死锁的现象。
问题二:如何解决窗口关闭时,潜在的死锁现象?
-
TCP 为每个连接设有⼀个持续定时器,只要 TCP 连接⼀⽅收到对⽅的零窗⼝通知,就启动持续计时器
-
如果计时器超时,就会发送窗口探测报文,而对方在确认这个探测报文时,给出自己现在的接收窗口大小。
- 如果窗口仍然为0,则又重新启动持续计时器;
- 如果接收窗口不是0,那么接触死锁。
-
窗⼝探测的次数⼀般为 3 次,每次⼤约 30-60 秒(不同的实现可能会不⼀样)。如果 3 次过后接收窗⼝还是 0 的话,有的 TCP 实现就会发 RST 报⽂来中断连接
3、糊涂窗口综合征
如果接收⽅太忙了,来不及取⾛接收窗⼝⾥的数据,那么就会导致发送⽅的发送窗⼝越来越⼩。到最后,如果接收⽅腾出⼏个字节并告诉发送⽅现在有⼏个字节的窗⼝,⽽发送⽅会义⽆反顾地发送这⼏个字节,这就是糊涂窗⼝综合症。(一般针对小窗口问题)
问题一:怎么让接收方不通告小窗口呢?
- 当「窗⼝⼤⼩」⼩于 min( MSS,缓存空间/2 ) ,也就是⼩于 MSS 与 1/2 缓存⼤⼩中的最⼩值时,就会向发送⽅通告窗⼝为 0 ,也就阻⽌了发送⽅再发数据过来
- 等到接收⽅处理了⼀些数据后,窗⼝⼤⼩ >= MSS,或者接收⽅缓存空间有⼀半可以使⽤,就可以把窗⼝打开让发送⽅发送数据过来
问题二:怎么让发送方避免发送小数据呢?
-
使⽤ Nagle 算法,该算法的思路是延时处理,它满⾜以下两个条件中的⼀条才可以发送数据:
-
要等到窗⼝⼤⼩ >= MSS 或是 数据⼤⼩ >= MSS
-
收到之前发送数据的 ack 回包
-
只要没满⾜上⾯条件中的⼀条,发送⽅⼀直在囤积数据,直到满⾜上⾯的发送条件。
TCP_NODELAY //TCP通过设置该选项来关闭Nagle算法,一般默认打开,但是对于交互性比较强的程序,如telnet或ssh则需要关闭
setsockopt(sock_fd, IPPROTO_TCP, TCP_NODELAY, (char *)&value, sizeof(int));
9.4、拥塞控制
拥塞控制目的是避免「发送⽅」的数据填满整个⽹络。
拥塞窗口:cwnd是发送方维护的一个状态变量,会根据网络的拥塞程度实现动态变化。
发送窗⼝ swnd 近似等于接收窗⼝ rwnd,引入拥塞窗口之后,此时发发送窗口swnd = min(cwnd, rwnd)。
拥塞窗口cwnd变化规则:如果未曾出现网络拥塞,则cwnd就会增大;一旦出现网络拥塞,cwnd就会减小。此时的网络拥塞的判断是,是否发生了超时重传,一旦发生了超时重传,即只要发送方没有在规定的时间内收到接收方ACK应答报文,则认为网络拥塞。
总述拥塞控制在刚开始传输就发送大量的数据,网络可能在一开始就很拥堵,持续发送就会越来越堵,拥堵的加剧会产生大量的丢包,大量的超时重传,严重影响传输。所以TCP引入a)慢启动机制,在开始发送数据的时候,先发送少量的数据探明当前网络的状况,再决定多大的速度进行传输,这时有种拥塞窗口的概念,发送刚开始定义拥塞窗口位1,每次收到ACK应答,拥塞窗口增加,发送数据前,将拥塞窗口与接收端反馈的窗口大小比对,取较小的值做为实际发送的窗口。慢启动只是说一开始发送的少,但是拥塞窗口的增加是指数级别的,为了控制拥塞窗口的增长,b)拥塞避免,设置拥塞窗口阙值,当拥塞窗口的大小超过阙值时,将不会按照原来的指数增长而是线性的增长。在慢启动开始的时候,慢启动的阙值等于窗口的最大值,一旦造成网络拥塞,发生超时重传,就会到d) 快重传接收方在收到一个失序的报文段就发出重复确认,发送方只要一连收到三个重复确认就应当立即重传对方未接受到的报文段,而不必继续等待设置重传计时器时间到期。e) 快恢复当收到3个重复的确认后,说明网络不那么糟糕,可以快速恢复,又进行慢启动,窗口阙值会降为发生网络拥塞时窗口大小的一半,同时拥塞窗口重置为1。
1、慢启动:
慢启动:也就是⼀点⼀点的提⾼发送数据包的数量。
慢启动规则:当发送⽅每收到⼀个 ACK,拥塞窗⼝ cwnd 的⼤⼩就会加 1,但是慢启动算法其发包的个数是指数性的增长。当到达慢启动门限ssthresh状态变量时,就要开始使用拥塞避免算法。
-
当cwnd < ssthresh 时,使⽤慢启动算法。
-
当 cwnd >= ssthresh 时,就会使⽤「拥塞避免算法」。
2、拥塞避免
当cwnd >= ssthresh时,就使用拥塞避免算法,⼀般来说 ssthresh 的⼤⼩是 65535 字节。
拥塞避免规则:
-
每当收到⼀个 ACK 时,cwnd 增加 1/cwnd
-
每当过了一个往返延迟时间RTT,cwnd大小加一。
目的是将慢启动算法中的指数增长变成了线性增长,但仍然处于增长状态,只是增长的速度更缓慢一些。但,随着数据包一直增加,也会导致网路拥塞,于是就存在丢包现象,就需要对丢包的数据进行重传,触发重传机制,也就进入了「拥塞发生算法」。
3、拥塞发生
一般来说,TCP拥塞控制默认认为网络丢包是由于网络拥塞导致的,所以一般的TCP拥塞控制算法以丢包为网络进入拥塞状态的信号。对于丢包有两种判定方式,一种是超时重传RTO[Retransmission Timeout]超时,另一个是收到三个重复确认ACK。
超时重传RTO是TCP协议保证数据可靠性的一个重要机制,其原理是在发送一个数据以后就开启一个计时器,在一定时间内如果没有得到发送数据报的ACK报文,那么就重新发送数据,直到发送成功为止。但是如果发送端接收到3个以上的重复ACK,TCP就意识到数据发生丢失,需要重传。这个机制不需要等到重传定时器超时,所以叫做快速重传,而快速重传后没有使用慢启动算法,而是拥塞避免算法,所以这又叫做快速恢复算法。
-
超时重传RTO[Retransmission Timeout]超时,TCP会重传数据包。(重新从慢启动开始,会造成网路卡顿情况)
- 由于发生丢包,将慢启动阈值ssthresh设置为当前cwnd的一半,即ssthresh = cwnd / 2.
- cwnd重置为1
- 进入慢启动过程
-
快速重传:3次重复ACK,不用等到RTO超时再进行重传
- cwnd大小缩小为当前的一半,cwnd = cwnd / 2;
- ssthresh设置为缩小后的cwnd大小,ssthresh = cwnd;
- 然后进入快速恢复算法Fast Recovery。
4、快重传、快恢复
-
cwnd = cwnd + 3 MSS,加3 MSS的原因是因为收到3个重复的ACK。
-
重传DACKs指定的数据包。
-
如果再收到DACKs,那么cwnd大小增加一。
-
如果收到新的ACK,表明重传的包成功了,那么退出快速恢复算法。将cwnd设置为ssthresh,然后进入拥塞避免算法。
问题一:为什么快速重传是选择三次ACK,而不是两次或者四次?
- 主要的考虑还是要区分包的丢失是由于链路故障还是乱序等其他因素引发。
- 两次duplicated ACK时很可能是乱序造成的!三次duplicated ACK时很可能是丢包造成的!四次duplicated ACK更更更可能是丢包造成的,但是这样的响应策略太慢。丢包肯定会造成三次duplicated ACK!综上是选择收到三个重复确认时窗口减半效果最好,这是实践经验。
问题二:丢包原因有哪些?
-
在没有fast retransmit / recovery 算法之前,重传依靠发送方的retransmit timeout,就是在timeout内如果没有接收到对方的ACK,默认包丢了,发送方就重传,包的丢失原因:
- 包checksum 出错
- 网络拥塞
- 网络断,包括路由重收敛,但是发送方无法判断是哪一种情况,于是采用最笨的办法,就是将自己的发送速率减半,即CWND 减为1/2,这样的方法对2是有效的,可以缓解网络拥塞,3则无所谓,反正网络断了,无论发快发慢都会被丢;但对于1来说,丢包是因为偶尔的出错引起,一丢包就对半减速不合理。
-
于是有了fast retransmit 算法,基于在反向还可以接收到ACK,可以认为网络并没有断,否则也接收不到ACK,如果在timeout 时间内没有接收到> 2 的duplicated ACK,则概率大事件为乱序,乱序无需重传,接收方会进行排序工作;而如果接收到三个或三个以上的duplicated ACK,则大概率是丢包,可以逻辑推理,发送方可以接收ACK,则网络是通的,可能是1、2造成的,先不降速,重传一次,如果接收到正确的ACK,则一切OK,流速依然(包出错被丢)。而如果依然接收到duplicated ACK,也就是DACK,则认为是网络拥塞造成的,此时降速则比较合理。
10、Nagle算法&&延迟ACK
10.1、Nagle算法:
Nagle算法是为了减少广域网的小分组数目,从而减小网络拥塞的出现;
- Nagle算法策略:
- 没有已发送为确认的报文时,立即发送数据
- 存在未确认报文时,直到「没有已发送未确认报⽂」或「数据⻓度达到 MSS ⼤⼩」时,再发送数据。
该算法要求一个tcp连接上最多只能有一个未被确认的未完成的小分组,在该分组ack到达之前不能发送其他的小分组,tcp需要收集这些少量的分组,并在ack到来时以一个分组的方式发送出去;其中小分组的定义是小于MSS的任何分组;
- Nagle算法优点:
- 自适应的,确认到达的越快,数据也就发送的越快;
- 在希望减少微小分组数目的低速广域网上,则会发送更少的分组;
TCP_NODELAY //TCP通过设置该选项来关闭Nagle算法,一般默认打开,但是对于交互性比较强的程序,如telnet或ssh则需要关闭
setsockopt(sock_fd, IPPROTO_TCP, TCP_NODELAY, (char *)&value, sizeof(int));
10.2、延迟ACK:
- 延迟ACK策略:
- 当有响应数据要发送时,ACK会随着响应数据一起立刻发送给对方;
- 当没有响应数据要发送时,ACK会延迟一段时间,以等待是否有响应数据可以一起发送;
- 如果在延迟等待发送ACK期间,对方的第二个数据报文到达了,则立即发送ACK。
如果tcp对每个数据包都发送一个ack确认,那么只是一个单独的数据包为了发送一个ack代价比较高,所以tcp会延迟一段时间,如果这段时间内有数据发送到对端,则捎带发送ack,如果在延迟ack定时器触发时候,发现ack尚未发送,则立即单独发送;
- 延迟ACK好处:
- 避免糊涂窗口综合症;
- 发送数据的时候将ack捎带发送,不必单独发送ack;
- 如果延迟时间内有多个数据段到达,那么允许协议栈发送一个ack确认多个报文段;
//关闭TCP延迟ACK
//通过在socket设置TCP_QUICKACK来关闭这个算法
setsockeopt(sock_fd, IPPROTO_TCP, TCP_QUICKACK, (char*)& value, sizeof(int));
10.3、当Nagle遇上延迟ACK:
试想如下典型操作,写-写-读,即通过多个写小片数据向对端发送单个逻辑的操作,两次写数据长度小于MSS,当第一次写数据到达对端后,对端延迟ack,不发送ack,而本端因为要发送的数据长度小于MSS,所以nagle算法起作用,数据并不会立即发送,而是等待对端发送的第一次数据确认ack;这样的情况下,需要等待对端超时发送ack,然后本段才能发送第二次写的数据,从而造成延迟;
10.4、关闭Nagle算法:
使用TCP套接字选项TCP_NODELAY可以关闭套接字选项;
如下场景考虑关闭Nagle算法:
-
对端不向本端发送数据,并且对延时比较敏感的操作;这种操作没法捎带ack;
-
如上写-写-读操作;对于此种情况,优先使用其他方式,而不是关闭Nagle算法:
- 使用writev,而不是两次调用write,单个writev调用会使tcp输出一次而不是两次,只产生一个tcp分节,这是首选方法;
- 把两次写操作的数据复制到单个缓冲区,然后对缓冲区调用一次write;
- 关闭Nagle算法,调用write两次;有损于网络,通常不考虑;
10.5、禁止Nagle和开启Nagle算法发送数据与确认示意图:
11 、TCP异常分析
11.1、TCP第一次握手的SYN丢包了,会发生什么?
客户端发起SYN包后,如果一直没有收到服务端的ACK,就会触发超时重传RTO机制。在Linux中,第一次捂手的SYN超时重传次数是由内核参数指定,tcp_syn_retires默认重传5次。因此,当客户端TCP第一次握手发生SYN包,在超过时间内没有收到服务端的ACK报文,就会超时重传SYN数据包,每次超时重传RTO是成倍增加的,直到超过SYN包重传次数,则客户端不再发送SYN包。
11.2、TCP第二次握手的SYN、ACK丢包了,会发生什么?
当第二次握手的SYN和ACK丢包时,客户端会超时重发SYN包,服务端会超时重发SYN + ACK包。在Linux中,第二次捂手的SYN + ACK 超时重传次数是由内核参数指定,tcp_synack_retires默认重传5次。超过重传次数之后,服务端的TCP连接主动断开,但是客户端仍然处于established状态。
11.3、TCP第三次握手的ACK丢包了,会发生什么?
由于服务端已经断开连接,此时客户端再次向服务端发送数据报文,则会一直处于超时重传状态,每重传一次,RTO翻倍增长,所以持续一段时间之后,共重传15次之后,客户端的talent才报错退出。最⼤超时重传次数是由 tcp_retries2 指定,默认值是 15 次。
11.4、如果客户端不发送数据,什么时候才会断开处于established状态的连接?
核心:保活机制keeping_alive
定义⼀个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作⽤,每隔⼀个时间间隔,发送⼀个「探测报⽂」,该探测报⽂包含的数据⾮常少,如果连续⼏个探测报⽂都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应⽤程序。
总结
在建⽴ TCP 连接时,如果第三次握⼿的 ACK,服务端⽆法收到,则服务端就会短暂处于 SYN_RECV 状态,⽽客户端会处于 ESTABLISHED 状态。
由于服务端⼀直收不到 TCP 第三次握⼿的 ACK,则会⼀直重传 SYN、ACK 包,直到重传次数超过tcp_synack_retries 值(默认值 5 次)后,服务端就会断开 TCP 连接。
⽽客户端则会有两种情况:
-
如果客户端没发送数据包,⼀直处于 ESTABLISHED 状态,然后经过 (保活机制时间)2 ⼩时 11 分 15 秒才可以发现⼀个「死亡」连接,于是客户端连接就会断开连接。
-
如果客户端发送了数据包,⼀直没有收到服务端对该数据包的确认报⽂,则会⼀直重传该数据包,直到重传次数超过 tcp_retries2 值(默认值 15 次)后,客户端就会断开 TCP 连接。
12、拆包和粘包
12.1、TCP和UDP哪个会发生粘包?
只有TCP会产生粘包,UDP不会。首先,TCP采用的SOCKET是SOCK_STREAM (流式套接字) ,UDP是SOCK_DGRAM(数据报套接字)。TCP基于字节流,虽然应用层和TCP传输层之间的数据交互是大小不等的数据块,但是TCP把这些数据块仅仅看成一串无结构的字节流,没有边界,并且TCP首部没有表示数据长度的字段。UDP是基于数据报发送,从UDP的帧结构可以看出,UDP的首部采用了16bits来指示UDP报文的长度,所以在应用层可以很好的将不同的数据报文区分开。
12.2、粘包、拆包的可能情况
粘包:接收端只收到一个数据包,但是TCP是不会出现丢包,所以就会出现一个数据包中包含了发送端发送的两个数据包的信息,由于接收端不知道这两个数据包的界限,就是粘包现象。
拆包和粘包:接收端接收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,就发生了拆包和粘包。
12.3、粘包、拆包发生的原因
- 发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包;
- 待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包;
- 要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次性发送出去,将会发生粘包;
- 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
12.4、解决办法
关键在于给每个数据包添加边界信息,一般有如下几种处理方式:
-
发送端给每个数据包添加包头,头部包含数据包的长度信息,这样接收端在接收到数据之后,可以通过包头的长度字段,来知道每个包的实际长度;
-
发送端将每个数据包封装成固定长度(不够的补0),这样接收端每次从接收缓冲区读取固定长度的数据,就可以把每个数据包拆分开来;
-
或者在数据包之间设置边界,如添加特殊符号等,接收端就可以通过这个边界将不同的数据包拆分开。这种方式需要保证,选择开始符和结束符时需要确保每条数据的内部不包含开始符和结束符。
13、UDP
13.1、UDP报文格式
⽬标和源端⼝:主要是告诉 UDP 协议应该把报⽂发给哪个进程。
包⻓度:该字段保存了 UDP ⾸部的⻓度跟数据的⻓度之和。
校验和:校验和是为了提供可靠的 UDP ⾸部和数据⽽设计。
13.2、UDP使用connect
1、UDP可以使用connect系统调用
UDP中的connect与TCP中的由本质的区别,TCP调用connect会引发三次握手,client与server建立连接。UDP是面向无连接的,调用connect是把对端IP和port记录下来;
2、UDP可以多次调用connect,TCP只能调用一次
UDP多次调用connect主要是希望指定一个新的IP和port连接,以及断开以前的IP和port连接。指定新连接直接设施connect的第二个参数(sockaddr_in sin),断开连接,需要将connect中第二个参数中的sin_family设置成 AF_UNSPEC;
3、UDP使用connect可以提高效率
LINUX系统有用户空间和内核空间之分,接收数据,数据从网卡上收上来,需要先交给系统内核,然后内核再交给上层应用程序(处于用户空间)。发送数据也是一样,数据需要从用户空间拷贝到内核,内核处理完之后,再交给网卡发出去。在用户态和内核态进行切换,非常耗时,对于高性能的服务器来说,其实应该减少这种耗时,但是如果切换无法避免,就尽量减少切换时拷贝的数据,那调用connect之后的UDP,内核相当于维护了一个“连接”,就可以调用send来发送数据了,那send对比于sendto其实参数少得多,那每次调用的时候就会少拷贝一些数据到内核空间。从另一个方面来讲,sendto的参数到内核空间以后,内核需要分配内存来存储这些参数值,当数据包发送出去之后,内核还需要释放掉这块内存,下次再调用sendto的时候,内核就需要再次分配内存存放这些临时的数据,就会形成一个不断地分配和释放临时内存的过程。那connect之后可以使用send,相当于维护了这个连接,所以后面每次进行发送数据,内核就不需要再分配删除内存了。
4、UDP使用connect可以得到错误信息的提示
在使用connect编写UDP SOCKET的时候,会遇到连接错误的提示(ECONNREFUSED),可以本来UDP是无连接的,报连接错误其实是ICMP带来的。当一个UDP socket去connect对端是,并没有发送任何数据包,仅仅只是在内核建立了一个映射,该映射的作用是为了把UCP和ICMP(IP协议的补充,检测网络连接)通道捆绑在一起,调用了connect之后,内核协议栈就维护了一个从源目的地的单向连接,当下层有ICMP错误信息返回时,内核就可以根据这个映射找到是哪个UDP的socket发的包失败了,进而可以把得到该错误信息了,要是没有connect,是得不到该错误信息的。
13.3、UDP使用bind
13.4、为什么UDP不可靠?如何实现UDP可靠传输?
标签:UDP,重传,ACK,TCP,发送,拆包,连接 来源: https://blog.csdn.net/loytuls/article/details/123426442