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分钟。为什么要有这个状态?两个原因:
- 保证最后一个ACK能到达对方(如果丢了,对方会重发FIN)
- 让网络中所有残留的报文都消失,避免影响新连接
你想想看,如果端口复用没处理好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连接的生命周期
下面这张图,把三次握手、数据传输、四次挥手串在一起。我建议你多看几遍,因为端口复用的所有问题,都发生在这个生命周期里。
这张图里,我特别标出了TIME_WAIT状态。为什么?因为端口复用最常踩的坑就在这里。你想想看,如果一个端口复用的服务,频繁创建和关闭连接,TIME_WAIT会堆积。堆积到一定程度,新连接就建立不了。
实战警告:我曾经在线上遇到过一个问题:Nginx反向代理后端服务,后端服务频繁重启。每次重启,Nginx和后端之间都会产生大量TIME_WAIT。结果Nginx的源端口全被占满,新请求全部超时。最后怎么解决的?调整了net.ipv4.tcp_tw_reuse参数,让TIME_WAIT状态的端口能被复用。嗯,这就是端口复用的一个典型应用场景。
好了,TCP的基础就回顾到这里。这些内容虽然基础,但后面讲端口复用机制时,你会反复用到。特别是三次握手的序列号交换、四次挥手的TIME_WAIT、以及端口号的范围划分——这些都是端口复用的底层支撑。