📚 第二章 TCP/IP基础知识
📄 一句话概括

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 秒实验

  1. 把 accept 循环改成只接一次,看第二次连接会怎样(会连上但收不到回应)。
  2. 数一下 socket 模块里以 SOCK_ 开头的常量有几个。
  3. 想一下:为什么 read/write 也能用在套接字上?这带来什么方便?

常见误区

  1. 以为 TCP/IP 靠「标准写得好」扩散。 靠的是「有免费能跑的代码」, BSD 那一份实现比任何文档都有效。
  2. 以为 socket 接口是为 TCP 设计的。 它是为「任何协议」设计的,地址族与类型都是参数。
  3. 以为多家实现只有好处。 它会带来行为差异,极端条件下不同系统表现可能不同。
自测三问(点开对答案) 1. TCP/IP 扩散最关键的一步是什么? —— **伯克利把实现放进 BSD 随源码免费分发。**想用的人不用照 RFC 自己写,编译就行。 2. socket 接口为什么能活几十年? —— **它把「协议」做成参数**:`socket(地址族, 类型, 协议)`。 加 IPv6、加蓝牙只是加常量,函数签名不变。 3. 为什么 IETF 强调「要有多个独立实现」? —— **只有一份实现时,标准里的歧义永远发现不了。**两家独立实现一对接,歧义立刻暴露。

下一节讲互联网怎么从科研网变成商用网。

相关笔记