HTTPUpgrade 传输
简介
HTTPUpgrade 是一种轻量级传输协议,模拟 WebSocket 握手(HTTP/1.1 Upgrade: websocket)但不使用 WebSocket 帧格式。升级握手完成后,连接变为原始 TCP 流。这使得它比完整的 WebSocket 传输更简单、更高效,同时仍能通过期望 HTTP Upgrade 流的中间设备。它支持 TLS、uTLS 指纹、PROXY 协议和早期数据。
协议注册
以 "httpupgrade" 名称注册(transport/internet/httpupgrade/httpupgrade.go:3):
const protocolName = "httpupgrade"- 拨号器:
httpupgrade/dialer.go:134-136 - 监听器:
httpupgrade/hub.go:165-167 - 配置:
httpupgrade/config.go:19-23
拨号流程
握手
dialhttpUpgrade(httpupgrade/dialer.go:46-115)执行手动 HTTP Upgrade:
func dialhttpUpgrade(ctx context.Context, dest net.Destination,
streamSettings *internet.MemoryStreamConfig) (net.Conn, error) {
// 1. 通过 internet.DialSystem 拨号原始 TCP
pconn, _ := internet.DialSystem(ctx, dest, streamSettings.SocketSettings)
// 2. 可选地用 TLS 包装
if tConfig != nil {
// 应用带 uTLS 指纹的 TLS 或标准 Go TLS
conn = tls.UClient(pconn, tlsConfig, fingerprint) // 或 tls.Client
}
// 3. 构建 HTTP 请求
req := &http.Request{
Method: http.MethodGet,
URL: &requestURL,
Header: make(http.Header),
}
req.Header.Set("Connection", "Upgrade")
req.Header.Set("Upgrade", "websocket")
// 4. 将请求直接写入连接
req.Write(conn)
// 5. 用 ConnRF 包装以读取响应
connRF := &ConnRF{Conn: conn, Req: req, First: true}
return connRF, nil
}与 WebSocket 传输不同,这里不使用 gorilla/websocket。HTTP 请求直接写入 TCP 流,响应也手动解析。
响应读取(ConnRF)
ConnRF 结构体(httpupgrade/dialer.go:19-44)拦截首次 Read 调用以解析 HTTP 响应:
type ConnRF struct {
net.Conn
Req *http.Request
First bool
}
func (c *ConnRF) Read(b []byte) (int, error) {
if c.First {
c.First = false
reader := bufio.NewReaderSize(c.Conn, len(b))
resp, err := http.ReadResponse(reader, c.Req)
if resp.Status != "101 Switching Protocols" ||
strings.ToLower(resp.Header.Get("Upgrade")) != "websocket" ||
strings.ToLower(resp.Header.Get("Connection")) != "upgrade" {
return 0, errors.New("unrecognized reply")
}
// 排空 bufreader 中缓冲的字节
return reader.Read(b[:reader.Buffered()])
}
return c.Conn.Read(b)
}关键设计:bufio.Reader 的大小限制为 len(b),确保 HTTP 响应头之后的任何数据(可能在同一 TCP 报文段中到达)都能被捕获并在首次读取中返回。
早期数据
当 Ed == 0(默认值)时,响应在拨号期间立即读取以确认升级成功(dialer.go:107-112)。当 Ed > 0 时,响应读取推迟到应用程序的首次 Read 调用,使拨号更快完成。
头部处理
通过 AddHeader 函数(dialer.go:120-122)添加自定义头部,该函数绕过 Go 的 MIME 头部规范化:
func AddHeader(header http.Header, key, value string) {
header[key] = append(header[key], value)
}这保留了头部名称的精确大小写(例如 "Web*S*ocket" 而非 "Websocket")。
监听流程
服务器架构
ListenHTTPUpgrade(httpupgrade/hub.go:115-163)创建原始 TCP 监听器(而非 HTTP 服务器):
func ListenHTTPUpgrade(ctx context.Context, address net.Address, port net.Port,
streamSettings *internet.MemoryStreamConfig, addConn internet.ConnHandler) (internet.Listener, error) {
// 通过 internet.ListenSystem 创建 TCP/Unix 监听器
// 可选地用 TLS 包装
serverInstance := &server{
config: transportConfiguration,
addConn: addConn,
innnerListener: listener,
}
go serverInstance.keepAccepting()
return serverInstance, nil
}与 WebSocket 传输(使用 http.Server)不同,HTTPUpgrade 接受原始连接并手动解析 HTTP 请求。
连接处理
server.Handle(httpupgrade/hub.go:34-42)和 server.upgrade(hub.go:45-103):
func (s *server) upgrade(conn net.Conn) (stat.Connection, error) {
connReader := bufio.NewReader(conn)
req, _ := http.ReadRequest(connReader)
// 验证 host 和路径
if len(s.config.Host) > 0 && !internet.IsValidHTTPHost(host, s.config.Host) {
return nil, errors.New("bad host")
}
if req.URL.Path != path {
return nil, errors.New("bad path")
}
// 验证升级头部
if connection != "upgrade" || upgrade != "websocket" {
return nil, errors.New("unrecognized request")
}
// 发送 101 响应
resp := &http.Response{
Status: "101 Switching Protocols",
StatusCode: 101,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: http.Header{},
}
resp.Header.Set("Connection", "Upgrade")
resp.Header.Set("Upgrade", "websocket")
resp.Write(conn)
return stat.Connection(newConnection(conn, remoteAddr)), nil
}X-Forwarded-For
从转发头部提取真实客户端 IP(hub.go:83-100),遵循 TrustedXForwardedFor 套接字设置。
线格式
客户端 -> 服务端:
GET /path HTTP/1.1
Host: example.com
Connection: Upgrade
Upgrade: websocket
User-Agent: Mozilla/5.0 ...
[自定义头部]
服务端 -> 客户端:
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
[原始双向 TCP 流]握手完成后,数据以原始字节流传输 —— 无 WebSocket 帧格式、无长度前缀、无掩码。这是与完整 WebSocket 传输的关键区别。
sequenceDiagram
participant Client as 客户端
participant Server as 服务端
Client->>Server: GET /path HTTP/1.1\r\nUpgrade: websocket\r\n...
Server->>Client: HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n\r\n
Note over Client,Server: 原始 TCP 流开始
Client->>Server: [代理数据字节]
Server->>Client: [代理数据字节]连接包装器
connection 结构体(httpupgrade/connection.go:5-19)非常简洁:
type connection struct {
net.Conn
remoteAddr net.Addr
}
func (c *connection) RemoteAddr() net.Addr {
return c.remoteAddr
}它只覆盖 RemoteAddr() 以支持 X-Forwarded-For。所有其他方法委托给底层 net.Conn。
PROXY 协议支持
客户端和服务端均支持 PROXY 协议:
- 服务端:继承自套接字设置。当
AcceptProxyProtocol为 true 时,底层internet.ListenSystem包装监听器(hub.go:117-122,hub.go:145-147)。 - 配置合并:传输配置或套接字设置中的
AcceptProxyProtocol均会触发 PROXY 协议(hub.go:121)。
配置选项
来自 httpupgrade/config.go:
- Path:URL 路径,以
/开头规范化(config.go:8-17) - Host:服务端验证用的预期 Host 头部
- Header:自定义 HTTP 头部(映射)
- AcceptProxyProtocol:在监听器上启用 PROXY 协议
- Ed:早期数据大小。非零时,响应解析推迟。
实现说明
- 无 WebSocket 帧:
101 Switching Protocols响应之后,数据以原始字节发送。这消除了 WebSocket 开销(每帧 2-14 字节)和掩码要求。 - 无 gorilla 依赖:与 WebSocket 传输不同,HTTPUpgrade 不使用
gorilla/websocket,直接构建和解析 HTTP 消息。 - 无状态服务器:每个连接独立处理,服务器不维护会话状态。
- 头部大小写保留:
AddHeader函数绕过 Go 的textproto.CanonicalMIMEHeaderKey,允许精确大小写的头部名称。这有助于匹配特定 CDN 或中间设备的期望。 - bufio 大小技巧:
ConnRF.Read创建的bufio.ReaderSize(conn, len(b))限制为调用者缓冲区大小。这确保缓冲读取器不会读取超过可返回的数据量,防止双缓冲导致的数据丢失。 - 监听器 TLS:与 gRPC(使用 gRPC 的凭证系统)不同,HTTPUpgrade 直接使用
tls.NewListener包装 net.Listener(hub.go:149-153)。 - 与 WebSocket 的比较:HTTPUpgrade 在代理用途上严格更简单、更高效。只有在需要 WebSocket 特定功能(心跳 ping、逐消息压缩、浏览器拨号器)时才应优先选择 WebSocket 传输。