笔者近期在学习 AI 相关技术的时候,发现 MCP 相关的调用方式很多都已经废弃 SSE(Server-Sent Event)而推荐使用 Streamable HTTP 了。于是在对二者进行学习和对比了之后,决定趁机将网络交互相关的应用层协议都总结一下。

本文不会特别深入和细节地探讨各个协议的底层实现,会尽量从第一性原理去介绍为什么要出现这些协议,以及这些协议的适应场景是什么。

要真正理解 HTTP、Streamable HTTP、SSE、WebSocket 和 gRPC这几者的区别,我们不能仅停留在 API 调用的表面,而需要回到网络通信的第一性原理:TCP 连接的使用方式、数据编码格式以及协议层面的抽象。绝大多数现代网络通信(除了 HTTP/3 基于 UDP)都建立在 TCP 之上。这五种技术的本质区别,实际上是如何利用 TCP 连接以及如何定义数据交换的格式

1. 协议速览

HTTP/1.1

这是互联网的基石。在最原始的模型中,它遵循一次请求,一次连接的模式(虽然 Keep-Alive 改善了复用,但逻辑上仍是独立的)。

  • 底层原理: 客户端发起 TCP 握手 -> 发送 HTTP Header + Body -> 服务器处理 -> 返回 HTTP Header + Body -> (可能) 关闭连接。
  • 通信模式: 严格的半双工(Half-duplex)逻辑。客户端不问,服务器不说。
  • 数据格式: 通常是 JSON (文本) 或 XML。可读性好,但序列化/反序列化有性能损耗。
  • 缺点: 头部冗余(Header overhead)大。如果要实时获取数据,必须使用 轮询 (Polling),这会制造大量的无效请求,浪费服务器资源和带宽。

关于 HTTP 更多细节可参阅:从 HTTP1.0 到 HTTP3 的演化

Streamable HTTP

Streamable HTTP 本质上仍然是 HTTP,但利用了 HTTP/1.1 的一个特性:Transfer-Encoding: chunked

  • 底层原理:
    1. 客户端发送标准 HTTP 请求。
    2. 服务器在响应头中声明 Transfer-Encoding: chunked,并不返回 Content-Length(因为长度未知)。
    3. 服务器通过同一个 TCP 连接,分批次(Chunk)发送数据块。
    4. 发送一个长度为 0 的块表示传输结束。
  • 通信模式: 单向流(Server -> Client)。连接保持打开,直到传输完成。
  • 适用场景: 生成式 AI(LLM)的打字机效果、大文件下载、动态生成的报表。

SSE

SSE 是 Streamable HTTP 的一种标准化封装,专门用于浏览器端的服务器推送。它规定了特定的 Content-Type: text/event-stream 和数据格式(data: ...)。

  • 底层原理:
    1. 客户端发起 HTTP 请求。
    2. 服务器挂起连接,不关闭。
    3. 服务器有数据时,通过该连接直接写入遵循特定格式的文本流。
    4. 浏览器原生支持 EventSource API,能自动处理断线重连。
  • 与 WebSocket 的本质区别: SSE 依然是 HTTP 协议。它不需要协议升级,防火墙友好,但只能 服务器 -> 客户端 单向传输。
  • 适用场景: 股票行情更新、新闻推送、CI/CD 日志流、系统状态监控。

关于 SSE 更多细节可参阅:Rust 实战丨SSE(Server-Sent Events)

WebSocket

WebSocket 旨在解决 HTTP "请求-响应"模式在实时双向通信上的无能。它从 HTTP 开始,但随后"背叛"了 HTTP。

  • 底层原理:
    1. 握手: 客户端发送一个 HTTP 请求,带上 Upgrade: websocket 头。
    2. 升级: 服务器如果同意,返回 101 Switching Protocols。
    3. 裸奔: 此刻起,HTTP 协议层消失,连接变成了原始的 TCP 通道(Over TCP)。
    4. 帧(Frame): 双方可以在这个通道上自由地发送自定义的二进制帧或文本帧,不再受 HTTP Header 的束缚。
  • 通信模式: 真正的全双工(Full-duplex)。客户端和服务器地位对等,谁都可以随时发消息。
  • 缺点: 状态管理复杂(需要处理心跳、重连、鉴权),且不支持 HTTP 的语义(如 404、500 状态码,需要自己定义业务层协议)。
  • 适用场景: 多人在线游戏、实时聊天室、协同编辑文档。

gRPC

gRPC 是 Google 开发的高性能框架。它和前面几个不在一个维度:WebSocket 是一种协议,而 gRPC 是一个框架,它默认基于 HTTP/2 协议。

  • 底层原理:
    1. HTTP/2 多路复用: 也就是在一个 TCP 连接上并发处理多个请求,解决了 HTTP/1.1 的队头阻塞问题。
    2. Protobuf (Protocol Buffers): 放弃 JSON,使用二进制序列化。数据极其紧凑,且需要预先定义 .proto 文件(Schema)。
    3. RPC (远程过程调用): 对开发者屏蔽了网络细节。你调用远程服务器的 GetUser() 方法,就像调用本地函数一样。
  • gRPC 支持四种模式:
    1. 简单 RPC(类似标准 HTTP)。
    2. 服务端流式(类似 SSE)。
    3. 客户端流式。
    4. 双向流式(类似 WebSocket)。
  • 缺点: 浏览器支持较差(需要 gRPC-Web 代理),调试不如 JSON 直观(是乱码的二进制)。
  • 适用场景: 微服务内部通信(高频、低延迟)、IoT 设备通信、多语言混合开发环境。

对比

技术方案 本质 (First Principles) 通信模型 数据形态 核心适用场景
HTTP/1.1 (REST) 短连接 问答式 (Request-Response) JSON (文本) 传统的 CRUD 业务,无实时性要求。
WebSocket 全双工隧道 (TCP 裸连) 自由对话 (Bi-directional) 二进制 / 文本 高频低延迟场景(游戏、协同编辑、IM)。
SSE (Standard) 单向订阅 (GET 请求) 广播式 (Pub / Sub) 文本 (data: ...) 轻量级推送(股票、新闻、简单的系统通知)。
Streamable HTTP 分块传输 (POST 请求) 管道式 (Pipeline) NDJSON / Bytes AI 生成(ChatGPT)、大文件下载、复杂 RPC。
gRPC RPC 框架 (HTTP/2) 远程调用 (Function Call) Protobuf (二进制) 微服务内部通信(高效、强类型、多语言)。

2. 为什么 MCP 弃用 SSE

color=orange 总结

废弃 SSE不是因为它传输文本格式不好,而是因为"长连接订阅模式"在复杂的RPC(远程过程调用)场景下是一种架构错误。

MCP 从 SSE 转向 Streamable HTTP,本质上是从异步的消息总线模式 回归到了 同步流式的 RPC模式。后者更简单、更健壮,也更符合 AI Agent这种"一来一回"的思考特性。

2.1 核心痛点:双通道架构

这是博客中提到的最直观的原因。

  • 旧的 SSE 模式(Legacy MCP): 你必须维护两条独立的连接才能完成一次对话:

    1. 听筒(GET /sse): 建立一个长连接,专门用来服务器说话(接收 Event)。
    2. 话筒(POST /messages): 每次要说话时,发起一个新的短连接,专门用来指令。

    比喻: 就像你给朋友打电话,手里拿着两个手机。左手拿一个手机只听不因,右手拿另一个手机只发短信。朋友回话还得通过左手的手机传过来。

  • 新的 Streamable HTTP 模式: 回归单通道。 你发送一个 POST 请求(说话),服务器直接在这个请求的 Response 里通过流式传输回话(听)。代码复杂度指数级下降。不再需要维护一个"永远在线"的幽灵连接,也不需要处理"指令发出去了,但接收通道断了"这种分布式系统里的脑裂状态。

2.2 基础设施的敌意

博客中重点提到了这一点,这是运维层面的第一性原理。

  • SSE 的长连接诅咒: 旧版 MCP 要求 /sse 连接必须一直活着
    • 现实世界: 企业的防火墙、Nginx 负载均衡器、云服务网关(AWS ALB, Cloudflare)非常讨厌占着茅坑不拉屎的空闲长连接。它们会强行切断这些连接(Timeout)。
    • 后果: 客户端必须不断写复杂的保活(Keep-alive)和重连逻辑。
  • Streamable HTTP 的优势: 它是按需(On-Demand)的。
    • 有任务?发个 POST,保持连接直到任务结束。
    • 没任务?连接自然关闭。
    • 这完全符合 HTTP 的设计初衷,对所有的中间件(Middleware)都非常友好。

2.3 状态管理的灾难

这也是博客中提到的关键技术细节。

  • SSE 的状态同步难题: 在旧模式下,因为"听"和"说"是分离的,很容易出现竞态条件(Race Condition)。
    • 比如:客户端刚发了一个 POST 指令,但在服务器回包之前,SSE 连接断了。此时服务器把结果推给了谁?数据丢了吗?客户端重连后还能收到刚才的结果吗?
    • 为了解决这个问题,需要引入复杂的 Session ID 和消息队列机制。
  • Streamable HTTP 的原子性: 请求和响应绑定在同一个 TCP 上下文里。
    • 连接断了 = 请求失败。逻辑非常清晰(要么成功,要么重试),不需要在应用层去猜测数据去哪了。

2.4 为什么博客中提到它依然支持 SSE 格式?

这是一个容易混淆的点。

博客中澄清了:Streamable HTTP 依然可以使用 text/event-stream 作为数据传输的格式(Framing),但它改变的是传输的载体(Transport)。

  • 旧 MCP: SSE 是一个订阅频道(Pub/Sub)。
  • 新 MCP: SSE 只是 POST 响应体里的一种编码方式

OpenAI 和 Anthropic 现在的做法也是如此:他们不再使用浏览器原生的 EventSource(那个只能 GET 的订阅 API),而是使用 fetch POST 请求,然后把响应体当成流来处理。