不能用 net/http.server 直接做多协议网关入口,因其在 accept 后强制解析 http 请求行和头部,对 websocket upgrade、grpc 二进制前缀、mqtt connect 报文等无感知,会立即关闭连接或 panic;必须用自定义 net.listener 在 accept 后仅读前 16 字节做协议探测,再将连接分发至对应协议服务实例,全程零消费 body、快分流、严守帧边界。

不能用 net/http.Server 直接做多协议网关入口,它只认 HTTP/1.1 和 HTTP/2 帧,对 WebSocket Upgrade、gRPC 二进制前缀、MQTT CONNECT 报文等完全无感知——连接会在 Accept 后立即被丢弃或 panic。
为什么 net/http.Server 不适合做统一协议入口
net/http.Server 在 accept 后强制解析 HTTP 请求行和头部,一旦发现不是合法 HTTP 开头(比如 gRPC 的 PRI * HTTP/2.0 或 MQTT 的 0x10 控制字节),就会直接关闭连接。更糟的是,它会提前读取并缓冲 r.Body,导致下游协议解析器收不到原始帧头,常见报错如:rpc error: code = Internal desc = transport: received the unexpected content-type "text/plain" 或 WebSocket 连接瞬间 400。
- HTTP/2 流量(如 gRPC)必须由
grpc.Server.Serve()直接接管底层net.Conn,不能走http.Handler中间层 - WebSocket 升级请求必须在
http.ResponseWriter上调用Hijack()拿到原始连接,否则 Upgrade 头会被中间件篡改或丢弃 - MQTT/CoAP/自定义 TCP 协议根本不在 HTTP 协议栈里,
http.Server连解析机会都不给
用 net.Listener 实现协议识别与分流
核心是自己包装 net.Listener,在 Accept() 返回 net.Conn 后,只读前 16 字节做协议探测,再把连接“塞回”对应协议的服务实例中处理。这个过程必须快、轻、零消费 Body。
- 读取后立即调用
conn.SetReadDeadline(),防止 TLS 握手卡住整个 accept 循环 - 用
switch判断特征字节:buf[0] == 0x16(TLS record)、bytes.HasPrefix(buf, []byte("GET "))(HTTP)、buf[0]&0xf0 == 0x10(MQTT CONNECT) - HTTP 流量交给改造过的
http.Server(禁用自动 body 解析);gRPC 流量交给grpc.Server.Serve(&oneConnListener{conn});MQTT 交给hmq或mochi-mqtt/server - 切忌在识别逻辑里调用
io.ReadAll(conn)—— 这会破坏所有后续协议的帧边界
如何让 HTTP 和非 HTTP 协议共存于单端口
物理隔离最稳,但若必须单端口,靠 ALPN + 路径前缀硬隔离。ALPN 可区分 h2 和 http/1.1,但无法可靠区分 gRPC(h2)和 WebSocket(h1 + Upgrade),所以得加路径约束。
- 注册三个 handler:
/grpc/→ 立即转给 gRPC codec;/ws/→http.Hijacker升级;其余路径走 REST/gRPC-Web - 对
/mqtt/{topic}这类路径,不走http.ServeMux,而是用http.HandlerFunc拦截后启动独立 goroutine,调用mqtt.ParsePacket()解析原始字节流 - 所有 handler 内禁止调用
r.ParseForm()、r.FormValue()或任何会触发r.Body消费的操作 - gRPC-Web 请求需检查
r.Header.Get("Content-Type") == "application/grpc-web+proto",并用io.MultiReader缓存前 1024 字节供下游重放
协议转换必须显式声明编解码器,不能依赖 magic bytes
一段二进制数据可能是 Protobuf、FlatBuffers、加密 payload 或自定义 TLV,靠前几个字节匹配极易出错,尤其在 TLS 流中——TLS record header 本身就会掩盖真实协议头。
- 对设备上报流量,优先用
encoding/cbor(体积比 JSON 小 30%~50%,Go 原生支持),而非 gob(不跨语言)或 Protobuf(需预编译 .proto) - MQTT PUBLISH 的 topic filter 必须用标准函数
mqtt.MatchTopic(),别用strings.Contains()——sensor/+和sensor/#语义完全不同 - 禁止在 PUBLISH 处理中同步调用外部 HTTP 接口,改用 channel 异步转发到 worker pool,否则高并发下 broker 会拖垮
- 同一路径
/api/users根据Accept头返回不同格式:application/grpc+json→ gRPC-JSON;text/event-stream→ SSE;application/cbor→ CBOR
真正难的不是写几个 switch 分流,而是所有协议共享一套上下文透传机制(context.Context)、熔断状态(sync.Map)、路由规则(etcd/Redis)——这些元数据必须在连接建立初期就注入,而不是等协议解析完才加载。











