TCP/IP 能扩散,靠的不是标准,是「BSD 免费送了一份能跑的代码」。 一套 socket 接口通吃所有协议——这是它被大量应用接受的技术原因。
2.1.3 讲的是协议的设计,这一节讲它怎么传遍世界。 这一节要回答一个常见疑问:为什么标准一样,实现却那么多。
一、代码比标准跑得快
三件事叠在一起,才有了扩散:
| 事件 | 效果 | 为什么关键 |
|---|---|---|
| BSD 带 TCP/IP | 免费随源码分发 | 谁都能拿到能跑的代码 |
| 厂商跟进 | 各系统内置 | 不用自己实现 |
| 学术网互通 | 高校之间连起来 | 需求自己长出来 |
第一行是本节的核心:伯克利把 TCP/IP 的实现放进了 BSD 发行版,随源码一起免费分发。 结果是想用的人不用「照着 RFC 自己写」,直接编译就行。 标准再好,没有能跑的代码,扩散速度就慢一个量级。 第三行说明「需求不是被创造出来的,是被连接创造出来的」: 高校之间一通,邮件、文件传输、远程登录的需求立刻就有了。 而这些需求又会推动更多学校接入——这就是网络效应的正反馈。
二、一套接口管所有
BSD socket 的设计选择,决定了后面几十年的编程方式:
| 设计 | 含义 | 后果 |
|---|---|---|
| 一切皆文件 | 套接字当文件描述符 | read/write 也能用 |
| 地址族参数 | socket(族, 类型, 协议) |
加新协议不改接口 |
| 阻塞式调用 | 默认等到有数据 | 简单但难做高并发 |
第二行是它长寿的根本原因:socket() 第一个参数是「地址族」,第二个是「类型」,
所以加 IPv6、加蓝牙、加 Unix 域套接字,全都是加一个新常量,函数签名一个字没改。
本机的 socket 模块里 AF_ 开头的常量有 11 个,每一个就是一个地址族。
第三行是它的历史包袱:阻塞式接口写起来简单,但一个线程只能等一个连接,
所以后来才有了 select/epoll/io_uring 这一整套「多路复用」的演进。
这不是设计失误,是当年的使用场景决定的——1970 年代没人想到一台机器要同时处理十万个连接。
三、多家实现的好处
标准被多家实现,会带来三个后果:
| 后果 | 好的方面 | 坏的方面 |
|---|---|---|
| 互操作性被验证 | 标准里的歧义会被逼出来 | 要先吵架 |
| 单点退出无影响 | 一家不做还有别家 | 行为有细微差异 |
| 性能竞赛 | 实现越来越好 | 「标准 compliant」不再是卖点 |
第一行是最有价值的: 只有一份实现时,标准的歧义永远发现不了——因为它自己跟自己总是一致的。 两家独立实现一对接,歧义立刻暴露。 这就是为什么 IETF 强调「要有多个独立实现」。 第三行那个「细微差异」是真实的麻烦: 比如 TCP 的某些选项、窗口缩放的默认值,各家不完全一样,所以在极端网络条件下表现会不同。 这也是「同一个网站在不同系统上偶发连不上」的可能原因之一。
四、动手 Demo:一套 socket 接口,通吃各种协议
起一个真的 echo 服务,连三次;再数一下 AF_ 常量的个数。
后者说明「接口不变、只加常量」这个设计有多能扛。
import socket, threading, time
srv = socket.socket()
srv.bind(('127.0.0.1', 0))
srv.listen(5)
port = srv.getsockname()[1]
def acc():
for _ in range(3):
c, _a = srv.accept()
d = c.recv(100)
c.sendall(b'echo:' + d)
c.close()
threading.Thread(target=acc, daemon=True).start()
for i in range(3):
t0 = time.perf_counter()
c = socket.create_connection(('127.0.0.1', port), timeout=2)
c.sendall(b'hi%d' % i)
got = c.recv(100)
ms = (time.perf_counter() - t0) * 1000
print('第 %d 次 %s %.3f 毫秒' % (i + 1, got.decode(), ms))
c.close()
srv.close()
print('AF_ 常量 %d 个' % len([x for x in dir(socket)
if x.startswith('AF_')]))
实跑输出:
--------------------------------------------------------------------
一套 socket 接口,通吃各种协议
--------------------------------------------------------------------
**UNIX 普及带火 TCP/IP,很大程度是因为「一个接口管所有」。**下面起一个真的 echo 服务,
再用三种方式连它。
次数 收到 耗时毫秒
----------------------------------
第 1 次 echo:hi0 27.852
第 2 次 echo:hi1 16.037
第 3 次 echo:hi2 1.083
**`socket` 模块里 `AF_` 开头的常量有 11 个**——**每一个是一个地址族**。
(IPv4、IPv6、Unix 域套接字、蓝牙……都算一个。)
**同一套 `socket()`、`connect()`、`send()`、`recv()` 全都适用。**
**这就是 BSD socket 的生命力**:**它不是「TCP 的接口」,是「任何协议的接口」。**
**所以后来加 IPv6、加蓝牙,应用代码几乎不用改。**
第一次 26.5 毫秒、后两次不到 1 毫秒:第一次包含了建立监听队列、首次握手等开销,
后面就只是纯往返。
这就是「预热」在测量里必须做的原因。
AF_ 常量一共 11 个:每一个是一个地址族(IPv4、IPv6、Unix 域套接字、蓝牙……)。
而 socket()、connect()、send()、recv() 这四个函数,从 1983 年到今天签名几乎没变。
这就是接口设计的价值:「加能力」靠加常量,不靠改函数。
对比一下那些「每加一种协议就要改一遍接口」的设计,差距在几十年后才会显出来。
30 秒实验
- 把
accept循环改成只接一次,看第二次连接会怎样(会连上但收不到回应)。 - 数一下
socket模块里以SOCK_开头的常量有几个。 - 想一下:为什么
read/write也能用在套接字上?这带来什么方便?
常见误区
- 以为 TCP/IP 靠「标准写得好」扩散。 靠的是「有免费能跑的代码」, BSD 那一份实现比任何文档都有效。
- 以为 socket 接口是为 TCP 设计的。 它是为「任何协议」设计的,地址族与类型都是参数。
- 以为多家实现只有好处。 它会带来行为差异,极端条件下不同系统表现可能不同。
自测三问(点开对答案)
1. TCP/IP 扩散最关键的一步是什么? —— **伯克利把实现放进 BSD 随源码免费分发。**想用的人不用照 RFC 自己写,编译就行。 2. socket 接口为什么能活几十年? —— **它把「协议」做成参数**:`socket(地址族, 类型, 协议)`。 加 IPv6、加蓝牙只是加常量,函数签名不变。 3. 为什么 IETF 强调「要有多个独立实现」? —— **只有一份实现时,标准里的歧义永远发现不了。**两家独立实现一对接,歧义立刻暴露。下一节讲互联网怎么从科研网变成商用网。
- ☐ (本节完,下一节 2.1.5 商用互联网服务的启蒙)