Skip to content

4.1 TCP 三次握手与四次挥手面试题


TCP 基本认识

TCP 头格式有哪些?

序列号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。

确认应答号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。

控制位:

  • ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1
  • RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。
  • SYN:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。
  • FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。

为什么需要 TCP 协议?TCP 工作在哪一层?

IP 层是「不可靠」的,它不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中的数据的完整性。

如果需要保障网络数据包的可靠性,那么就需要由上层(传输层)的 TCP 协议来负责。

因为 TCP 是一个工作在传输层可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的。

什么是 TCP?

一种传输层通信协议。

  • 面向连接:一对一连接,不能像 UDP 协议可以一个主机同时向多个主机发送消息;
  • 可靠的:可以保证一个报文一定能够到达接收端,并且无差错、不重复、有序;
  • 字节流:消息被编码成字节报文帧,一个用户消息会被拆分成多个报文或者多个消息一个报文,流式传输
    • 可能出现粘包:多个消息的某个部分在同一个TCP报文中,消息间无明显边界
    • 可以通过以下方式避免
      • 1、特殊字符作为边界:如回车或换行,会导致消息中无法使用这类特殊字符
      • 2、自定义消息结构,如json结构
  • TCP 会通过一下信息将客户端和服务端达成共识:
    • Socket:由 IP 地址和端口号组成
    • 序列号:用来解决乱序问题等,最大 4G,超过从 0 开始
    • 窗口大小:用来做流量控制
  • 通过以下四元组确定一个 TCP 连接
    • 源地址、目的地址(IP头部中,确定主机)
    • 源端口、目的端口(TCP头部中,确定进程)
    • 最大连接数就是 IP 数 ✖️ 端口数,但是每个连接都要占用内存,且系统有最大文件打开数量,所以很难达到理论值
  • 适用于文件传输、HTTP等

UDP

  • 无连接:支持多对多通信
  • 不可靠传输:尽最大可能交付数据,无序,可能丢包
  • 面向报文:每个 UDP 报文就是一个用户完整消息,由IP层进行分片,TCP在传输层分片
  • 只需要确定目标端口和源端口(确定对方的进程即可)
  • 适用于DNS、视频、广播等

TCP 和 UDP 允许使用同一个端口,因为同一个进程可以同时有可靠和不可靠传输,直播软件等直播是UDP、用户信息、聊天时TCP这种,在内核中时完全独立的,其实是在内核中两个进程

TCP 连接建立

