You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于nhooyr.io/websocket的代理拦截yamux多路复用WebSocket数据时出现部分乱码,如何解析完整可读内容?

基于nhooyr.io/websocket的代理拦截yamux多路复用WebSocket数据时出现部分乱码,如何解析完整可读内容?

问题背景

我原本有一个直接连接WebSocket服务器的客户端:

WS_Server <--> WS_Client

现在需要一个正向代理来拦截客户端和服务器之间的数据,架构变成:

WS_Server <--> Proxy <--> WS_Client

我用nhooyr.io/websocket实现了一个简单的HTTP服务器作为代理:客户端通过dial连接代理,代理accept连接后再dial目标WS服务器,之后在两个连接之间转发数据。其中客户端到代理的连接未加密,代理到服务器的连接是TLS加密的。

目前代理能正常工作,也能拦截到数据(就是代码里的msg变量),但只有极少部分数据是可读的,大部分都是乱码。我用Wireshark抓包后,解包看到的内容和日志里的一致。另外需要说明的是,这个WebSocket连接之上还使用了yamux做多路复用。

我的代理代码

...

import (
    ...
    "nhooyr.io/websocket"
)

func main() {
    http.HandleFunc("/rest/v1/client/proxy", handleWebSocketConnection)
    err := http.ListenAndServe(":8081", nil)
    if err != nil {
        log.Fatalf("Failed to start proxy server: %v", err)
    }
}

func handleWebSocketConnection(w http.ResponseWriter, r *http.Request) {
    // Accept WebSocket conn from Client
    connA, err := websocket.Accept(w, r, nil)
    if err != nil {
        log.Printf("Failed to accept WebSocket connection: %v", err)
        return
    }
    defer connA.Close(200, "Closing connectionA")

    // Connect to Dest Server
    caCert, err := os.ReadFile(caCertPath)
    caPool := x509.NewCertPool()
    if !caPool.AppendCertsFromPEM(caCert) {
        log.Fatalf("Failed to append CA certificate")
    }
    tlsConfig := &tls.Config{
        RootCAs: caPool,
    }
    httpClient := &http.Client{
        Transport: &http.Transport{
            TLSClientConfig: tlsConfig,
        },
    }
    // Dial to dest 
    connB, _, err := websocket.Dial(r.Context(), DestWsAddr, &websocket.DialOptions{
        HTTPClient: httpClient,
    })
    if err != nil {
        log.Printf("Failed to connect to Dest: %v", err)
        return
    }
    defer connB.Close(200, "Closing connectionB")

    // Start goroutines to forward messages between client and agg
    go forwardMessages(connA, connB)
    go forwardMessages(connB, connA)

    // Keep the connection open indefinitely
    select {}
}

func forwardMessages(src, dst *websocket.Conn) {
    ctx := context.Background()
    for {
        // Read messages from the source connection
        msgType, msg, err := src.Read(ctx)
        log.Printf(" : [%v]", string(msg))
        if err != nil {
            log.Printf("Failed to read message: %v", err)
            return
        }

        // Write the message to the destination connection
        err = dst.Write(ctx, msgType, msg)
        if err != nil {
            log.Printf("Failed to write message: %v", err)
            return
        }
    }
}

日志片段

