因推流握手请求(如OBS、FFmpeg发来的POST或GET)常含原始二进制或query参数,Gin若未显式读取Body、未正确解析query、未返回明确200状态码,或未注册OPTIONS路由,易导致405错误或空响应。

为什么用 gin.Context 处理推流握手容易 405 或空响应
推流端(如 OBS、FFmpeg)发来的握手请求通常是 POST,且常带原始二进制数据或特定格式的 query 参数(比如 app、stream、ts),但默认 Gin 路由不自动解析这类非标准表单或无 body 的请求。如果只写 router.POST("/publish", handler) 却没显式读取 c.Request.Body 或解析 query,Gin 会因未消费请求体导致连接异常中断,推流端报 “Connection reset” 或超时。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 务必在 handler 开头调用
c.Request.Body.Close()(哪怕不读),否则连接可能被复用或阻塞 - 用
c.Query("app")和c.Query("stream")提取关键参数,不要依赖c.PostForm—— 推流端极少发application/x-www-form-urlencoded - 若推流端发送 raw binary(如某些 RTMP over HTTP 封装),需用
c.GetRawData(),但注意它会消耗 body,后续不能再用c.PostForm等 - 响应必须写
c.String(200, "OK")或c.Status(200),不能只写c.Abort()或漏掉状态码 —— 推流端依赖明确 HTTP 状态确认握手成功
如何让同一接口兼容 FFmpeg、OBS 和自研推流 SDK
不同推流工具对握手协议的理解差异很大:FFmpeg 默认发 GET /publish?app=live&stream=test;OBS 可能发 POST /publish 带相同 query;某些 SDK 则用 POST /publish 并在 body 写 JSON。硬编码单一方法会导致部分客户端失败。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 注册两个路由:
router.GET("/publish", handshakeHandler)和router.POST("/publish", handshakeHandler),共用一个 handler 函数 - 在 handler 中统一用
c.Query("app")和c.Query("stream")获取核心字段,它们在 GET 和 POST 中都有效 - 若需区分来源,检查
c.GetHeader("User-Agent"),比如含ffmpeg就走轻量逻辑,含OBS可额外校验c.GetHeader("X-OBS-Version") - 避免在 handler 里做耗时操作(如 DB 查询、鉴权远程调用),推流握手必须在 100ms 内返回,否则推流端会重试或放弃
c.Header("Access-Control-Allow-Origin", "*") 为什么不能解决跨平台预检失败
浏览器环境推流(如 WebRTC 模拟推流)会先发 OPTIONS 预检,而多数音视频 SDK 不发预检 —— 所以加 CORS 头对 OBS/FFmpeg 无效,反而可能干扰某些嵌入式设备。更常见问题是:Gin 默认不注册 OPTIONS 路由,导致预检直接 404,浏览器控制台报 “CORS error”,实际和跨域无关。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 仅当明确支持浏览器推流时才加 CORS,且用
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS"),并单独注册router.OPTIONS("/publish", func(c *gin.Context) { c.Status(204) }) - 移动端或桌面端 SDK 推流时,删掉所有
Access-Control-*头 —— 它们不看这个,加了反而增加响应体积 - 若用 Nginx 做前置代理,把 CORS 和预检处理交给 Nginx,Gin 层保持干净,避免 Gin 中间件重复设置 header 导致冲突
推流握手成功后,怎么安全传递到后端流媒体服务(如 SRS、ZLMediaKit)
Gin 只负责 HTTP 握手,真正的音视频数据走 RTMP、HTTP-FLV 或 WebSocket,所以握手通过后必须把 app 和 stream 映射为后端服务可识别的地址。常见错误是直接拼接字符串生成 rtmp://127.0.0.1:1935/live/test,但没校验输入,导致路径遍历或注入。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 对
c.Query("app")和c.Query("stream")做白名单校验:match, _ := regexp.MatchString(`^[a-zA-Z0-9_-]{1,32}$`, app),拒绝含/、..、控制字符的输入 - 不要硬编码后端地址,从环境变量读:
backendAddr := os.Getenv("STREAM_BACKEND_ADDR"),便于 Docker/K8s 下切换 SRS 或 ZLMediaKit - 若需鉴权,生成一次性 token 写入 Redis(key 为
publish:{app}:{stream},过期设 60s),再把 token 拼进重定向 URL 或响应 body,供流媒体服务回调校验 - 记录日志时脱敏:
log.Printf("publish req from %s for %s/%s", c.ClientIP(), app, stream),避免泄露 stream 名暴露业务结构
真正麻烦的不是写接口,而是每个推流端对“握手成功”的定义不同——有的要 200+OK,有的要 204,有的甚至要求响应 body 是特定二进制字节。上线前必须拿真实设备抓包比对。


















