TCP握手挥手
# 0. TCP协议的认识
什么是 TCP 连接?
用于保证 可靠性 和 流量控制 维护的某些-状态信息,这些信息的组合,包括Socket、序列号和窗口大小称为连接。
所以我们可以知道,建立一个 TCP 连接是需要客户端与服务器端达成上述三个信息的共识。
- Socket:由 IP 地址和端口号组成
- 序列号:用来解决乱序问题等
- 窗口大小:用来做流量控制
# 1. TCP协议特点
- 面向连接(虚连接)
- 点对点传输。每一条TCP连接只能有两个端点,无法进行广播或多播
- 可靠有序,不丢不重。TCP提供可靠的交付服务。无差错,不丢失,不重复,按序到达
- 全双工通信
- 面向字节流
【面向连接】:一定是「一对一」才能连接,不能像 UDP 协议 可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的;
【可靠】:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端;
【字节流】:TCP将数据看成仅仅是一 连串的无结构的字节流。消息是「没有边界」的,所以无论我们消息有多大都可以进行传输。并且消息是「有序的」,当「前一个」消息没有收到的时候,即使它先收到了后面的字节已经收到,那么也不能扔给应用层去处理,同时对「重复」的报文会自动丢弃
【全双工通信】:发送方和接收方可以同时发送数据,接收数据。协议两端都设有发送缓存,接收缓存
# 2. TCP报文首部格式
20B
的固定首部 + 选项字段,4B对齐方式
【源端口】:发送方端口,16位
【目的端口】:接收方端口,16位
【序号】:本报文段所发送的数据-的第一个字节的序号
【确认号】:期望收到下一个报文段数据的第一个字节的序号
【数据偏移】:TCP首部长度,最大长度为(2^4^ - 1) * 4B = 60B
,固定首部20B
+ 可变头部40B
。由于首部长度不固定,所以数据起始位置不固定
【六个控制位】
紧急位
URG
:发送方紧急处理位。URG=1
表示此段报文有紧急数据,要立即发送出去,不用在缓存队列中排队,配合紧急指针插队优先处理确认位
ACK
:ACK=1
,确认号有效推送位
PSH
:接收方紧急处理位。PSH=1
时,接收方尽快向应用进程交付此段报文,不必等缓存队列填满复位
RST
:RST=1
时,表示TCP与主机的连接出现严重差错,必须释放连接再重新建立同步位
SYN
:SYN=1
,表明一个连接 请求/连接接收报文终止位
FIN
:FIN=1
,表明发送方数据已发完,要求释放连接
【窗口】:接收方接收窗口的大小,即现在允许发送方发送的数据量,根据接收方的窗口大小,设置发送方的发送缓存
【检验和】:检验首部 + 数据,检验时要加上12B
伪首部「伪IP数据报首部」;第四个字段为协议字段,TCP为6
,UDP为17
【紧急指针】:URG=1
时才有意义,指出本报文段中紧急数据的位置
【选项】:最大报文段长度MSS、窗口扩大、时间戳、选择确认
【填充】:填充0保证4字节对齐
# 序号&确认号
序号:本报文段所发送的数据-的第一个字节的序号
确认号:确认上一次发送的成功收到。期望收到下一个报文段数据的第一个字节的序号
序号的初始值是在建立连接后,随机生成的
# URG&PSH
紧急位
URG
:发送方紧急处理位。URG=1
表示此段报文有紧急数据,要立即发送出去,不用在缓存队列中排队,配合紧急指针插队优先处理推送位
PSH
:接收方紧急处理位。PSH=1
时,接收方尽快向应用进程交付此段报文,不必等缓存队列填满
# 3. 三次握手
【握手阶段】
- 建立链接前需要 Server 端先监听端口,因此 Server 端建立链接前的初始状态就是 LISTEN 状态,这时 Client 端准备建立链接。在第一次消息发送中,Client随机选取一个序列号作为自己的初始序号发送给Server,Client 端的链接状态变成了 SYN_SENT 状态;
- Server收到了来自Client的连接请求,如果在资源条件合理的情况下,服务器为该TCP连接分配缓存和变量。Server使用ACK对Client的数据包进行确认,因为已经收到了序列号为
x
的数据包,准备接收序列号为x+1
的包,所以ack=x+1
,同时Server告诉Client自己的初始序列号,就是seq=y
;发送完 ACK 和 SYN 后,Server 端的链接状态就变成了 SYN_RCVD - Client 收到 Server 的 ACK 后,Client 端的链接状态就变成了 ESTABLISHED 状态。同时,Client 向 Server 端发送 ACK,回复 Server 端的 SYN 请求。Client回复
seq=x+1
、ack=y+1
,Client为该TCP连接分配缓存和变量
# 4. 四次挥手
【挥手阶段】
- Client准备关闭连接,首先主动向Server发送一个 TCP 首部
FIN
标志位被置为1
的报文,Client随机选取一个序列号seq=u
作为自己的初始序号发送给Server,之后客户端进入 FIN_WAIT_1 状态。 - Server回应
ACK
对Client的数据包进行确认,回复确认号ack=u+1
,同时Server告诉Client自己的初始序列号seq=v
;客户到服务器这个方向的连接就成了一一半关闭 CLOSED_WAIT 状态。由于Server可能还与他客户端同时在通信,或者可能此时对A的数据没有发送完,此次不会回复FIN
标志位。 - 第三条消息Server告诉Client可以断开连接,数据已传输完。回复
FIN=1
,ACK=1
确认。Server依然回复第二次的确认号ack=u+1
,因为这段时间Clinet处于半关闭状态,并没有在发送数据 。Server回复seq=w
,进入 LAST_ACK状态 - Clint收到Server的反馈后,回送
ACK=1
,由于要断开连接不再发送数据了,seq=u+1
(序号为上次的ack),ack=w+1
。发送完后Clinet进入 TIME_WAIT状态。Client在等待 2MSL 后,确保Server收到第四条消息后,自动进入 CLOSE状态。Server收到第四条消息后进入了 CLOSE 状态。至此连接接关闭!
# 【三次握手即原因】
# 为什么是三次
所以,重要的是为什么三次握手才可以初始化Socket、序列号和窗口大小,并建立 TCP 连接。
以下原因:
- 三次握手才可以: 阻止历史重复连接-的-初始化(主要原因)
- 三次握手才可以: 同步双方的初始序列号和序号
- 三次握手才可以: 避免资源浪费
不使用「两次握手」和「四次握手」的原因:
- 「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
- 「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。
# 原因一:避免历史连接
我们来看看 RFC 793 指出的 TCP 连接使用三次握手的首要原因:
The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.
三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。
网络环境是错综复杂的,往往并不是如我们期望的一样,先发送的数据包,可能会由于网络拥堵等乱七八糟的原因,会使得旧的数据包,先到达目标主机,那么这种情况下 TCP 三次握手是如何避免的呢?
三次握手避免历史连接
客户端连续发送多次 SYN 建立连接的报文,在网络拥堵等情况下:
- 一个「旧 SYN 报文」比「最新的 SYN 」 报文早到达了服务端;
- 那么此时服务端就会回一个
SYN + ACK
报文给客户端; - 客户端收到后可以根据自身的上下文,判断这是一个历史连接(序列号过期或超时),那么客户端就会发送
RST
报文给服务端,表示中止这一次连接。
如果是两次握手连接,就不能判断当前连接是否是历史连接。
三次握手则可以在客户端(发送方)准备发送第三次报文时,客户端因有足够的上下文来判断当前连接是否是历史连接:
- 如果是历史连接(序列号过期或超时),则第三次握手发送的报文是
RST
报文,以此中止历史连接; - 如果不是历史连接,则第三次发送的报文是
ACK
报文,通信双方就会成功建立连接;
所以, TCP 使用三次握手建立连接的最主要原因是防止历史连接初始化了连接。
# 原因二:同步双方初始序列号
TCP 协议的通信双方, 都必须维护一个「序列号」, 序列号是可靠传输的一个关键因素,它的作用:
- 接收方可以去除重复的数据;
- 接收方可以根据数据包的序列号按序接收;
- 可以标识发送出去的数据包中, 哪些是已经被对方收到的;
可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带「初始序列号」的 SYN
报文的时候,需要服务端回一个 ACK
应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。
四次握手其实也能够可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,所以就成了「三次握手」。
# 原因三:避免资源浪费(冗余连接)
如果只有「两次握手」,当客户端的 SYN
请求连接在网络中阻塞,客户端没有接收到 ACK
报文,就会重新发送 SYN
,由于没有第三次握手,服务器不清楚客户端是否收到了自己发送的建立连接的 ACK
确认信号,所以每收到一个 SYN
就只能先主动建立一个连接,这会造成什么情况呢?
如果客户端的 SYN
阻塞了,重复发送多次 SYN
报文,那么服务器在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。
# 为什么不是两次
1. 两次握手会造成消息滞留
- 服务器重复接受无用的连接请求
SYN
报文,而造成重复分配资源
2. 只能得知客户端具有发送的能力,不知道其是否有接收能力
- 两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收
# 为什么不是四次
三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。
# 【四次挥手及原因】
# 为什么是四次
关闭连接时,客户端向服务端发送
FIN
时,仅仅表示客户端不再发送数据了,但是还能接收数据服务器收到客户端的
FIN
报文时,先回一个ACK
应答报文;而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送FIN
报文给客户端来表示同意现在关闭连接。
服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK
和 FIN
一般都会分开发送,从而比三次握手导致多了一次。
# 为什么不是三次
四次才能确保数据能够完成传输。
关闭连接时,当服务器收到客户端的FIN报文通知时,它仅仅表示客户端没有数据发送给服务器了;但未必服务器所有的数据都全部发送给对方了
服务器不会关闭SOCKET,可能还需要发送一些数据给客户端之后,所以第二次只发送ACK
确认报文,第三次再发送FIN
报文给对方来表示同意现在可以关闭连接了
所以它ACK
报文和FIN
报文多数情况下都是分开发送的。
挥手的时候为什么是分开的时候发送呢?
- 一起发送可能会导致一方被迫强制关闭
因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。
但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭 SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。
如果服务器段将ACK(第二次挥手)和FIN(第三次挥手)合并成一块发过去的话,这就意味着一方关闭,另一方也要被迫关闭,若此时有服务器继续单向的发送片段给客户端的需求则无法实现。
故需要四步挥手。
# 【TIME_WAIT 等待 2MSL】
MSL
是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。
Client发送完第四次消息后,并不知道Server是否接收到了这次的消息,因为是可靠传输,但是挥手只是4次,它需要默默等待Server接收到消息后才下线。(🤔PS:真实负责的好男人呀!)
如果报文丢失了或者Server接收到错误的报文:
- Server重复第三次挥手过程,让Client再重新发送一次
TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是:
网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。
比如,如果被动关闭方没有收到断开连接的最后的 ACK 报文,就会触发超时重发 Fin 报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 个 MSL。
2MSL
的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时。
等待2MSL是为了确认服务器能够收到第四次挥手消息
# 初始序列号ISN的初始化
为什么客户端和服–务端的初始序列号 ISN 是不相同的?
因为网络中的报文会延迟、会复制重发、也有可能丢失,这样会造成的不同连接之间产生互相影响,所以为了避免互相影响,客户端和服务端的初始序列号是随机且不同的。
ISN是不能硬编码的,不然会出问题的——比如:如果连接建好后始终用1来做ISN,如果client发了30个segment过去,但是网络断了,于是 client重连,又用了1做ISN,但是之前连接的那些包到了,于是就被当成了新连接的包,此时,client的Sequence Number 可能是3,而Server端认为client端的这个号是30了。全乱了。
RFC793 (opens new window)中说,ISN会和一个假的时钟绑在一起,这个时钟会在每4微秒对ISN做加一操作,直到超过2^32^,又从0开始。这样,一个ISN的周期大约是4.55个小时。因为,我们假设我们的TCP Segment在网络上的存活时间不会超过MSL(Maximum Segment Lifetime ),所以,只要MSL的值小于4.55小时,那么,我们就不会重用到ISN。
这样做可以防止ISN重用,另外出于安全考虑,也不应该对ISN硬编码。
# SYN洪泛攻击
https://zh.wikipedia.org/wiki/SYN_flood
TCP 连接建立是需要三次握手,假设攻击者短时间伪造不同 IP 地址的 SYN
报文,服务端每接收到一个 SYN
报文,就进入SYN_RCVD
状态,但服务端发送出去的 ACK + SYN
报文,无法得到未知 IP 主机的 ACK
应答,久而久之就会占满服务端的 SYN 接收队列(未连接队列),消耗服务器资源,使得服务器不能为正常用户服务。
SYN Flood是当前最流行的DoS (opens new window)(拒绝服务攻击)与DDoS (opens new window)(Distributed Denial Of Service分布式拒绝服务攻击 (opens new window))的方式之一。
【解决方式】
- 缩短SYN Timeout时间
- 设置SYN Cookie
- SYN flood可以用DCN防火墙来拦截
第一种:缩短SYN Timeout时间
由于SYN Flood攻击的效果取决于服务器上保持的SYN半连接数 (opens new window),这个值=SYN攻击的频度 x SYN Timeout,所以通过缩短从接收到SYN报文到确定这个报文无效并丢弃该连接的时间,例如设置为20秒以下(过低的SYN Timeout设置可能会影响客户的正常访问),可以成倍的降低服务器的负荷。
第二种方法:设置SYN Cookie
就是给每一个请求连接的IP地址分配一个Cookie,如果短时间内连续受到某个IP的重复SYN报文,就认定是受到了攻击,以后从这个IP地址来的包会被丢弃。
可是上述的两种方法只能对付比较原始的SYN Flood攻击
第三种方法:DCN防火墙来拦截
防火墙做一个中继,起到保护作用。
- SYN网关
- 被动SYN网关
- SYN中继
I. SYN网关
防火墙收到 服务端的 SYN/ACK 包后 , 会立刻发送一个ACK包 给服务端,减少半连接数。(当客户端真正的ACK包到达时,有数据 则转发给服务端,没有数据则丢弃该包。)
II. 被动SYN网关
防火墙的超时期限远小于服务器的超时期限,当 超过 防火墙的期限后,客户端 还没有发送ACK包,防火墙就会向服务器发送RST包,节约了半连接时间。
III. SYN中继
得先跟防火墙三次握手成功,才能和 服务端 进行连接。
# Client-Server状态变化
# Client
SYN_SENT
- 客户端发起第 1 次握手后,连接状态为 SYN_SENT ,等待服务端内核进行应答,如果服务端来不及处理(例如服务端的 backlog 队列已满)就可以看到这种状态的连接。ESTABLISHED
- 表示连接处于正常状态,可以进行数据传送。客户端收到服务器回复的 SYN+ACK 后,对服务端的 SYN 单独回复(第 3 次握手),连接建立完成,进入 ESTABLISHED 状态。服务端程序收到第 3 次握手包后,也进入 ESTABLISHED 状态。FIN_WAIT_1
- 客户端发送了关闭连接的 FIN 报文后,等待服务端回复 ACK 确认。FIN_WAIT_2
- 表示我方已关闭连接,正在等待服务端关闭。客户端发了关闭连接的 FIN 报文后,服务器发回 ACK 应答,但是没进行关闭,就会处于这种状态。TIME_WAIT
- 双方都正常关闭连接后,客户端会维持 TIME_WAIT 一段时间,以确保最后一个 ACK 能成功发送到服务器端。停留时长为 2 倍的 MSL (报文最大生存时间),Linux 下大约是 60 秒。所以在一个频繁建立短连接的服务器上通常可以看到成千上万的 TIME_WAIT 连接。
# Server
LISTEN
- 表示当前程序正在监听某个端口时。SYN_RCVD
- 服务端收到第 1 次握手后,进入 SYN_RCVD 状态,并回复一个 SYN+ACK(第 2 次握手),再等待对方确认。ESTABLISHED
- 表示连接处于正常状态,可以进行数据传送。完成 TCP3 次握手后,连接建立完成,进入 ESTABLISHED 状态。CLOSE_WAIT
- 表示客户端已经关闭连接,但是本地还没关闭,正在等待本地关闭。有时客户端程序已经退出了,但服务端程序由于异常或 BUG 没有调用 close()函数对连接进行关闭,那在服务器这个连接就会一直处于 CLOSE_WAIT 状态,而在客户机已经不存在这个连接了。LAST_ACK
- 表示正在等待客户端对服务端的关闭请求进行最终确认。