...
...
2024/11/13 06:13:56 : [HTTP/1.1 200 OK
]
2024/11/13 06:13:56 : []
2024/11/13 06:13:56 : [Content-Length: 0
]
2024/11/13 06:13:56 : []
2024/11/13 06:13:56 : [
]
2024/11/13 06:13:56 : []
2024/11/13 06:13:56 : [��p�    V�Cd^너+�Cn2A�/�RS�x�e8 �W��R����c;E�.�ى�+D2td&gt;�K�,*'internalServer.server.abc.xyz.com
�+3&$ ����,�-)��b�a(�%/�s�L����9]
2024/11/13 06:13:56 : [�]
0��M&�� �3 06:13:56 : [zv�-ee�j�N�=���&�m8T[���=��P �W��R����c;E�.�ى�+D2td&gt;�K.+3$ ��{6�
         `�N���NAw��^X��y,R5`��i_���>K4�6Y��aɂ�w;vSѰ]
                                                     �n��[���`���ab�+J��%�=���bu���O�p�iUSV�/_���9�U������[Z�~cn-�l�v��i>X���}U�I�ds�Z�v�?&�r������<�����Y�T��λ���T܌

问题分析与解决思路

别急,我来帮你梳理下问题所在:

1. 乱码的核心原因:yamux的二进制帧格式

你看到的可读内容应该是yamux建立连接时的握手HTTP报文,而后面的乱码是yamux的二进制帧数据。yamux是基于字节流的多路复用协议,它会把上层数据封装成自己的二进制帧格式(包含长度、类型、流ID等头部信息),这些帧不是纯文本,直接转成string自然会显示乱码。

2. 如何解析yamux数据?

要读取到可读的上层数据,你需要在代理中把yamux的帧解码出来,而不是直接转发原始的WebSocket消息。具体步骤如下:

  • 在代理中接入yamux的会话:当代理和客户端、代理和服务器的WebSocket连接建立后,分别把这两个WebSocket连接转换成yamux的Session。因为WebSocket本身是双向字节流,你可以用websocket.NetConn把websocket.Conn转换成标准的net.Conn,这样yamux就能直接使用。
  • 监听yamux的流:yamux会话会创建多个子流,你需要在代理中监听这些子流,然后读取子流里的实际数据(这些数据才是你要的可读内容),同时还要负责转发子流的数据到另一端的yamux会话。

修改后的核心代码大概是这样:

func handleWebSocketConnection(w http.ResponseWriter, r *http.Request) {
    // 省略原有的连接建立代码...

    // 把WebSocket连接转换成net.Conn,供yamux使用
    clientConn := websocket.NetConn(r.Context(), connA, websocket.MessageBinary)
    defer clientConn.Close()

    serverConn := websocket.NetConn(r.Context(), connB, websocket.MessageBinary)
    defer serverConn.Close()

    // 初始化yamux会话:客户端侧作为客户端会话,服务器侧作为服务器会话(或者根据实际情况调整)
    clientSession, err := yamux.Client(clientConn, nil)
    if err != nil {
        log.Printf("Failed to create client yamux session: %v", err)
        return
    }
    defer clientSession.Close()

    serverSession, err := yamux.Server(serverConn, nil)
    if err != nil {
        log.Printf("Failed to create server yamux session: %v", err)
        return
    }
    defer serverSession.Close()

    // 启动goroutine处理两边的yamux流
    go handleYamuxSessions(clientSession, serverSession)
    go handleYamuxSessions(serverSession, clientSession)

    select {}
}

func handleYamuxSessions(srcSession, dstSession *yamux.Session) {
    for {
        // 接受来自源会话的子流
        srcStream, err := srcSession.Accept()
        if err != nil {
            log.Printf("Failed to accept yamux stream: %v", err)
            return
        }
        // 创建对应的目标子流
        dstStream, err := dstSession.Open()
        if err != nil {
            log.Printf("Failed to open yamux stream: %v", err)
            srcStream.Close()
            continue
        }
        // 转发两个流之间的数据,同时可以在这里拦截解析数据
        go forwardStream(srcStream, dstStream)
    }
}

func forwardStream(src, dst io.ReadWriteCloser) {
    defer src.Close()
    defer dst.Close()

    // 这里可以用io.Copy同时转发,或者自己读取数据后再写入
    buffer := make([]byte, 4096)
    for {
        n, err := src.Read(buffer)
        if err != nil {
            if err != io.EOF {
                log.Printf("Failed to read from stream: %v", err)
            }
            return
        }
        data := buffer[:n]
        // 这里可以解析data,因为这是yamux子流里的实际上层数据,如果是文本的话转string就可读了
        log.Printf("Intercepted data: %s", string(data))
        _, err = dst.Write(data)
        if err != nil {
            log.Printf("Failed to write to stream: %v", err)
            return
        }
    }
}

3. 额外注意事项

  • WebSocket消息类型:yamux使用的是二进制消息类型,所以在调用websocket.NetConn时指定websocket.MessageBinary,确保数据传输的正确性。
  • 流的生命周期管理:每个yamux子流都需要正确关闭,避免资源泄漏。
  • 数据编码格式:即使解析了yamux流,如果上层数据本身是二进制格式(比如Protobuf),你还是需要对应的解码器来转换成可读内容,这时候可以根据业务情况添加对应的解码逻辑。

总结

你现在看到的乱码是yamux的原始帧数据,不是最终的业务数据。只要在代理中加入yamux的会话处理,解码出子流里的内容,就能得到可读的实际数据啦。

备注:内容来源于stack exchange,提问作者ray an

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.15 10:03:12