目录
- 一、为什么同样是通信,选型差别这么大
- 二、域名:socket 之前还有一步
- 三、TCP:在不靠谱的信道上做靠谱传输
- 四、TLS:在人人可听的信道上悄悄说话
- 五、HTTP:三次队头阻塞
- 六、微服务之间怎么调:gRPC 为什么赢
- 七、RPC 里的 stub 到底是什么
- 八、异步通信:MQ 为什么不用 HTTP
- 九、Agent 与 LLM 之间的流式通信
- 十、Agent 与工具:MCP 的两种传输
- 十一、对外 API:TLS 的运维现实
- 十二、中间件:注册中心、MySQL、Redis
- 十三、一张表:什么技术用什么通信
- 十四、自检清单
- 附:本文的核查方式
- 附:延伸阅读
一、为什么同样是通信,选型差别这么大
先把日常见到的现象摆在一起:
| 场景 | 用的什么 |
|---|---|
| 浏览器 → 后端 | HTTP/1.1、HTTP/2 或 HTTP/3 |
| 后端 → 后端(同步、要流式) | gRPC over HTTP/2 + Protobuf |
| 后端 → 后端(Java 生态内) | Dubbo Triple 或 REST |
| 后端 → 后端(异步解耦) | 消息队列(Kafka / RocketMQ / RabbitMQ) |
| Agent → LLM | HTTP + SSE 流式 |
| Agent → 本地工具 | MCP over stdio |
| Agent → 远程工具 | MCP over Streamable HTTP |
| 应用 → MySQL | MySQL 自己的二进制协议,跑在 TCP 上 |
同样是”两个程序说话”,为什么有的走 HTTP、有的走自定义协议、有的干脆连网络都不要?
大部分技术文章会按”协议族谱”讲:先讲 TCP/IP 四层模型,再逐个讲每个协议的报文格式。这种讲法有个致命缺陷——你学完之后依然不知道下一个项目该选什么。
这篇文章换一个组织方式:每种通信方式,都是被一个具体问题逼出来的。 把问题讲清楚,选型自己就浮出来了。
打个比方:你不会问”汽车和飞机哪个好”,你会问”我要跨越的是城市还是大洋”。 协议选型也一样——没有优劣,只有”你要对付的是什么问题”。
所以全文有一个反复出现的句式:
它要解决的原始问题是什么?
四个判据贯穿全文,值得先记住:
| 判据 | 问的是 |
|---|---|
| 边界在哪 | 通信的两端是什么关系?进程内、同机、跨机、跨公网 |
| 方向 | 谁主动说话?请求-响应、单向推送,还是双向 |
| 数据类型 | 文本够用,还是需要强类型 / 二进制 |
| 失败代价 | 丢一条消息要不要紧?重复一次行不行 |
二、域名:socket 之前还有一步
严格说,网络通信的第一跳不是 socket,是把一个名字变成一个 IP。
socket.connect() 需要的是 IP 和端口,但你知道的是 api.example.com。中间这一步就是 DNS。它经常被跳过不讲,但它出的问题一点都不比后面少。
2.1 同一个域名,两个进程可能解析出不同的结果
这是最容易被忽略的一点:解析走哪条路,取决于谁在问。
- 浏览器通常有自己的 DoH(DNS over HTTPS,RFC 8484)配置——为了防运营商劫持,很多浏览器默认开启。
- 你的 Java 进程走的是操作系统解析器,通常是
/etc/hosts→ 本地 DNS 缓存 → 运营商 DNS 这条传统路径。 - 如果服务器上跑了
systemd-resolved之类,还会再多一层本地缓存。
后果:同一台机器上,浏览器能打开的域名,你的 Java 进程可能解析失败——或者解析到另一个 IP。排查时会怀疑人生,因为两边看起来”都对”。
另一条相关的路是 DoT(DNS over TLS,RFC 7858),在专用端口上用 TLS 跑 DNS,和 DoH 的区别是它还是纯 DNS 协议,只是套了加密。
2.2 为什么你会看到”偶发慢 200 毫秒”
现在 IPv6 和 IPv4 并存,但不是每台机器两条路都通。如果客户端傻等 IPv6 超时再回退 IPv4,用户就要白等一个超时。
解决办法叫 Happy Eyeballs(RFC 8305):两条路同时试,IPv6 先发,落后 250 毫秒还没连上就把 IPv4 也发出去,谁先成功用谁。
所以 250ms 这个数字不是随便来的——当你的服务出现”大部分请求很快、偶尔慢 250ms 左右”时,Happy Eyeballs 的回退逻辑是嫌疑人之一。
2.3 本节要记住的
- DNS 是不可跳过的第一跳,它出错时现象是”连不上”,但根因不在网络层。
- 解析结果可能因进程而异,排查时先确认两边用的是不是同一条解析路径。
- IPv6/IPv4 并存带来了 250ms 级别的偶发延迟,这是设计使然,不是玄学。
三、TCP:在不靠谱的信道上做靠谱传输
DNS 给了你 IP,下一步是建立连接。这里要回答的问题是:
IP 网络不保证任何事——包会丢、会乱序、会重复、会迟到。怎么在它之上做出一条”看起来可靠”的管道?
3.1 问题有多严重
IP 层是**尽力而为(best-effort)**的:
- 包可能丢失(路由器队列满了就丢,不通知任何人)
- 包可能乱序(不同包走不同路径,先发的后到)
- 包可能重复(重传导致)
- 包可能到达时已损坏(虽然校验和能挡住大部分)
而绝大多数程序需要的心智模型是”我按顺序发出去,对方就按顺序收到”。TCP 的全部复杂度,都来自这个缺口。
3.2 TCP 的解法:给每个字节编号
TCP 的核心手段其实很朴素——给字节流编号,然后让对方确认:
| 机制 | 解决什么 |
|---|---|
| 序列号 / 确认号 | 乱序 → 接收方按序号重排 |
| 超时重传 | 丢包 → 没收到确认就重发 |
| 校验和 | 损坏 → 对不上就丢弃 |
| 滑动窗口 | 性能 → 不必”发一个等一个” |
| 连接状态 | 双方对”现在进行到哪”有共识 |
注意最后一条——TCP 必须在两端维护状态,这就是它”面向连接”的含义。
3.3 那三次握手是干什么的
sequenceDiagram
participant C as 客户端
participant S as 服务端
C->>S: SYN (我要连,我的序号是 x)
S->>C: SYN + ACK (同意,我的序号是 y,确认收到 x)
C->>S: ACK (确认收到 y)
Note over C,S: 连接建立
为什么是三次而不是两次? 因为网络里可能残留旧的、延迟到达的连接请求。如果只握两次,服务端单方面收到一个迟到的老 SYN 就会建立连接并等在那里,白白占用资源。
三次握手让双方都确认了对方的收发能力:
- 服务端从第一次 SYN 知道:客户端的发送能力正常
- 客户端从 SYN+ACK 知道:服务端的收发都正常
- 服务端从最后的 ACK 知道:客户端的接收能力正常
关闭要四次,因为 TCP 是全双工的,两个方向各关各的。关完之后还有 TIME_WAIT——主动关闭方要等一段时间(通常 2 倍报文最大生存时间)才释放。它存在的理由是万一最后的 ACK 丢了,对方会重发 FIN,此时还能响应。
工程含义:如果服务器上大量连接处于
TIME_WAIT,说明这台机器在主动关连接。高并发短连接场景下端口会被耗尽。这在 Java 后端调用下游、以及 nginx 转发时都会碰到。
3.4 拥塞控制:从”丢包就是堵车”到”建个模型”
TCP 面对的第二个问题是:发多快? 发太快会把中间路由器挤爆,发太慢浪费带宽。这个自我调节机制就是拥塞控制。
第一代思路很直白:丢包 = 堵车。
经典算法是 CUBIC(RFC 9438,2023 年 8 月成为标准,取代 RFC 8312),它是 Linux、Windows、Apple 三家的默认算法。CUBIC 在拥塞避免阶段用一个三次函数来涨窗口:
W(t) = C(t − K)³ + Wmax
其中 Wmax 是上次拥塞时的窗口大小,C 取 0.4,乘性减小因子 β 取 0.7(传统 Reno 是 0.5——降得没那么狠)。
这张曲线的意思是:在接近上次拥堵点之前慢慢涨(凹段),过了拥堵点再加速试探(凸段)。
注意一件常被说错的事:CUBIC 仍然是丢包判据的算法。 RFC 9438 原文写得很清楚:
Same as Reno, CUBIC is a loss-based congestion control algorithm.
它换掉的是”怎么涨”,不是”看什么信号”。
第二代思路换掉了判据本身。
BBR(Bottleneck Bandwidth and Round-trip propagation time,Google 2016 年提出)不再把丢包当拥堵信号,而是建一个路径模型:用近期测到的投递速率、往返时延和丢包率,估算这条路的瓶颈带宽和传播时延,再据此决定发多快、在途多少数据。
它成立的理由是:丢包 ≠ 拥塞。在无线链路上,丢包可能只是信号抖动;在浅缓冲的路由器上,缓冲还没填满包就丢了,此时降速纯属冤枉。
BBR 的标准状态需要说清楚:它在 IETF 里没有正式地位。draft-cardwell-iccrg-bbr-congestion-control-02 已经 Expired & archived,被 draft-cardwell-ccwg-bbr 取代。也就是说,BBR 是工程上大量使用、但标准上仍未定稿的东西——写进文档时别说成”BBR 是标准”。
3.5 本节要记住的
- TCP 的所有复杂度来自一个缺口:上层想要可靠有序,底层只给尽力而为。
- 三次握手解决的是旧的重复连接请求,四次挥手和 TIME_WAIT 解决的是双向关闭的可靠性。
- 拥塞控制的演进方向是换判据:从”丢包 = 拥堵”(CUBIC)到”建路径模型”(BBR)。
- 常识性错误提醒:CUBIC 是丢包型算法,只是增长函数换成了三次曲线。
四、TLS:在人人可听的信道上悄悄说话
TCP 给了一条可靠的管道,但它是明文的。而这条管道会经过很多你控制不了的地方:路由器、运营商、代理、CDN。
问题:数据要经过一堆你不信任的中间节点,怎么保证它们看不到、也改不了?
4.1 一次 TLS 1.3 握手要几个来回
一个。 这是 TLS 1.3(RFC 8446)相对 TLS 1.2 最直观的改进。
sequenceDiagram
participant C as 客户端
participant S as 服务端
C->>S: ClientHello (支持的版本、密码套件、密钥交换材料)
S->>C: ServerHello + {EncryptedExtensions, Certificate, CertificateVerify, Finished}
C->>S: {Finished}
Note over C,S: 之后双方可以发应用数据
关键点:从 ServerHello 之后,服务端发的所有东西都是加密的——包括证书本身。
4.2 TLS 1.3 砍掉了什么,为什么
对比 TLS 1.2,TLS 1.3 做了几处减法,每一处都有明确理由:
| 变化 | 理由 |
|---|---|
| 只剩 AEAD 密码套件 | 老式的”先加密再校验”(MAC-then-Encrypt)有攻击面,认证加密一体才安全 |
| 去掉静态 RSA 和静态 DH 密钥交换 | 它们不提供前向保密——私钥一旦泄露,历史流量全部可解 |
| 密码套件拆成两部分 | 认证/密钥交换与 record 加密分开描述,避免组合爆炸 |
版本协商挪到 supported_versions 扩展 | 老办法藏在记录层版本号里,中间件经常改坏它 |
ChangeCipherSpec 移除 | 仅保留兼容中间件的最小实现 |
4.3 前向保密:为什么重要
没有前向保密时:攻击者今天录下你的加密流量,等哪天拿到服务器私钥,就能把历史流量全部解密。这叫”先存后解”(store now, decrypt later)。
有前向保密时:每次会话用临时密钥协商,会话密钥用完即弃。私钥泄露也解不开过去的会话。
这就是为什么”抗量子”这件事现在就要做——攻击者不需要今天就破解,他们只需要今天开始录。
4.4 0-RTT:省一个来回,代价是什么
TLS 1.3 允许对曾经连接过的服务端发 0-RTT 数据:客户端在第一个包里就带上应用数据,不用等握手完成。
代价很明确,RFC 8446 原文两句话:
- 这批数据不具备前向保密——它只用 PSK 派生的密钥加密。
- 不保证跨连接不可重放——攻击者可以把这包原样重放。
工程含义:0-RTT 只适合幂等且无副作用的请求。如果拿它发”下单""支付”这类请求,就等于开了一个重放攻击的口子。
4.5 抗量子:今天在线上跑到了哪一步
这是本文里可以当场验证的一条。在本机执行:
echo | openssl s_client -connect www.cloudflare.com:443 \
-servername www.cloudflare.com -alpn h2,http/1.1 -brief
实测输出(2026-10-15):
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Peer certificate: CN=www.cloudflare.com
Signature type: ecdsa_secp256r1_sha256
Verification: OK
Negotiated TLS1.3 group: X25519MLKEM768
注意最后一行:X25519MLKEM768。
这是一个混合密钥交换:经典的 X25519 加上抗量子的 ML-KEM-768。之所以是混合而不是纯抗量子,是因为万一 ML-KEM 被证明有缺陷,X25519 还在兜底——这是密码学上”不要押单一赌注”的常规做法。
部署现状(这是本号主张”抗量子已经来了”的依据):
- Cloudflare 在 2025 年 10 月底公布:超过 50% 的人类流量已被抗量子密钥交换保护。
- 客户端铺开的时间线:2024 年 3 月 Chrome 桌面默认开启 → 2024 年 8 月 Go 默认 → 2024 年 11 月 Chrome Android 与 Firefox 桌面 → 2025 年 4 月 OpenSSL 默认 → 2025 年 10 月 Apple 随 iOS/iPadOS/macOS 26 默认开启。
但同一份数据里有另一半,不写出来这句话就是误导:
- 2025 年 9 月一项对 top 100k 域名的扫描显示,39% 支持抗量子(半年前是 28%)。
- Cloudflare 背后的源站,只有 3.7% 支持抗量子(2023 年是 0.5%)。
边缘超过一半,源站不到 4%——13 倍差距。 所以真正该记住的判据不是”抗量子来没来”,而是:
TLS 在哪里终止,抗量子就是谁的责任。
流量在你的服务器上解密 → 抗量子是你的事。 流量在 CDN 边缘解密 → 抗量子是 CDN 的事,但边缘到源站那一段仍可能没保护。
4.6 本节要记住的
- TLS 1.3 是减法:1-RTT、只剩 AEAD、去掉静态密钥交换。
- 前向保密是对抗”先存后解”的关键,也是抗量子要提前做的原因。
- 0-RTT 不安全,只能用于幂等请求。
- 抗量子已经在边缘成为主流、在源站仍是少数——判据是 TLS 在哪终止。
五、HTTP:三次队头阻塞
有了 TCP 和 TLS,还需要一层约定”我说的是什么”。
问题:怎么让两边对”请求长什么样、成功失败怎么表示”有共识?
HTTP 的语义部分(方法、状态码、头部字段)从 1.1 到 3 几乎没变。真正反复被推翻的是怎么把这些语义放到连接上——而驱动这件事的,是同一个问题反复出现。
5.1 什么是队头阻塞
一条队列里,前面那件事没完成,后面所有事都得等。 这就是队头阻塞(Head-of-Line Blocking,HOL)。
HTTP 的三代演进,可以概括成把队头阻塞从三个不同的层次上逐个拆掉。
5.2 第一次:HTTP/1.1 的应用层队头阻塞
HTTP/1.1 默认一条连接上同时只有一个请求在途——你发请求、等响应、再发下一个。
流水线(pipelining)本来是解法:连发多个请求。但它在实践中基本失败了——一个响应慢,后面全被堵住,而且中间件支持极不一致,所以浏览器默认都关掉了它。
于是浏览器的实际做法是:同时开 6 条 TCP 连接。
这里埋下了两个问题:
- 每条连接都要各自握手、各自 TLS——开销乘以 6。
- TCP 的拥塞控制不会跨连接共享。 6 条连接各自探测各自的速度,谁也不知道另外 5 条也在发同样的数据。总吞吐不是 6 倍,而且更容易堵。
5.3 第二次:HTTP/2 解决了应用层,但把问题推到了 TCP 层
HTTP/2 的做法是二进制分帧 + 多路复用:把每个请求拆成帧,帧打上”我是第几条流”的标记,然后所有流的帧混在同一条 TCP 连接上发。
应用层的队头阻塞没了——慢请求不会挡住快请求。
但 TCP 层的问题浮出来了:TCP 不知道帧的边界。
TCP 眼里只有一串字节。如果任何一个包丢了,TCP 必须停下等它重传——因为它是按字节顺序交付的。在等待期间,所有流的数据都被堵在缓冲区里,哪怕这个丢包只属于其中一条流。
这就是 HTTP/2 的悖论:它把多路复用做得越好,一条连接上承载的流越多,一次丢包造成的连带伤害就越大。
5.4 第三次:HTTP/3 把流做进传输层
HTTP/3(RFC 9114)的做法是换掉下面的传输层,用 QUIC(RFC 9000/9001/9002,基于 UDP)。
QUIC 的核心创新是:可靠性是按流提供的,不是按连接提供的。 每条流自己管自己的重传,一条流丢包不影响其他流。同时拥塞控制仍是整条连接级别的——这样既避免了跨连接不共享信息的毛病,又避免了跨流互相堵。
代价也很实在:
- UDP 常被中间设备封禁,所以客户端必须准备回退到 TCP 版 HTTP。
- 因为 QUIC 不保证有序传输,HTTP/2 的头部压缩方案 HPACK 用不了(它依赖有序),HTTP/3 换成了 QPACK。
- HTTP/3 不支持
Upgrade和 101 状态码,也不能用Transfer-Encoding。
5.5 HTTP/2 留下的一个教训:设计得好,不等于会被用
HTTP/2 曾经设计了一套精细的流优先级机制(用依赖树表达”先给我这些”)。RFC 9113 在 2022 年修定 HTTP/2 时,把它废弃了,理由写得非常直接:
the prioritization signaling in RFC 7540 was not successful.
具体是三个原因:机制过于复杂、各客户端表达优先级的方式不一致、大量服务端干脆忽略它。
顺便,同一份修订里 h2c 的 Upgrade 令牌也被废弃了:
This usage was never widely deployed.
这是本文最值得记住的一课:一个协议特性的成败,不取决于它设计得多优雅,而取决于实现了没有、用了没有。现在需要优先级表达的场景,指向的是另一个规范(RFC 9218)。
5.6 今天的占比
- HTTP/3 已被 40.9% 的网站使用(W3Techs,2026 年 10 月报告)。
- HTTP/2 仍是主流,但它相对 1.1 的优势只在并发请求多的场景成立——单请求场景下,多路复用没有用武之地,反而多了分帧开销。
5.7 本节要记住的
- 三代 HTTP 演进 = 三次队头阻塞:应用层(1.1)→ TCP 层(2)→ 按流隔离(3)。
- HTTP/2 的固有问题:TCP 看不见帧边界,一次丢包堵住所有流。
- HTTP/3 换来的是按流可靠 + 连接级拥塞控制,代价是 UDP 可达性。
- 设计得好不等于会被用——RFC 9113 废弃优先级机制就是证据。
六、微服务之间怎么调:gRPC 为什么赢
到这里为止讲的是”客户端和服务器”——两端是陌生人的关系。但微服务之间不一样:两端都是你自己写的。
这个差别把所有前提都改了:
| 维度 | 浏览器 ↔ 后端 | 后端 ↔ 后端 |
|---|---|---|
| 谁能改 | 只有你,客户端是别人的浏览器 | 两边都是你,可以同步升级 |
| 契约在哪 | 文档 / OpenAPI,靠自觉 | 共享的 IDL 文件,编译器强制 |
| 性能敏感度 | 网络是主要成本 | 序列化也是成本 |
| 调试方式 | 人肉看、curl 手敲 | 有工具,但要自动化的多 |
所以后端之间可以要求”先定义接口,再生成代码”——这正是 gRPC 的核心。
6.1 gRPC 要解决的三件事
- 跨语言:Java 服务调 Go 服务,接口定义必须语言中立 →
.proto文件(Protocol Buffers IDL)。 - 强类型:字段类型、必填可选,在编译期就检查,而不是等到线上。
- 流式:不只是”一问一答”,还要支持服务端持续推、客户端持续发、双向同时。
6.2 为什么要跑在 HTTP/2 上
因为多路复用。
一个 Java 服务要并发调用下游几百次,如果每次调用占一条 TCP 连接,连接数会爆炸,而且又是那个老问题——拥塞控制不跨连接共享。
HTTP/2 让这些并发调用共用一条连接、各自是一条流。谁先返回谁先交付,互不阻塞。
6.3 线缆上发生了什么(理解层次,不抠字节)
一次 gRPC 调用在 HTTP/2 上长这样:
| 部分 | 内容 |
|---|---|
| 路径 | /<服务名>/<方法名> |
| content-type | application/grpc(可带 +proto、+json 后缀) |
| 必需请求头 | te: trailers |
| 超时 | grpc-timeout,数字 + 单位字母(H/M/S/m/u/n) |
| 消息体 | 一条或多条长度前缀消息 |
| 结果状态 | trailers 里 |
两个必须记住的点:
第一,消息边界和 DATA 帧边界无关。 gRPC 的每条消息是 1 字节压缩标志 + 4 字节大端长度 + 消息体,但这只是字节流里的格式——HTTP/2 的 DATA 帧怎么切,和消息边界没有任何关系。一条消息可能跨几个 DATA 帧,一个 DATA 帧也可能装不满一条消息。
第二,也是最大的工程坑——状态码不在 HTTP 状态码里,在 trailers 里。
grpc-status:十进制 ASCII 整数。grpc-message:UTF-8 描述,百分号编码。- 调用成功时,状态也走 trailers。
这意味着:一次失败的 gRPC 调用,HTTP 状态码是
200。
grpc-status: 5 是 NOT_FOUND,grpc-status: 14 是 UNAVAILABLE——但粗看 HTTP 层,你只会看到 200 OK。拿普通 HTTP 客户端去调 gRPC、然后盯着 HTTP 状态码判断成败,是必然翻车的。
规范里还有一条专门为这个坑打的补丁:如果 content-type 不是 application/grpc 开头,服务端应该回 HTTP 415。理由写得很直接——因为 gRPC 的错误用 200 表示,不显式区分的话,别的 HTTP/2 客户端会把一个 gRPC 错误响应当成成功。
6.4 四种调用方式
gRPC 在 IDL 层面就把流式写进去了:
| 类型 | 形状 | 典型用途 |
|---|---|---|
| 一元(Unary) | 一问一答 | 普通查询、下单 |
| 服务端流 | 一个请求,一串响应 | 拉大结果集、订阅变更 |
| 客户端流 | 一串请求,一个响应 | 上传、聚合上报 |
| 双向流 | 两边各自读写,互不阻塞 | 实时对话、协同 |
同一个 IDL 文件能表达这四种,这是 gRPC 相对 REST 最实质的优势——REST 想表达流式,得另找 SSE 或 WebSocket 拼上。
6.5 什么时候不该用 gRPC
- 要给人调、被人肉调试:REST + JSON 更省事,
curl就能试。 - 要暴露给不是你的客户端:浏览器对 gRPC 的支持需要额外代理层(gRPC-Web)。
- 调用频率低、团队不熟:IDL 的编译链路是实打实的成本,低频调用摊不平。
七、RPC 里的 stub 到底是什么
上一节说 gRPC 的客户端代码是”生成的”。生成出来的那个东西,就叫 stub。
这个词很容易让人困惑,因为它在四个地方出现过,指的东西并不完全一样。
7.1 从 RMI 说起
Java RMI 是最早把这套讲清楚的。它的设计是 stub / skeleton 对称结构:
| stub(客户端) | skeleton(服务端) | |
|---|---|---|
| 角色 | 远程对象的本地代理 | 分派到真正的实现 |
| 参数 | 序列化后发出去 | 反序列化收进来 |
| 结果 | 反序列化收进来 | 序列化后发出去 |
关键性质:stub 实现的是和远程对象相同的那组接口。 所以调用方写 service.hello("world"),看起来和调本地方法一模一样——网络、序列化、等待,全部被藏在方法签名后面。
Java 文档的原话是:stub 是”远程对象的客户端本地代表或代理”,它”隐藏了参数的序列化和网络层通信,以便向调用者呈现一个简单的调用机制”。
7.2 gRPC 的 stub
同一个东西。 gRPC 官方文档的描述是:客户端持有”一个叫 stub 的本地对象”,它”实现了和服务相同的方法”。调用这些方法时,stub 把参数包成 Protobuf 消息、发出去、再把响应解回来。
语言里通常还有几种变体——阻塞式、Future 式、异步式——区别在怎么等,不在通信本身。
7.3 Dubbo 的本地存根
这个是变体,不是同一种东西,值得说清楚。
Dubbo 的”本地存根”是一个可选的装饰器:你在消费者进程里再包一层同接口的实现,它持有真正的远程代理,可以在远程调用前后做事情——参数校验、缓存、容错降级、打桩。
区别在哪? 本地准的”本地性”不是它提供的——它下面那层远程代理已经把远程调用包装成接口调用了。本地存根是在已有代理外面再套一层,位置比 stub 更靠外。
7.4 测试里的 stub——这个词是借来的
单元测试里的 stub 是 test double 的一种:提供预设返回值,不校验交互(那是 mock 干的事)。
它和”本地性”毫无关系。 一个完全本地的方法,它的测试替身也叫 stub。
7.5 怎么记才不混
别硬找一个定义统摄全部。记住来路和分岔:
graph LR
A["RMI stub<br/>(1997)"] --> B["gRPC stub<br/>(生成的客户端代理)"]
A --> C["Dubbo 本地存根<br/>(套在代理外面的装饰器)"]
A -.->|借词| D["测试 stub<br/>(预设返回值的替身)"]
- RPC 这条线上的不变量:stub 是客户端一侧的本地替身,让远程调用看起来像本地调用。
- Dubbo 本地存根是这条线上的一个装饰器变体。
- 测试 stub 是借的词,跟本地性无关——看到它别往 RPC 上想。
八、异步通信:MQ 为什么不用 HTTP
前面讲的都是同步调用:发出去,等结果。
但有三类问题,同步调用天生解决不了:
| 问题 | 同步调用的表现 |
|---|---|
| 级联失败 | 下游挂了,上游线程全部阻塞,故障往上蔓延 |
| 流量尖峰 | 一秒来 10 万请求,下游只有每秒 1 万的容量,直接被压垮 |
| 本来不需要立刻有结果 | 发个短信、写个日志、刷新个缓存——调用方并不关心结果 |
8.1 MQ 的本质:把”什么时候处理”转移走
消息队列的核心动作只有一句话:
发送方把消息交给代理就算完成,“什么时候处理”由接收方自己决定。
这一下解开了三个问题:
- 下游挂了?消息存在队列里,下游恢复后再消费。
- 流量尖峰?队列当蓄水池,下游按自己的速度拉。
- 不需要立刻有结果?发送方根本不等。
这是”解耦”这个词真正的含义——不是”代码不互相依赖”这种抽象说法,而是时间上的解耦。
8.2 那 HTTP 能不能干这个?
能凑合,但每个语义都是拧着的。
| 需求 | HTTP 的表达 | 拧在哪 |
|---|---|---|
| 消息要保序 | 无此语义 | 得自己实现 |
| 消费失败要重投 | 无此语义 | 得自己实现 |
| 消费者按自己的能力拉 | 无此语义 | 得自己实现背压 |
| 一个消息给多个消费者 | 无此语义 | 得自己实现广播 |
| 消息要能重放 | 无此语义 | 得自己实现 |
| 长时间不消费也不丢 | 服务端要保持连接 | 连接是有状态的、会断 |
HTTP 的语义是”我现在要一个答案”。 而 MQ 要的是”投递 + 保序 + 可重放 + 背压”。这是两套完全不同的关注点,硬套 HTTP 等于用 HTTP 当传输层,上面再自己实现一整套消息语义——那还不如直接定义一个协议。
至于”为什么每家的 MQ 协议都不一样”——这属于生态与历史原因(各家独立演化、绑定自己的客户端生态),不是技术必然。本文不展开。
8.3 三个主流 MQ 的通信底座
| 产品 | 协议 | 特征 |
|---|---|---|
| Kafka | 自有的二进制协议,跑在 TCP 上 | 为高吞吐设计,批量 + 零拷贝 |
| RabbitMQ | AMQP 0-9-1 | 标准化的消息协议,路由模型丰富 |
| RocketMQ | 自有二进制协议 | 为电商场景设计,事务消息是重点 |
共同点:都是自定义二进制协议跑在 TCP 上,没有一家用 HTTP 做主线。原因就是上面那张表——HTTP 的语义对不上。
8.4 本节要记住的
- MQ 解决的是时间解耦:发送方不等结果,接收方自己决定何时处理。
- HTTP 语义是”要答案”,MQ 语义是”投递”——对不上,所以各家自己定义协议。
- “为什么每家协议都不一样”是生态原因,本文不展开。
九、Agent 与 LLM 之间的流式通信
现在换到 Agent 场景。这里的第一段通信是 Agent → LLM。
9.1 问题:LLM 的回答是”挤”出来的
一次 LLM 调用生成 2000 个 token,可能要几十秒。
如果用普通的请求-响应模式:客户端发请求 → 服务端生成完 2000 个 token → 一次性返回。用户在这几十秒里盯着一个空白界面。
但生成的本质是逐 token 的——第一个 token 可能 200ms 就出来了。白白浪费了这几十秒的可见性。
9.2 为什么是 SSE 而不是 WebSocket
第一反应通常是”要用 WebSocket 吧,实时嘛”。但这里不需要。
| 需求 | SSE | WebSocket |
|---|---|---|
| 服务端 → 客户端单向推 | ✅ | ✅ |
| 客户端 → 服务端 | ❌(另发普通请求即可) | ✅ |
| 走标准 HTTP | ✅ | ❌(升级后不再是 HTTP) |
| 浏览器自动重连 | ✅ 内置 | ❌ 要自己写 |
| 穿过 HTTP 基础设施 | ✅(就是 HTTP) | ⚠️(部分代理不认) |
LLM 流式只需要单向——客户端发的那一次请求已经用普通 POST 发过了,回来只需要一条单向的流。用 WebSocket 会引入它全部的双向复杂度,却一点用不上。
这里有个通用原则:够用就好。 SSE 是”单向推送”这个问题的最小解。
9.3 SSE 的关键细节
流是纯文本,MIME 类型 text/event-stream,UTF-8:
event: delta
data: {"text": "你"}
data: {"text": "好"}
: keep-alive
- 每条消息以空行结束。
- 字段有
data(内容)、event(事件类型)、id(用于断线续传)、retry(重连间隔,毫秒)。 - 以
:开头的行是注释——这就是保活心跳的标准做法,服务端定期发一个冒号行,防止中间设备把空闲连接掐掉。
三个限制必须知道:
| 限制 | 说明 |
|---|---|
| 只能传文本 | 二进制要先编码(通常 base64) |
| 不能自定义请求头 | 浏览器的 EventSource 只允许带 Cookie(withCredentials)——认证只能靠 Cookie 或 URL 参数 |
| HTTP/1.1 下每域名仅 6 条 | 每浏览器每域名 6 条连接上限,Chrome 和 Firefox 都标了 wontfix。多标签页场景特别痛 |
最后那条正是 HTTP/2 的价值所在:走 HTTP/2 时,连接上限变成并发流上限,默认 100,问题自然消失。
9.4 工程坑:SSE 会被反向代理缓冲
SSE 的数据是一点一点挤出来的,但 nginx 默认会缓冲响应体——等攒够一批再发。结果是流式变成了”分几次大块到达”,实时性全没了。
解法是加一个头:
X-Accel-Buffering: no;
这个头专门告诉 nginx “别缓冲这条响应”。任何做 SSE 的服务端都应该带上它——这条经验在 MCP 的 Streamable HTTP 里也被写进了规范。
十、Agent 与工具:MCP 的两种传输
Agent 场景的第二段通信是 Agent → 工具。这是 MCP 要解决的问题:
模型怎么调用一个它从没见过的东西?
MCP 的答案是:工具自己描述自己(工具名、参数 schema、说明),Agent 在运行时读取这些描述,然后调用。
这套设计最值得讲的地方是:它定义了两种传输,而这两种的选择几乎是被场景唯一决定的。
⚠️ 本节依据 MCP
2026-07-28规范文本。 该版本是一次破坏性修订,各 SDK 与服务端的跟进进度我没有逐一验证。
10.1 本地工具:为什么是 stdio
当 MCP 服务端和 Agent 在同一台机器上时,MCP 用的是 stdio——标准输入输出。
为什么不用 HTTP?因为那台机器上根本不需要网络。
| 用 HTTP 需要处理 | stdio 需要处理 |
|---|---|
| 端口分配冲突 | 无 |
| 认证授权(谁能调) | 无(同机同用户) |
| TLS 加密 | 无(数据不出机器) |
| 服务发现 | 无(父进程直接拉起来) |
| 防火墙 | 无 |
两个进程在同一台机器上通信,最自然的方式就是管道。 这属于前面说的”没得选”那一类——不是权衡后的选择,是这个场景下的唯一合理形态。
stdio 的工作方式:客户端把服务端拉成一个子进程,往它的 stdin 写 JSON-RPC 消息,从 stdout 读回消息,以换行分隔,且消息内不能含换行。
这里有一条约束值得单独说:
服务端绝不能往 stdout 写任何不是合法 MCP 消息的东西。日志只能写 stderr。
这条约束存在的原因很直接——stdout 是协议通道。往里面随便打印一行调试信息,就会把协议流冲垮。“日志走 stderr”不是风格建议,是协议要求。
10.2 远程工具:Streamable HTTP 为什么长这样
跨机器时就得走网络了。但 Streamable HTTP 的形状和普通 REST 很不一样:
- 服务端只暴露一个 endpoint,而且只接受 POST(GET 和 DELETE 直接 405)。
- 每个 JSON-RPC 请求/通知各发一次 POST。
- 服务端每个请求二选一:回一个 JSON 对象,或者回一条 SSE 流。
为什么要允许 SSE? 因为工具执行可能要很久(跑测试、查数据库),中间需要发进度通知。这条 SSE 流的作用域严格限定于本次请求——不是一条全局的长连接。
安全上有一条必读:服务端必须校验 Origin 头,以防御 DNS rebinding 攻击——否则一个恶意网页可以把你的浏览器变成跳板,去操纵你本机的 MCP 服务端。本地运行时,服务端还应该只绑 127.0.0.1,而不是 0.0.0.0。
(还记得上一节的 X-Accel-Buffering: no 吗?它在 MCP 规范里同样要求——只要做 SSE,就得对付缓冲。)
10.3 2026-07-28 这次大改:为什么把握手和 session 都删了
这是本节最重要的增量。当前 MCP 规范版本是 2026-07-28,它对传输层做了几处破坏性修改。
理解这次改动的钥匙是一个词:无状态。
先看改之前是什么样:客户端连上后,必须先发一个 initialize 握手,服务端可能会分配一个 Mcp-Session-Id,之后所有请求都要带上它。服务端记住了每个客户端的状态。
这带来什么问题?
- 很难水平扩展:客户端的状态绑在某一个服务端实例上,负载均衡不能随便转发——除非把所有实例的状态共享起来。
- 基础设施不友好:普通的负载均衡器、网关、CDN 看到的是一堆带自定义 session 头的请求,很难做路由和缓存。
- 重启即失联:服务端重启,所有 session 作废。
2026-07-28 的改法:
| 改动 | 原因 |
|---|---|
移除 initialize 握手 | 改为每个请求自带协议版本和客户端能力(放在 _meta 里)——请求变自包含 |
移除 Mcp-Session-Id | 需要跨调用状态时,改用服务端签发的显式 handle,当普通工具参数传 |
新增 server/discover | 一次性查出服务端支持哪些版本、能力、身份——替代原先握手完成的事 |
移除 GET 流和 resources/subscribe | 改为 subscriptions/listen:一条长活的 POST 响应流,按类型订阅变更 |
移除 ping、logging/setLevel | 日志级别改成每个请求在 _meta 里指定 |
| 服务端发起请求 → 改为 MRTR | 见下 |
| 移除 SSE 断流续传 | 不再支持 Last-Event-ID |
MRTR 是这次最烧脑的一处,值得单独说。
改之前,服务端想反过来问客户端(比如”我需要用户确认一下""帮我调一次模型”),会在 SSE 流上自己发一个 JSON-RPC 请求。这要求连接必须是双向、有状态的。
改之后,服务端不再自己发请求,而是返回一个特殊的中间结果 InputRequiredResult,里面带着”我需要什么”,然后客户端带着答案重试原请求。
sequenceDiagram
participant C as 客户端
participant S as 服务端
C->>S: POST tools/call (id: 1)
Note over S: 需要用户确认
S-->>C: InputRequiredResult (inputRequests: 需要确认)
Note over C: 客户端去问用户
C->>S: POST tools/call (id: 2) 原参数 + inputResponses
S-->>C: 最终结果
为什么这么改? 因为一次请求-响应 + 一次重试,每一跳都是独立的、可路由的、无状态的。而原来的双向流要求整条连接从始至终保留状态。
代价也很实在:客户端的实现变复杂了(要处理重试),而且流断了就是在途请求丢了——必须换一个新的 request ID 重新发。
10.4 旧客户端撞上新服务端会怎样
这张表是给正在做升级的人用的:
| 旧客户端的行为 | 新服务端的响应 |
|---|---|
GET MCP endpoint(开 SSE 流) | 405 Method Not Allowed |
DELETE + Mcp-Session-Id(终止 session) | 405 Method Not Allowed |
请求带 Mcp-Session-Id | 被忽略,不再签发或回显 |
请求带 Last-Event-ID(断流续传) | 被忽略,流不可恢复 |
先发 initialize 握手 | 握手已不存在;每个请求自带版本与能力 |
发 ping / logging/setLevel | 已移除 |
调 resources/subscribe | 已移除,改用 subscriptions/listen |
| 等服务端在流上发独立请求 | 改为在 InputRequiredResult 里返回,客户端重试 |
错误码对照(写排查手册时可以直接抄):
| 情况 | HTTP | JSON-RPC |
|---|---|---|
| 不支持的协议版本 | 400 | -32022,body 里列出支持的版本 |
| 头与 body 不一致 | 400 | -32020 |
| 方法不存在 | 404 | -32601 |
| 通知被接受 | 202 | 无 body |
规范给的向后兼容探测流程:先直接发一个现代请求;如果回 400,先看 body——body 里是可识别的现代错误,说明对方是新服务端(该改请求,不是降级);body 空或不认识,才回退到 initialize 走旧版。
10.5 顺带的弃用:Roots、Sampling、Logging
同一版还把三个特性标记为弃用(Deprecated),一年内不会删除,但新实现不该再采用:
| 特性 | 建议的替代 |
|---|---|
| Roots | 改用工具参数、resource URI 或服务端配置来传目录 |
| Sampling | 直接对接 LLM 厂商 API |
| Logging | stdio 写 stderr;可观测性用 OpenTelemetry |
看这三条能读出一个方向:MCP 在收窄自己。Sampling 意味着服务端要反向调模型——那依赖一条双向有状态连接,正是本次要删的东西。把这类”反向依赖”拆出去,协议本体才可能做到无状态。
10.6 本节要记住的
- 传输的选择由场景唯一决定:同机器 → stdio(没得选);跨机器 → Streamable HTTP。
- stdio 的硬约束:stdout 是协议通道,日志必须走 stderr。
- 2026-07-28 大改的主线索是无状态:删握手、删 session、服务端请求改为 MRTR 重试。
- 改动的收益是基础设施友好(可路由、可缓存、可水平扩展),代价是客户端要处理重试。
十一、对外 API:TLS 的运维现实
前面讲的是后端之间的内部通信。对外 API 是另一回事——面对的是不可控的客户端和公开网络,所以这里的问题从”性能”变成了”运维”。
11.1 证书有效期正在快速缩短
这是最近几年对后端运维影响最直接的一条政策变化(CA/Browser Forum Ballot SC081,2025 年 4 月通过):
| 生效日 | 证书最长有效期 | 域名验证数据复用期 |
|---|---|---|
| 2026-03-15 | 200 天 | 200 天 |
| 2027-03-15 | 100 天 | 100 天 |
| 2029-03-15 | 47 天 | 10 天 |
这张表只有一个结论:手工签发已经不可能。 一年要签 8 次,而且验证数据 10 天就失效——ACME 这类自动化从”最佳实践”变成了唯一选项。
如果你的服务还在靠”到期前人工去换个证书”,现在就是迁移的时候,不用等到 2029。
11.2 HTTP 的缓存约定:三个最容易配错的头
这一块不属于”传输”(这次讲的通信方式),但属于对外 API 里最容易出事故的部分。三个头,每一个配错都会造成真实故障:
Cache-Control —— 语义最容易被想当然的一个:
| 值 | 含义 | 什么时候用错会出事故 |
|---|---|---|
no-store | 完全不存 | 想表达”别缓存”时写了 no-cache |
no-cache | 可以存,但每次必须回源校验 | 和 no-store 不是一回事 |
private | 只有浏览器能存,中间代理不能 | 忘写 → 带用户身份的响应被 CDN 缓存,串给别的用户 |
public | 谁都能存 | 误用在登录后的接口上 |
最危险的是漏写 private。 一个返回用户个人信息的接口,只要 CDN 把它当公共资源缓存了,下一个访问者就会看到别人的数据。这是真实发生过的事故类型。
ETag —— 解决”内容没变就别重传”:
服务端给资源算一个指纹(ETag)返回,客户端下次带上 If-None-Match,服务端比对,没变就回 304 Not Modified、不带 body。
省的是带宽,不是往返——客户端仍然要问一次。真正想省往返,才需要配合 max-age。
Vary —— 一个漏了会串数据的头:
Vary 告诉缓存”这个响应还取决于哪些请求头”。最经典的漏配是压缩:
服务端按
Accept-Encoding返回了 gzip 或未压缩的响应。如果没写Vary: Accept-Encoding,CDN 可能把 gzip 版本存下来,然后发给一个不支持 gzip 的客户端——对方拿到一堆乱码。
同理,任何按请求头变化的响应都必须列进 Vary:语言、设备类型、认证状态。
📎 顺带区分一下:本号另一篇《Agent 的缓存保温》讲的是 LLM 的 prompt 缓存,和这里 HTTP 层的缓存是两套完全不同的东西——只是中文都叫”缓存”,容易串。那篇讲的是”同一次前缀别被逐出”,这篇讲的是”这个响应能不能复用”。同名不同物。
十二、中间件:注册中心、MySQL、Redis
内部调用的三大件,通信方式各有各的道理。
12.1 注册中心:为什么要长连接
注册中心解决的是:“服务 B 的实例有哪些、各自的 IP 是什么”——而且答案随时在变。
- 如果客户端轮询:变更至少要等一个轮询周期才能被发现,而且大部分轮询是白跑。
- 所以主流做法是长连接 + 服务端推送:客户端与服务端保持一条长连接,实例列表一变,服务端立刻推下去。
判据是”变更必须秒级到达”——这也是为什么注册中心几乎都自己造通信机制(Nacos 用自有的长连接 + gRPC),而不是”客户端定时调个 REST 接口”。
顺带一提 Redis 作为注册中心的方案:靠 Key 过期 + 订阅通知,是”能用但把问题推给中间件”的做法,延迟和可靠性都不如专用方案。
12.2 MySQL / Redis:为什么不用 HTTP
这两个是同一个道理的两个实例:
| 协议 | 为什么不用 HTTP | |
|---|---|---|
| MySQL | 自有二进制协议 | 需要连接状态(事务、会话变量、准备语句、游标)——HTTP 的无状态语义对不上 |
| Redis | RESP | 需要极低开销(一次往返要做好几十条命令),文本协议头纯属浪费 |
共同判据:它们要的是”有状态的会话”。
一条 MySQL 连接上是有上下文的——你 BEGIN 了一个事务,后续所有语句都在这个事务里;你设置了一个会话变量,后续语句都受影响。这跟 HTTP”每个请求自包含”的设计是相反的。
而 Redis 的判据不同:它是为了极致性能——一次网络往返里塞满命令(pipipeline),协议本身越简单越好。
这是”判据不同、结论不同”的好例子:MySQL 不用 HTTP 是因为状态语义对不上;Redis 不用 HTTP 是因为开销。
12.3 一笔带过的部分
下面这些本文不展开,它们大多属于场景决定、没什么可权衡的:
- 服务网格(Istio / Envoy):Envoy sidecar 接管流量,处理 HTTP/2 与 gRPC 代理,Istiod 兼作 CA 签发证书做网格内 mTLS。
- 前端实时推送、浏览器渲染优化、移动端、音视频:与本文的”后端选型”主线关系较远。
十三、一张表:什么技术用什么通信
把全文的结论汇总成一张表。第三列才是这张表的价值——它给出的是判据,不是答案。 场景变了,答案会变,但判据不会。
| 场景 | 通信方式 | 判据(为什么) |
|---|---|---|
| 客户端 → 服务端 | HTTP/1.1 → HTTP/2 → HTTP/3 | 队头阻塞出现在哪一层,就在哪一层解决 |
| 后端 → 后端(强类型、要流式) | gRPC over HTTP/2 + Protobuf | 两端都是自己人 → 可以要求 IDL;并发调用多 → 需要多路复用 |
| 后端 → 后端(Java 生态内) | Dubbo Triple | 同上;差异主要在生态与历史,不是技术 |
| 后端 → 后端(异步解耦) | 消息队列 | 判据是”发送方不需要立刻有结果” |
| 对外 API | REST/JSON over HTTPS | 要给人调、要能 curl 手试 |
| 服务发现 | 长连接 + 服务端推送 | 判据是”变更必须秒级到达” |
| Agent → LLM | HTTP + SSE | 生成是逐 token 的,且只需要单向 |
| Agent → 本地工具 | MCP over stdio | 同一台机器,网络是多余的 |
| Agent → 远程工具 | MCP over Streamable HTTP | 跨机器;要能穿过普通 HTTP 基础设施(故无状态) |
| 应用 → MySQL | 自有二进制协议 | 需要有状态的会话(事务、会话变量) |
| 应用 → Redis | RESP | 需要极低开销,协议越简单越好 |
| 传输层 | TCP | 上层要可靠有序,底层只给尽力而为 |
| 连接加密 | TLS 1.3 | 中间节点不可信,且要防”先存后解” |
必须说清楚的一句话:
这张表只讲技术判据。真实世界的选型里,生态、团队栈、运维成本、历史包袱的权重,常常不低于技术原因。“国内 Java 后端大量用 Dubbo、Go 生态更偏 gRPC”这类现象,主要是生态原因,不是技术优劣。
本文只讲技术那部分——不是因为它更重要,而是因为只有它是稳定、可迁移、可以教给你的。
十四、自检清单
读完这篇,如果下面每一条你都能答上来,说明这根线索接上了:
- 为什么
socket.connect()之前还有一步?同一台机器上两个进程解析同一个域名,结果一定一样吗? - 为什么 TCP 握手是三次而不是两次?TIME_WAIT 存在的理由是什么?
- CUBIC 和 BBR 的根本差别是什么?为什么说 CUBIC 仍然是丢包型算法?
- 前向保密防的是什么攻击?它和”抗量子”为什么要提前做有什么关系?
- 0-RTT 快在哪,代价是什么?什么样的请求绝对不能用 0-RTT?
- HTTP/2 解决了哪一层队头阻塞?为什么它反而可能让问题更严重?
- gRPC 调用失败时,HTTP 状态码是什么?怎么拿到真正的错误码?
- stub 在 RMI / gRPC / Dubbo / 单元测试里,哪个是借词?为什么不变量只适用于其中一部分?
- 为什么 MQ 不用 HTTP?“时间解耦”具体解的是什么?
- LLM 流式为什么用 SSE 而不是 WebSocket?SSE 的三个限制分别是什么?
- MCP over stdio 为什么不能往 stdout 打日志?
- MCP 2026-07-28 为什么把握手和 session 都删了?收益和代价分别是什么?
- 漏写
Cache-Control: private和漏写Vary: Accept-Encoding,分别会造成什么后果?
附:本文的核查方式
带日期的数字都有出处(截至 2026-10-15):
| 数字 | 来源 |
|---|---|
| HTTP/3 覆盖 40.9% 网站 | W3Techs,2026 年 10 月报告 |
| 抗量子保护超 50% 人类流量;源站 3.7% | Cloudflare,2025 年 10 月底 |
| top 100k 域名 39% 支持抗量子 | Jan Schaumann 扫描,2025 年 9 月 |
| 证书 200/100/47 天时间表 | CA/Browser Forum Ballot SC081 |
MCP 当前规范版本 2026-07-28 | modelcontextprotocol.io |
规范类内容与部署现状的区分:
本文中关于协议规范的论断依据官方文本(RFC、MCP 规范、gRPC 官方文档)。这些规范已经发布,但各语言 SDK 与线上服务的跟进进度我没有逐一验证——读到”规范已经这么定了”时,请把它理解为”标准如此”,而不是”你的服务器现在就是这样”。
本文不涉及(按范围有意略过):STOMP、STUN/TURN、AMQP 细节、服务网格展开、浏览器渲染与音视频。
附:延伸阅读
- 协议文本:RFC 9293(TCP)、RFC 9438(CUBIC)、RFC 8446(TLS 1.3)、RFC 9113(HTTP/2)、RFC 9114(HTTP/3)
- gRPC:gRPC over HTTP/2 线缆协议、核心概念
- MCP:传输规范(2026-07-28)、版本历史
- 本仓库内相关:《异步编程(一):阻塞 I/O 与非阻塞 I/O》、《微服务拆分》、《Agent 的缓存保温》(讲 LLM prompt 缓存,与本篇的 HTTP 缓存不是一回事)