TCP协议基础回顾:三次握手与四次挥手、TCP头部结构、端口号的作用与范围

说实话,很多搞网络的人,学了几年TCP,真到排查问题的时候,还是抓瞎。我见过不少同事,背八股文背得滚瓜烂熟,但一看到Wireshark里的SYN包,就不知道下一步该看什么了。

端口复用,说白了就是多个服务共享同一个端口。但你要想玩转它,TCP的基础必须扎实。这一节,咱们就把TCP的底裤翻出来看看。

TCP三次握手:不是简单的“你好你好你好”

三次握手,大家都听过。但你真的理解它为什么是三次,而不是两次或四次吗?

我习惯把三次握手拆成三个核心目标:

  • 确认双方都在线——网络是通的
  • 交换初始序列号——这是可靠传输的基石
  • 协商窗口、MSS等参数——为后续数据传输做准备

来看一个标准的握手流程:

客户端                         服务端
  |                              |
  |-------- SYN (seq=x) -------->|  第一步:客户端发起连接
  |                              |
  |<---- SYN+ACK (seq=y, ack=x+1)-|  第二步:服务端确认并回应
  |                              |
  |-------- ACK (seq=x+1, ack=y+1)->|  第三步:客户端确认
  |                              |
  |========== 连接建立 ==========>|

关键点:第三次握手是可以携带数据的。我在项目中就遇到过,有些优化方案会利用第三次握手提前发送HTTP请求,减少一次RTT。这叫TCP Fast Open,后面讲端口复用优化时会提到。

避坑指南:我曾经排查过一个线上问题,客户端一直报连接超时。抓包一看,服务端发了SYN+ACK,客户端却一直没回ACK。最后发现是客户端的半连接队列满了,内核直接丢弃了SYN+ACK。嗯,这就是典型的半连接队列溢出。

四次挥手:优雅告别,还是强行断开?

四次挥手比握手复杂一些。为什么?因为TCP是全双工的,每一方向都要单独关闭。

我见过很多开发者,写代码时只调了close(),以为就完事了。结果服务端TIME_WAIT堆积如山,端口全被占满。这就是不懂四次挥手的代价。

主动关闭方                    被动关闭方
  |                              |
  |-------- FIN (seq=u) -------->|  第一次:主动方说“我不发了”
  |                              |
  |<------- ACK (ack=u+1) -------|  第二次:被动方说“知道了”
  |                              |
  |<------- FIN (seq=v) --------|  第三次:被动方说“我也不发了”
  |                              |
  |-------- ACK (ack=v+1)------->|  第四次:主动方说“好的”
  |                              |
  |==== 等待2MSL后关闭 =========|

注意:第二次和第三次挥手之间,被动关闭方可能还有数据要发送。这就是半关闭状态。很多端口复用的问题,就出在这个阶段——服务端以为连接已经关了,但客户端还在等数据。

这里有个细节,我建议你记住:TIME_WAIT状态持续2MSL。MSL是报文最大生存时间,通常30秒到2分钟。为什么要有这个状态?两个原因:

  1. 保证最后一个ACK能到达对方(如果丢了,对方会重发FIN)
  2. 让网络中所有残留的报文都消失,避免影响新连接

你想想看,如果端口复用没处理好TIME_WAIT,新连接就可能收到旧连接的脏数据。这就是我为什么强调,做端口复用一定要理解TIME_WAIT。

TCP头部结构:20字节里藏着多少秘密?

TCP头部固定20字节,但就是这20字节,决定了整个传输的可靠性。我每次抓包,第一眼就看头部,因为问题往往就藏在里面。

字段 长度 作用
源端口 16位 标识发送进程
目的端口 16位 标识接收进程
序列号 32位 保证数据有序到达
确认号 32位 告诉对方下一个期望收到的字节
数据偏移 4位 头部长度,单位是4字节
标志位 9位 SYN、ACK、FIN、RST等控制位
窗口大小 16位 流量控制,告诉对方自己能收多少
校验和 16位 检测头部和数据是否损坏
紧急指针 16位 URG标志位有效时才有意义

端口复用的关键字段:源端口、目的端口、序列号、确认号。这四个字段组合起来,唯一标识一个TCP连接。端口复用,本质上就是让多个连接共享同一个目的端口,但通过源IP+源端口来区分。

举个例子:你访问百度,你的源端口是随机分配的(比如54321),目的端口是80。百度返回数据时,源端口是80,目的端口是54321。内核就是靠这个四元组(源IP、源端口、目的IP、目的端口)来区分不同连接的。

端口号的作用与范围:从0到65535

端口号,说白了就是进程的“门牌号”。一个IP地址可以跑很多服务,怎么区分?靠端口号。

端口号范围分三段:

  • 0-1023:知名端口,需要root权限才能绑定。HTTP(80)、HTTPS(443)、SSH(22)都在这里。
  • 1024-49151:注册端口,一般给应用程序用。比如MySQL(3306)、Redis(6379)。
  • 49152-65535:动态/私有端口,客户端临时使用。你每次访问网站,系统都会从这个范围里挑一个给你。

经验之谈:我曾经遇到一个场景,服务器上跑了100多个Web服务,每个都要绑定80端口。怎么办?端口复用!通过不同的域名或路径来区分,但底层都监听在同一个80端口上。这就是后面要讲的SO_REUSEADDR和SO_REUSEPORT。

你可能会问:端口号只有65535个,够用吗?

够,也不够。对于服务端来说,一个端口可以接受成千上万个连接,因为每个连接由四元组唯一标识。但对于客户端来说,如果并发请求太多,源端口可能不够用。我记得有一次压测,客户端机器上的临时端口全被占满了,新连接直接报错。解决办法?要么扩大临时端口范围,要么用端口复用。

核心逻辑图:TCP连接的生命周期

下面这张图,把三次握手、数据传输、四次挥手串在一起。我建议你多看几遍,因为端口复用的所有问题,都发生在这个生命周期里。

TCP连接生命周期 TCP连接生命周期 CLOSED SYN_SENT ESTABLISHED FIN_WAIT_1 FIN_WAIT_2 TIME_WAIT CLOSED LISTEN SYN_RCVD ESTABLISHED CLOSE_WAIT LAST_ACK CLOSED SYN SYN+ACK ACK FIN ACK FIN ACK 端口复用关注点 TIME_WAIT / 四元组唯一性

这张图里,我特别标出了TIME_WAIT状态。为什么?因为端口复用最常踩的坑就在这里。你想想看,如果一个端口复用的服务,频繁创建和关闭连接,TIME_WAIT会堆积。堆积到一定程度,新连接就建立不了。

实战警告:我曾经在线上遇到过一个问题:Nginx反向代理后端服务,后端服务频繁重启。每次重启,Nginx和后端之间都会产生大量TIME_WAIT。结果Nginx的源端口全被占满,新请求全部超时。最后怎么解决的?调整了net.ipv4.tcp_tw_reuse参数,让TIME_WAIT状态的端口能被复用。嗯,这就是端口复用的一个典型应用场景。

好了,TCP的基础就回顾到这里。这些内容虽然基础,但后面讲端口复用机制时,你会反复用到。特别是三次握手的序列号交换、四次挥手的TIME_WAIT、以及端口号的范围划分——这些都是端口复用的底层支撑。