深入理解 TCP 协议(三):连接管理机制 —— 三次握手和四次挥手详解 目录前言一、为什么 TCP 需要建立连接1.1 TCP 是面向连接的协议1.2 连接到底是什么1.3 建立连接时必须要解决的问题1.3.1 确认双方都具备通信条件1.3.2 同步双方的初始序号1.3.3 协商 TCP 通信所需要的参数1.4 小结二、TCP 三次握手2.1 第一次握手2.2 第二次握手2.3 第三次握手2.4 为什么需要三次握手2.5 三次握手过程中的 TCP 状态变化2.6 listen()、connect()、accept() 系统调用三、TCP 四次挥手3.1 为什么 TCP 需要四次挥手3.2 第一次挥手3.3 第二次挥手3.4 第三次挥手3.5 第四次挥手3.6 为什么需要 TIME_WAIT3.7 TIME_WAIT 的等待时间思考四次握手和三次挥手四、TCP 的其他特性与应用4.1 面向字节流4.2 粘包问题4.3 TCP 异常情况4.4 基于 TCP 的应用层协议4.5 用 UDP 实现可靠传输经典面试题前言在前两篇文章中我们分别从TCP 报文格式和TCP 数据传输机制两个方面对 TCP 协议进行了深入介绍。在第一篇文章中我们从 TCP 报头入手介绍了 TCP 报文中的各个字段以及序号、确认序号、窗口大小、标志位等重要字段的作用。在第二篇文章中我们进一步研究了 TCP 建立连接之后的数据传输过程介绍了确认应答、超时重传、滑动窗口、流量控制、拥塞控制、延迟应答、捎带应答等机制理解了 TCP 是如何在可靠性与传输效率之间进行平衡的。但是到目前为止我们讨论的都是TCP 连接建立之后数据是如何可靠、高效地传输的。而在真正进行数据传输之前还有一个问题需要解决TCP 连接究竟是如何建立起来的TCP 是一个面向连接的传输层协议。通信双方在正式传输数据之前需要先建立连接当数据传输完成后也需要按照一定的流程关闭连接。因此本篇文章将作为 TCP 系列的最后一篇重点介绍 TCP 的连接管理机制深入理解TCP 为什么需要三次握手三次握手具体做了什么TCP 连接建立过程中双方的状态如何变化TCP 为什么需要四次挥手为什么 TCP 关闭连接后还需要等待TIME_WAITTCP 的各种连接状态分别代表什么通过本篇文章我们将从连接建立、数据传输到连接关闭完整串起 TCP 的整个生命周期。一、为什么 TCP 需要建立连接1.1 TCP 是面向连接的协议TCP 是一个面向连接的、可靠的字节流传输协议。所谓面向连接指的是 TCP 在正式传输数据之前通信双方需要先建立连接。例如客户端需要向服务器发送数据客户端 服务器 建立 TCP 连接 ────────────────→ ←──────────────── ────────────────→ TCP 连接建立完成 发送数据 ────────────────→ ←────────────────只有当 TCP 连接建立完成之后双方才会正式进行数据传输。而当通信完成之后双方也不能简单地认为“数据发完了就结束了”而是需要按照 TCP 规定的流程关闭连接释放连接所占用的资源。因此TCP 的一次完整通信可以简单理解为建立连接 - 数据传输 - 关闭连接这也是 TCP 与 UDP 一个非常重要的区别UDP 是无连接的。1.2 连接到底是什么所谓“面向连接”并不是说 TCP 在通信双方之间建立了一条真实存在的物理线路。而网络中的数据依然是通过 IP 网络进行转发的中间经过路由器、交换机等网络设备到达目标主机。直接抛出结论TCP 所建立的“连接”本质上是一种由通信双方操作系统维护的、有状态的通信关系。我们可以先从最简单的角度理解。假设客户端IP192.168.1.10 端口50000服务器IP192.168.1.20 端口8080那么客户端与服务器之间的 TCP 通信可以通过下面四个信息确定源 IP 源端口 目的 IP 目的端口 192.168.1.10 50000 192.168.1.20 8080这四个信息被称为TCP 连接的四元组。可以表示为192.168.1.10:50000 ↓ 192.168.1.20:8080通过这个四元组操作系统能够确定哪一台主机的哪个进程正在与哪一台主机的哪个进程进行 TCP 通信。但是要注意四元组只是标识一条 TCP 连接并不等于 TCP 连接本身。因为 TCP 是一个有状态的协议连接建立之后操作系统还需要维护大量与连接相关的信息。例如我们前面已经学习过的TCP 当前状态 发送序号 确认序号 发送窗口 接收窗口 重传相关信息 拥塞控制相关信息 发送缓冲区 接收缓冲区 ...这些信息共同描述了当前这条 TCP 通信进行到了什么程度以及接下来应该如何继续进行。因此从操作系统的角度来看一条 TCP 连接实际上对应着内核维护的一组连接状态和相关资源。后面我们学习 TCP 状态转换时会看到LISTEN ↓ SYN_SENT ↓ ESTABLISHED ↓ FIN_WAIT_1 ↓ TIME_WAIT ↓ CLOSED这些状态实际上就是 TCP 在整个连接生命周期中维护的不同状态。所以可以把 TCP 连接理解为通信双方围绕一次 TCP 通信建立起来的一套有状态的通信关系。而连接管理就是负责维护这套通信关系的生命周期建立 ↓ 维护 ↓ 关闭1.3 建立连接时必须要解决的问题理解了“面向连接”和“TCP 连接”之后我们再思考一个问题为什么 TCP 不能直接开始传输数据而必须先建立连接因为 TCP 是一个可靠的、有状态的字节流协议。在真正发送应用层数据之前通信双方需要先完成一些必要的信息同步。1.3.1 确认双方都具备通信条件首先客户端需要告诉服务器“我想和你建立 TCP 通信。”服务器收到之后也需要明确告诉客户端“我知道你想和我建立连接并且我也准备好了。”客户端还需要进一步确认“我知道你已经准备好了。”只有经过这样的交互双方才能真正建立起一致的连接状态。这实际上就是后面三次握手要解决的问题之一。所以三次握手并不是简单的“你好 → 你好 → 你好”而是在通过三次报文交互让双方逐渐建立起对这条 TCP 连接的共同认知。1.3.2 同步双方的初始序号这个问题与上一篇文章中的序号和确认序号直接相关。TCP 并不是随便给数据编号而是在建立连接时为这次 TCP 通信确定一个初始序列号ISN。例如客户端初始序号1000 服务器初始序号5000那么客户端后续发送的数据就会从自己的序号开始1000 1001 1002 1003 ...服务器发送的数据则从自己的序号开始5000 5001 5002 5003 ...因此双方在正式传输数据之前需要让对方知道自己的初始序号。这个过程就是序列号同步。而这也是三次握手中非常重要的一项工作。后面我们分析三次握手的时候会看到客户端 → 服务器 SYN 序号 服务器 → 客户端 SYN ACK 序号 确认序号 客户端 → 服务器 ACK 确认序号1.3.3 协商 TCP 通信所需要的参数建立 TCP 连接时双方还可以通过 TCP 报头中的选项字段交换一些通信参数。例如MSS最大报文段长度窗口扩大因子时间戳等这些参数会影响后续 TCP 数据传输。例如我们前面学习过TCP 的窗口大小与流量控制有关。如果接收方能够接收的数据比较少那么它就需要通过窗口相关信息告诉发送方“我现在只能接收这么多数据。”因此在连接建立阶段双方不仅是在“确认对方存在”还会为后续的数据传输建立必要的通信参数和初始状态。1.4 小结所以TCP 在正式传输数据之前进行连接管理并不是多此一举。它需要通过连接建立过程完成TCP连接建立 ↓ ┌───────────┼───────────┐ ↓ ↓ ↓ 确认通信双方 同步初始序号 协商通信参数 ↓ ↓ ↓ └───────────┼───────────┘ ↓ 建立连接状态 ↓ 数据传输因此我们可以把 TCP 建立连接理解为通信双方在正式传输数据之前通过一系列报文交互确认彼此的通信状态并同步后续数据传输所需要的信息从而建立一套双方都认可的 TCP 连接状态。那么接下来最关键的问题就是TCP 到底是通过什么过程完成这些工作的为什么偏偏是三次握手而不是两次或者四次二、TCP 三次握手在上一节中我们已经知道TCP 在正式传输数据之前需要先建立一套双方都认可的连接状态。那么问题来了TCP 是如何建立这套连接状态的答案就是三次握手三次握手的宏观理解就是 TCP 在建立连接时通信双方之间进行的三次 TCP 报文交互。整个过程可以详细表示为2.1 第一次握手客户端首先向服务器发送一个 SYN 报文表示客户端希望与服务器建立 TCP 连接。注SYN 报文是指 SYN 标志位被置为 1 的 TCP 报文。此时 TCP 报头中的SYN 1 Seq x注x 是客户端随机选择的初始序列号。服务器收到客户端发送的 SYN 报文之后可以明确知道1. 有一个客户端希望与自己建立 TCP 连接2. 客户端的初始序列号是 x同时客户端发送 SYN 后会进入SYN_SENT 状态。即表示客户端已经向服务器发送连接请求等待服务器的应答。所以第一次握手之后客户端CLOSED - SYN_SENT 服务器LISTEN此时TCP 连接还没有建立完成因为服务器收到了客户端的请求但客户端还不知道服务器是否收到了自己的请求以及服务器是否同意建立连接。所以还需要第二次握手。2.2 第二次握手服务器收到客户端发送的 SYN 后如果同意建立连接就会向客户端发送一个 SYN ACK 报文。这个报文同时完成两件事情1. 确认客户端的 SYN服务器通过ACK x 1 来告诉客户端你的 SYN 我已经收到了2. 告诉客户端服务器自己的初始序列号服务器在第二次握手中需要告诉客户端服务器的初始序列号假设服务器选择的初始序列号是 ySeq y因此第二次握手的 TCP 报文时SYN 1 ACK 1 Seq y 1 Ack x 1当服务器发送完 SYN ACK 后会进入SYN_RECV 状态即表示服务器已经收到客户端的连接请求并且已经向客户端回复了连接请求。而此时客户端收到第二次握手之后可以知道服务器确实收到了我的 SYN并且服务器也愿意建立 TCP 连接。同时客户端也获得了服务器的初始序列号 y。但是此时还有一个问题服务器并不知道客户端有没有收到自己的 SYN ACK。所以还需要第三次握手。2.3 第三次握手客户端收到服务器发送的 SYN ACK 后需要向服务器发送一个 ACK 报文。ACK 1 Seqx 1 Ack y 1其中Ack y 1 表示客户端已经收到服务器序号为 y 的 SYN。当服务器收到这个 ACK 后服务器就可以确认客户端已经收到了我的 SYN ACK。此时双方已经完成了必要的信息同步TCP 连接正式建立。服务器和客户端均进入ESTABLISHED 状态此时 TCP 三次握手完成就可以开始进行正式的数据传输。2.4 为什么需要三次握手对于 TCP 来讲为什么恰好需要三次握手呢为什么不是两次以及其他次数呢对于这个问题让我们从双方需要确认的信息来看。第一次握手客户端告诉服务器客户端 → 服务器 SYN Seq x客户端告诉服务器“我想和你建立连接我的初始序列号是 x。”服务器收到后可以确认客户端具备发送能力。但是客户端此时并不知道服务器是否收到了自己的 SYN。第二次握手服务器告诉客户端服务器 → 客户端 SYN ACK Seq y Ack x 1服务器告诉客户端“你的 SYN 我收到了这是我的初始序列号 y。”此时客户端可以确认服务器具备接收能力和发送能力。但是服务器还不知道客户端有没有收到自己的 SYN ACK。第三次握手客户端再次确认客户端 → 服务器 ACK Ack y 1客户端告诉服务器“你的 SYN ACK 我收到了。”此时服务器也可以确认客户端具备接收能力。于是双方就完成了确认客户端知道 服务器能够接收我的数据 服务器知道 客户端能够接收我的数据所以三次握手实际上完成了一个非常重要的过程让客户端和服务器都确认对方具备正常通信的能力并完成双方初始序列号的同步。为什么不能是两次握手过程假设只有两次客户端 服务器 SYN ─────────────────────────→ ←──────────────────── SYN ACK此时服务器认为“我已经把 SYN ACK 发给客户端了。”但是服务器无法确认客户端是否真正收到了这个 SYN ACK。如果客户端因为网络问题根本没有收到第二次握手那么客户端我没有建立成功 服务器我以为你建立成功了双方对于连接状态就可能产生不一致。因此还需要第三次 ACK让服务器确认客户端已经收到我的 SYN ACK。所以两次握手无法让服务器确认客户端已经成功接收到自己的响应。这也是三次握手相比两次握手的重要意义。为什么不能是其他次握手呢对于 TCP 三次握手双方都已经确认对方具备正常通信的能力并已经完成双方序列号的同步准备工作已经完成所以不需要额外的次数来进行 TCP 连接的建立。思考对于 TCP 三次握手的过程难道只建立序列号的共识吗对于 TCP 报头中的 16 位窗口字段选项字段2.5 三次握手过程中的 TCP 状态变化注每个状态在内核中的实现本质就是一个宏。结合上图可以看到客户端和服务器在三次握手过程中经历了不同的状态。第一次握手客户端进入 SYN_SENT最开始客户端处于 CLOSED 状态服务器处于 LISTEN 状态。当客户端希望与服务器建立连接时客户端向服务器发送一个 SYN 报文。发送 SYN 后客户端不能认为连接已经建立因为它还没有收到服务器的确认。因此客户端进入SYN_SENTSYN_SENT 可以理解为已经发送 SYN正在等待服务器确认。此时服务器仍然处于 LISTEN 状态等待客户端的连接请求。第二次握手服务器进入 SYN_RECV服务器在 LISTEN 状态下收到客户端发送的 SYN 报文。服务器确认客户端确实希望建立连接后会向客户端发送 SYN ACK 报文。发送之后服务器同样不能认为连接已经建立因为它还需要确认客户端是否收到了自己发送的 SYN ACK所以服务器进入SYN_RECVSYN_RECV 可以理解为已经收到客户端的 SYN也已经回复 SYN ACK正在等待客户端最后的 ACK。与此同时客户端收到服务器发送的 SYN ACK 后就已经能够确认服务器收到了我的 SYN并且同意建立连接。因此客户端可以进入ESTABLISHED也就是说客户端在收到第二次握手后就已经认为连接建立成功了。第三次握手服务器进入 ESTABLISHED客户端进入 ESTABLISHED 后会向服务器发送第三次握手的 ACK。服务器处于 SYN_RECV 状态收到这个 ACK 后就可以确认客户端已经成功收到我的 SYN ACK。至此双方关于连接建立所需要确认的信息已经完成。此时服务器可以进入ESTABLISHED最终双方都进入ESTABLISHEDESTABLISHED 表示TCP 连接正式建立双方可以开始进行数据传输。整个状态变化过程因此三次握手可以从状态变化的角度概括为客户端 CLOSED ↓ 发送 SYN SYN_SENT ↓ 收到 SYN ACK ESTABLISHED服务器 CLOSED ↓ listen() LISTEN ↓ 收到 SYN发送 SYN ACK SYN_RECV ↓ 收到 ACK ESTABLISHED最终三次握手完成 ↓ 客户端 ESTABLISHED ↕ 服务器 ESTABLISHED ↓ 正式传输数据三次握手的本质不仅仅是三个 TCP 报文的交互同时也是客户端和服务器 TCP 状态逐步同步的过程。2.6 listen()、connect()、accept() 系统调用前面我们从 TCP 协议角度分析了 TCP 三次握手的整个过程但是在实际的 Linux 网络编程中我们并不会手动编写代码去发送这三个报文那么服务器与客户端是如何完成 TCP 三次握手的呢对于 TCP 服务器代码的编写通常会执行socket(); bind(); listen(); accept();对于 TCP 客户端代码的编写通常会执行socket(); connect();这些系统调用和 TCP 三次握手到底是什么关系呢本小节将为你揭晓答案。1 listen() 让服务器进入监听状态服务器创建 socket 并完成 bind() 后还不能直接接收客户端的连接请求。服务器需要调用listen(sockfd, backlog)告诉操作系统这个 socket 用来监听客户端的 TCP 连接请求。调用 listen() 后服务器对应的 TCP socket 就会进入监听状态即 LISTEN。注listen() 本身并不是发送 TCP 报文更不是执行三次握手它只是让内核知道这个 socket 是一个监听 socket可以用来接收连接请求。2connect() 客户端发起连接当客户端调用connect(sockfd, ...);表示客户端请求与服务器建立 TCP 连接。注connect() 只是应用程序请求内核建立 TCP 连接的入口而 TCP 的三次握手则由操作系统内核中的 TCP 协议栈自动完成。这也就是为什么我们写 TCP 客户端代码时不需要我们手动进行 TCP 三次握手。(3) accpet() 获取已经建立的连接当服务器调用accept(listenfd, ...);表示应用层告诉操作系统把 listenfd 已经建立好的连接交给我。对于 accept()它会返回一个新的 socket这个 socket 负责和某一个已经建立连接的客户端进行数据通信而 listenfd 继续负责监听新的连接。注accpet() 只是应用层用来获取 TCP 连接它并不参与 TCP 三次握手的过程即使服务器不进行 accept()服务器和客户端的连接依旧正常建立。总结一句话listen() 负责让服务器进入监听状态, connect() 负责让客户端请求建立 TCP 连接而 accept() 负责让服务器应用程序获取已经建立好的 TCP 连接。三次握手由操作系统内核中的 TCP 协议栈自动完成应用程序并不需要手动参与三个 TCP 报文的发送。三、TCP 四次挥手3.1 为什么 TCP 需要四次挥手先回答一个核心问题TCP 为什么不能像 UDP 一样通信结束后直接关闭而是需要进行挥手因为 TCP 是面向连接的协议通信双方的操作系统会为 TCP 连接创建相应的数据结构对 TCP 连接进行管理。而 UDP 是无连接的操作系统只需要将数据报交给网络协议栈进行发送即可。因此当 TCP 通信结束时通信双方需要通过一定的机制通知对方关闭连接并释放操作系统中维护的相关资源这个过程就是TCP 的连接关闭过程也就是四次挥手。但是仅仅因为 TCP 是面向连接的还不能解释为什么需要四次挥手。这是因为 TCP 是一个全双工通信协议。一条 TCP 连接建立后客户端和服务器之间实际上存在两个独立的数据传输方向客户端 ─────────────→ 服务器 数据传输方向 1 客户端 ←───────────── 服务器 数据传输方向 2因此TCP 连接的关闭并不是简单地 “一方关闭整个连接立即关闭”而是需要分别关闭两个方向的数据传输。例如客户端已经没有数据需要发送了可以先关闭客户端 → 服务器但此时服务器可能还有数据需要发送给客户端服务器 → 客户端所以服务器不能因为客户端关闭了发送方向就立即关闭整个 TCP 连接。这就是 TCP 通常需要通过多次报文交互完成连接关闭的根本原因。TCP 是全双工的连接的两个通信方向需要分别关闭因此 TCP 的连接关闭过程通常需要四次挥手。整个过程可以详细表示为3.2 第一次挥手客户端和服务器在 TCP 协议中地位是相同的。本节我们假设客户端主动关闭连接。当客户端已经没有数据需要发送给服务器时客户端会向服务器发送一个 FIN 报文。FIN 报文FIN 标志位被置为 1 的 TCP 报文。FIN 标志位表示客户端已经没有数据需要发送请求关闭 客户端 - 服务器 这一方向的数据传输。客户端发送 FIN 报文后进入FIN_WAIT_1 状态FIN_WAIT_1表示客户端已经发送 FIN 报文正在等待服务器确认。注对于第一次挥手TCP 连接并没有立即关闭因为 TCP 是全双工的此时只是表示客户端 - 服务器 这个方向已经关闭但是 服务器 - 客户端 这个方向依旧可以传输数据。所以客户端此时仍然需要保持连接。3.3 第二次挥手服务器收到客户端发送的 FIN 报文后首先需要告诉客户端你发送的 FIN 报文我已经收到。因此服务器向客户端发送一个 ACK 报文客户端 服务器 FIN ───────────────────────→ ←────────────────────── ACK服务器收到 FIN 报文后进入CLOSE_WAIT 状态而客户端收到服务器返回的 ACK 后进入FIN_WAIT_2 状态CLOSE_WAIT 表示客户端 - 服务器 这个方向已经关闭即客户端已经没有数据向服务器发送但 服务器 - 客户端 这个方向没有关闭即服务器需要处理完客户端之前发送的数据。FIN_WAIT_2 表示正在等待服务器发送 FIN 报文。对于这种连接状态被称为半关闭状态。注对于第二次挥手TCP 连接并没有完全关闭而是处于半关闭状态。客户端发送 FIN 只代表客户端不再向服务器发送数据但服务器仍可能需要处理已经接收到的数据或者继续向客户端发送剩余数据因此服务器不会立即关闭 TCP 连接。3.4 第三次挥手当服务器的数据也全部处理和发送完毕并且服务器操作系统发现服务器处于 CLOSE_WAIT 状态。此时服务器向客户端发送 FIN 报文:客户端 服务器 FIN ───────────────────────→ ←────────────────────── ACK ←────────────────────── FIN服务器发送 FIN 报文后进入LAST_ACK 状态LAST _ACK 表示服务器已经发送 FIN 报文等待客户端对这个 FIN 报文进行最后确认。客户端收到服务器的 FIN 后说明服务器 - 客户端这个方向的数据传输也结束了。注对于第三次挥手TCP 连接依旧没有完全关闭。服务器发送 FIN 报文只代表服务器已经没有数据需要发送此时还需要等待客户端发送最后一个 ACK 报文确认客户端已经收到服务器发送的 FIN 报文。3.5 第四次挥手客户端收到服务器的 FIN 后向服务器发送 ACK客户端 服务器 FIN ───────────────────────→ ←────────────────────── ACK ←────────────────────── FIN ACK ───────────────────────→服务器收到这个 ACK 后进入CLOSED 状态注对于第四次挥手服务器收到客户端发送的 ACK 后进入 CLOSED状态此时服务器的 TCP 连接正式关闭操作系统可以释放为该 TCP 连接维护的相关内核数据结构。而客户端发送最后一个 ACK 报文后并不会立即进入 CLOSED 状态而是进入 TIME_WAIT 状态需要等待一段时间后才会进入 CLOSED 状态。此时客户端的 TCP 连接才正式关闭操作系统可以释放为该 TCP 连接维护的相关内核数据结构。3.6 为什么需要 TIME_WAIT为什么服务器可以直接关闭而客户端却需要等待这是因为客户端发送的最后一个 ACK 报文虽然已经发出但客户端无法确定这个 ACK 报文是否成功到达服务器。假设最后一个 ACK 报文在网络传输过程中丢失对于服务器来说由于在一定时间内没有收到客户端的 ACK就会重新发送 FIN 报文。如果客户端发送 ACK 后立即进入 CLOSED 状态那么当服务器重新发送 FIN 时客户端已经没有对应的 TCP 连接来处理这个 FIN也就无法再次向服务器发送 ACK。而客户端进入 TIME_WAIT 状态后会在一段时间内继续维护这条 TCP 连接。如果服务器重新发送 FIN客户端仍然能够接收到该 FIN并重新发送 ACK从而保证服务器最终能够正常关闭连接。如果客户端处于 TIME_WAIT 期间只要收到服务器重传的 FIN就会再次 ACK但是如果网络本身持续异常导致客户端发送的 ACK 始终无法到达服务器那么对于客户端来讲TIME_WAIT时间过后会直接关闭 TCP 连接而对于服务器来讲服务器不可能无限等待而是重传上限后TCP 认为当前连接已经无法关闭此时服务器会放弃重传并关闭连接。因此客户端不能在发送最后一个 ACK 后立即进入 CLOSED而需要通过 TIME_WAIT 保留一段时间的连接状态以提高最后一个 ACK 成功到达服务器的可靠性。除了保证最后一个 ACK 能够被服务器收到之外TIME_WAIT 还可以避免旧 TCP 连接中的报文影响后续的新连接。TCP 报文在网络中传输时并不一定能够在连接关闭后立即消失。例如旧连接 客户端 ─────────────→ 服务器 某个报文 ↓ 网络延迟如果 TCP 连接刚刚关闭客户端又立即使用完全相同的四元组建立一条新的 TCP 连接那么网络中残留的旧报文就有可能被新连接误认为是当前连接的数据。因此客户端进入 TIME_WAIT 后等待一段时间可以让旧连接中残留的报文在网络中自然消失从而降低对后续连接产生影响的可能性。所以, TIME_WAIT 主要解决两个问题保证最后一个 ACK 丢失后客户端仍然能够重新响应服务器的 FIN。让旧连接中残留的报文在网络中消失避免影响后续建立的相同连接。因此TIME_WAIT 并不是 TCP 多余的等待而是 TCP 为了保证连接能够可靠关闭并避免旧连接报文影响新连接而设计的状态。思考服务器关闭后为什么不能立即 bind 原来的端口号3.7 TIME_WAIT 的等待时间前面我们已经知道客户端发送最后一个 ACK 后并不会立即进入 CLOSED而是进入 TIME_WAIT 状态。那么问题来了TIME_WAIT 到底需要等待多长时间TCP 中通常使用2MSLMaximum Segment Lifetime最长报文段寿命作为 TIME_WAIT 的等待时间。这里的 MSL可以简单理解为一个 TCP 报文在网络中允许存在的最长时间。为什么是 2MSL而不是 1MSL这是因为客户端发送最后一个 ACK 后需要考虑一种情况客户端 服务器 ←──────────── FIN ACK ────────────────→ ↓ 网络延迟 ↓ 服务器收到如果 ACK 正常到达服务器那么服务器就可以关闭连接。但是客户端无法直接知道服务器到底有没有收到这个 ACK。因此客户端需要等待足够长的时间使得客户端发送的最后一个 ACK 能够到达服务器如果 ACK 丢失服务器重新发送的 FIN 也能够到达客户端客户端能够再次发送 ACK。最极端情况下可以理解为客户端 ───── ACK ─────→ 服务器 最长 MSL 客户端 ←──── FIN ────── 服务器 最长 MSL两段时间加起来就是MSL MSL 2MSL思考四次握手和三次挥手在理解 TCP 四次挥手之后可能会产生一个有意思的问题既然 TCP 关闭连接需要四次挥手那么为什么建立连接只需要三次握手而不是四次握手其实从信息交互的角度来看TCP 三次握手也可以理解成需要完成四个动作客户端我想建立连接 服务器我收到了你的请求 服务器我也想和你建立连接 客户端我收到了你的请求但是服务器在回复客户端的连接请求时可以将确认客户端 SYN 的 ACK和服务器自己的 SYN放在同一个 TCP 报文中SYN ↓ SYN ACK ↓ ACK因此原本需要完成的四个动作被压缩成了三个 TCP 报文。所以三次握手并不是少完成了一次确认而是第二次握手通过 SYN ACK 同时完成了两个动作。那么问题来了TCP 四次挥手是不是也可以通过这种方式减少一次变成三次挥手答案是可以在正常的四次挥手中客户端 → 服务器FIN 客户端 ← 服务器ACK 客户端 ← 服务器FIN 客户端 → 服务器ACK第二次挥手的 ACK 和第三次挥手的 FIN在时间上并不一定能够同时发送。因为服务器收到客户端的 FIN 后服务器可能还有数据没有发送完成。例如客户端我不发数据了 服务器收到但是我还有数据要发所以服务器需要先发送 ACK继续处理和发送自己的数据等数据发送完成后再发送 FIN。但是如果服务器收到客户端的 FIN 时服务器也已经没有数据需要发送。那么服务器就可以将确认客户端 FIN 的 ACK 服务器自己的 FIN合并到同一个 TCP 报文中客户端 服务器 FIN ───────────────────────→ ←────────────────────── FIN ACK ACK ───────────────────────→这样原本的四次挥手就变成了三次报文交互。所以可以总结为TCP 四次挥手是通常情况下的连接关闭过程但并不意味着网络中一定会出现四个独立的 TCP 报文。如果服务器收到 FIN 后恰好没有数据需要发送那么 ACK 和 FIN 可以合并从而形成“三次挥手”。四、TCP 的其他特性与应用4.1 面向字节流在 深入理解TCP协议一TCP报文格式详解 文章中我们知道 TCP 报头字段中没有直接描述 TCP 数据长度的字段。为什么 TCP 报头中不直接增加一个数据长度字段呢实际上TCP 并不是无法知道一个 TCP 报文中携带了多少数据。TCP 报文封装在 IP 数据报中IP 层能够提供整个 IP 数据报的长度而 TCP 报头中的 4 位首部长度字段可以确定 TCP 报头的长度。因此TCP 可以计算出当前 TCP 报文携带的数据长度。既然 TCP 能知道 TCP 报文携带的数据长度那为什么不能像 UDP 那样以数据报的形式交付给应用层而是以字节流的形式交付给应用层在深入理解 TCP 协议二TCP 可靠传输与高效通信机制 滑动窗口机制中我们知道 TCP 并不是将应用层一次交付的完整数据直接封装成一个 TCP 报文而是将发送缓冲区中的连续字节划分成多个 TCP 报文进行传输。因此即使 TCP 能够明确知道每一个 TCP 报文携带了多少数据也无法通过 TCP 报文的边界来确定应用层数据的边界。例如应用层向 TCP 交付一段完整数据HelloWorldTCP 可能根据当前网络状况将其划分为TCP 报文 1Hello TCP 报文 2World但对于接收方应用层而言它并不会知道 Hello 和 World 分别来自两个 TCP 报文也不会认为它们是两个独立的数据。TCP 会将这些数据重新组织成连续的字节流交付给应用层HelloWorld因此TCP 报文 的边界并不是应用层数据的边界。TCP 只保证字节流能够可靠、有序地传输而不会保留应用层数据之间的边界这也是 TCP 面向字节流的本质。4.2 粘包问题既然 TCP 不负责维护应用层数据的边界那么应用层连续发送多个数据接收方又应该如何区分这些数据呢这就是 TCP粘包问题。注TCP 粘包问题中的包是指应用层的数据包。避免粘包问题的核心思想明确两个数据包的边界。避免粘包问题的措施1制定定长的数据包如果报文数据不足以特殊字符填充2对于变长的数据包可以在数据包的起始位置约定一个数据包总长度的字段3对于变长的数据包可以在数据包之间添加特殊分隔符4.3 TCP 异常情况进程终止进程终止会自动关闭文件描述符操作系统对其套接字自动进行四次挥手。机器重启和进程终止情况一样。机器断电或者网线断开接收端认为连接存在一旦接收端有写入操作接收端发现连接对端无响应就会自动释放该 TCP 连接即使没有写入操作TCP 协议也内置了一个保活定时器会定期询问对方是否存在。4.4 基于 TCP 的应用层协议HTTPHTTPSSSHFTPSMTPTelnet当然还有你自己写的基于 TCP 的应用层协议对比维度TCPUDP连接方式面向连接无连接数据交付方式面向字节流面向数据报可靠性可靠传输尽最大努力交付不保证可靠数据边界不保留应用层消息边界保留数据报边界是否存在粘包/拆包存在需要应用层解决消息边界不存在 TCP 意义上的粘包/拆包连接管理三次握手、四次挥手无传输效率机制较多开销相对较大首部简单开销较小传输速度通常更适合稳定可靠的数据传输通常更适合低延迟、实时性要求高的场景适用场景HTTP/HTTPS、文件传输、数据库通信、SSH 等DNS、DHCP、实时音视频、在线游戏、直播等归根结底TCP 和 UDP 之间没有优缺点只是 TCP 和 UDP 的特点不同所以采用什么传输层协议是需要根据场景而定。4.5 用 UDP 实现可靠传输经典面试题参考 TCP 的可靠性机制回答例如引入序列号保证数据顺序引入确认应答机制确认数据是否到达引入超时重传机制......所以面试的时候可以总结成一句话UDP 本身是不可靠的如果希望基于 UDP 实现可靠传输可以在应用层引入序列号、确认应答、超时重传、去重、滑动窗口等机制从而解决 UDP 的丢包、乱序、重复以及传输效率等问题本质上就是在 UDP 之上实现一套类似 TCP 的可靠传输机制。不过这里有一个很重要的面试加分点既然 UDP 上面最终又实现了这么多 TCP 的机制那为什么不直接使用 TCP答案是UDP 自定义可靠传输并不一定是为了“替代 TCP”而是为了在需要可靠性的同时保留 UDP 的一些特性例如可以由应用层自己控制重传策略、报文格式以及部分传输行为。