TCP三次握手与四次挥手
1.TCP 头格式有哪些?
标注颜⾊的表示与本⽂关联⽐较⼤的字段,其他字段不做详细阐述。
序列号:在建⽴连接时由计算机⽣成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送⼀次数
据,就「累加」⼀次该「数据字节数」的⼤⼩。⽤来解决⽹络包乱序问题。
确认应答号:指下⼀次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号
以前的数据都已经被正常接收。⽤来解决丢包的问题。
控制位:
ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建⽴连接时的 SYN 包之
外该位必须设置为 1 。
RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。
SYN:该位为 1 时,表示希望建⽴连接,并在其「序列号」的字段进⾏序列号初始值的设定。
FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通
信双⽅的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。
2.为什么需要 TCP 协议? TCP ⼯作在哪⼀层?
IP 层是「不可靠」的,它不保证⽹络包的交付、不保证⽹络包的按序交付、也不保证⽹络包中的数据的
完整性。如果需要保障⽹络数据包的可靠性,那么就需要由上层(传输层)的 TCP 协议来负责。因为 TCP 是⼀个⼯作在传输层的可靠数据传输的服务,它能确保接收端接收的⽹络包是⽆损坏、⽆间隔、
⾮冗余和按序的。
3.什么是 TCP ?
TCP 是⾯向连接的、可靠的、基于字节流的传输层通信协议。
⾯向连接:⼀定是「⼀对⼀」才能连接,不能像 UDP 协议可以⼀个主机同时向多个主机发送消息,
也就是⼀对多是⽆法做到的;
可靠的:⽆论的⽹络链路中出现了怎样的链路变化,TCP 都可以保证⼀个报⽂⼀定能够到达接收端;
字节流:⽤户消息通过 TCP 协议传输时,消息可能会被操作系统「分组」成多个的 TCP 报⽂,如果
接收⽅的程序如果不知道「消息的边界」,是⽆法读出⼀个有效的⽤户消息的。并且 TCP 报⽂是
「有序的」,当「前⼀个」TCP 报⽂没有收到的时候,即使它先收到了后⾯的 TCP 报⽂,那么也不
能扔给应⽤层去处理,同时对「᯿复」的 TCP 报⽂会⾃动丢弃。
4.什么是 TCP 连接?
我们来看看 RFC 793 是如何定义「连接」的:
Connections:
The reliability and flow control mechanisms described above require that TCPs initialize and maintain
certain status information for each data stream. The combination of this information, including sockets,
sequence numbers, and window sizes, is called a connection.
简单来说就是,⽤于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括 Socket**、序列号**
和窗⼝⼤⼩称为连接。
所以我们可以知道,建⽴⼀个 TCP 连接是需要客户端与服务端达成上述三个信息的共识。
Socket:由 IP 地址和端⼝号组成
序列号:⽤来解决乱序问题等
窗⼝⼤⼩:⽤来做流ᰁ控制
5.如何唯⼀确定⼀个 TCP 连接呢?
TCP 四元组可以唯⼀的确定⼀个连接,四元组包括如下:
源地址
源端⼝
⽬的地址
⽬的端⼝
源地址和⽬的地址的字段(32 位)是在 IP 头部中,作⽤是通过 IP 协议发送报⽂给对⽅主机。
源端⼝和⽬的端⼝的字段(16 位)是在 TCP 头部中,作⽤是告诉 TCP 协议应该把报⽂发给哪个进程。
6.有⼀个 IP 的服务端监听了⼀个端⼝,它的 TCP 的最⼤连接数是多少?
服务端通常固定在某个本地端⼝上监听,等待客户端的连接请求。
因此,客户端 IP 和端⼝是可变的,其理论值计算公式如下:
对 IPv4,客户端的 IP 数最多为 2 的 32 次⽅,客户端的端⼝数最多为 2 的 16 次⽅,也就是服务端
单机最⼤ TCP 连接数,约为 2 的 48 次⽅。
当然,服务端最⼤并发 TCP 连接数远不能达到理论上限,会受以下因素影响
⽂件描述符限制,每个 TCP 连接都是⼀个⽂件,如果⽂件描述符被占满了,会发⽣ Too many open
files。Linux 对可打开的⽂件描述符的数ᰁ分别作了三个⽅⾯的限制:
系统级:当前系统可打开的最⼤数ᰁ,通过 cat /proc/sys/fs/file-max 查看;
⽤户级:指定⽤户可打开的最⼤数ᰁ,通过 cat /etc/security/limits.conf 查看;
进程级:单个进程可打开的最⼤数ᰁ,通过 cat /proc/sys/fs/nr_open 查看;
内存限制,每个 TCP 连接都要占⽤⼀定内存,操作系统的内存是有限的,如果内存资源被占满后,
会发⽣ OOM。
7.UDP 和 TCP 有什么区别呢?分别的应⽤场景是?
UDP 不提供复杂的控制机制,利⽤ IP 提供⾯向「⽆连接」的通信服务。
UDP 协议真的⾮常简,头部只有 8 个字节(64 位),UDP 的头部格式如下
⽬标和源端⼝:主要是告诉 UDP 协议应该把报⽂发给哪个进程。
包⻓度:该字段保存了 UDP ⾸部的⻓度跟数据的⻓度之和。
校验和:校验和是为了提供可靠的 UDP ⾸部和数据⽽设计,防⽌收到在⽹络传输中受损的 UDP 包。
TCP 和 UDP 区别:
1. 连接
TCP 是⾯向连接的传输层协议,传输数据前先要建⽴连接。
UDP 是不需要连接,即刻传输数据。
2. 服务对象
TCP 是⼀对⼀的两点服务,即⼀条连接只有两个端点。
UDP ⽀持⼀对⼀、⼀对多、多对多的交互通信
3. 可靠性
TCP 是可靠交付数据的,数据可以⽆差错、不丢失、不᯿复、按序到达。
UDP 是尽最⼤努⼒交付,不保证可靠交付数据。但是我们可以基于 UDP 传输协议实现⼀个可靠的传
输协议,⽐如 QUIC 协议,具体可以参⻅这篇⽂章:如何基于 UDP 协议实现可靠传输
4. 拥塞控制、流ᰁ控制
TCP 有拥塞控制和流ᰁ控制机制,保证数据传输的安全性。
UDP 则没有,即使⽹络⾮常拥堵了,也不会影响 UDP 的发送速率。
5. ⾸部开销
TCP ⾸部⻓度较⻓,会有⼀定的开销,⾸部在没有使⽤「选项」字段时是 20 个字节,如果使⽤了
「选项」字段则会变⻓的。
UDP ⾸部只有 8 个字节,并且是固定不变的,开销较⼩。
6. 传输⽅式
TCP 是流式传输,没有边界,但保证顺序和可靠。
UDP 是⼀个包⼀个包的发送,是有边界的,但可能会丢包和乱序。
7. 分⽚不同
TCP 的数据⼤⼩如果⼤于 MSS ⼤⼩,则会在传输层进⾏分⽚,⽬标主机收到后,也同样在传输层组
装 TCP 数据包,如果中途丢失了⼀个分⽚,只需要传输丢失的这个分⽚。
UDP 的数据⼤⼩如果⼤于 MTU ⼤⼩,则会在 IP 层进⾏分⽚,⽬标主机收到后,在 IP 层组装完数
据,接着再传给传输层。
TCP 和 UDP 应⽤场景:
由于 TCP 是⾯向连接,能保证数据的可靠性交付,因此经常⽤于:
FTP ⽂件传输;
HTTP / HTTPS;
由于 UDP ⾯向⽆连接,它可以随时发送数据,再加上 UDP 本身的处理既简单⼜⾼效,因此经常⽤于:
包总ᰁ较少的通信,如 DNS 、 SNMP 等;
视频、⾳频等多媒体通信;
⼴播通信;
8.为什么 UDP 头部没有「⾸部⻓度」字段,⽽ TCP 头部有「⾸部⻓度」字段呢?
原因是 TCP 有可变⻓的「选项」字段,⽽ UDP 头部⻓度则是不会变化的,⽆需多⼀个字段去记录 UDP
的⾸部⻓度。
9.为什么 UDP 头部有「包⻓度」字段,⽽ TCP 头部则没有「包⻓度」字段呢?
先说说 TCP 是如何计算负载数据⻓度
因为为了⽹络设备硬件设计和处理⽅便,⾸部⻓度需要是 4 字节的整数倍。如果去掉
UDP 的「包⻓度」字段,那 UDP ⾸部⻓度就不是 4 字节的整数倍了,所以我觉得这可能是为了补
全 UDP ⾸部⻓度是 4 字节的整数倍,才补充了「包⻓度」字段。
10.TCP 和 UDP 可以使⽤同⼀个端⼝吗?
答案:可以的。
在数据链路层中,通过 MAC 地址来寻找局域⽹中的主机。在⽹际层中,通过 IP 地址来寻找⽹络中互连的
主机或路由器。在传输层中,需要通过端⼝进⾏寻址,来识别同⼀计算机中同时通信的不同应⽤程序。
所以,传输层的「端⼝号」的作⽤,是为了区分同⼀个主机上不同应⽤程序的数据包。
传输层有两个传输协议分别是 TCP 和 UDP,在内核中是两个完全独⽴的软件模块。
当主机收到数据包后,可以在 IP 包头的「协议号」字段知道该数据包是 TCP/UDP,所以可以根据这个信
息确定送给哪个模块(TCP/UDP)处理,送给 TCP/UDP 模块的报⽂根据「端⼝号」确定送给哪个应⽤程
序处理
因此,TCP/UDP 各⾃的端⼝号也相互独⽴,如 TCP 有⼀个 80 号端⼝,UDP 也可以有⼀个 80 号端⼝,
⼆者并不冲突。
11.TCP 三次握⼿过程是怎样的?
TCP 是⾯向连接的协议,所以使⽤ TCP 前必须先建⽴连接,⽽建⽴连接是通过三次握⼿来进⾏的。三次
握⼿的过程如下图:
⼀开始,客户端和服务端都处于 CLOSE 状态。先是服务端主动监听某个端⼝,处于 LISTEN 状
态
- 客户端会随机初始化序号( client_isn ),将此序号置于 TCP ⾸部的「序号」字段中,同时把
SYN 标志位置为 1 ,表示 SYN 报⽂。接着把第⼀个 SYN 报⽂发送给服务端,表示向服务端发
起连接,该报⽂不包含应⽤层数据,之后客户端处于 SYN-SENT 状态
服务端收到客户端的 SYN 报⽂后,⾸先服务端也随机初始化⾃⼰的序号( server_isn ),将此序
号填⼊ TCP ⾸部的「序号」字段中,其次把 TCP ⾸部的「确认应答号」字段填⼊ client_isn + 1 ,
接着把 SYN 和 ACK 标志位置为 1 。最后把该报⽂发给客户端,该报⽂也不包含应⽤层数据,
之后服务端处于 SYN-RCVD 状态。
从上⾯的过程可以发现第三次握⼿是可以携带数据的,前两次握⼿是不可以携带数据的,这也是⾯试常问
的题。
⼀旦完成三次握⼿,双⽅都处于 ESTABLISHED 状态,此时连接就已建⽴完成,客户端和服务端就可以
相互发送数据了。
12.如何在 Linux 系统中查看 TCP 状态?
TCP 的连接状态查看,在 Linux 可以通过 netstat -napt 命令查看。
13为什么是三次握⼿?不是两次、四次?
相信⼤家⽐较常回答的是:“因为三次握⼿才能保证双⽅具有接收和发送的能⼒。”
这回答是没问题,但这回答是⽚⾯的,并没有说出主要的原因。
在前⾯我们知道了什么是 TCP 连接:
⽤于保证可靠性和流ᰁ控制维护的某些状态信息,这些信息的组合,包括 Socket**、序列号和窗⼝⼤**
⼩称为连接。
所以,᯿要的是为什么三次握⼿才可以初始化 Socket**、序列号和窗⼝⼤⼩并建⽴** TCP 连接。
接下来,以三个⽅⾯分析三次握⼿的原因
-
三次握⼿才可以阻⽌᯿复历史连接的初始化(主要原因)
-
三次握⼿才可以同步双⽅的初始序列号
-
三次握⼿才可以避免资源浪费
原因⼀:避免历史连接
我们来看看 RFC 793 指出的 TCP 连接使⽤三次握⼿的⾸要原因:
The principle reason for the three-way handshake is to prevent old duplicate connection initiations from
causing confusion.
简单来说,三次握⼿的⾸要原因是为了防⽌旧的重复连接初始化造成混乱
我们考虑⼀个场景,客户端先发送了 SYN(seq = 90)报⽂,然后客户端宕机了,⽽且这个 SYN 报⽂还
被⽹络阻塞了,服务端并没有收到,接着客户端᯿启后,⼜᯿新向服务端建⽴连接,发送了 SYN(seq =
100)报⽂(注意!不是᯿传 SYN,᯿传的 SYN 的序列号是⼀样的)。
看看三次握⼿是如何阻⽌历史连接的:
原因⼆:同步双⽅初始序列号
TCP 协议的通信双⽅, 都必须维护⼀个「序列号」, 序列号是可靠传输的⼀个关键因素,它的作⽤:
接收⽅可以去除᯿复的数据;
接收⽅可以根据数据包的序列号按序接收;
可以标识发送出去的数据包中, 哪些是已经被对⽅收到的(通过 ACK 报⽂中的序列号知道);
可⻅,序列号在 TCP 连接中占据着⾮常᯿要的作⽤,所以当客户端发送携带「初始序列号」的 SYN 报
⽂的时候,需要服务端回⼀个 ACK 应答报⽂,表示客户端的 SYN 报⽂已被服务端成功接收,那当服务
端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样⼀来⼀回,才能确保双⽅
的初始序列号能被可靠的同步。
四次握⼿其实也能够可靠的同步双⽅的初始化序号,但由于第⼆步和第三步可以优化成⼀步,所以就成了
「三次握⼿」。
⽽两次握⼿只保证了⼀⽅的初始序列号能被对⽅成功接收,没办法保证双⽅的初始序列号都能被确认接
收。
原因三:避免资源浪费
如果只有「两次握⼿」,当客户端发⽣的 SYN 报⽂在⽹络中阻塞,客户端没有接收到 ACK 报⽂,就
会᯿新发送 SYN ,由于没有第三次握⼿,服务端不清楚客户端是否收到了⾃⼰回复的 ACK 报⽂,所
以服务端每收到⼀个 SYN 就只能先主动建⽴⼀个连接,这会造成什么情况呢?
如果客户端发送的 SYN 报⽂在⽹络中阻塞了,᯿复发送多次 SYN 报⽂,那么服务端在收到请求后就
会建⽴多个冗余的⽆效链接,造成不必要的资源浪费。
14.为什么每次建⽴ TCP 连接时,初始化的序列号都要求不⼀样呢?
主要原因有两个⽅⾯:
为了防⽌历史报⽂被下⼀个相同四元组的连接接收(主要⽅⾯);
为了安全性,防⽌⿊客伪造的相同序列号的 TCP 报⽂被对⽅接收;
接下来,详细说说第⼀点。
假设每次建⽴连接,客户端和服务端的初始化序列号都是从 0 开始:
过程如下:
客户端和服务端建⽴⼀个 TCP 连接,在客户端发送数据包被⽹络阻塞了,然后超时᯿传了这个数据
包,⽽此时服务端设备断电᯿启了,之前与客户端建⽴的连接就消失了,于是在收到客户端的数据包
的时候就会发送 RST 报⽂。
紧接着,客户端⼜与服务端建⽴了与上⼀个连接相同四元组的连接;
在新连接建⽴完成后,上⼀个连接中被⽹络阻塞的数据包正好抵达了服务端,刚好该数据包的序列号
正好是在服务端的接收窗⼝内,所以该数据包会被服务端正常接收,就会造成数据错乱。可以看到,如果每次建⽴连接,客户端和服务端的初始化序列号都是⼀样的话,很容易出现历史报⽂被下
⼀个相同四元组的连接接收的问题。
如果每次建⽴连接客户端和服务端的初始化序列号都「不⼀样」,就有⼤概率因为历史报⽂的序列号「不
在」对⽅接收窗⼝,从⽽很⼤程度上避免了历史报⽂,⽐如下图:
相反,如果每次建⽴连接客户端和服务端的初始化序列号都「⼀样」,就有⼤概率遇到历史报⽂的序列号
刚「好在」对⽅的接收窗⼝内,从⽽导致历史报⽂被新连接成功接收。
所以,每次初始化序列号不⼀样很⼤程度上能够避免历史报⽂被下⼀个相同四元组的连接接收,注意是很
⼤程度上,并不是完全避免了(因为序列号会有回绕的问题,所以需要⽤时间戳的机制来判断历史报⽂,
详细看篇:TCP 是如何避免历史报⽂的?)。
15.初始序列号 ISN 是如何随机产⽣的?
起始 ISN 是基于时钟的,每 4 微秒 + 1,转⼀圈要 4.55 个⼩时。
RFC793 提到初始化序列号 ISN 随机⽣成算法:ISN = M + F(localhost, localport, remotehost,
remoteport)。
M 是⼀个计时器,这个计时器每隔 4 微秒加 1。
F 是⼀个 Hash 算法,根据源 IP、⽬的 IP、源端⼝、⽬的端⼝⽣成⼀个随机数值。要保证 Hash 算
法不能被外部轻易推算得出,⽤ MD5 算法是⼀个⽐较好的选择。
可以看到,随机数是会基于时钟计时器递增的,基本不可能会随机成⼀样的初始化序列号。
16.既然 IP 层会分⽚,为什么 TCP 层还需要 MSS 呢?
我们先来认识下 MTU 和 MSS
MTU :⼀个⽹络包的最⼤⻓度,以太⽹中⼀般为 1500 字节;
MSS :除去 IP 和 TCP 头部之后,⼀个⽹络包所能容纳的 TCP 数据的最⼤⻓度;
如果在 TCP 的整个报⽂(头部 + 数据)交给 IP 层进⾏分⽚,会有什么异常呢?
当 IP 层有⼀个超过 MTU ⼤⼩的数据(TCP 头部 + TCP 数据)要发送,那么 IP 层就要进⾏分⽚,把数
据分⽚成若⼲⽚,保证每⼀个分⽚都⼩于 MTU。把⼀份 IP 数据报进⾏分⽚以后,由⽬标主机的 IP 层来进
⾏᯿新组装后,再交给上⼀层 TCP 传输层。
这看起来井然有序,但这存在隐患的,那么当如果⼀个 IP 分⽚丢失,整个 IP 报⽂的所有分⽚都得重传。
因为 IP 层本身没有超时᯿传机制,它由传输层的 TCP 来负责超时和᯿传。
当某⼀个 IP 分⽚丢失后,接收⽅的 IP 层就⽆法组装成⼀个完整的 TCP 报⽂(头部 + 数据),也就⽆法将
数据报⽂送到 TCP 层,所以接收⽅不会响应 ACK 给发送⽅,因为发送⽅迟迟收不到 ACK 确认报⽂,所以
会触发超时᯿传,就会᯿发「整个 TCP 报⽂(头部 + 数据)」。
因此,可以得知由 IP 层进⾏分⽚传输,是⾮常没有效率的。
所以,为了达到最佳的传输效能 TCP 协议在建⽴连接的时候通常要协商双⽅的 MSS 值,当 TCP 层发现
数据超过 MSS 时,则就先会进⾏分⽚,当然由它形成的 IP 包的⻓度也就不会⼤于 MTU ,⾃然也就不⽤
IP 分⽚了。
经过 TCP 层分⽚后,如果⼀个 TCP 分⽚丢失后,进⾏重发时也是以 MSS 为单位,⽽不⽤᯿传所有的分
⽚,⼤⼤增加了᯿传的效率。
17.第⼀次握⼿丢失了,会发⽣什么?
当客户端想和服务端建⽴ TCP 连接的时候,⾸先第⼀个发的就是 SYN 报⽂,然后进⼊到 SYN_SENT 状
态。
在这之后,如果客户端迟迟收不到服务端的 SYN-ACK 报⽂(第⼆次握⼿),就会触发「超时᯿传」机
制,᯿传 SYN 报⽂,⽽且重传的 SYN 报⽂的序列号都是⼀样的。
不同版本的操作系统可能超时时间不同,有的 1 秒的,也有 3 秒的,这个超时时间是写死在内核⾥的,如
果想要更改则需要᯿新编译内核,⽐较麻烦。
当客户端在 1 秒后没收到服务端的 SYN-ACK 报⽂后,客户端就会᯿发 SYN 报⽂,那到底᯿发⼏次呢?
在 Linux ⾥,客户端的 SYN 报⽂最⼤᯿传次数由 tcp_syn_retries 内核参数控制,这个参数是可以⾃定义
的,默认值⼀般是 5。
# cat /proc/sys/net/ipv4/tcp_syn_retries
5
通常,第⼀次超时᯿传是在 1 秒后,第⼆次超时᯿传是在 2 秒,第三次超时᯿传是在 4 秒后,第四次超时
᯿传是在 8 秒后,第五次是在超时᯿传 16 秒后。没错,每次超时的时间是上⼀次的 2 倍。
当第五次超时᯿传后,会继续等待 32 秒,如果服务端仍然没有回应 ACK,客户端就不再发送 SYN 包,然
后断开 TCP 连接。
所以,总耗时是 1+2+4+8+16+32=63 秒,⼤约 1 分钟左右
18.第⼆次握⼿丢失了,会发⽣什么?
当服务端收到客户端的第⼀次握⼿后,就会回 SYN-ACK 报⽂给客户端,这个就是第⼆次握⼿,此时服务
端会进⼊ SYN_RCVD 状态。
第⼆次握⼿的 SYN-ACK 报⽂其实有两个⽬的 :
第⼆次握⼿⾥的 ACK, 是对第⼀次握⼿的确认报⽂;
第⼆次握⼿⾥的 SYN,是服务端发起建⽴ TCP 连接的报⽂;
所以,如果第⼆次握⼿丢了,就会发⽣⽐较有意思的事情,具体会怎么样呢?
因为第⼆次握⼿报⽂⾥是包含对客户端的第⼀次握⼿的 ACK 确认报⽂,所以,如果客户端迟迟没有收到第
⼆次握⼿,那么客户端就觉得可能⾃⼰的 SYN 报⽂(第⼀次握⼿)丢失了,于是客户端就会触发超时重传
机制,重传 SYN 报⽂。
然后,因为第⼆次握⼿中包含服务端的 SYN 报⽂,所以当客户端收到后,需要给服务端发送 ACK 确认报
⽂(第三次握⼿),服务端才会认为该 SYN 报⽂被客户端收到了。
那么,如果第⼆次握⼿丢失了,服务端就收不到第三次握⼿,于是服务端这边会触发超时重传机制,重传
SYN-ACK 报⽂。
在 Linux 下,SYN-ACK 报⽂的最⼤᯿传次数由 tcp_synack_retries 内核参数决定,默认值是 5。
# cat /proc/sys/net/ipv4/tcp_synack_retries
5
因此,当第⼆次握⼿丢失了,客户端和服务端都会᯿传:
客户端会᯿传 SYN 报⽂,也就是第⼀次握⼿,最⼤᯿传次数由 tcp_syn_retries 内核参数决定;
服务端会᯿传 SYN-ACK 报⽂,也就是第⼆次握⼿,最⼤᯿传次数由 tcp_synack_retries 内核参数
决定。
19.第三次握⼿丢失了,会发⽣什么?
服务端的 SYN-ACK 报⽂后,就会给服务端回⼀个 ACK 报⽂,也就是第三次握⼿,此时客户端
状态进⼊到 ESTABLISH 状态。
因为这个第三次握⼿的 ACK 是对第⼆次握⼿的 SYN 的确认报⽂,所以当第三次握⼿丢失了,如果服务端
那⼀⽅迟迟收不到这个确认报⽂,就会触发超时᯿传机制,᯿传 SYN-ACK 报⽂,直到收到第三次握⼿,
或者达到最⼤᯿传次数。
注意,ACK 报⽂是不会有重传的,当 ACK 丢失了,就由对⽅重传对应的报⽂。
20.什么是 SYN 攻击?如何避免 SYN 攻击?
我们都知道 TCP 连接建⽴是需要三次握⼿,假设攻击者短时间伪造不同 IP 地址的 SYN 报⽂,服务端每
接收到⼀个 SYN 报⽂,就进⼊ SYN_RCVD 状态,但服务端发送出去的 ACK + SYN 报⽂,⽆法得到
未知 IP 主机的 ACK 应答,久⽽久之就会占满服务端的半连接队列,使得服务端不能为正常⽤户服务
在 TCP 三次握⼿的时候,Linux 内核会维护两个队列,分别是:
半连接队列,也称 SYN 队列;
全连接队列,也称 accept 队列;
我们先来看下 Linux 内核的 SYN 队列(半连接队列)与 Accpet 队列(全连接队列)是如何⼯作的
正常流程:
- 当服务端接收到客户端的 SYN 报⽂时,会创建⼀个半连接的对象,然后将其加⼊到内核的「 SYN 队
列」;
- 接着发送 SYN + ACK 给客户端,等待客户端回应 ACK 报⽂;
服务端接收到 ACK 报⽂后,从「 SYN 队列」取出⼀个半连接对象,然后创建⼀个新的连接对象放⼊
到「 Accept 队列」;
- 应⽤通过调⽤ accpet() socket 接⼝,从「 Accept 队列」取出连接对象。
不管是半连接队列还是全连接队列,都有最⼤⻓度限制,超过限制时,默认情况都会丢弃报⽂。
SYN 攻击⽅式最直接的表现就会把 TCP 半连接队列打满,这样当 TCP 半连接队列满了,后续再在收到
SYN 报⽂就会丢弃,导致客户端⽆法和服务端建⽴连接
避免 SYN 攻击⽅式,可以有以下四种⽅法:
调⼤ netdev_max_backlog;
增⼤ TCP 半连接队列;
开启 tcp_syncookies;减少 SYN+ACK ᯿传次数
⽅式⼀:调⼤ netdev_max_backlog
当⽹卡接收数据包的速度⼤于内核处理的速度时,会有⼀个队列保存这些数据包。控制该队列的最⼤值如
下参数,默认值是 1000,我们要适当调⼤该参数的值,⽐如设置为 10000:
net.core.netdev_max_backlog = 10000
⽅式⼆:增⼤ TCP 半连接队列
增⼤ TCP 半连接队列,要同时增⼤下⾯这三个参数:
增⼤ net.ipv4.tcp_max_syn_backlog
增⼤ listen() 函数中的 backlog
增⼤ net.core.somaxconn
具体为什么是三个参数决定 TCP 半连接队列的⼤⼩,可以看这篇:可以看这篇:TCP 半连接队列和全连
接队列满了会发⽣什么?⼜该如何应对?
⽅式三:开启 net.ipv4.tcp_syncookies
开启 syncookies 功能就可以在不使⽤ SYN 半连接队列的情况下成功建⽴连接,相当于绕过了 SYN 半连
接来建⽴连接。
具体过程:
当 「 SYN 队列」满之后,后续服务端收到 SYN 包,不会丢弃,⽽是根据算法,计算出⼀个
cookie 值;
将 cookie 值放到第⼆次握⼿报⽂的「序列号」⾥,然后服务端回第⼆次握⼿给客户端;
服务端接收到客户端的应答报⽂时,服务端会检查这个 ACK 包的合法性。如果合法,将该连接对象
放⼊到「 Accept 队列」。
最后应⽤程序通过调⽤ accpet() 接⼝,从「 Accept 队列」取出的连接。
可以看到,当开启了 tcp_syncookies 了,即使受到 SYN 攻击⽽导致 SYN 队列满时,也能保证正常的连接
成功建⽴。
net.ipv4.tcp_syncookies 参数主要有以下三个值:
0 值,表示关闭该功能;
1 值,表示仅当 SYN 半连接队列放不下时,再启⽤它;
2 值,表示⽆条件开启功能;那么在应对 SYN 攻击时,只需要设置为 1 即可。
⽅式四:减少 SYN+ACK ᯿传次数
当服务端受到 SYN 攻击时,就会有⼤ᰁ处于 SYN_REVC 状态的 TCP 连接,处于这个状态的 TCP 会᯿传
SYN+ACK ,当᯿传超过次数达到上限后,就会断开连接。
那么针对 SYN 攻击的场景,我们可以减少 SYN-ACK 的᯿传次数,以加快处于 SYN_REVC 状态的 TCP
连接断开。
SYN-ACK 报⽂的最⼤᯿传次数由 tcp_synack_retries 内核参数决定(默认值是 5 次),⽐如将
tcp_synack_retries 减少到 2 次:、
$ echo 2 > /proc/sys/net/ipv4/tcp_synack_retries
21.TCP 四次挥⼿过程是怎样的?
天下没有不散的宴席,对于 TCP 连接也是这样, TCP 断开连接是通过四次挥⼿⽅式。
双⽅都可以主动断开连接,断开连接后主机中的「资源」将被释放,四次挥⼿的过程如下图:
- 客户端打算关闭连接,此时会发送⼀个 TCP ⾸部 FIN 标志位被置为 1 的报⽂,也即 FIN 报⽂,
之后客户端进⼊ FIN_WAIT_1 状态。
-
服务端收到该报⽂后,就向客户端发送 ACK 应答报⽂,接着服务端进⼊ CLOSE_WAIT 状态。
-
客户端收到服务端的 ACK 应答报⽂后,之后进⼊ FIN_WAIT_2 状态。
-
等待服务端处理完数据后,也向客户端发送 FIN 报⽂,之后服务端进⼊ LAST_ACK 状态。
-
客户端收到服务端的 FIN 报⽂后,回⼀个 ACK 应答报⽂,之后进⼊ TIME_WAIT 状态
-
服务端收到了 ACK 应答报⽂后,就进⼊了 CLOSE 状态,⾄此服务端已经完成连接的关闭。
-
客户端在经过 2MSL ⼀段时间后,⾃动进⼊ CLOSE 状态,⾄此客户端也完成连接的关闭。你可以看到,每个⽅向都需要⼀个 FIN 和⼀个 ACK,因此通常被称为四次挥⼿。
这⾥⼀点需要注意是:主动关闭连接的,才有 TIME_WAIT 状态。
22.为什么挥⼿需要四次?
再来回顾下四次挥⼿双⽅发 FIN 包的过程,就能理解为什么需要四次了。
关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。
服务端收到客户端的 FIN 报⽂时,先回⼀个 ACK 应答报⽂,⽽服务端可能还有数据需要处理和发
送,等服务端不再发送数据时,才发送 FIN 报⽂给客户端来表示同意现在关闭连接。
从上⾯过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK 和 FIN ⼀般都会分
开发送,因此是需要四次挥⼿。
但是在特定情况下,四次挥⼿是可以变成三次挥⼿的
23.为什么 TIME_WAIT 等待的时间是 2MSL**?**
MSL 是 Maximum Segment Lifetime,报⽂最⼤⽣存时间,它是任何报⽂在⽹络上存在的最⻓时间,超
过这个时间报⽂将被丢弃。因为 TCP 报⽂基于是 IP 协议的,⽽ IP 头中有⼀个 TTL 字段,是 IP 数据报
可以经过的最⼤路由数,每经过⼀个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发
送 ICMP 报⽂通知源主机。
MSL 与 TTL 的区别: MSL 的单位是时间,⽽ TTL 是经过路由跳数。所以 MSL 应该要⼤于等于 TTL 消耗
为 0 的时间,以确保报⽂已被⾃然消亡。
TTL 的值⼀般是 64**,**Linux 将 MSL 设置为 30 秒,意味着 Linux 认为数据报⽂经过 64 个路由器的时间不
会超过 30 秒,如果超过了,就认为报⽂已经消失在⽹络中了。
TIME_WAIT 等待 2 倍的 MSL,⽐较合理的解释是: ⽹络中可能存在来⾃发送⽅的数据包,当这些发送⽅
的数据包被接收⽅处理后⼜会向对⽅发送响应,所以⼀来⼀回需要等待 2 倍的时间。
⽐如,如果被动关闭⽅没有收到断开连接的最后的 ACK 报⽂,就会触发超时᯿发 FIN 报⽂,另⼀⽅接收
到 FIN 后,会᯿发 ACK 给被动关闭⽅, ⼀来⼀去正好 2 个 MSL。
可以看到 2MSL****时⻓ 这其实是相当于⾄少允许报⽂丢失⼀次。⽐如,若 ACK 在⼀个 MSL 内丢失,这样被
动⽅᯿发的 FIN 会在第 2 个 MSL 内到达,TIME_WAIT 状态的连接可以应对。
为什么不是 4 或者 8 MSL 的时⻓呢?你可以想象⼀个丢包率达到百分之⼀的糟糕⽹络,连续两次丢包的概
率只有万分之⼀,这个概率实在是太⼩了,忽略它⽐解决它更具性价⽐。
2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端
的 ACK 没有传输到服务端,客户端⼜接收到了服务端᯿发的 FIN 报⽂,那么 2MSL 时间将重新计时。
在 Linux 系统⾥ 2MSL 默认是 60 秒,那么⼀个 MSL 也就是 30 秒。Linux 系统停留在
TIME_WAIT 的时间为固定的 60 秒。
其定义在 Linux 内核代码⾥的名称为 TCP_TIMEWAIT_LEN:
#define TCP_TIMEWAIT_LEN (60HZ) / how long to wait to destroy TIME-WAIT
state, about 60 seconds */如果要修改 TIME_WAIT 的时间⻓度,只能修改 Linux 内核代码⾥ TCP_TIMEWAIT_LEN 的值,并᯿新编
译 Linux 内核。
24.针对 TCP 应该如何 Socket 编程?
服务端和客户端初始化 socket ,得到⽂件描述符;
服务端调⽤ bind ,将 socket 绑定在指定的 IP 地址和端⼝;
服务端调⽤ listen ,进⾏监听;
服务端调⽤ accept ,等待客户端连接;
客户端调⽤ connect ,向服务端的地址和端⼝发起连接请求;
服务端 accept 返回⽤于传输的 socket 的⽂件描述符;
客户端调⽤ write 写⼊数据;服务端调⽤ read 读取数据;
客户端断开连接时,会调⽤ close ,那么服务端 read 读取数据的时候,就会读取到了 EOF ,待
处理完数据后,服务端调⽤ close ,表示连接关闭。
这⾥需要注意的是,服务端调⽤ accept 时,连接成功了会返回⼀个已完成连接的 socket,后续⽤来传
输数据。
所以,监听的 socket 和真正⽤来传送数据的 socket,是「两个」 socket,⼀个叫作监听 socket,⼀个叫
作已完成连接 socket。
成功连接建⽴之后,双⽅开始通过 read 和 write 函数来读写数据,就像往⼀个⽂件流⾥⾯写东⻄⼀样。
25.listen 时候参数 backlog 的意义?
Linux内核中会维护两个队列:
半连接队列(SYN 队列):接收到⼀个 SYN 建⽴连接请求,处于 SYN_RCVD 状态;
全连接队列(Accpet 队列):已完成 TCP 三次握⼿过程,处于 ESTABLISHED 状态;
int listen(int socketfd, int backlog)
-
参数⼀ socketfd 为 socketfd ⽂件描述符
-
参数⼆ backlog,这参数在历史版本有⼀定的变化
在早期 Linux 内核 backlog 是 SYN 队列⼤⼩,也就是未完成的队列⼤⼩。
在 Linux 内核 2.2 之后,backlog 变成 accept 队列,也就是已完成连接建⽴的队列⻓度,所以现在通常认
为 backlog 是 accept 队列。
但是上限值是内核参数 somaxconn 的⼤⼩,也就说 accpet 队列⻓度 = min(backlog, somaxconn)****。
26.accept 发⽣在三次握⼿的哪⼀步?
- 客户端的协议栈向服务端发送了 SYN 包,并告诉服务端当前发送序列号 client_isn,客户端进⼊
SYN_SENT 状态;
- 服务端的协议栈收到这个包之后,和客户端进⾏ ACK 应答,应答的值为 client_isn+1,表示对 SYN
包 client_isn 的确认,同时服务端也发送⼀个 SYN 包,告诉客户端当前我的发送序列号为server_isn,服务端进⼊ SYN_RCVD 状态;
客户端协议栈收到 ACK 之后,使得应⽤程序从 connect 调⽤返回,表示客户端到服务端的单向连
接建⽴成功,客户端的状态为 ESTABLISHED,同时客户端协议栈也会对服务端的 SYN 包进⾏应答,
应答数据为 server_isn+1;
- ACK 应答包到达服务端后,服务端的 TCP 连接进⼊ ESTABLISHED 状态,同时服务端协议栈使得
accept 阻塞调⽤返回,这个时候服务端到客户端的单向连接也建⽴成功。⾄此,客户端与服务端两
个⽅向的连接都建⽴成功。
从上⾯的描述过程,我们可以得知客户端 connect 成功返回是在第⼆次握⼿,服务端 accept 成功返回是
在三次握⼿成功之后。
27.客户端调⽤ close 了,连接是断开的流程是什么?
- 客户端调⽤ close ,表明客户端没有数据需要发送了,则此时会向服务端发送 FIN 报⽂,进⼊
FIN_WAIT_1 状态;
- 服务端接收到了 FIN 报⽂,TCP 协议栈会为 FIN 包插⼊⼀个⽂件结束符 EOF 到接收缓冲区中,应
⽤程序可以通过 read 调⽤来感知这个 FIN 包。这个 EOF 会被放在已排队等候的其他已接收的数
据之后,这就意味着服务端需要处理这种异常情况,因为 EOF 表示在该连接上再⽆额外数据到达。
此时,服务端进⼊ CLOSE_WAIT 状态;
- 接着,当处理完数据后,⾃然就会读到 EOF ,于是也调⽤ close 关闭它的套接字,这会使得服务
端发出⼀个 FIN 包,之后处于 LAST_ACK 状态;
- 客户端接收到服务端的 FIN 包,并发送 ACK 确认包给服务端,此时客户端将进⼊ TIME_WAIT 状
态;
-
服务端收到 ACK 确认包后,就进⼊了最后的 CLOSE 状态;
-
客户端经过 2MSL 时间之后,也进⼊ CLOSE 状态;