📚 第六章 TCP与UDP
📄 一句话概括

服务器侧是「定死的」,客户端侧是「临时挑的」:一个要让人找得到,一个只要唯一就行。 临时端口会被「连号分配」,所以「下一个端口是多少」是可预测的。

📝 本节和前面几节的关系

6.2.1 讲了三段端口,这一节讲「具体怎么落到一个数字上」。 6.2.5 会讲「同一端口怎么被两个协议共用」,这一节先讲分配。

一、服务器侧:定死一个

服务器的端口必须「事先知道」:因为客户端连过来的时候,端口是它自己要填的。 所以服务器只有两个选择:用知名端口,或者用登记过的端口。 「定死」的代价是「改不了」:一旦用户习惯了你在这个端口,换端口就等于「服务下线」。 所以「80 改到 8080」这种事,往往要在文档里写很久,还得靠重定向过渡。 另一个现实约束是权限:1024 以下在多数系统上要管理员权限, 所以开发环境常用 8080、3000、5000 这些高位端口,到了生产再映射回 80 或 443。

二、客户端侧:临时挑一个

客户端不需要「被人找到」,只需要「唯一」:所以它从动态端口池里随便拿一个没被占的。 这个动作是内核做的,应用程序不用管——你调用 connect(),源端口就自动有了。 「随便拿」其实是「按顺序找下一个空闲的」:所以短时间内连续建连,源端口会是连号的。 这一点在安全上有个副作用:攻击者能预测你下一个端口,从而「盲猜」注入报文。 所以现代系统会加随机偏移,只是整体还是递增的。 临时端口会「回收再用」:连接关闭之后,那个端口要等一段时间才能再分配, TCP 的这个等待就是 TIME_WAIT(6.4.4 讲过)。 所以「短连接太多」会让临时端口池变紧,而不是「用完了就永远没了」。

三、撞车了怎么办

两种撞车,处理方式完全不同:

场景 结果 办法
两个程序抢同一个端口 后起的报「地址已被占用」 换端口或先停前一个
重启时端口还在 TIME_WAIT 也能起来,但连接会被拒 开 SO_REUSEADDR

第一行是「真冲突」:内核里同一个「协议加端口」只能有一个监听者。 所以「服务起不来」的时候,先看是不是有一个旧进程还占着。 第二行是「假冲突」:旧连接的 TIME_WAIT 状态还占着那个「四元组」, 但监听本身是允许复用的。 SO_REUSEADDR 就是让内核「允许绑一个还在 TIME_WAIT 的端口」,这是服务端程序的标配选项。

四、动手 Demo:临时端口是怎么挑的

连续 bind 六个「端口 0」,看系统分给什么。 端口 0 的意思是「随便给我一个」。

import socket

socks = []
for _ in range(6):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(('127.0.0.1', 0))
    socks.append(s)
print([s.getsockname()[1] for s in socks])

实跑输出:

--------------------------------------------------------------------
临时端口是怎么挑的
--------------------------------------------------------------------
**系统维护一个「临时端口池」,每次连接从里面取一个**,**用完过一会儿再还回去。**

  第几个     分到的端口
  ----------------------
  1          57381
  2          57382
  3          57383
  4          57384
  5          57385
  6          57386

**六次分配是连号的**:**因为系统按顺序找「下一个空闲的」**,**而不是随机挑。**
**这一点在安全上有个副作用**:**攻击者能猜到你下一个临时端口。**
**所以现代系统会加随机偏移**,只是顺序性还留着。

**这也解释了「端口耗尽」长什么样**:**池子用完的时候,新连接会直接报「无法分配本地地址」**,
**而已经建立的连接照样能用。**
**症状和 5.6.4 讲的 NAT 表满了一模一样。**

六个端口是连号的,说明分配是「顺序找下一个空闲」而不是随机。 这一点和「安全上要随机化」是冲突的,所以真实系统的策略是「整体递增加随机起点」。 注意这里用的是 bind 而不是 connect:两者的源端口来源是同一个池子, 所以「监听一个临时端口」和「连出去一个临时端口」会互相消耗。 这也是「客户端和服务端在同一台机器上跑」时容易端口紧张的原因。 最后一个观察:端口池是「全局共享」的,不是「每个进程一份」。 所以一个进程把端口吃光,别的进程也开不了新连接——这正是「端口耗尽」会影响到全机的原因。

30 秒实验

  1. 算一下:动态端口有 16384 个,连续建 20000 条短连接会怎样?
  2. 想一下:为什么服务端程序几乎都要开 SO_REUSEADDR?
  3. 想一下:为什么「源端口连号」会是安全问题?

常见误区

  1. 以为「客户端端口也是固定的」。 是临时分配的,每次连接都可能不同。
  2. 以为「端口用完就永久少了」。 会回收,只是 TIME_WAIT 期间不能复用。
  3. 以为「端口池每个进程一份」。 全局共享,一个进程能吃光。
自测三问(点开对答案) 1. 服务器的端口是怎么定的? —— **用知名端口或登记过的端口**,因为客户端必须事先知道。 2. 客户端端口谁分配? —— **内核从动态端口池里分配**,应用调 connect 时自动就有了。 3. SO_REUSEADDR 解决什么问题? —— **允许绑定一个还在 TIME_WAIT 的端口**,避免重启时服务起不来。

下一节看「端口号和协议是怎么绑在一起的」。

相关笔记