TCP 三次握手与四次挥手
# TCP 头部
上面就是 TCP 协议头部的格式,它非常重要,是理解其它内容的基础,下面将每个字段的信息都详细的说明一下:
- Source Port 16 位,用于标识源端口号(发送机器 TCP 端口)
- Destination Port 16 位,用于标识目的端口号(接收端口)
- Sequence Number 32 位,用于 TCP 段的字节级别编号。如果使用 TCP 连接,则为数据的每个字节分配一个序列号。如果设置了 SYN 标志(在三向握手连接初始化期间),则是初始序列号。 然后,实际第一个数据字节的序列号将是此序列号加 1。例如,让设备在特定 TCP 报头中的数据的第一个字节在该字段 50000 中将具有其序列号。如果此数据包有 500 字节中的数据,那么此设备发送的下一个数据包将具有 50000 + 500 + 1 = 50501 的序列号。
- Acknowledgment Number 32 位,确认序列号包含发送确认的一端所期望收到的下一个序号,因此,确认序号应当是上次已成功收到数据字节序号加 1。不过,只有当标志位中的 ACK 标志(下面介绍)为 1 时该确认序列号的字段才有效。主要用来解决不丢包的问题;
- Header Length 4 位,显示 header 中 32 位字的数量。需要这个值是因为任选字段的长度是可变的。也称为数据偏移字段(Data Offset field)。 header 的最小为 5 个字(二进制模式为 0101)。
- Reserved 6 位,常被设为 0;
- Control Bit Flags
我们之前已经知道 TCP 是面向连接的协议。 面向连接协议的含义是,在可以传输任何数据之前,必须获得(obtained)并确认可靠的连接。 控制位控制着连接建立,数据传输和连接终止的整个过程。
控制位列出如下:它们依次为 URG,ACK,PSH,RST,SYN,FIN。每个标志位的含义如下:
- URG:Urgent Pointer,此标志表示 TCP 包的紧急指针域(后面马上就要说到)有效,用来保证 TCP 连接不被中断,并且督促中间层设备要尽快处理这些数据;
- ACK:Acknowledgement,此标志表示应答域有效。就是说前面所说的 TCP 应答号将会包含在 TCP 数据包中;有两个取值:0 和 1,为 1 的时候表示应答域有效,反之为 0;
- PSH:push,这个标志位表示传送操作。所谓 Push 操作就是指在数据包到达接收端以后,立即传送给应用程序,而不是在缓冲区中排队;
- RST:reset,这个标志表示连接复位请求。用来复位那些产生错误的连接,也被用来拒绝错误和非法的数据包;
- SYN:synchronous,表示同步序号,用来建立连接。SYN 标志位和 ACK 标志位搭配使用,当连接请求的时候,SYN=1,ACK=0;连接被响应的时候,SYN=1,ACK=1;这个标志的数据包经常被用来进行端口扫描。扫描者发送一个只有 SYN 的数据包,如果对方主机响应了一个数据包回来,就表明这台主机存在这个端口;但是由于这种扫描方式只是进行 TCP 三次握手的第一次握手,因此这种扫描的成功表示被扫描的机器不很安全,一台安全的主机将会强制要求一个连接严格的进行 TCP 的三次握手;
- FIN: finish,表示发送端已经达到数据末尾。也就是说双方的数据传送完成,没有数据可以传送了,发送 FIN 标志位的 TCP 数据包后,连接将被断开。这个标志的数据包也经常被用于进行端口扫描。
提示
需要注意的是:
- 不要将确认序号 ack 与标志位中的 ACK 搞混了。
- 确认方 ack=发起方 req+1,两端配对。
- Window 16bits,窗口字段用来控制对方发送的数据量,单位为字节。也就是有名的滑动窗口,用来进行流量控制。TCP 连接的一端根据设置的缓存空间大小确定自己的接收窗口大小,然后通知对方以确定对方的发送窗口的上限。
- Checksum 16bits,检验和字段检验的范围包括首部和数据这两部分。在计算检验和时,要在 TCP 报文段的前面加上 12 字节的伪首部。
- Urgent Pointer 紧急指针字段,16bits,紧急指针指出在本报文段中的紧急数据的最后一个字节的序号。
- Option 长度可变,TCP 首部可以有多达 40 字节的可选信息,用于把附加信息传递给终点,或用来对齐其它选项。
# 三次握手与四次挥手
# 三次握手(3-way Handshake)
所谓三次握手(Three-way Handshake),是指建立一个 TCP 连接时,需要客户端和服务器总共发送 3 个包。
三次握手的目的是连接服务器指定端口,建立 TCP 连接,并同步连接双方的序列号和确认号,交换 TCP 窗口大小信息。在 socket 编程中,客户端执行 connect()
时。将触发三次握手。
第一次握手(SYN=1, ISN=x): 设备 A(客户端)发送一个 SYNchronize 标志设置为 1,以及初始序列号 ISN (Initial Sequence Number) 为 x 的 TCP 连接请求报文段;
发送完毕后,客户端进入
SYN_SEND
状态,等待服务器的确认;第二次握手(SYN=1, ACK=1, ISN=y, ACKnum=x+1): 设备 B(服务器)接收设备 A 的 SYN 报文段,需要对这个 SYN 报文段进行确认,返回 SYN = 1,ACK = 1,ISN = Y(设备 B 的初始序列号),确认编号(Acknowledgment Number )= x + 1(设备 B 接收到的设备 A 的 ISN 加 1)
发送完毕后,服务器端进入
SYN_RECV
状态。此时服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称之为半连接队列。第三次握手(ACK=1, ACKnum=y+1) 设备 A(客户端)向设备 B(服务器)发送一个 TCP 报文段,以确认接收到设备 B 的 ISN,标志设置为 ACK = 1,序列号(Sequence number)= x+1,确认号(Acknowledgment number)= y+1
发送完毕后,客户端进入
ESTABLISHED
状态,当服务器端接收到这个包时,也进入ESTABLISHED
状态,TCP 握手结束。
完成了三次握手,客户端和服务器端建立全双工通信(full-duplex communication),开始使用约定的序列号和确认号( sequence 和 acknowledge numbers)传送数据。
三次握手的过程的示意图如下:
- 3 way handshake, TCP Three-way handshake, TCP Synchronization (opens new window)
- TCP 3-Way Handshake (SYN,SYN-ACK,ACK) (opens new window)
- TCP 3-Way Handshake Process (opens new window)
- TCP three-way handshake (opens new window)
# 四次挥手(TCP Connection Termination)
当客户端和服务器通过三次握手建立了 TCP 连接以后,数据传送完毕,肯定是要断开 TCP 连接。TCP 的连接的解除需要发送四个包,因此称为四次挥手(Four-way handshake)。客户端或服务器均可主动发起挥手动作,在 socket 编程中,任何一方执行 close()
操作即可产生挥手操作。
第一次挥手(FIN=1,seq=x)
(从客户端获得 FIN)——此处假定客户端应用程序决定要关闭连接。 (请注意,服务器也可以选择关闭连接)。这将导致客户端将 FIN 位设置为 1 的 TCP 报文段发送到服务器,并进入 FIN_WAIT_1 状态。当客户端处于 FIN_WAIT_1 状态时,它会等待来自服务器的带有 ACK 确认(acknowledgment)的 TCP 段。
发送完毕后,客户端进入
FIN_WAIT_1
状态,此时仍然可以接受数据。第二次挥手(ACK=1,ACKnum=x+1)
(来自服务器的 ACK)——当服务器从发件人(客户端)收到 FIN 位段时,服务器立即向发件人(客户端)发送确认(ACK)报文段。Acknowledgment Number 为 Sequence Number 加 1;表明自己接受到了客户端关闭连接的请求,但还没有准备好关闭连接。
发送完毕后,服务器端进入
CLOSE_WAIT
状态,客户端接收到这个确认包之后,进入FIN_WAIT_2
状态,等待服务器端发送关闭连接段。第三次挥手(FIN=1,seq=y)
(来自服务器的 FIN)服务器端准备好关闭连接时,向客户端发送结束连接请求,FIN 置为 1。
发送完毕后,服务器端进入
LAST_ACK
状态,等待来自客户端的最后一个 ACK。第四次挥手(ACK=1,ACKnum=y+1)
(来自客户端的 ACK)客户端接收到来自服务器端的关闭请求,发送一个确认报文段,并进入
TIME_WAIT
状态,TIME_WAIT 状态允许客户端在 ACK 丢失的情况下重新发送最终确认的 ACK 包。服务器端接收到这个确认包之后,关闭连接,进入
CLOSED
状态。客户端等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入
CLOSED
状态。处于 TIME_WAIT 状态的客户端所花费的时间取决于其实现,但典型值为 30 秒,1 分钟和 2 分钟。等待之后,连接正式关闭,并且客户端上的所有资源(包括端口号和缓冲区数据)都被释放。
四次挥手的示意图如下:
# 完整状态转换图
# 疑问解惑
# ISN 是固定的吗
三次握手的一个重要功能是客户端和服务端交换 ISN(Initial Sequence Number), 以便让对方知道接下来接收数据的时候如何按序列号组装数据。
如果 ISN 是固定的,攻击者很容易猜出后续的确认号,因此 ISN 是动态生成的。
想象一下如果写死一个值,比如 0 ,那么假设已经建立好连接了,client 也发了很多包比如已经第 20 个包了,然后网络断了之后 client 重新,端口号还是之前那个,然后序列号又从 0 开始,此时服务端返回第 20 个包的 ack,客户端是不是傻了?
所以 RFC793 中认为 ISN 要和一个假的时钟绑定在一起,ISN 每四微秒加一,当超过 2 的 32 次方之后又从 0 开始,要四个半小时左右发生 ISN 回绕。
所以 ISN 变成一个递增值,真实的实现还需要加一些随机值在里面,防止被不法份子猜到 ISN。
# 为什么要三次握手
三次的目的是为了保证服务端请求客户端时的可靠通信。两次只能保证客户端到服务端的单向可靠通信。
TCP 建立连接时通过三次握手可以有效地避免历史错误连接的建立,减少通信双方不必要的资源消耗,三次握手能够帮助通信双方获取初始化序列号,它们能够保证数据包传输的不重不丢,还能保证它们的传输顺序,不会因为网络传输的问题发生混乱,到这里不使用『两次握手』和『四次握手』的原因已经非常清楚了:
两次握手:无法避免历史错误连接的初始化,浪费接收方的资源; 四次握手:TCP 协议的设计可以让我们同时传递 ACK 和 SYN 两个控制信息,减少了通信次数,所以不需要使用更多的通信次数传输相同的信息; 我们重新回到在文章开头提的问题,为什么使用类比解释 TCP 使用三次握手是错误的?这主要还是因为,这个类比没有解释清楚核心问题 —— 避免历史上的重复连接
在谢希仁著《计算机网络》第四版中讲“三次握手”的目的是“为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误”。为了解决“网络中存在延迟的重复分组”的问题。
为什么 A 还要发送一次确认呢?这主要是为了防止己失效的连接请求报文段突然又传送到了 B ,因而产生错误。 所谓“己失效的连接请求报文段”是这样产生的:
正常情况下:A 发出连接请求,但因连接请求报文丢失而未收到确认。于是 A 再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接。A 共发送了两个连接请求报文段。其中,第一个丢失,第二个到达了 B。没有“已失效的连接请求报文段”。
现假定出现一种异常情况:即 A 发出的第一个连接请求报文段并没有丢失,而是在某些网络结点长时间滞留了,以致延误到连接释放以后的某个时间才到达 B。本来这是一个早己失效的报文段。但 B 收到此失效的连接请求报文段后,就误认为是 A 又发出一次新的连接请求。于是就向 A 发出确认报文段,同意建立连接,假定不采用“三次握手”,那么只要 B 发出确认,新的连接就建立了。 由于现在 A 并没有发出建立连接的请求。因此不会理睬 B 的确认。也不会向 B 发送数据。但 B 却以为新的运输连接己经建立了,并一直等待 A 发来数据。B 的许多资源就这样白白浪费了。 采用三次握手的办法可以防止上述现象的发生。例如在刚才的情况下,A 不会向 B 的确认发出确认。B 由于收不到确认。就知道 A 并没有要求建立连接。
这就很明白了,防止服务器端的一直等待而浪费资源。
在 Google Groups 的 TopLanguage 中看到一帖讨论 TCP“三次握手”觉得很有意思。贴主提出“TCP 建立连接为什么是三次握手?”的问题,在众多回复中,有一条回复写道:“这个问题的本质是:信道不可靠, 但是通信双发需要就某个问题达成一致。而要解决这个问题, 无论你在消息中包含什么信息, 三次通信是理论上的最小值。所以三次握手不是 TCP 本身的要求, 而是为了满足"在不可靠信道上可靠地传输信息"这一需求所导致的。请注意这里的本质需求,信道不可靠, 数据传输要可靠。三次达到了, 那后面你想接着握手也好, 发数据也好, 跟进行可靠信息传输的需求就没关系了。因此,如果信道是可靠的, 即无论什么时候发出消息, 对方一定能收到, 或者你不关心是否要保证对方收到你的消息, 那就能像 UDP 那样直接发送消息就可以了。”这可视为对“三次握手”目的的另一种解答思路。
- 通俗理解
但是为什么一定要进行三次握手来保证连接是双工的呢,一次不行么?两次不行么?我们举一个现实生活中两个人进行语言沟通的例子来模拟三次握手。 引用网上的一些通俗易懂的例子,虽然不太正确,后面会指出,但是不妨碍我们理解,大体就是这么个理解法。
- 第一次对话: 老婆让甲出去打酱油,半路碰到一个朋友乙,甲问了一句:哥们你吃饭了么? 结果乙带着耳机听歌呢,根本没听到,没反应。甲心里想:跟你说话也没个音,不跟你说了,沟通失败。说明乙接受不到甲传过来的信息的情况下沟通肯定是失败的。 如果乙听到了甲说的话,那么第一次对话成功,接下来进行第二次对话。
- 第二次对话: 乙听到了甲说的话,但是他是老外,中文不好,不知道甲说的啥意思也不知道怎样回答,于是随便回答了一句学过的中文 :我去厕所了。甲一听立刻笑喷了,“去厕所吃饭”?道不同不相为谋,离你远点吧,沟通失败。说明乙无法做出正确应答的情况下沟通失败。 如果乙听到了甲的话,做出了正确的应答,并且还进行了反问:我吃饭了,你呢?那么第二次握手成功。 通过前两次对话证明了乙能够听懂甲说的话,并且能做出正确的应答。 接下来进行第三次对话。
- 第三次对话: 甲刚和乙打了个招呼,突然老婆喊他,“你个死鬼,打个酱油咋这么半天,看我回家咋收拾你”,甲是个妻管严,听完吓得二话不说就跑回家了,把乙自己晾那了。乙心想:这什么人啊,得,我也回家吧,沟通失败。说明甲无法做出应答的情况下沟通失败。 如果甲也做出了正确的应答:我也吃了。那么第三次对话成功,两人已经建立起了顺畅的沟通渠道,接下来开始持续的聊天。 通过第二次和第三次的对话证明了甲能够听懂乙说的话,并且能做出正确的应答。 可见,两个人进行有效的语言沟通,这三次对话的过程是必须的。 为了保证服务端能收接受到客户端的信息并能做出正确的应答而进行前两次(第一次和第二次)握手,为了保证客户端能够接收到服务端的信息并能做出正确的应答而进行后两次(第二次和第三次)握手。 这个例子举得挺好的。不过个人感觉为什么是三次而不是二次,不是因为为了证明甲能听懂乙并回应(第二次乙能正确的响应甲说明俩人之间沟通已无障碍了),而是怕出现以下情况而浪费感情。这个情景是这样的(例子有点不实际意会就好):甲在路上跟乙打招呼,由于刮风什么的这句活被吹跑了,然后甲又跟打了个招呼,乙听到了并作出了回应。此时不管是三次握手还是两次握手两个人都能愉快的沟通。0.1 秒后俩人四次挥手告别了。此时被风刮跑的那句话又传到了乙的耳朵里,乙认为甲又要跟他沟通,所以做出了响应的回应。(问题出现了)假如采用 2 次握手,乙就认定了甲要跟他沟通,于是就不停的等,浪费感情。可如果是采用 3 次握手,乙等了一会后发现甲没有回应他就认为甲走了然后自己也就走了! 这就很明白了,其实第三步是防止了乙的一直等待而浪费自己的时间,而不是为了保证甲能够正确回应乙的信息。
# 为什么要四次分手
TCP 协议是一种面向连接的、可靠的、基于字节流的运输层通信协议。
TCP 是全双工模式,这就意味着,当主机 1 发出 FIN 报文段时,只是表示主机 1 已经没有数据要发送了,主机 1 告诉主机 2,它的数据已经全部发送完毕了;但是,这个时候主机 1 还是可以接受来自主机 2 的数据;当主机 2 返回 ACK 报文段时,表示它已经知道主机 1 没有数据发送了,但是主机 2 还是可以发送数据到主机 1 的;当主机 2 也发送了 FIN 报文段时,这个时候就表示主机 2 也没有数据要发送了,就会告诉主机 1,我也没有数据要发送了,之后彼此就会愉快地中断这次 TCP 连接。
如果要正确的理解四次分手的原理,就需要了解四次分手过程中的状态变化:
- FIN_WAIT_1: 这个状态要好好解释一下,其实 FIN_WAIT_1 和 FIN_WAIT_2 状态的真正含义都是表示等待对方的 FIN 报文。而这两种状态的区别是:FIN_WAIT_1 状态实际上是当 SOCKET 在 ESTABLISHED 状态时,它想主动关闭连接,向对方发送了 FIN 报文,此时该 SOCKET 即进入到 FIN_WAIT_1 状态。而当对方回应 ACK 报文后,则进入到 FIN_WAIT_2 状态,当然在实际的正常情况下,无论对方何种情况下,都应该马上回应 ACK 报文,所以 FIN_WAIT_1 状态一般是比较难见到的,而 FIN_WAIT_2 状态还有时常常可以用 netstat 看到。(主动方)
- FIN_WAIT_2:上面已经详细解释了这种状态,实际上 FIN_WAIT_2 状态下的 SOCKET,表示半连接,也即有一方要求 close 连接,但另外还告诉对方,我暂时还有点数据需要传送给你(ACK 信息),稍后再关闭连接。(主动方)
- CLOSE_WAIT:这种状态的含义其实是表示在等待关闭。怎么理解呢?当对方 close 一个 SOCKET 后发送 FIN 报文给自己,你系统毫无疑问地会回应一个 ACK 报文给对方,此时则进入到 CLOSE_WAIT 状态。接下来呢,实际上你真正需要考虑的事情是察看你是否还有数据发送给对方,如果没有的话,那么你也就可以 close 这个 SOCKET,发送 FIN 报文给对方,也即关闭连接。所以你在 CLOSE_WAIT 状态下,需要完成的事情是等待你去关闭连接。(被动方)
- LAST_ACK: 这个状态还是比较容易好理解的,它是被动关闭一方在发送 FIN 报文后,最后等待对方的 ACK 报文。当收到 ACK 报文后,也即可以进入到 CLOSED 可用状态了。(被动方)
- TIME_WAIT: 表示收到了对方的 FIN 报文,并发送出了 ACK 报文,就等 2MSL 后即可回到 CLOSED 可用状态了。如果 FINWAIT1 状态下,收到了对方同时带 FIN 标志和 ACK 标志的报文时,可以直接进入到 TIME_WAIT 状态,而无须经过 FIN_WAIT_2 状态。(主动方)
- CLOSED: 表示连接中断。
# 为什么连接的时候是三次握手,关闭的时候却是四次握手
因为 TCP 是全双工协议,当 Server 端收到 Client 端的 SYN 连接请求报文后,可以直接发送 SYN+ACK 报文。其中 ACK 报文是用来应答的,SYN 报文是用来同步的。但是关闭连接时,当 Server 端收到 FIN 报文时,很可能并不会立即关闭 SOCKET,所以只能先回复一个 ACK 报文,告诉 Client 端,"你发的 FIN 报文我收到了"。只有等到我 Server 端所有的报文都发送完了,我才能发送 FIN 报文,因此不能一起发送。故需要四步握手。
# 为什么 TIME_WAIT 状态需要经过 2MSL(maximum segment lifetime:最大报文段生存时间)才能返回到 CLOSE 状态
可靠地实现 TCP 全双工连接的终止
在进行关闭连接四路握手协议时,最后的 ACK 是由主动关闭端发出的,如果这个最终的 ACK 丢失,服务器将重发最终的 FIN,因此客户端必须维护状态信息允许它重发最终的 ACK。如果不维持这个状态信息,那么客户端将响应 RST 分节,服务器将此分节解释成一个错误(在 java 中会抛出 connection reset 的 SocketException)。因而,要实现 TCP 全双工连接的正常终止,必须处理终止序列四个分节中任何一个分节的丢失情况,主动关闭的客户端必须维持状态信息进入 TIME_WAIT 状态。
允许老的重复分节在网络中消逝
TCP 分节可能由于路由器异常而“迷途”,在迷途期间,TCP 发送端可能因确认超时而重发这个分节,迷途的分节在路由器修复后也会被送到最终目的地,这个原来的迷途分节就称为 lost duplicate。在关闭一个 TCP 连接后,马上又重新建立起一个相同的 IP 地址和端口之间的 TCP 连接,后一个连接被称为前一个连接的化身 (incarnation),那么有可能出现这种情况,前一个连接的迷途重复分组在前一个连接终止后出现,从而被误解成从属于新的化身。为了避免这个情况,TCP 不允许处于 TIME_WAIT 状态的连接启动一个新的化身,因为 TIME_WAIT 状态持续 2MSL,就可以保证当成功建立一个 TCP 连接的时候,来自连接先前化身的重复分组已经在网络中消逝。
# 参考资料
- TCP Connection Establishment and Termination (opens new window)
- Transmission Control Protocol (TCP) Tutorials (opens new window)
- 通俗大白话来理解 TCP 协议的三次握手和四次分手 (opens new window)
- TCP 三次握手详解及释放连接过程 (opens new window)
- 简析 TCP 的三次握手与四次分手 (opens new window)
- TCP 协议设计原理 (opens new window)
- TCP 协议中的三次握手和四次挥手(图解) (opens new window)
- 理解 TCP 为什么需要进行三次握手(白话) (opens new window)
- TCP 为什么是三次握手,为什么不是两次或四次? (opens new window)
- 面试时,你被问到过 TCP/IP 协议吗? (opens new window)
- TCP 协议 (opens new window)
- TCP 三次握手及四次挥手详细图解 (opens new window)
- TCP 协议三次握手过程分析 (opens new window)
- 百度百科:SYN 攻击 (opens new window)
- TCP-Keepalive-HOWTO (opens new window)
- 万字长文 | 23 个问题 TCP 疑难杂症全解析_yes 的练级攻略的博客-CSDN 博客 (opens new window)