转载自《面试官,不要再问我三次握手和四次挥手——猿人谷》
膜拜大佬
三次握手(Three-way Handshake)其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。
进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。
三次握手建立连接的首要目的是同步序列号
。只有同步了序列号才有可靠的传输,TCP 协议的许多特性都是依赖序列号实现的,比如流量控制、消息丢失后的重发等等,这也是三次握手中的报文被称为 SYN 的原因,因为 SYN 的全称就叫做 Synchronize Sequence Numbers。
实质上其实就是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号,交换TCP窗口大小
信息。
刚开始客户端处于 Closed 的状态,服务端处于 Listen 状态。
进行三次握手:
第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN。此时客户端处于 SYN_SENT
状态。
首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。
客户端的发送能力、服务端的接收能力是正常的。
第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s)。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_RCVD
的状态。
在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y。
服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。
第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED
状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED
状态,此时,双方已建立起了连接。
确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。
客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。
发送第一个SYN的一端将执行主动打开(active open),接收这个SYN并发回下一个SYN的另一端执行被动打开(passive open)。
在socket编程中,客户端执行connect()时,将触发三次握手。
三次握手才能确认双方的接收与发送能力是否正常。
两次握手出错:
如客户端发出连接请求,但因连接请求报文丢失而未收到确认,于是客户端再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,客户端共发出了两个连接请求报文段,其中第一个丢失,第二个到达了服务端,但是第一个丢失的报文段只是在**某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达服务端,此时服务端误认为客户端又发出一次新的连接请求,于是就向客户端发出确认报文段,同意建立连接,不采用三次握手,只要服务端发出确认,就建立新的连接了,**此时客户端忽略服务端发来的确认,也不发送数据,则服务端一致等待客户端发送数据,浪费资源。
服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立其连接,服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称之为半连接队列。
当服务器收到 SYN 报文后,服务器会立刻回复 SYN+ACK
报文,既确认了客户端的序列号,也把自己的序列号发给了对方
。此时,服务器端出现了新连接,状态是 SYN_RCVD。这个状态下,服务器必须建立一个 SYN 半连接队列
来维护未完成的握手信息,当这个队列溢出后,服务器将无法再建立新连接。
开启 syncookies
功能就可以在不使用 SYN 队列的情况下成功建立连接。服务器根据当前状态计算出一个值,放在己方发出的 SYN+ACK 报文中发出,当客户端返回 ACK 报文时,取出该值验证,如果合法,就认为连接建立成功。
修改 tcp_syncookies
参数即可,其中值为 0 时表示关闭该功能,2 表示无条件开启功能,而 1 则表示仅当 SYN 半连接队列放不下时,再启用它。由于 syncookie 仅用于应对 SYN 泛洪攻击
(攻击者恶意构造大量的 SYN 报文发送给服务器,造成 SYN 半连接队列溢出,导致正常客户端的连接无法建立),这种方式建立的连接,许多 TCP 特性都无法使用。所以,应当把 tcp_syncookies 设置为 1,仅在队列满时再启用
。
服务器收到 ACK 后连接建立成功,此时,内核会把连接从 SYN 半连接队列中移出,再移入 accept 队列,等待进程调用 accept 函数时把连接取出来
。如果进程不能及时地调用 accept 函数,就会造成 accept 队列溢出,最终导致建立好的 TCP 连接被丢弃。
实际上,丢弃连接只是 Linux 的默认行为,我们还可以选择向客户端发送 RST 复位报文
,告诉客户端连接已经建立失败
。打开这一功能需要将 tcp_abort_on_overflow
参数设置为 1。
服务器发送完SYN-ACK包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传。如果重传次数超过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。
注意,每次重传等待的时间不一定相同,一般会是指数增长,例如间隔时间为 1s,2s,4s,8s……
关于建连接时
SYN超时
。试想一下,如果server端接到了clien发的SYN后回了SYN-ACK后client掉线了,server端没有收到client回来的ACK,那么,这个连接处于一个中间状态,即没成功,也没失败。于是,server端如果在一定时间内没有收到的TCP会重发SYN-ACK。在Linux下,默认重试次数为5次
,重试的间隔时间从1s开始每次都翻售,5次的重试时间间隔为1s, 2s, 4s, 8s, 16s,总共31s,第5次发出后还要等32s都知道第5次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 =63s
,TCP才会把断开这个连接。
客户端发送 SYN 开启了三次握手,之后客户端连接的状态是 SYN_SENT,然后等待服务器回复 ACK 报文。正常情况下,服务器会在几毫秒内返回 ACK,但如果客户端迟迟没有收到 ACK 会怎么样呢?客户端会重发 SYN,重试的次数由 tcp_syn_retries
参数控制,默认是 6 次:
net.ipv4.tcp_syn_retries = 6
第 1 次重试发生在 1 秒钟后,接着会以翻倍的方式在第 2、4、8、16、32 秒共做 6 次重试,最后一次重试会等待 64 秒,如果仍然没有返回 ACK,才会终止三次握手。所以,总耗时是 1+2+4+8+16+32+64=127 秒
,超过 2 分钟。
**如果这是一台有明确任务的服务器,你可以根据网络的稳定性和目标服务器的繁忙程度修改重试次数,调整客户端的三次握手时间上限。**比如内网中通讯时,就可以适当调低重试次数,尽快把错误暴露给应用程序。
当一端为建立连接而发送它的SYN时,它为连接选择一个初始序号。ISN随时间而变化,因此每个连接都将具有不同的ISN。ISN可以看作是一个32比特的计数器,每4ms加1 。
客户端和服务端交换 ISN(Initial Sequence Number),以便让对方知道接下来接收数据的时候如何按序列号组装数据。
如果 ISN 是固定的,攻击者很容易猜出后续的确认号,因此 ISN 是动态生成的。
第三次握手的时候,可以携带数据的。第一次、第二次握手不可以携带数据。
假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据。因为攻击者根本就不理服务器的接收、发送能力是否正常,然后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。
也就是说,第一次握手不可以放数据,其中一个简单的原因就是会让服务器更加容易受到攻击了。而对于第三次的话,此时客户端已经处于 ESTABLISHED 状态。对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据。
**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后,就下线了,于是服务器需要默认等63s
才会断开连接,这样,攻击者就可以把服务器的syn连接的队列耗尽,让正常的连接请求不能处理。
解决
当第三次握手失败时,服务器并不会重传ack报文,而是直接发送RST报文段,进入CLOSED状态
。这样做的目的是为了防止SYN洪泛攻击
。
建立一个连接需要三次握手,而终止一个连接要经过四次挥手(也有将四次挥手叫做四次握手的)。这由TCP的半关闭(half-close)造成的。所谓的半关闭
,其实就是TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。
TCP 连接的拆除需要发送四个包,因此称为四次挥手(Four-way handshake),客户端或服务端均可主动发起挥手动作。
刚开始双方都处于ESTABLISHED
状态,假如是客户端先发起关闭请求。四次挥手的过程如下:
FIN_WAIT1
状态。CLOSE_WAIT
状态。LAST_ACK
的状态。TIME_WAIT
状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED
状态。通过上面的分析,可以看出四次挥手涉及两种报文:FIN 和 ACK。FIN 就是 Finish 结束连接的意思,谁发出 FIN 报文,就表示它将不再发送任何数据,关闭这一方向的传输通道。ACK 是 Acknowledge 确认的意思,它用来通知对方:你方的发送通道已经关闭
。
四次挥手过程总结:
FIN_WAIT1
。当被动方收到 FIN 报文后,内核自动回复 ACK 报文,连接状态由 ESTABLISHED 变为 CLOSE_WAIT
,顾名思义,它在等待进程调用 close 函数关闭连接。当主动方接收到这个 ACK 报文后,连接状态由 FIN_WAIT1 变为 FIN_WAIT2
,主动方的发送通道就关闭了。close 函数
,进而触发内核发送 FIN 报文,此时被动方连接的状态变为 LAST_ACK
。当主动方收到这个 FIN 报文时,内核会自动回复 ACK,同时连接的状态由 FIN_WAIT2 变为 TIME_WAIT,Linux 系统下大约 1 分钟
后 TIME_WAIT 状态的连接才会彻底关闭。而被动方收到 ACK 报文后,连接就会关闭。当连接处于半关闭状态
时,TCP 是允许单向传输数据的。当主动方关闭连接时,被动方仍然可以在不调用 close 函数的状态下,长时间发送数据,此时连接处于半关闭状态。这一特性是 TCP 的双向通道互相独立所致
,却也使得关闭连接必须通过四次挥手才能做到。
TIME_WAIT状态也称为2MSL等待状态
。每个具体TCP实现必须选择一个报文段最大生存时间MSL(Maximum Segment Lifetime),它是任何报文段被丢弃前在网络内的最长时间。这个时间是有限的,因为TCP报文段以IP数据报在网络内传输,而IP数据报则有限制其生存时间的TTL字段。
对一个具体实现所给定的MSL值,处理的原则是:当TCP执行一个主动关闭,并发回最后一个ACK,该连接必须在TIME_WAIT状态停留的时间为2倍的MSL。这样可让TCP再次发送最后的ACK以防这个ACK丢失(另一端超时并重发最后的FIN)。
这种2MSL等待的另一个结果是这个TCP连接在2MSL等待期间,定义这个连接的插口(客户的IP地址和端口号,服务器的IP地址和端口号)不能再被使用。这个连接只能在2MSL结束后才能再被使用。
保证客户端发送的最后一个ACK报文段能够到达服务端。
这个ACK报文段有可能丢失,使得处于LAST-ACK状态的服务端收不到对已发送的FIN+ACK报文段的确认,服务端超时重传FIN+ACK报文段,而客户端能在2MSL时间内收到这个重传的FIN+ACK报文段,接着客户端重传一次确认,重新启动2MSL计时器,最后客户端和服务端都进入到CLOSED状态,若客户端在TIME-WAIT状态不等待一段时间,而是发送完ACK报文段后立即释放连接,则无法收到服务端重传的FIN+ACK报文段,所以不会再发送一次确认报文段,则服务端无法正常进入到CLOSED状态。
防止“已失效的连接请求报文段”出现在本连接中。
客户端在发送完最后一个ACK报文段后,再经过2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文段。
防止这次延迟的断连请求发生在下一次正常连接上。
如果主动方不保留 TIME_WAIT 状态,会发生什么呢?
此时连接的端口恢复了自由身,可以复用于新连接了
。然而,被动方的 FIN 报文可能再次到达,这既可能是网络中的路由器重复发送,也有可能是被动方没收到 ACK 时基于 tcp_orphan_retries 参数重发。这样,正常通讯的新连接就可能被重复发送的 FIN 报文误关闭
。保留 TIME_WAIT 状态,就可以应付重发的 FIN 报文,当然,其他数据报文也有可能重发,所以 TIME_WAIT 状态还能避免数据错乱。
TIME-WAIT状态如果过多,会占用系统资源。Linux下有几个参数可以调整TIME-WAIT状态时间:
在socket的TIME_WAIT状态结束之前,该socket所占用的本地端口号将一直无法释放。高TCP并发并且采用短连接方式进行通讯的通讯系统在高并发高负载下运行一段时间后,就常常会出现做为客户端的程序无法向服务端建立新的socket连接的情况。
此时用“netstat -tanlp”命令查看系统将会发现机器上存在大量处于TIME_WAIT状态的socket连接,并且占用大量的本地端口号
。最后,当该机器上的可用本地端口号被占完(或者达到用户可使用的文件句柄上限),而旧的大量处于TIME_WAIT状态的socket尚未被系统回收时,就会出现无法向服务端创建新的socket连接的情况。此时系统几乎停转,空有再好的性能也发挥不出来。
解决TIME-WAIT状态过多的情况,一般做法是打开系统的TIMEWAIT重用和快速回收
。然而,主动进行关闭的链接才会进入TIME-WAIT状态
,所以最好的办法:尽量不要让服务器主动关闭链接,除非一些异常情况,如客户端协议错误、客户端超时等等。
粗的实线箭头表示正常的客户端状态变迁,粗的虚线箭头表示正常的服务器状态变迁。