建立过程

  • 1、服务端主动监听某个端口,处于 LISTEN 状态
  • 2、客户端:发送给服务端请求连接,之后客户端处于 SYN-SENT 状态。
    • TCP 序列号:随机初始化序号(client_isn
    • SYN标志位: 1,表示 SYN 报文。`
    • 不含应用层数据
      • 不确定性很高,浪费带宽,且如果这个时候需要服务器处理,被 SYN 攻击会更加简单
    • 如果报文丢失,会触发超时重传,重传 SYN 报文的序列号保持一致,一般重传五次,每次等待时间翻倍(1s 开始),一直收不到断开本次连接
  • 3、服务端收到 SYN 报文后,发给客户端响应报文,之后服务端处于 SYN-RCVD 状态
    • TCP 序列号:随机初始化序号(server_isn
    • SYN标志位和ACK标志位: 1,表示 ACK 报文(这里相当于响应和请求合并了)
    • 确认应答号: client_isn + 1
    • 不含应用层数据
    • 如果报文丢失,客户端没有收到 ACK 报文,会重传,客户端没有收到 ACK 报文,也会重传
  • 4、客户端收到服务端报文后,发送给服务端响应报文,之后客户端处于 ESTABLISHED 状态
    • ACK标志位: 1,表示 ACK 报文
    • 确认应答号: server_isn + 1
    • 可以携带客户到服务端的数据,因为大概率能成功
    • 如果报文丢失,服务器会一直收不到 ACK 报文,会重传 SKY-ACK 报文
      • ACK 报文是不会有重传的,只能等对方重传
  • 5、服务端收到客户端的应答报文后,也进入 ESTABLISHED 状态,可以正常响应数据
  • 之后相互发送数据

序列号

  • 作用:
    • 1、接收方可以去除重复的数据
    • 2、接收方可以根据数据包的序列号按序接收;
    • 3、可以标识发送出去的数据包中,哪些是已经被对方收到的(通过 ACK 报文中的序列号知道);
  • 为了防止上一次连接的数据包被下一次连接接受(序列号在下一次的有效范围内),造成混乱,所以随机初始化序列号

为什么三次握手:

  • 1、确保双方具备接受和发送的能力
  • 2、组织重复历史连接的初始化
    • 如果客户端发送 SYN 报文后宕机,重启后重新发送 SYN 报文
    • 两次报文都到达服务器,服务器回复两个 SYN 报文,但是序列号不一样
    • 客户端对于旧的报文的回复会因为匹配不上自己预期的序列号,回复 RST,服务器释放旧的连接,避免了重复历史连接的初始化
    • 如果只有两次握手,服务器回复 SYN 报文就已经 established 了,可能会直接发送数据,浪费资源,如果一次握手,服务器可能都收不到 RST 报文
  • 3、同步双方的初始序列化
    • 双方初始化序列号后都需要得到对方的应答,保证双方可靠传输
  • 4、避免资源浪费
    • 如果少于三次握手:如果服务器没有收到 AKC 报文,会重复发送,服务器会在 ACK 发送之后就建立连接,造成资源浪费
  • 三次握手已经建立可靠连接,多余三次握手就没必要了

如何在 Linux 系统中查看 TCP 状态?

TCP 的连接状态查看,在 Linux 可以通过 netstat -napt 命令查看。

TCP 连接状态查看

既然 IP 层会分片,为什么 TCP 层还需要 MSS 呢?

我们先来认识下 MTU 和 MSS

MTU 与 MSS

  • MTU:一个网络包的最大长度,以太网中一般为 1500 字节;
  • MSS:除去 IP 和 TCP 头部之后,一个网络包所能容纳的 TCP 数据的最大长度;

如果在 TCP 的整个报文(头部 + 数据)交给 IP 层进行分片,会有什么异常呢?

当 IP 层有一个超过 MTU 大小的数据(TCP 头部 + TCP 数据)要发送,那么 IP 层就要进行分片,把数据分片成若干片,保证每一个分片都小于 MTU。把一份 IP 数据报进行分片以后,由目标主机的 IP 层来进行重新组装后,再交给上一层 TCP 传输层。

这看起来井然有序,但这存在隐患的,那么当如果一个 IP 分片丢失,整个 IP 报文的所有分片都得重传

因为 IP 层本身没有超时重传机制,它由传输层的 TCP 来负责超时和重传。

当某一个 IP 分片丢失后,接收方的 IP 层就无法组装成一个完整的 TCP 报文(头部 + 数据),也就无法将数据报文送到 TCP 层,所以接收方不会响应 ACK 给发送方,因为发送方迟迟收不到 ACK 确认报文,所以会触发超时重传,就会重发「整个 TCP 报文(头部 + 数据)」。

因此,可以得知由 IP 层进行分片传输,是非常没有效率的。

所以,为了达到最佳的传输效能 TCP 协议在建立连接的时候通常要协商双方的 MSS 值,当 TCP 层发现数据超过 MSS 时,则就先会进行分片,当然由它形成的 IP 包的长度也就不会大于 MTU,自然也就不用 IP 分片了。

握手阶段协商 MSS

经过 TCP 层分片后,如果一个 TCP 分片丢失后,进行重发时也是以 MSS 为单位,而不用重传所有的分片,大大增加了重传的效率。

TCP 连接断开

TCP 四次挥手过程是怎样的?

双方都可以主动断开连接,如果客户端主动断开:

  • 1、客户端发送一个 FIN 报文,之后客户端进入 FIN_WAIT_1 状态。
    • FIN 标志位: 1
    • 如果报文丢失,会触发重传
  • 2、服务端收到后,就向客户端发送 ACK 应答报文,接着服务端进入 CLOSE_WAIT 状态。
    • 如果报文丢失,客户端没收到 ACK 报文,就会触发重传
    • 服务端还可以单向发送数据,客户端可以接收数据不能发送
  • 3、客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态
    • 服务端发送完数据后,向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态。
      • 这个阶段不会持续太久,一般超过 60s 没收到服务端的 FIN 报文,客户端直接关闭
      • 如果报文丢失,服务端会触发超时重传,一直没收到,服务端主动关闭(客户端保证了自己一段时间后自动关闭)
      • 如果服务端没有数据发送,那么 FIN 报文和 ACK 报文可以合并发送,成为三次挥手,或者将数据一次整合,和 FIN-ACK 报文一起发送,也是三次挥手
  • 4、客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态
    • 如果报文丢失,服务端会触发 FIN 包重传
  • 服务端收到了 ACK 应答报文后,就进入了 CLOSE 状态,至此服务端已经完成连接的关闭。
  • 客户端在经过 2MSL 一段时间后,自动进入 CLOSE 状态,至此客户端也完成连接的关闭。
  • FIN 报文是需要进程主动发起的,ACK 报文是内核自动响应的
  • 挥手四次是因为被动断开方可能还需要发送数据

但是在特定情况下,四次挥手是可以变成三次挥手的,具体情况可以看这篇:TCP 四次挥手,可以变成三次吗?

time_wait

  • 主动关闭方,才会有该状态
  • 为什么等待时间是 2MSL(Maximum Segment Lifetime,报文最大生存时间
    • 至少允许 ACK 报文丢失一次,当 ACK 丢失,服务端会重传 FIN 报文
    • 而如果网络不好,多次丢失,那么也没必要等待了,并且长时间 time_wait 会一直占用资源
    • 每次收到服务端的 FIN 报文,这个时间会重置
  • 为什么需要 time_wait 状态
    • 防止历史连接中的数据,被后面相同四元组的连接错误接受
      • 本次连接中数据,会在这段时间里面被丢弃或者被接收,不会保留到下一次连接中(避免有数据包在下一次连接的接受窗口内造成数据混乱)
    • 保证被动关闭连接以防,能被正确的关闭
      • 如果客户端的 ACK 报文丢失,服务端会重传 FIN 报文,如果没有 time_wait,客户端直接关闭连接,收到重传的 FIN 报文,会回 RST 报文,服务端收到后认为是一个错误,结束的不够优雅
  • time_wait 过多会占用系统资源,造成一些不可用的情况
  • 优化:由客户端持有 time_wait
    • 使用长连接,如果使用短连接,由于需要服务端主动关闭连接,服务端会出现大量的 TIME_WAIT
    • 客户端长时间不发送请求,主动断开连接,否则nginx 会在超时后调用服务端主动断开连接,导致服务端持有大理胖 time_wait,客户端需要发送时,重新建立连接

如果已经建立了连接,出现故障了怎么办?

  • 客户端:如宕机、断电
    • 服务端无法感知,连接会一直处于 ESTABLISH 状态
    • 服务端会触发保活机制,发送探测报文,如果多次没有收到响应,认为当前 TCP 连接 死亡,断开连接,如果得到响应,重置保活时间
      • 如果客户端没有启动,那么服务端收不到任何响应
      • 如果客户端重启了,那么因为本地找不到对应的 TCP 连接,会发送 RST 报文,服务端直接关闭
      • 由于时间比较长,可以在客户端增加探活
    • 如果是进程崩溃,操作系统在挥手资源时会发送 FIN 报文,主动断开连接
  • 服务端:

在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔,以下都为默认值:

shell
net.ipv4.tcp_keepalive_time=7200
net.ipv4.tcp_keepalive_intvl=75  
net.ipv4.tcp_keepalive_probes=9
  • tcp_keepalive_time=7200:表示保活时间是 7200 秒(2 小时),也就 2 小时内如果没有任何连接相关的活动,则会启动保活机制
  • tcp_keepalive_intvl=75:表示每次检测间隔 75 秒;
  • tcp_keepalive_probes=9:表示检测 9 次无响应,认为对方是不可达的,从而中断本次的连接。

客户端调用 close 了,连接是断开的流程是什么?

我们看看客户端主动调用了 close,会发生什么?

  • 客户端调用 close,表明客户端没有数据需要发送了,则此时会向服务端发送 FIN 报文,进入 FIN_WAIT_1 状态;
  • 服务端接收到了 FIN 报文,TCP 协议栈会为 FIN 包插入一个文件结束符 EOF 到接收缓冲区中,应用程序可以通过 read 调用来感知这个 FIN 包。这个 EOF 会被放在已排队等候的其他已接收的数据之后,这就意味着服务端需要处理这种异常情况,因为 EOF 表示在该连接上再无额外数据到达。此时,服务端进入 CLOSE_WAIT 状态;
  • 接着,当处理完数据后,自然就会读到 EOF,于是也调用 close 关闭它的套接字,这会使得服务端发出一个 FIN 包,之后处于 LAST_ACK 状态;
  • 客户端接收到服务端的 FIN 包,并发送 ACK 确认包给服务端,此时客户端将进入 TIME_WAIT 状态;
  • 服务端收到 ACK 确认包后,就进入了最后的 CLOSE 状态;
  • 客户端经过 2MSL 时间之后,也进入 CLOSE 状态;

没有 accept,能建立 TCP 连接吗?

答案:可以的

accpet 系统调用并不参与 TCP 三次握手过程,它只是负责从 TCP 全连接队列取出一个已经建立连接的 socket,用户层通过 accpet 系统调用拿到了已经建立连接的 socket,就可以对该 socket 进行读写操作了。

更想了解这个问题,可以参考这篇文章:没有 accept,能建立 TCP 连接吗?

没有 listen,能建立 TCP 连接吗?

答案:可以的

客户端是可以自己连自己的形成连接(TCP 自连接),也可以两个客户端同时向对方发出请求建立连接(TCP 同时打开),这两个情况都有个共同点,就是没有服务端参与,也就是没有 listen,就能 TCP 建立连接。

更想了解这个问题,可以参考这篇文章:服务端没有 listen,客户端发起连接建立,会发生什么?

详细介绍一下 TCP 的四次挥手机制,为什么要有 TIME_WAIT 状态,为什么需要四次握手?服务器出现了大量 CLOSE_WAIT 状态如何解决?

大量 CLOSE_WAIT 表示程序出现了问题,对方的 socket 已经关闭连接,而我方忙于读或写没有及时关闭连接,需要检查代码,特别是释放资源的代码,或者是处理请求的线程配置。

正在精进