Skip to content

TCP/IP 与网络传输基础 ​


❓ 面试官:请详细描述一下 TCP 的三次握手过程?为什么不能是两次? ​

频率:🔥🔥🔥🔥🔥

💡 一句话总结(先抛结论): 三次握手是为了确认通信双方的发送和接收能力都正常,并同步双方的初始序列号。如果是两次握手,服务端无法确认客户端是否能正常接收消息,容易产生“已失效的连接请求”带来的资源浪费。

📝 详细展开(面试高分剧本):

  1. 第一次握手(Client -> Server):
    • 客户端发送 SYN 报文(SYN=1,seq=x),进入 SYN_SENT 状态。
    • 潜台词:“服务器你好,我要建连接,我的初始序列号是 x。”
  2. 第二次握手(Server -> Client):
    • 服务端收到 SYN,发送 SYN+ACK 报文(SYN=1, ACK=1, seq=y, ack=x+1),进入 SYN_RCVD 状态。
    • 潜台词:“收到你的请求(确认了你的发送能力),同意建立连接,我的序列号是 y。”
  3. 第三次握手(Client -> Server):
    • 客户端收到 SYN+ACK,发送 ACK 报文(ACK=1, ack=y+1),进入 ESTABLISHED 状态。服务端收到后也进入 ESTABLISHED 状态。
    • 潜台词:“收到你的确认(确认了服务端的发送和接收能力),我们开始聊天吧!”

🌟 追问:为什么不能是两次握手?

  • 历史乱序请求问题:假设客户端发了第一个 SYN,在网络中滞留了。客户端超时后发了第二个 SYN,完成了两次握手并通信结束断开连接。此时第一个滞留的 SYN 突然到达服务端,服务端以为是新请求,只要发了 ACK 就算建立连接,然后傻傻等待客户端发数据,导致服务端资源浪费。
  • 用三次握手的话,当服务端回复 ACK 时,客户端一核对发现这不是我现在的请求,就会发 RST 中断连接。

❓ 面试官:那四次挥手呢?为什么挥手要四次而握手只要三次? ​

频率:🔥🔥🔥🔥

💡 一句话总结(先抛结论): 因为 TCP 是全双工通信(双方可以同时收发)。握手时,服务端的“同意连接(ACK)”和“我也要连接(SYN)”可以合并在一个包里发。但断开时,由于可能还有未发完的数据,只能先回复“知道了(ACK)”,等数据发完再单独发“我也要断开(FIN)”,所以分成了两步。

📝 详细展开:

  1. 第一次挥手:客户端发送 FIN 报文,表示“我没有数据要发了”,进入 FIN_WAIT_1。
  2. 第二次挥手:服务端收到 FIN,回复 ACK(我收到了你的断开请求),进入 CLOSE_WAIT。此时客户端进入 FIN_WAIT_2。
    • (此时是半关闭状态,客户端不发了,但如果服务端还有数据,还可以继续发给客户端)
  3. 第三次挥手:服务端数据发完了,发送 FIN 报文给客户端,表示“我也发完了,断开吧”,进入 LAST_ACK。
  4. 第四次挥手:客户端收到 FIN,回复 ACK,进入 TIME_WAIT 状态。服务端收到 ACK 后立刻关闭。客户端等待 2MSL(报文最大生存时间的 2 倍)后,也真正关闭。

❓ 面试官:为什么客户端最后还要等待 2MSL 的时间才关闭?(TIME_WAIT 状态的作用) ​

频率:🔥🔥🔥🔥

💡 一句话总结(先抛结论):

  1. 保证最后一个 ACK 能够成功到达服务端。
  2. 让本次连接中产生的所有网络游荡报文都在网络中自然消亡,避免干扰下一个新连接。

📝 详细展开(实战加分项):

  • 保证可靠断开:如果客户端发完最后一次 ACK 就立刻关闭,万一这个 ACK 丢了,服务端会重发 FIN。如果客户端已经关闭,就会回送一个 RST(重置),导致服务端报错异常关闭。等待 2MSL 就是为了如果丢包,还能再接收一次服务端的重发 FIN 并补发 ACK。
  • 防止报文混淆:等待 2 个最大报文生存时间(去向 1 个,回向 1 个),可以确保这个旧连接的所有数据包都在网络中死透了,下次复用同样的 IP 和端口建立新连接时,不会收到上一次的“幽灵数据包”。

🔥 生产故障场景(加分项): "在真实生产环境中,如果短时间有大量的高并发短连接(比如压测或者遭遇了 HTTP 攻击),服务器端会出现大量的 TIME_WAIT 状态端口,导致本地端口(总共只有 6 万多个)被耗尽,无法建立新连接。解决方案是修改 Linux 内核参数 sysctl.conf,开启 tcp_tw_reuse(允许重用 TIME_WAIT 端口)和 tcp_tw_recycle(快速回收,但在 NAT 环境下可能丢包,新版 Linux 已废弃)。"


❓ 面试官:什么是 TCP 粘包和拆包?如何解决? ​

频率:🔥🔥🔥

💡 一句话总结(先抛结论): TCP 是基于字节流的,它不认识业务上的“一条消息”边界。它只会根据缓冲区大小和网络状况,把一坨字节随意切分(拆包)或者拼在一起发(粘包)。这就导致接收端可能收到“半条消息”或者“连在一起的两条消息”。

🛠️ 常见解决方案:

  1. 固定长度:每次发送的包都固定 100 字节,不够就补空格。接收端每次精准读 100 字节。(缺点:浪费带宽)
  2. 特定分隔符:在每条消息末尾加一个特定的字符,比如 \r\n 或者 $_$。接收端一直读,直到读到这个特殊字符就算一条完整消息。(FTP 协议常用)
  3. 消息头加长度(最主流、最优雅):把消息分为 Header 和 Body。Header 里固定几个字节用来存放 Body 的总长度。接收端先读 Header 获取长度 N,然后再去流里精准读取 N 个字节即可。(Dubbo、Kafka 等绝大部分中间件协议的底层设计都是这个)。