服务器侧是「定死的」,客户端侧是「临时挑的」:一个要让人找得到,一个只要唯一就行。 临时端口会被「连号分配」,所以「下一个端口是多少」是可预测的。
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 秒实验
- 算一下:动态端口有 16384 个,连续建 20000 条短连接会怎样?
- 想一下:为什么服务端程序几乎都要开 SO_REUSEADDR?
- 想一下:为什么「源端口连号」会是安全问题?
常见误区
- 以为「客户端端口也是固定的」。 是临时分配的,每次连接都可能不同。
- 以为「端口用完就永久少了」。 会回收,只是 TIME_WAIT 期间不能复用。
- 以为「端口池每个进程一份」。 全局共享,一个进程能吃光。
自测三问(点开对答案)
1. 服务器的端口是怎么定的? —— **用知名端口或登记过的端口**,因为客户端必须事先知道。 2. 客户端端口谁分配? —— **内核从动态端口池里分配**,应用调 connect 时自动就有了。 3. SO_REUSEADDR 解决什么问题? —— **允许绑定一个还在 TIME_WAIT 的端口**,避免重启时服务起不来。下一节看「端口号和协议是怎么绑在一起的」。
- ☐ (本节完,下一节 6.2.5 端口号与协议)