
本文介绍如何在 go 中通过首字节特征识别客户端协议类型,实现在同一 tcp 端口上智能分流 http 和 rtmp 请求,避免端口冲突与协议转换开销,无需修改第三方库或引入 rtmpt 封装。
本文介绍如何在 go 中通过首字节特征识别客户端协议类型,实现在同一 tcp 端口上智能分流 http 和 rtmp 请求,避免端口冲突与协议转换开销,无需修改第三方库或引入 rtmpt 封装。
在标准 Web 服务中,HTTP 与 RTMP 通常需绑定不同端口(如 :80/:443 与 :1935),但实际流媒体场景常需统一入口(如 CDN 边缘节点或嵌入式网关),以简化防火墙配置、DNS 解析和客户端连接逻辑。Go 原生 net/http 无法直接处理非 HTTP 协议,而 gortmp 等库基于裸 TCP 实现,二者天然隔离。核心挑战在于:如何在不预先约定协议(如 URL 路径 /rtmp)的前提下,仅凭一次 TCP 连接完成协议判别与路由?
答案是利用协议握手阶段的首字节指纹差异进行早期识别:
-
RTMP 握手起始字节为
0x03(RTMP Handshake Version),见 RTMP 规范 §4.1; -
HTTP 请求首行为 ASCII 文本(如
GET / HTTP/1.1\r\n),首个字节必为可打印字符(0x20–0x7E),绝不会是0x03。
据此,我们构建一个协议感知代理层(Protocol-Aware Proxy):监听单一端口,接收原始 TCP 连接,读取首字节后立即决策——转发至本地 HTTP 服务或 RTMP 服务。该方案完全透明,客户端无感知,且规避了 RTMPT(HTTP 封装 RTMP)带来的额外延迟与兼容性问题。
以下是精简可靠的核心实现:
package main
import (
"fmt"
"net"
"net/http"
"net/netip"
"os"
"time"
"github.com/zhangpeihao/gortmp"
)
const (
ListenPort = ":8000"
HTTPPort = ":8001" // HTTP 服务独立运行于 localhost:8001
RTMPPort = ":8002" // RTMP 服务独立运行于 localhost:8002
)
// createLocalConnection 创建到本地后端服务的 TCP 连接
func createLocalConnection(port string) (net.Conn, error) {
dialer := &net.Dialer{Timeout: 5 * time.Second}
return dialer.Dial("tcp", "127.0.0.1"+port)
}
// proxyConnection 根据首字节分流请求
func proxyConnection(clientConn net.Conn) {
defer clientConn.Close()
// 仅读取 1 字节用于协议识别
firstByte := make([]byte, 1)
n, err := clientConn.Read(firstByte)
if err != nil || n == 0 {
fmt.Printf("Failed to read first byte: %v\n", err)
return
}
var backendConn net.Conn
switch firstByte[0] {
case 0x03: // RTMP handshake version
backendConn, err = createLocalConnection(RTMPPort)
default: // 默认视为 HTTP(含 HTTPS、WebSocket 等文本协议)
backendConn, err = createLocalConnection(HTTPPort)
}
if err != nil {
fmt.Printf("Failed to connect to backend: %v\n", err)
return
}
defer backendConn.Close()
// 将已读取的首字节写入后端
_, _ = backendConn.Write(firstByte[:n])
// 双向数据透传(goroutine 并发处理读写)
done := make(chan struct{})
go func() {
// 客户端 → 后端
ioCopyWithClose(backendConn, clientConn, done)
}()
// 后端 → 客户端
ioCopyWithClose(clientConn, backendConn, done)
}
// ioCopyWithClose 封装 io.Copy 并自动关闭目标连接
func ioCopyWithClose(dst, src net.Conn, done chan struct{}) {
defer dst.Close()
_, _ = io.Copy(dst, src)
close(done)
}
func main() {
// 启动独立 HTTP 服务(监听 :8001)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
fmt.Fprint(w, "Hello from HTTP!")
})
go func() {
fmt.Println("HTTP server listening on", HTTPPort)
http.ListenAndServe(HTTPPort, nil)
}()
// 启动独立 RTMP 服务(监听 :8002)
go func() {
server := gortmp.NewServer()
server.HandlePublish(func(s *gortmp.Session, streamName string) error {
fmt.Printf("RTMP publish: %s\n", streamName)
return nil
})
fmt.Println("RTMP server listening on", RTMPPort)
server.ListenAndServe(RTMPPort)
}()
// 主监听器:单端口协议分流
listener, err := net.Listen("tcp", ListenPort)
if err != nil {
panic(fmt.Sprintf("Failed to listen on %s: %v", ListenPort, err))
}
defer listener.Close()
fmt.Printf("Protocol-aware proxy listening on %s (HTTP+RTMP)\n", ListenPort)
for {
conn, err := listener.Accept()
if err != nil {
fmt.Printf("Accept error: %v\n", err)
continue
}
go proxyConnection(conn)
}
}
✅ 关键设计说明:
- 零协议假设:不依赖 TLS SNI、ALPN 或 HTTP Upgrade 头,纯 TCP 层决策,兼容所有 RTMP 客户端(OBS、FFmpeg)及浏览器 HTTP 请求;
- 低延迟启动:仅阻塞读取 1 字节(毫秒级),后续全双工透传,无缓冲放大;
- 进程隔离:HTTP 与 RTMP 服务独立运行,崩溃互不影响;代理层仅作路由,不解析协议内容;
- 可扩展性:新增协议(如 WebSocket、SRT)只需扩展
switch分支并启动对应后端。
⚠️ 注意事项:
- 此方案不适用于 TLS 加密流量(因 TLS ClientHello 首字节为
0x16,与 RTMP0x03冲突),若需 HTTPS+RTMPS 共存,应使用 ALPN 协商或前置 Nginx 做 TLS 终止; - 生产环境需添加连接数限制、超时控制、日志采样及熔断机制;
-
gortmp库需确保版本兼容 Go 1.20+,建议 vendor 锁定 commit。
通过此架构,你获得了一个轻量、可靠、符合云原生理念的单端口多协议网关——它不是“在 HTTP 上跑 RTMP”,而是让 HTTP 和 RTMP 在同一扇门后各行其道。










