http.ServeMux 不支持 WebSocket 升级,因其仅匹配请求方法和路径,不处理 Upgrade 头或调用 Hijack(),导致返回 200 而非 101;需自定义 handler 或改用 gorilla/mux 实现 SSE/WS 双协议路由。

不能直接用 http.ServeMux 同时处理 SSE 和 WebSocket 请求——它没有内置连接升级支持,也不维护长连接生命周期。你得用更底层的 http.Handler 组合逻辑,或换用支持路由分发的中间件层。
为什么 Mux 本身不支持 WebSocket 升级
http.ServeMux 是纯 HTTP/1.1 请求分发器,只匹配 GET/POST 等方法和路径,不检查 Upgrade: websocket 头,也不会调用 http.Hijack()。一旦你注册了 /ws 路由,它仍会走标准响应流程,导致客户端收到 200 + HTML 或空响应,而非 101 Switching Protocols。
常见错误现象:
- 浏览器报错
WebSocket connection to 'ws://...' failed: Error during WebSocket handshake - 服务端日志里看不到
gorilla/websocket的Upgrader.Upgrade()调用痕迹 - 用
curl -i -H "Upgrade: websocket" http://localhost/ws得到 200 而非 101
解决方案是绕过 ServeMux 的默认 dispatch,手动接管连接:
立即学习“go语言免费学习笔记(深入)”;
- 注册一个通配路由(如
"/ws"或"/sse")到自定义http.HandlerFunc - 在 handler 内部判断
r.Header.Get("Upgrade") == "websocket",再交给gorilla/websocket.Upgrader - 对 SSE 路由则只设头、保持连接、流式写入
data:块
如何用 gorilla/mux 替代原生 ServeMux 实现双协议路由
gorilla/mux 是兼容的替代品,它允许你为同一路径注册不同方法的 handler,也支持子路由器嵌套。关键点在于:WebSocket handler 必须是最终 endpoint,不能再被其他中间件拦截或修改 response writer。
实操建议:
- 安装:
go get github.com/gorilla/mux - 为 WebSocket 单独注册
Methods("GET").HandlerFunc(wsHandler),避免与其他方法冲突 - 为 SSE 注册
Methods("GET").Headers("Accept", "text/event-stream").HandlerFunc(sseHandler),靠 header 区分 - 不要在这些 handler 上加 gzip 中间件或日志中间件——它们会提前 write header,破坏 SSE/WS 流式语义
示例片段:
router := mux.NewRouter()
upgrader := websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}
router.HandleFunc("/stream", sseHandler).Methods("GET").
Headers("Accept", "text/event-stream")
router.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
defer conn.Close()
// handle ws logic
}).Methods("GET")
并发模型下 SSE 连接容易泄漏的几个坑
Go 的 SSE handler 本质是长期阻塞的 goroutine,每个连接都占一个 goroutine 和 TCP 连接。没做清理,1000 个客户端就可能拖垮服务。
必须显式控制:
- 设置
context.WithTimeout或心跳检测,超时后主动 close response writer - 在 handler 开头调用
w.(http.Hijacker).Hijack()并 deferconn.Close()(仅 SSE 不需要 hijack,但需注意 writer 是否被其他中间件 wrap) - 用
net/http.TimeoutHandler包裹整个 SSE handler 是无效的——它只管初始响应,不管后续流式写入 - 务必在写入前检查
conn.CloseNotify()或用responseWriter.CloseNotify()(已废弃),推荐改用http.DetectContentType+context.Context.Done()配合 channel select
MCP Server 的 SSE 中继要特别注意工具调用上下文
如果你把 MCP 的 sseServer.Start() 直接挂到 mux 下,会丢失请求上下文(比如 client IP、trace ID、auth token),导致 echoHandler 无法获取调用来源或做权限校验。
正确做法是:
- 不在
sseServer.Start()启动独立 server,而是将 MCP 的HandleSSE方法提取为 handler 函数 - 在 mux handler 中手动构造
mcp.ServerOptions,传入带 auth middleware 的 context - 确保
callToolRequest的params.arguments解析不依赖原始 request body(SSE 是单向流,MCP 工具调用实际走的是另一路 POST /tool_call) - 别把
mcp-go/server的serveSSE和serveHTTP混用在同一端口——前者监听并阻塞,后者才适配 mux
真正可部署的中继结构是:mux → 分流到 /mcp/sse(流式响应)和 /mcp/tool_call(普通 JSON POST),两者共享同一个 mcp.Server 实例和 tool registry。
最易被忽略的一点:SSE 连接关闭后,mcp-go 默认不会自动清理内部 session map,如果每个连接都注册唯一 client id,不手动调用 server.RemoveClient(id),内存会缓慢增长。这个动作必须放在 handler 的 defer 或 context cancel 回调里。


















