不能在同一个 net.Listen("tcp", ...) 上混用 HTTP 和自定义 TCP 协议,因为 HTTP 是需完整解析请求行/头/body 的应用层协议,而裸 TCP 仅为字节流,语义冲突必然导致粘包、解析失败或连接被 reset。

直接结论:别在同一个 net.Listen("tcp", ...) 上混用 HTTP 和自定义 TCP 协议——HTTP 是应用层协议,需要完整解析请求行、头、body;裸 TCP 只是字节流,两者语义冲突,强行共用端口必然导致粘包、解析失败或连接被 reset。
为什么不能让 HTTP Server 和 TCP Server 共享一个 listener
HTTP 服务(如 http.Server)内部会调用 conn.Read() 并期望读到完整的 HTTP 请求(含 GET /path HTTP/1.1\r\n...),而你的 TCP 服务也从同一连接读取,结果就是:
- HTTP server 读到一半 TCP 包,报
malformed HTTP request或直接 panic - TCP server 读到 HTTP 头碎片,当成二进制 payload 解析失败
- 连接被任一端提前关闭,另一方收不到 EOF 或卡死在
Read() - 无法区分 incoming connection 是 HTTP 还是 raw TCP——没有 magic number 或握手前缀可判别
正确做法:端口分离 + 统一生命周期管理
每个协议独占一个端口,但共享配置、日志、信号处理和 shutdown 流程。关键不是“复用连接”,而是“复用控制权”。
- HTTP 用
http.Server监听:8080,路由注册走http.HandleFunc或自定义http.ServeMux - TCP 用
net.Listen("tcp", ":8081"),自己实现帧解析(如 TLV、换行分隔、固定头长),避免粘包 - 把两个 server 封装为
Listener接口:Addr() string、ListenAndServe() error、Shutdown(context.Context) error - 顶层
App.Run()启动 goroutine 并发跑两者,收到os.Interrupt时先调http.Server.Shutdown(),再调 TCP listener 的 close logic
如何让 TCP 和 HTTP 调用同一套业务逻辑
核心是“协议无关的服务接口”。不要让 handler 或 connection handler 直接写业务,而是提取纯 Go 方法:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 定义
type UserService interface { CreateUser(ctx context.Context, req *CreateUserReq) (*CreateUserResp, error) } - HTTP adapter 中:
req := parseHTTPBody(r.Body); resp, err := svc.CreateUser(r.Context(), req) - TCP adapter 中:
req := decodeTCPFrame(buf); resp, err := svc.CreateUser(ctx, req) - 错误统一转成
AppError,由各自 adapter 决定怎么返回(HTTP 写 status+JSON,TCP 写 error code + msg)
容易被忽略的细节:TCP 粘包与上下文取消传递
HTTP 天然带 context.WithTimeout 和 Request.Context(),TCP 没有——你必须自己做。
- 在 TCP 协议设计里预留超时字段,或约定每个 frame 前 4 字节为 deadline(Unix timestamp)
- 启动处理 goroutine 时传入
context.WithTimeout(parentCtx, timeout),并在 read/write 中检查ctx.Err() - 不要用
time.AfterFunc清理连接,它不感知 context cancel;改用select { case - HTTP 的
http.Server.ReadTimeout和 TCP 的conn.SetReadDeadline()必须配平,否则一方已断连,另一方还在等
真正难的不是启动两个服务,而是让它们在崩溃、重启、超时、信号中断时行为一致——这要求所有协议入口都经过同一套 context 生命周期、错误分类、指标打点和日志字段注入。否则看似支持多协议,实则各管一段,运维起来全是黑洞。

















