【TCP/IP】TCP 三次握手与四次挥手
TCP 报文格式
TCP 报文格式简介
如下图所示,TCP 报文由 TCP Header 和 TCP 数据组成:
TCP Header 的最大长度为 60 字节(byte),而必须要有的固定长度也就是图一的前 5 层的 20 字节(byte),每层占有 32bit,也就是 32/8=4 字节,5 层,5*4 = 20 字节,那么第六层的可选项和填充也就是 Tcp Options字段最大为 60-20=40字节(byte)。填充是为了使TCP首部为4字节(32bit)的整数倍。
TCP首部格式
-
Source Port:源端口,16 位(bit),2 个字节(byte)。
-
Destination Port:目的端口,16 位,2 个字节。
-
Sequence Number:序号,发送数据包中的第一个字节的序列号,32 位。
-
Acknowledgment Number:确认序列号,32 位。
-
Data Offset:数据偏移,4 位,该字段的值是TCP首部(包括选项)长度除以4。
-
标志位:6 位,共6 个标志位
-
Window:窗口,表示接收缓冲区的空闲空间,16 位,2 个字节,用来告诉TCP连接对端自己能够接收的最大数据长度。
-
Checksum:校验和,16 位,2 个字节。
-
Urgent Pointers:紧急指针,16 位,2 个字节,只有 URG 标志位被设置时该字段才有意义,表示紧急数据相对序列号(Sequence Number字段的值)的偏移。
-
选项和填充:最常见的可选字段是最长报文大小,又称为 MSS(Maximum Segment Size),每个连接方通常都在通信的第一个报文段(为建立连接而设置SYN标志为1的那个段)中指明这个选项,它表示本端所能接受的最大报文段的长度。选项长度不一定是 32 位的整数倍,所以要加填充位,即在这个字段中加入额外的零,以保证 TCP 头是 32 的整数倍。
-
数据:TCP 报文段中的数据部分是可选的。在一个连接建立和一个连接终止时,双方交换的报文段仅有 TCP 首部。如果一方没有数据要发送,也使用没有任何数据的首部来确认收到的数据。在处理超时的许多情况中,也会发送不带任何数据的报文段。
TCP 标志位
TCP 标志位代表了当前请求的目的。一共有六种:
-
SYN(synchronous): 发送/同步标志,用来建立连接,和 ACK 标志位搭配使用。
-
A 请求与 B 建立连接时,SYN=1,ACK=0;
-
B 确认与 A 建立连接时,SYN=1,ACK=1。
-
-
ACK(acknowledgement):确认标志,表示确认收到请求
-
PSH(push) :表示推送操作,就是指数据包到达接收端以后,不对其进行队列处理,而是尽可能的将数据交给应用程序处理。
-
FIN(finish):结束标志,表示关闭一个 TCP 连接。
-
RST(reset):重置复位标志,用于复位对应的 TCP 连接。
-
URG(urgent):紧急标志,用于保证 TCP 连接不被中断,并且督促中间层设备尽快处理。
TCP 序列号、确认号
序列号和确认号是 TCP 实现可靠传输的依赖。TCP 使用序列号来记录发送数据包的顺序。TCP 传送一个数据包后,只有在指定时间里收到这个包的确认信息,才会将其从队列中删除,否则会重新发送该数据包。对接收方而言,通过数据分段中的序列号可以保证数据能够按照正常的顺序进行重组。
序列号 Sequence Number
同步序列编号(Synchronize Sequence Numbers)表示同步序号,用来建立连接,该标志仅在三次握手建立 TCP 连接时有效。
它提示 TCP 连接的服务端检查序列编号,该序列编号为 TCP 连接初始端(一般是客户端)的初始序列编号。TCP 序列编号是一个范围从 0~4294967295
(\(2^{32}-1\)) 的32位计数器,通过 TCP 连接交换的数据中每一个字节都经过序列编号。在 TCP 报头中的序列编号栏包括了 TCP 分段中第一个字节的序列编号。
其中,
-
在 SYN flag 置 1 时,表示当前连接的初始序列号(Initial Sequence Number,ISN);
-
在 SYN flag 置 0 时,表示当前报文段中的第一个字节的序列号。
序列号的规则:
-
握手阶段,
[SYN]
包即使没有传送数据,也会消耗一个序列号。因此,建立连接后的序列号从 ISN + 1 开始; -
挥手阶段,
[FIN/ACK]
包即使没有传送数据,也会消耗掉一个序列号; -
数据传输阶段,序列号 = 第一个报文段的序列号 + 已经发送的字节数;
-
比如第一个报文段的序列号为 S,已经发送了 100 个字节,则下一个报文段的序列号为 S + 100;
-
如果某个报文段不携带数据,不会消耗序列号,下一个报文段还是用相同的序列号发送;
-
正常情况下,B 给 A 的确认号,就是 A 下一个报文段的序列号;
-
客户端三次握手第三步的
[ACK]
包,和传输阶段的第一个报文段,有相同的序列号。
确认号 Acknowledge Number
ACK flag 置 1 时才有效,表示接收方期待的下一个报文段的序列号,一般是上次收到的报文段 seq + 1。
三次握手
三次握手过程
-
第一次握手:客户端请求建立连接,向服务端发送一个同步报文(
SYN=1
),同时选择一个随机数 seq = x 作为初始序列号; -
第二次握手:服务端收到连接请求报文后,如果同意建立连接,则向客户端发送同步确认报文(
SYN=1,ACK=1
),确认号为 ack = x + 1,同时选择一个随机数 seq = y 作为初始序列号; -
第三次握手:客户端收到服务端的确认后,向服务端发送一个确认报文(
ACK=1
),确认号为 ack = y + 1,序列号为 seq = x + 1。
这时就完成了三次握手,连接建立成功。随后,客户端和服务端的序列号将分别从 x + 1 和 y + 1 开始进行传输。
为什么需要三次握手,而不是两次或四次?
为什么不是两次握手?
如果只有两次握手,那么服务端向客户端发送 SYN/ACK
报文后,就会认为连接建立,但是如果客户端没有收到报文,那么客户端是没有建立连接的,这就会导致服务端的资源浪费。
为什么不是四次握手?
理论上我们总可以使用更多的通信次数交换相同的信息,使用两次握手无法建立 TCP 连接,而使用三次握手是建立连接所需要的最小次数:
-
第一次握手:服务端确认“自己收、客户端发”报文功能正常;
-
第二次握手:客户端确认“自己发、自己收、服务端收、客户端发”报文功能正常,客户端认为连接已建立;
-
第三次握手:服务端确认“自己发、客户端收”报文功能正常,此时双方均建立连接,可以正常通信。
SYN 攻击
SYN 攻击属于 DOS 攻击的一种,它利用 TCP 协议缺陷,通过发送大量的半连接请求,耗费 CPU 和内存资源。
原理
在三次握手过程中,服务器发送 [SYN/ACK]
包(第二个包)之后、收到客户端的 [ACK]
包(第三个包)之前的 TCP 连接称为半连接(half-open connect),此时服务器处于 SYN_RECV
(等待客户端响应)状态。
-
如果接收到客户端的
[ACK]
,则 TCP 连接成功, -
如果未接收到客户端的
[ACK]
,则会不断重发[SYN/ACK]
请求,直至成功。
SYN 攻击的攻击者,在短时间内伪造大量不存在的 IP 地址,向服务器不断地发送 [SYN]
包,服务器回复 [SYN/ACK]
包,并等待客户的确认。由于源地址是不存在的,服务器需要不断的重发直至超时。
这些伪造的 [SYN]
包将长时间占用未连接队列,影响了正常的 SYN
,导致目标系统运行缓慢、网络堵塞甚至系统瘫痪。
检测
当在服务器上看到大量的半连接状态时,特别是源 IP 地址是随机的,基本上可以断定这是一次 SYN 攻击。
防范
主要有两大类,一类是通过防火墙、路由器等过滤网关防护,另一类是通过加固 TCP/IP 协议栈防范,如增加最大半连接数,缩短超时时间。但 SYN 攻击不能完全被阻止,除非将 TCP 协议重新设计,否则只能尽可能的减轻 SYN 攻击的危害。
四次挥手
四次挥手过程
-
第一次挥手:客户端向服务端发送连接释放报文(
FIN=1,ACK=1
),主动关闭连接,同时等待服务端的确认;-
序列号 seq = u,即客户端上次发送的报文的最后一个字节的序号 + 1;
-
确认号 ack = k, 即服务端上次发送的报文的最后一个字节的序号 + 1。
-
-
第二次挥手:服务端收到连接释放报文后,立即发出确认报文(
ACK=1
);确认报文:序列号 seq = k,确认号 ack = u + 1;
这时,TCP 连接处于半关闭状态,即客户端到服务端的连接已经释放了,但是服务端到客户端的连接还未释放。这表示客户端已经没有数据发送了,但是服务端可能还要给客户端发送数据。
-
第三次挥手:服务端向客户端发送连接释放报文(
FIN=1,ACK=1
),主动关闭连接,同时等待客户端的确认;-
序列号 seq = w,即服务端上次发送的报文的最后一个字节的序号 + 1。如果半关闭状态,服务端没有发送数据,那么 w == k;
-
确认号 ack = u + 1,与第二次挥手相同,因为这段时间客户端没有发送数据。
-
-
第四次挥手:客户端收到服务端的连接释放报文后,立即发出确认报文(
ACK=1
);确认报文:序列号 seq = u + 1,确认号为 ack = w + 1。
此时,客户端就进入了
TIME-WAIT
状态。注意此时客户端到 TCP 连接还没有释放,必须经过 \(2 \times MSL\)(最长报文段寿命)的时间后,才进入CLOSED
状态。而服务端只要收到客户端发出的确认,就立即进入CLOSED
状态。可以看到,服务端结束 TCP 连接的时间要比客户端早一些。
TCP 规定,[FIN/ACK]
包即使没有传送数据,也会消耗掉一个序列号。[FIN/ACK]
包是第一、三次挥手:
-
第一次挥手时,客户端的序列号 seq = u,消耗一个序列号。因此:
-
第二次挥手时,服务端的确认号 ack = u + 1
-
第四次挥手时,客户端的序列号 seq = u + 1
-
-
第三次挥手时,服务端的序列号 seq = w,消耗一个序列号。因此:
- 第四次挥手时,客户端的确认号 ack = w + 1
为什么需要四次挥手
因为 TCP 是全双工的,一方关闭连接后,另一方还可以继续发送数据,所以四次挥手,将断开连接分成两个独立的过程。
为什么第四次挥手,客户端的 TIME-WAIT 状态必须等待 2MSL 的时间才能返回到 CLOSED 状态?
主要有两个原因:
-
确保
ACK
报文能够到达服务端,从而使服务端正常关闭连接。第四次挥手时,客户端第四次挥手的
ACK
报文不一定会到达服务端。服务端会超时重传FIN/ACK
报文,此时如果客户端已经断开了连接,那么就无法响应服务端的二次请求,这样服务端迟迟收不到FIN/ACK
报文的确认,就无法正常断开连接。MSL
是报文段在网络上存活的最长时间。客户端等待 \(2 \times MSL\) 时间,即「客户端ACK
报文 1MSL 超时 + 服务端 FIN 报文 1MSL 传输」,就能够收到服务端重传的FIN/ACK
报文,然后客户端重传一次ACK
报文,并重新启动 2MSL 计时器。如此保证服务端能够正常关闭。那如果服务端重发的
FIN
没有成功地在 2MSL 时间里传给客户端,会怎样?服务端会继续超时重试直到断开连接,见下文。 -
防止已失效的连接请求报文段出现在之后的连接中。
TCP 要求在 \(2 \times MSL\) 内不使用相同的序列号。客户端在发送完最后一个 ACK 报文段后,再经过时间 \(2 \times MSL\),就可以保证本连接持续的时间内产生的所有报文段都从网络中消失。
这样就可以使下一个连接中不会出现这种旧的连接请求报文段。或者即使收到这些过时的报文,也可以不处理它。
如果已经建立了连接,但是客户端出现故障了怎么办?
或者说,如果三次握手阶段、四次挥手阶段的包丢失了怎么办?比如上面描述的“服务端重发 FIN”的问题。
简而言之,通过定时器 + 超时重试机制,尝试获取确认,直到最后会自动断开连接。
具体而言,TCP 设有一个保活计时器,服务器每收到一次客户端的数据,都会重新复位这个计时器,时间通常是设置为 2 小时。若 2 小时还没有收到客户端的任何数据,服务器就开始重试:每隔 75 秒发送一个探测报文段,若一连发送 10 个探测报文后客户端依然没有回应,那么服务器就认为连接已经断开了。
TIME_WAIT 是主动断开连接的一方、还是被动断开连接的一方会进入的状态?
TIME_WAIT
是主动断开连接的一方会进入的状态。
TIME_WAIT
需要等待 2MSL,在大量短连接的情况下,TIME_WAIT
会太多,这也会消耗很多系统资源。对于服务器来说,在 HTTP 协议里指定 KeepAlive(浏览器重用一个 TCP 连接来处理多个 HTTP 请求),由浏览器来主动断开连接,可以一定程度上减少服务器的这个问题。
TCP 状态图
这些状态也是 netstat
命令 State 一列的取值。
三次握手阶段:
客户端:
TCP 连接状态 | 含义 |
---|---|
SYN_SENT | 发送了连接请求,等待远端的确认(三次握手第 1 步的结果) |
ESTABLISHED | socket 已经建立了连接 |
服务端:
TCP 连接状态 | 含义 |
---|---|
LISTEN | 监听来自远程应用的 TCP 连接请求 |
SYN_RECEIVED | 收到了连接请求并发送了确认报文,等待最终的确认(三次握手第 2 步的结果) |
ESTABLISHED | socket 已经建立了连接,这是数据传输阶段的状态 |
四次挥手阶段: |
客户端:
TCP 连接状态 | 含义 |
---|---|
FIN_WAIT1 | 发送了连接终止请求,等待确认,通常持续时间较短。 |
FIN_WAIT2 | 发送了连接终止请求并收到了远程的确认,等待远程 TCP 的连接终止请求,这个状态表明远程在收到此 socket 的连接终止请求后,没有立刻关闭它的 socket。 |
CLOSING | 发送了连接终止请求后,收到了远程的连接终止请求,正在等待远程对连接终止请求的确认,这个状态表明双方同时进入关闭状态。 |
TIME_WAIT | 等待足够的时间,以确保远程 TCP 收到其连接终止请求的确认。 |
CLOSED | socket 已经没有连接状态。 |
服务端:
TCP 连接状态 | 含义 |
---|---|
CLOSE_WAIT | 收到了远程的连接终止请求,正在等待本地的应用程序发出连接终止请求。 |
LAST_ACK | 等待先前发送的连接终止请求的确认,只有在发送连接终止请求前先收到了远程的连接终止请求时,才会进入此状态。 |
CLOSED | socket 已经没有连接状态。 |
UDP 协议是一个无状态的协议,在 netstat 命令中,UDP socket 的 State 一列总是 UDP。
参考资料:TCP connection status - IBM
使用 tcpdump 抓包分析
tcpdump工具操作
tcpdump
是一个命令行工具,可以打印网络接口上传输的 TCP 报文。
在一个终端窗口执行以下命令,监听与 www.baidu.com
传输的数据包:
tcpdump -n host www.baidu.com
参数说明:
-
-n:不要将
host.port
转成域名 -
host:监听发到特定主机与端口的流量
如图,成功开始监听:
随后再开启一个终端窗口,执行以下命令,访问 www.baidu.com:
curl www.baidu.com
可以看到 tcpdump
打印了如下信息:
其中一定包含三次握手建立连接、发送数据包、四次挥手断开连接这几个过程。接下来一一分析。约定:客户端就是本机,服务端就是百度的服务器。
三次握手过程
-
第一个包:客户端发往服务端;
Flags [S]
表示标志位SYN=1
,seq 是随机生成的客户端初始序列号。 -
第二个包:服务端发到客户端;
Flags [S.]
表示标志位SYN=1, ACK=1
,其中,.
表示ACK
,seq 是随机生成的服务端初始序列号,ack 是客户端初始序列号 + 1。 -
第三个包:客户端发往服务端。
Flags [.]
表示标志位ACK=1
。
至此,连接建立,另外,可以看出,这三个握手包的 length 都是 0。从第三个握手包开始,之后的序列号 seq、确认号 ack 均使用相对于初始化序列号的偏移来表示,便于阅读。
数据传输过程
-
客户端向服务端发送 HTTP 请求头;
其中,
Flags [P.]
表示:PSH=1, ACK=1
,PSH
标志位的作用是告诉服务端收到这个数据包后立刻处理,因为已经没有进一步要发的数据包了。seq 1:78
表示:其数据大小为 77 字节,序列号为 1,数据字段每个字节消耗的范围是:\(\text{[}1, 78\text{)}\)。序列号从 1 开始的,是因为握手时
[SYN]
包消耗了一个序列号。 -
服务端收到客户端的数据包后,立刻先返回一个
ACK
包;确认号 ack=78,length 为 0。
-
服务端处理完请求后,依次返回给客户端两个长度为 1360 字节的数据包;
这两个数据报文的序列号分别为 seq = 1、seq = 1361,即
seq 1:1361
和seq 1361:2721
。序列号从 1 开始,是因为握手时
[SYN/]
包消耗了一个序列号。 -
客户端收到这两个数据包后,立刻返回一个
ACK
包;确认号 ack = 2721,length 为 0。这个确认号表示已经收到了服务端发来的 2720 字节的数据(1360 + 1360 = 2720)。
-
服务端再发送一个长度为 61 字节的数据包,序列号为 seq=2721,服务端数据传输完毕;
-
客户端收到数据包后,立刻返回一个
ACK
包,确认号 ack = 2782,客户端数据接收完毕; -
可以看到,数据传输阶段 B 给 A 的确认号,就是 A 下一个数据包的序列号。
四次挥手过程
-
第一个包:客户端发往服务端,
Flags [F.]
表示标志位FIN=1,ACK=1
;seq 是客户端上次发送的报文的最后一个字节的序号 + 1,在这里客户端上次发送的报文就是长度为 77 的 HTTP 请求头,故 seq = 78。
确认号 ack = 2782, 是服务端上次发送的报文的最后一个字节的序号 + 1。
-
第二个包:服务端发到客户端,
Flags [.]
表示标志位 ACK=1;确认号 ack = 78 + 1 = 79,78 是客户端发送的第一个挥手包的序列号,这表示
[FIN/ACK]
包会消耗一个序列号。 -
第三个包:服务端发到客户端,
Flags [F.]
表示标志位FIN=1,ACK=1
;序列号 seq = 2782,这是服务端上次发送的报文的最后一个字节的序号 + 1。由于服务端在第二次挥手后没有继续传输数据,所以这里的序列号和第一个挥手包的确认号相同。
-
第四个包:客户端发往服务端,
Flags [.]
表示标志位ACK=1
。确认号 ack = 2782 + 1 = 2783,这表示
[FIN/ACK]
包会消耗一个序列号
参考: