小明在一家 AI 公司上班,他的智能助手经常要“借工具”:查数据库、读文件、抓网页。这些工具都存放在楼下的“工具坊”里——也就是 MCP 服务端。传话的方式有两种。第一种是“楼内传话管”:工具坊就开在同一栋楼里,两边用一根又短又粗的铜管接起来,纸条塞进去,结果原路递回来,又快又省事,可惜管子只有那么长,出了这栋楼就够不着了。第二种是“驻店快递员”:工具坊在城市的另一头,你只能靠快递系统下单,接单后店里派一位快递员常驻在你公司门口,一有结果就立刻敲门送到;但快递员只会单向送件,你想发新请求,还得重新走一遍快递系统。楼内铜管,就是 MCP 的 Stdio 传输;驻店快递员,就是 MCP 的 HTTP+SSE 传输。搞懂这两套机制的原理、性能差异、适用场景以及断线重连策略,你就能像小明一样,在任何项目里选对“传话方式”。

岗位关联

传输层是 MCP 架构的地基:选对 Stdio 与 SSE、看懂连接与重连,你才能把 Agent 的工具调用从 Demo 推向生产环境,这是 AI 应用工程师的必备基本功。

目录
  1. 可视化演示
  2. Stdio传输原理
  3. SSE传输原理
  4. 性能特点对比
  5. 适用场景判断
  6. SSE连接管理
  7. 断线重连策略
  8. 未来传输扩展
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

动态演示MCP传输层的核心机制,通过输入/处理/存储/输出四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

MCP传输层:Stdi Stdio传输方式的工作原理与 SSE传输方式与HTTP长连接 核心机制可视化 MCP传输层整体架构与关键组件

Stdio传输原理

周末,小明干脆把“工具坊”搬进自己电脑里——查本地文件、操作数据库,全是自家东西。工具坊是一个独立的程序,得靠“楼内传话管”沟通:小明启动它,它就变成了自己机器上的一个子进程。这根铜管的一端叫“标准输入”(stdin),另一端叫“标准输出”(stdout)。MCP 客户端把请求写成纸条塞进 stdin,服务端处理完再把结果从 stdout 递回来。两边约定好的“纸条格式”是 JSON-RPC 2.0:每个请求都带 id、method、params,每个响应回 id 和 result。纸条与纸条之间怎么分清边界?用换行符——一行一个 JSON 对象,好比每张纸条末尾盖一个“封口戳”。

换行分帧的实现极其简单:写消息时在 JSON 末尾加一个 \n,读消息时按行读取即可。这种设计天然支持流式处理,任何语言都能轻松解析。还有一个细节:标准错误输出(stderr)被约定为日志通道,只用来打印服务端的调试信息,不参与业务消息,相当于铜管旁边的“小铃铛”,响不响都不影响传纸条。Stdio 的生命周期与子进程绑定——进程被杀,会话随之结束;不监听端口、不走网络,外部根本摸不到这根管子,这是它安全边界简单的根本原因。

铜管方案快,但管子长度有限——Stdio 只能在同机进程之间使用。一旦工具坊搬去别的城市,小明就得换个思路请快递员了。

request→stdinserver→stdoutresponse,frame=JSON+newline\displaystyle \text{request} \xrightarrow{\text{stdin}} \text{server} \xrightarrow{\text{stdout}} \text{response},\qquad \text{frame} = \text{JSON} + \text{newline}
公式说明:Stdio 的本质是“请求经 stdin 流入、响应经 stdout 流出”的双向管道,每条消息由 JSON 文本加一个换行符构成一个完整帧。
  1. 请求经 stdin 流入客户端把 JSON-RPC 请求写成一行文本,写入子进程的标准输入,相当于把纸条塞进铜管。
    req→stdinserver\displaystyle \text{req} \xrightarrow{\text{stdin}} \text{server}
  2. 响应经 stdout 回流服务端处理好后,把响应同样写成一行,写到标准输出;客户端逐行读取。
    server→stdoutresp\displaystyle \text{server} \xrightarrow{\text{stdout}} \text{resp}
  3. 换行符划定帧边界每条消息以换行符 \n 结尾,读到一个换行就知道“这封纸条写完了”。
    frame=JSON+newline\displaystyle \text{frame} = \text{JSON} + \text{newline}

SSE传输原理

项目越来越大,小明要部署一个云端的“网页分析工具”。工具坊在城市的另一头,没法拉一根横跨城市的铜管,只能请“驻店快递员”。这套快递体系在协议层面对应的是 HTTP+SSE 传输。第一步,小明发一个 GET /sse 请求——服务端收到后并不急着结束响应,而是把响应“撑开”,变成一条持续开口的事件流,内容类型是 text/event-stream。紧接着,服务端发来一条特殊的 endpoint 事件,告诉小明:“以后要下单,请寄到 /messages/xxx 这个地址。”这就相当于快递员上门报到,并留下后续下单的地址。

之后的每次工具调用都是一次“下单”:小明向 endpoint 发一个 POST 请求,里面是 JSON-RPC 消息;服务端通常先回 202 Accepted 表示“收到了”,真正的处理结果稍后通过 SSE 事件推回。SSE 事件本身格式很简单:每个事件由若干字段组成(data、id、event 等),字段名后跟冒号和值,事件与事件之间用空行分隔。最关键的特性是单向:数据只从服务端流向客户端,客户端想在事件流里“说”点什么是不可能的,只能走独立的 POST。也正因如此,SSE 能轻松穿过防火墙、复用成熟的 HTTP 基础设施。

快递员只管送件不管收件,单向但胜在稳定——只要把“驻守管理”做好,远程协作就能放心交给它。

SSE=GET /sse⏟establish+POST message⏟send+event stream⏟receive\displaystyle \text{SSE} = \underbrace{\text{GET /sse}}_{\text{establish}} + \underbrace{\text{POST message}}_{\text{send}} + \underbrace{\text{event stream}}_{\text{receive}}
公式说明:SSE 传输由三部分组成——GET 请求建立事件流、POST 请求发送 JSON-RPC 消息、事件流负责接收服务端单向推送的结果。
  1. GET /sse 建立事件流客户端发出普通 GET 请求,服务端以 text/event-stream 持续响应,不关闭连接。
    client→GET /sseserver,200 text/event-stream\displaystyle \text{client} \xrightarrow{\text{GET /sse}} \text{server},\quad \text{200 text/event-stream}
  2. endpoint 事件报到服务端先用一条 SSE 事件下发回传地址,客户端后续用它发 POST。
    event: endpoint⇒POST→/messages/{id}\displaystyle \text{event: endpoint} \Rightarrow \text{POST} \to \text{/messages/\{id\}}
  3. POST 发请求、事件流收结果每次调用工具都走一次 POST,结果由事件流单向推回。
    POST→202 Accepted;result→SSE event\displaystyle \text{POST} \to 202\ \text{Accepted};\quad \text{result} \to \text{SSE event}

性能特点对比

铜管和快递各有各的成本账。小明算了算:铜管传纸条,几乎零开销;快递送件,每单都有包装费、路程费和签收时间。落实到协议上,Stdio 走的是本地进程间通信:没有 TCP 三次握手、没有 HTTP 头、没有网络路由,每条消息只需要一个换行符做边界,延迟通常在微秒到亚毫秒级。SSE 则不同:每发一个 POST 请求,都要经历完整的 HTTP 往返,再加上服务端的 202 响应和后续事件推送,单次调用延迟比本地管道高出几个数量级;长连接还需要持续占用一个连接资源,并时刻受代理网关超时策略的约束。

性能不只有延迟这一个维度。吞吐方面,Stdio 处理大量小消息时优势明显;SSE 受限于 HTTP 栈和网络带宽,事件越密集,协议开销占比越高。资源占用方面,Stdio 每次拉起子进程都有启动成本,不适合频繁创建销毁;SSE 长连接省了握手,但服务端要为每个客户端维护连接状态,多客户端并发时占用大量文件描述符和内存。安全边界也是一个隐性差异:Stdio 不暴露端口,天然屏蔽外部访问;SSE 是网络服务,必须考虑鉴权、限流和 TLS 加密。

想追求极致速度和消息密集场景,就选楼内铜管;跨城协作的“运费”绕不开,但可以用合理的设计把开销摊薄。

LStdio≈Llocal≪LSSE≈LTCP+LHTTP+Lnet\displaystyle L_{\text{Stdio}} \approx L_{\text{local}} \ll L_{\text{SSE}} \approx L_{\text{TCP}} + L_{\text{HTTP}} + L_{\text{net}}
公式说明:Stdio 的延迟约等于本地进程间通信延迟,远小于 SSE 的延迟——后者由 TCP 握手、HTTP 往返和网络传播三部分叠加而成。
  1. 本地 IPC 延迟同一台机器上进程间通信的延迟,量级在微秒到亚毫秒。
    Llocal∼10−6 s\displaystyle L_{\text{local}} \sim 10^{-6}\ \text{s}
  2. TCP 三次握手SSE 每次建立或恢复连接都要先完成握手,耗时约一个半网络往返。
    LTCP=1.5×RTT\displaystyle L_{\text{TCP}} = 1.5 \times \text{RTT}
  3. HTTP 往返与网络传播请求头、响应头、代理转发、路由等待逐一叠加,把延迟放大到毫秒级以上。
    LSSE=LTCP+LHTTP+Lnet\displaystyle L_{\text{SSE}} = L_{\text{TCP}} + L_{\text{HTTP}} + L_{\text{net}}

适用场景判断

选铜管还是快递员,小明总结出一条经验:先看店铺开在哪里、有多少人要上门,再定传话方式。选 Stdio 的三个条件可以归纳为“同机、单客户端、无网络需求”:服务端能以子进程形式被拉起(比如一个包装了 CLI 或本地数据库的脚本)、只有当前 Agent 一个人用、不需要对外暴露服务。典型场景包括本地开发调试、私有工具接入、离线环境运行。只要有一条不满足——服务端在另一台机器上、多个 Agent 都要连、或者要让非本机进程访问——就该果断选 SSE。

SSE 的典型场景也很清晰:云端托管的工具网关,多个客户端共享一个服务实例;服务端需要主动推送进度或事件(SSE 的单向推送是天然优势);客户端环境只允许 HTTP 出站流量,比如浏览器或受限容器。真实系统里两种方式还可以混用:本地起一个 Stdio 网关进程,由它把请求转发给远程的 SSE 服务,相当于“楼内设个收发室,外面的事交给快递员”。这样既保住了本地的低延迟,又打通了跨网络的远程工具链。

位置和人数决定了协作方式;条件一旦变了,传话方式也要跟着换,这就是架构师的直觉。

T={Stdio,same-host∧single-clientSSE,cross-host∨multi-client\displaystyle T = \begin{cases} \text{Stdio}, & \text{same-host} \land \text{single-client} \\ \text{SSE}, & \text{cross-host} \lor \text{multi-client} \end{cases}
公式说明:传输选型是一个条件判断——同机且单客户端时选 Stdio;只要出现跨机、多客户端或需要推送中的任意一个条件,就选 SSE。
  1. Stdio 的三个条件同机、单客户端、无需联网,三者同时满足才选管道。
    same-host∧single-client∧¬network\displaystyle \text{same-host} \land \text{single-client} \land \neg \text{network}
  2. SSE 的触发条件跨机、多客户端、需要服务端推送,任何一个成立就走 HTTP。
    cross-host∨multi-client∨push\displaystyle \text{cross-host} \lor \text{multi-client} \lor \text{push}
  3. 混合中转本地网关聚合多个远程服务,对外统一暴露 Stdio,对内转发到 SSE。
    Agent↔gateway↔remote SSE\displaystyle \text{Agent} \leftrightarrow \text{gateway} \leftrightarrow \text{remote SSE}

SSE连接管理

快递员驻守期间,小明最怕两件事:小区保安把快递员当成可疑人员赶走(代理超时掐线),或者快递员送错件、丢件没法追踪。所以驻守管理要有规矩。第一件事是“报平安”:HTTP 长连接挂在代理网关(Nginx、Kong 等)后面时,网关通常有一个空闲超时,比如 60 秒没有任何数据传输就强制断开。为了让连接不“假死”,服务端要周期性发送心跳——SSE 规范里的注释行(以冒号开头)是最轻量的保活方式,客户端解析事件流时会自动忽略它,不会污染业务数据。

第二件事是消息追踪:服务端为每条 SSE 事件分配递增的 id 字段,客户端记录最后收到的 id,一旦断线就知道该从哪条消息之后续传,避免漏收。第三件事是生命周期管理:连接建立后先完成 initialize 握手,服务端记录会话状态;会话结束时,服务端发送事件或直接关闭流,客户端收到后应当停止重连;主动关闭时客户端也应通知服务端释放资源,防止连接泄漏。保活间隔有一个经验值:取“网关超时的一半”——网关 60 秒超时,就每 30 秒发一个心跳,这样既不会惊扰网关,又留足了余量。

快递员定期按门铃报平安,每件包裹都编号登记,失联也有据可查——这就是连接管理的全部艺术。

Theartbeat≤Tproxy timeout2\displaystyle T_{\text{heartbeat}} \le \frac{T_{\text{proxy timeout}}}{2}
公式说明:心跳周期应当小于等于代理网关超时时间的一半,这样即使网络略有抖动,连接也不会被网关误杀。
  1. 网关超时阈值代理网关设定空闲超时,比如 60 秒无数据就判定连接死亡并强制断开。
    Tproxy timeout=60 s\displaystyle T_{\text{proxy timeout}} = 60\ \text{s}
  2. 心跳周期留余量心跳间隔取超时的一半,保证“最后一声平安”离门槛足够远。
    Theartbeat=30 s≤602\displaystyle T_{\text{heartbeat}} = 30\ \text{s} \le \frac{60}{2}
  3. 事件序号追踪客户端记录最后收到的事件 id,断线后据此判断从哪条消息续传。
    lastID=max⁡{id1,id2,…,idk}\displaystyle \text{lastID} = \max\{\text{id}_1, \text{id}_2, \ldots, \text{id}_k\}

断线重连策略

天下没有不堵车的城市,快递员也可能被堵在半路、手机没电、彻底失联。小明不能干等,他需要一套“催单策略”。客户端通常通过 EventSource 的 onerror 事件或流读取异常感知断线。最朴素的做法是立刻重连,但立刻重连往往撞上网络抖动的高峰,所有客户端一起重试还会形成“重连风暴”。成熟的策略是指数退避:第一次等 0.5 秒,第二次等 1 秒,第三次等 2 秒……每次翻倍,直到一个上限(如 30 秒),并加上随机抖动,让大家的重试时间错开。SSE 协议甚至内置了 retry 字段,服务端可以主动告诉客户端“断线后请等多少毫秒再重连”。

重连成功不等于会话恢复。SSE 事件流重新建立后,客户端必须再次完成 initialize 握手;如果服务端支持会话关联,可以通过会话标识恢复部分上下文,但 sessions 可能已被超时回收,此时客户端要能从头重建整个会话。消息层面还要做幂等:已经发出但没收到响应的请求,重发时必须保持同一个 id,服务端才能据此去重,避免同一个“删除文件”的工具被执行两次。最后的兜底是整体超时——连续重试 N 次仍失败,就放弃本次连接并向上层编排上报错误。

催单要有耐心、有节奏,更要让每笔“失联的快递单”可追溯、可重发——重连策略的本质就是不确定性下的鲁棒性设计。

tn=min⁡(t0×2n, tmax⁡)+jitter,t0=0.5, tmax⁡=30\displaystyle t_n = \min(t_0 \times 2^{n},\ t_{\max}) + \text{jitter},\qquad t_0 = 0.5,\ t_{\max} = 30
公式说明:第 n 次重试的等待时间等于初始延迟乘以 2 的 n 次方,再封顶到 30 秒,最后叠加一个随机抖动。
  1. 初始延迟第一次重试等 t₀ = 0.5 秒,给网络一点喘息时间。
    t0=0.5 s\displaystyle t_0 = 0.5\ \text{s}
  2. 指数增长每次失败,等待时间乘 2,n 次失败后等待 t₀ × 2ⁿ。
    tn=t0×2n\displaystyle t_n = t_0 \times 2^{n}
  3. 封顶与抖动超过 30 秒不再增长;叠加随机 jitter 错开各客户端的重试时刻,防止风暴。
    tn=min⁡(t0×2n, 30)+jitter\displaystyle t_n = \min(t_0 \times 2^{n},\ 30) + \text{jitter}

未来传输扩展

铜管够快但有距离,快递员能跨城但只会单向送件。小明忍不住想:能不能发明一个“双向对讲机”?既快又能远程,两边随时可以互喊。这正是 MCP 传输层的演进方向。协议从最初的 Stdio 与 HTTP+SSE,逐步演进出 Streamable HTTP 规范:它对客户端更友好、支持请求/响应与流式响应,同时保留了 SSE 事件流作为服务端推送的载体。再往后看,WebSocket 是呼声最高的新传输方式——一次握手后全双工,服务端能主动推送,客户端也能随时发消息,不再需要“POST 发送 + 事件流接收”的双通道分裂。

流式传输是另一个重点:大文件、长耗时工具的结果可以分块返回,客户端边收边渲染,而不是等全部完成才算结束。传输效率公式揭示了演进的本质:协议开销占比越低越好。WebSocket 的帧头只有 2 到 14 字节,相比 SSE 的 “data: “ 前缀、双换行和 HTTP 头,在高频小消息场景下节省的字节相当可观。未来还可能支持多路复用——一条连接上并发多个请求,进一步降低连接成本。这些演进的共同目标只有一个:让”传话”更快、更省、更双向。

“双向对讲机”的一天终会到来,但在那之前,理解铜管和快递员的底层逻辑,依然是所有传话方案的地基。

η=SpayloadSpayload+Soverhead→ WebSocket Soverhead≈2∼14 B\displaystyle \eta = \frac{S_{\text{payload}}}{S_{\text{payload}} + S_{\text{overhead}}} \quad \xrightarrow{\ \text{WebSocket}\ } \quad S_{\text{overhead}} \approx 2\sim 14\ \text{B}
公式说明:传输效率等于有效数据除以总传输字节;WebSocket 将额外的协议开销压缩到 2~14 字节,从而大幅提升效率。
  1. 传输效率定义有效数据字节数占总传输字节数的比例,η 越接近 1 越高效。
    η=SpayloadStotal\displaystyle \eta = \frac{S_{\text{payload}}}{S_{\text{total}}}
  2. 协议开销HTTP 头、SSE 事件包装、握手消息等非业务字节,都要从总字节里扣除。
    Soverhead=Stotal−Spayload\displaystyle S_{\text{overhead}} = S_{\text{total}} - S_{\text{payload}}
  3. WebSocket 低开销帧头仅 2~14 字节,效率显著高于 SSE 的事件包装。
    ηws>ηsse\displaystyle \eta_{\text{ws}} > \eta_{\text{sse}}

AI 应用场景

场景一:本地私有工具接入(Stdio 场景)

阶段:工具接入与执行

解决的问题:团队内部的脚本、本地数据库通常不能暴露到网络;传统做法是写一堆 Shell 胶水代码,参数解析、错误处理全是重复劳动。用 Stdio 把脚本包装成 MCP server,Agent 通过标准输入输出直接调用,无需开端口、无需鉴权,进程退出即清理,安全边界清晰。

具体案例:数据分析师小张的 Agent 需要查询本地 ClickHouse 的报表。他写了一个 30 行的包装器,用子进程方式启动,Agent 每次用 JSON-RPC 请求查询,结果毫秒级返回,开发调试零网络成本。

场景二:云端多租户工具网关(SSE 场景)

阶段:服务部署与运维

解决的问题:工具部署在云上,多个 Agent 或团队要共享;纯 Stdio 无法跨网络。SSE 长连接配合心跳保活与指数退避重连,让 Agent 能稳定调用远程工具,并享受“服务端主动推送进度”的能力。

具体案例:某 SaaS 公司把“网页抓取+摘要”工具做成远程 MCP server,几十个 Agent 同时接入。运维配置 30 秒心跳,客户端实现 0.5 秒起步的退避重连,线上断连故障率大幅下降。

场景三:混合架构中转网关(演进方向)

阶段:架构设计

解决的问题:内部网络希望统一出口、统一鉴权,同时要访问多个远程 MCP 服务。本地网关进程用 Stdio 接住 Agent,内部转发到各个远程 SSE 端点,未来可平滑升级 WebSocket 全双工。

具体案例:一家银行在 DMZ 区部署 MCP 网关,前端对接内部 Agent(Stdio),后端对接风控模型服务(SSE),所有出站流量只走网关白名单,既保内网速度又守安全合规。

Python 演示

python78 行
import jsonimport subprocessimport sysimport threadingimport timeimport urllib.requestfrom http.server import BaseHTTPRequestHandler, HTTPServer # ---------- 1. Stdio 式消息帧: 一行一个 JSON-RPC ----------def send_request(proc, method, params, req_id=1):    msg = json.dumps({"jsonrpc": "2.0", "id": req_id,                      "method": method, "params": params})    proc.stdin.write(msg + "\n")    proc.stdin.flush()    return json.loads(proc.stdout.readline()) def stdio_server():    for line in sys.stdin:        req = json.loads(line)        reply = {"jsonrpc": "2.0", "id": req["id"],                 "result": {"city": req["params"]["city"],                            "weather": "sunny"}}        sys.stdout.write(json.dumps(reply) + "\n")        sys.stdout.flush() # ---------- 2. SSE 服务端: 每隔 0.5 秒推一条事件 ----------class SSEHandler(BaseHTTPRequestHandler):    def do_GET(self):        if self.path != "/sse":            self.send_error(404)            return        self.send_response(200)        self.send_header("Content-Type", "text/event-stream")        self.end_headers()        for i in range(5):            payload = "data: " + json.dumps({"seq": i, "status": "done"}) + "\n\n"            self.wfile.write(payload.encode())            self.wfile.flush()            time.sleep(0.5)     def log_message(self, *args):        pass def start_sse_server():    server = HTTPServer(("127.0.0.1", 8765), SSEHandler)    threading.Thread(target=server.serve_forever, daemon=True).start()    return server # ---------- 3. SSE 客户端: 带指数退避的重连 ----------def read_sse(url, max_tries=5):    delay = 0.5    for attempt in range(max_tries):        try:            resp = urllib.request.urlopen(url, timeout=3)            print("[attempt %d] connected" % (attempt + 1))            events = []            for raw in resp:                line = raw.decode().strip()                if line.startswith("data:"):                    event = json.loads(line[5:])                    events.append(event)                    print("got:", event)            return events        except Exception as exc:            print("disconnected(%s), retry in %.1fs" % (exc, delay))            time.sleep(delay)            delay = min(delay * 2, 8)    return [] if __name__ == "__main__":    if sys.argv[-1] == "server":        stdio_server()    else:        proc = subprocess.Popen([sys.executable, __file__, "server"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True)        print("Stdio result:", send_request(proc, "get_weather", {"city": "Hangzhou"}))        start_sse_server()        read_sse("http://127.0.0.1:8765/sse")        proc.terminate()
目的:先模拟 Stdio 的“一行一个 JSON-RPC”消息帧,再启动一个 SSE 服务端持续推送事件,最后用带指数退避的客户端读取事件流,完整演示两种传输方式的核心机制。
  1. proc.stdin.write(msg + "\n")把 JSON-RPC 消息加上换行符写入子进程的标准输入,这个换行符就是“帧边界”。
  2. return json.loads(proc.stdout.readline())从标准输出逐行读取响应并解析,一行对应一帧。
  3. self.send_header("Content-Type", "text/event-stream")声明 SSE 类型,告诉客户端这是一个持续事件流而不是普通 JSON 响应。
  4. payload = "data: " + json.dumps({"seq": i, "status": "done"}) + "\n\n"SSE 事件由 data: 前缀与双换行组成,双换行是一条事件结束的标记。
  5. resp = urllib.request.urlopen(url, timeout=3)建立 SSE 长连接,迭代响应对象即可持续读取事件。
  6. delay = min(delay * 2, 8)指数退避的核心:每次失败等待时间翻倍,并封顶在 8 秒。
  7. proc = subprocess.Popen([sys.executable, __file__, "server"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True)以子进程方式拉起服务端,并把标准输入输出接成管道,这就是 Stdio 的“铜管”。

常见误区

误区“SSE 不是也能实时收发吗?服务端推送、客户端再发消息,双向都可以,以后直接用 SSE 替代 WebSocket 就行。”

正解SSE 的英文全称是 Server-Sent Events,本质是服务端单向推送。客户端要发消息,必须另起一个独立的 HTTP POST 请求,事件流本身没有“上行”通道。方向上不对称,语义也不同:SSE 是“听广播”,WebSocket 是“打电话”。用比喻说:驻店快递员只负责从店里往你家送件,你要寄东西得自己再去一趟快递网点。需要严格双向、低延迟交互时,WebSocket 才更合适。

为何常见:平时用 EventSource 时既能“发”(其实是后台又发了一个 POST)又能“收”,容易把两条通道误当成一条双向通道。

误区“Stdio 不走网络、不开端口,外部根本访问不到,肯定安全,不用考虑鉴权。”

正解本地确实屏蔽了网络攻击面,但“不联网”不等于“不可信”。任何能启动该子进程的用户或进程,都可以读写它的 stdin/stdout;如果服务端本身有缺陷,恶意输入照样能触发危险操作。跨用户场景(比如服务运行在共享主机上)仍然需要做进程权限、文件系统权限隔离,必要时对消息内容做校验。用比喻说:铜管只保证“传得近”,不保证“传话的人可信”。

为何常见:人们习惯把“安全”等同于“不可达”,忽略了本地边界内部的威胁。

误区“只要我每秒钟发一个心跳,代理网关永远超不了时,连接肯定稳如泰山。”

正解心跳只解决“代理误杀空闲连接”的问题,弥补不了真实的网络断连;频率过高反而增加无谓的流量、CPU 和日志开销。合理做法是让心跳间隔略小于代理超时的一半(如代理 60 秒,心跳就设 25~30 秒),真正的稳定性要靠断线检测加指数退避重连来兜底。快递员每分钟狂按十次门铃只会烦扰保安,关键是他失联后有没有人补位。

为何常见:把“保活”误当成“加固”,以为频率越高效果越好,忽略了开销与收益的平衡。

误区“只要 EventSource 重新连上了,之前的状态就还在,继续发消息就行。”

正解重连成功后,客户端必须重新走 initialize 握手;服务端可能因超时回收了会话状态,此时要重建整个会话。另外,断线前发出但未确认的请求,重发时必须保持同一个 id,服务端才能据此去重——否则同一个“删除文件”的工具可能被执行两次。用比喻说:新来的快递员不认路,也不带你的历史包裹,一切都要重新交接;之前没送达的件要按原单号再发一次。

为何常见:长连接“看起来一直在”,让人误以为服务端的会话状态也不会丢。

面试题

Q1. MCP 的 Stdio 传输中,消息通过什么传递?

单选题 · easy

  • A.客户端与服务端间的 TCP 长连接
  • B.子进程的标准输入与标准输出← 正确答案
  • C.HTTP POST 的请求与响应
  • D.操作系统共享内存映射文件

答案:B。Stdio 就是“楼内传话管”:客户端以子进程方式拉起服务端,消息通过标准输入输出传递。TCP 长连接是 SSE/WebSocket 的传输基础,共享内存不是 MCP 的标准传输方式。

Q2. SSE 事件流中,一条事件结束的标记是什么?

单选题 · easy

  • A.以单个换行符分隔
  • B.以空行分隔每条事件← 正确答案
  • C.以分号作为分隔符
  • D.以响应结束标记分隔

答案:B。SSE 事件之间用空行(连续两个换行)分隔,相当于快递员每送完一件就停一下再送下一件;单个换行只是字段内部换行,不是事件边界。

Q3. 在 HTTP+SSE 传输中,客户端发送 JSON-RPC 请求的方式是?

单选题 · mid

  • A.通过 GET 请求的查询参数
  • B.在 SSE 事件流内写数据
  • C.向 endpoint 地址发 POST← 正确答案
  • D.用 WebSocket 的 send 方法

答案:C。事件流是单向的“收件通道”,客户端要发请求,必须向服务端下发的 endpoint 地址发 POST,服务端回 202 后稍后把结果沿事件流推回,这正是“下单走快递系统”的映射。

Q4. 下列哪种情况最适合选用 Stdio 传输?

单选题 · mid

  • A.云端 SaaS 供多个租户共享
  • B.同机包装本地 CLI 工具← 正确答案
  • C.跨公网访问内网工具
  • D.需要服务端主动推送消息

答案:B。选 Stdio 的条件是“同机、单客户端、无需网络”,本地包装 CLI 正是典型场景;云端共享、跨公网、需要推送都属于 SSE 的适用范围。

Q5. 关于 SSE 长连接管理,正确的做法包括?

多选题 · mid

  • A.服务端定时发注释事件做心跳← 正确答案
  • B.心跳间隔越小连接一定越稳
  • C.服务端给事件分配递增的 id← 正确答案
  • D.连接断开后客户端立即放弃

答案:AC。心跳可以用 SSE 注释行实现,事件 id 用于追踪与续传;但心跳间隔并非越小越好,断开后也不能直接放弃——重连策略才是稳定性的关键。

Q6. SSE 事件流使用的 MIME 类型是?

单选题 · easy

  • A.用 application/json 编码
  • B.用 text/plain 编码
  • C.用 text/event-stream 编码← 正确答案
  • D.用 text/html 编码

答案:C。SSE 的 MIME 类型是 text/event-stream,这是客户端识别“持续事件流”的依据;普通 JSON 或纯文本都不是流式事件协议。

Q7. 指数退避重连策略中,退避时间序列通常是?

单选题 · mid

  • A.间隔保持恒定不变
  • B.0.5s 后逐次翻倍← 正确答案
  • C.间隔逐次递减加快
  • D.仅在初始时重试一次

答案:B。指数退避的节奏是 0.5s、1s、2s、4s……逐次翻倍直到封顶,常加随机抖动防止重连风暴;恒定间隔或递减都不符合退避原则。

Q8. 关于“断线重连后会话恢复”,下列说法正确的是?

多选题 · hard

  • A.重连后须重新 initialize 握手← 正确答案
  • B.服务端用 session id 关联状态← 正确答案
  • C.重发的请求可随意更换 id
  • D.sessions 回收后需重建会话← 正确答案

答案:ABD。重连后需重新 initialize,会话可能已因超时被回收,此时要重建;服务端用 session id 关联历史状态,重发的请求保持同一 id 才能幂等去重,随意更换 id 会导致重复执行。

Q9. 假设代理网关 60 秒无数据就断开连接,最合理的心跳间隔是?

单选题 · hard

  • A.每秒一次心跳
  • B.每 10 秒一次心跳← 正确答案
  • C.每 55 秒一次心跳
  • D.每 90 秒一次心跳

答案:B。安全的心跳间隔应不超过网关超时的一半,即 30 秒;10 秒稳妥且开销可接受。55 秒太贴近阈值,90 秒必然超时被掐断,1 秒又过于频繁。

Q10. 下列关于 MCP 传输层未来演进的说法,正确的是?

多选题 · hard

  • A.WebSocket 提供全双工通信能力← 正确答案
  • B.流式传输把工具结果分块返回← 正确答案
  • C.Streamable HTTP 沿用 SSE 事件流← 正确答案
  • D.SSE 已被彻底废弃不再使用

答案:ABC。WebSocket 全双工、流式分块传输都是明确演进方向,Streamable HTTP 仍以 SSE 事件流作为服务端推送载体;但 SSE 远未废弃,它依然是重要的服务端推送协议。