目录

go gin 框架如何使用 websocket

在 Web 开发中,实时通讯是很多场景的核心需求 —— 比如在线聊天、实时数据监控、订单状态推送、协作工具同步等。

而 WebSocket 作为 HTML5 标准的实时通讯协议,能实现客户端与服务端的全双工通信,相比轮询、长轮询更高效、低延迟。

那么在 go gin 框架中应该如何使用 websocket 呢?

一、安装

Gin本身没有内置WebSocket模块,业界常用的是gorilla/websocket库,这个库成熟稳定,文档也比较全。

安装命令:

go get -u github.com/gorilla/websocket

这里提一嘴,为什么不用标准库?主要是标准库的WebSocket支持比较基础,很多实际开发需要的功能比如心跳检测、消息分片都得自己实现,gorilla/websocket已经封装好了,能省不少事。

二、实现:Gin+WebSocket

这部分是核心,主要分服务端和客户端两部分。

(一)服务端实现

服务端的核心逻辑是接收客户端的HTTP请求,把它升级成WebSocket连接,然后持续监听客户端消息并处理。

直接上代码,关键地方我加了详细注释:

package main

import (
    "log"
    "net/http"

    "github.com/gin-gonic/gin"
    "github.com/gorilla/websocket"
)

// 定义升级器,用于把HTTP连接升级为WebSocket
var upgrader = websocket.Upgrader{
    // 允许跨域,实际生产环境要指定具体的Origin,不要用*
    CheckOrigin: func(r *http.Request) bool {
        return true
    },
    // 设置读写缓冲区大小,根据实际需求调整
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
}

// WebSocket处理函数
func wsHandler(c *gin.Context) {
    // 1. 升级HTTP连接为WebSocket连接
    conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
    if err != nil {
        log.Printf("升级连接失败: %v", err)
        return
    }
    // 3. 延迟关闭连接,确保资源释放
    defer func() {
        if err := conn.Close(); err != nil {
            log.Printf("关闭连接失败: %v", err)
        }
    }()

    log.Println("客户端连接成功")

    // 2. 持续监听客户端消息
    for {
        // 读取客户端发送的消息,messageType是消息类型(文本/二进制),p是消息内容
        messageType, p, err := conn.ReadMessage()
        if err != nil {
            log.Printf("读取消息失败: %v", err)
            break
        }
        // 打印客户端消息
        log.Printf("收到客户端消息: %s", p)

        // 3. 回复客户端消息(echo模式,实际开发可替换为业务逻辑)
        err = conn.WriteMessage(messageType, []byte("服务端已收到: "+string(p)))
        if err != nil {
            log.Printf("发送消息失败: %v", err)
            break
        }
    }
}

func main() {
    // 初始化Gin引擎,生产环境用gin.ReleaseMode
    r := gin.Default()

    // 注册WebSocket路由,注意用GET方法
    r.GET("/ws", wsHandler)

    // 启动服务,监听8080端口
    if err := r.Run(":8080"); err != nil {
        log.Fatalf("服务启动失败: %v", err)
    }
}

这里有个细节得注意:WebSocket路由必须用GET方法,因为WebSocket握手是基于GET请求的。

另外,生产环境里CheckOrigin一定不能写return true,要根据自己的域名设置白名单,

比如return r.Header.Get("Origin") == "https://yourdomain.com",防止跨域攻击。

(三)客户端实现

客户端用原生的WebSocket API就行,写个简单的HTML页面就能测试。

创建一个index.html文件,内容如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Gin WebSocket测试</title>
</head>
<body>
    <h1>Gin WebSocket测试工具</h1>
    <div>
        <input type="text" id="messageInput" placeholder="输入消息">
        <button onclick="sendMessage()">发送</button>
        <button onclick="closeConn()">关闭连接</button>
    </div>
    <div id="messageList" style="margin-top: 20px;"></div>

    <script>
        // 初始化WebSocket连接,注意协议是ws(http对应ws,https对应wss)
        let ws = new WebSocket("ws://localhost:8080/ws");
        let messageList = document.getElementById("messageList");
        let messageInput = document.getElementById("messageInput");

        // 连接成功回调
        ws.onopen = function() {
            addMessage("连接成功");
        };

        // 接收服务端消息回调
        ws.onmessage = function(event) {
            addMessage("服务端回复: " + event.data);
        };

        // 连接关闭回调
        ws.onclose = function() {
            addMessage("连接关闭");
        };

        // 连接错误回调
        ws.onerror = function(error) {
            addMessage("连接错误: " + error);
        };

        // 发送消息函数
        function sendMessage() {
            let msg = messageInput.value.trim();
            if (!msg) {
                alert("请输入消息");
                return;
            }
            ws.send(msg);
            addMessage("客户端发送: " + msg);
            messageInput.value = "";
        }

        // 关闭连接函数
        function closeConn() {
            ws.close();
        }

        // 添加消息到页面
        function addMessage(content) {
            let div = document.createElement("div");
            div.textContent = new Date().toLocaleString() + ": " + content;
            messageList.appendChild(div);
        }
    </script>
</body>
</html>

(三)启动测试

  1. 把服务端代码保存为main.go,客户端代码保存为index.html,放在同一个目录下;
  2. 执行go run main.go启动服务端,看到“[GIN-debug] Listening and serving HTTP on :8080”说明启动成功;
  3. 用浏览器打开index.html,输入消息点击“发送”,就能看到客户端和服务端的消息交互了;
  4. 同时观察服务端控制台,会打印客户端连接信息和收到的消息。

四、进阶优化

(一)心跳检测:防止连接被断开

很多网关或服务器会断开长时间没有数据交互的连接,所以必须加心跳检测。

原理就是客户端和服务端定期互相发送心跳包,证明连接还活着。

修改服务端的wsHandler函数,添加心跳检测逻辑

func wsHandler(c *gin.Context) {
    conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
    if err != nil {
        log.Printf("升级连接失败: %v", err)
        return
    }
    defer conn.Close()

    log.Println("客户端连接成功")

    // 设置心跳超时时间:30秒内没收到客户端消息就关闭连接
    conn.SetReadDeadline(time.Now().Add(30 * time.Second))
    // 心跳重置函数,每次收到客户端消息就刷新超时时间
    conn.SetPongHandler(func(string) error {
        conn.SetReadDeadline(time.Now().Add(30 * time.Second))
        return nil
    })

    // 启动心跳发送协程,每10秒发一次心跳包
    go func() {
        ticker := time.NewTicker(10 * time.Second)
        defer ticker.Stop()
        for range ticker.C {
            // 发送ping消息(心跳包)
            if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
                log.Printf("发送心跳失败: %v", err)
                return
            }
        }
    }()

    // 消息监听逻辑不变
    for {
        messageType, p, err := conn.ReadMessage()
        if err != nil {
            log.Printf("读取消息失败: %v", err)
            break
        }
        log.Printf("收到客户端消息: %s", p)
        err = conn.WriteMessage(messageType, []byte("服务端已收到: "+string(p)))
        if err != nil {
            log.Printf("发送消息失败: %v", err)
            break
        }
    }
    log.Println("客户端连接断开")
}

客户端也需要对应处理ping消息,在JS里添加:

// 处理服务端的ping消息,自动回复pong
ws.onping = function(event) {
    ws.pong();
    addMessage("收到服务端心跳包");
};

(二)消息分片:处理大消息

如果要传输超过缓冲区大小的大消息,gorilla/websocket会自动分片,但需要确保客户端能正确处理。

服务端不需要额外配置,客户端原生WebSocket也支持自动拼接分片消息,所以只要缓冲区设置合理(前面代码里的ReadBufferSizeWriteBufferSize),一般没问题。

如果消息特别大(比如几MB),建议在业务层做分片,比如分块发送并带序号,服务端再拼接。

常见问题

Q1、连接失败:升级HTTP为WebSocket失败

问题现象:服务端报错“upgrader: request header not present”或客户端报“WebSocket connection to ‘ws://xxx’ failed”。

常见原因

  • 路由用了POST方法,WebSocket必须用GET;
  • 跨域问题,CheckOrigin没有允许客户端域名;
  • 请求头里没有WebSocket相关的字段,可能被网关拦截了。

解决方案

  • 确保路由是GET方法;
  • 生产环境配置正确的CheckOrigin白名单;
  • 如果有nginx等网关,配置转发时保留Upgrade和Connection头,比如nginx配置:
location /ws {
    proxy_pass http://localhost:8080;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
}

Q2、消息发送失败:写入连接时出错

问题现象:服务端报“write: broken pipe”或“websocket: close sent”。

常见原因

  • 客户端已经断开连接,但服务端还在发消息;
  • 心跳超时导致连接被关闭。

解决方案

  • 发送消息前检查连接状态,或用defer和错误处理捕获写入错误;
  • 调整心跳超时时间,根据实际网络情况设置(一般10-60秒)。

Q3、消息粘包:多个消息被合并接收

问题现象:客户端连续发两条消息,服务端一次收到了合并后的内容。

常见原因:TCP是流式协议,没有消息边界,当发送频率过高时会出现粘包。

解决方案

  • 定义消息格式,比如用JSON包裹,每个消息带长度字段;
  • 发送消息后加微小延迟(不推荐,影响性能);
  • 使用gorilla/websocket的NextReaderNextWriter方法,明确消息边界。

总结

go gin 使用 websocket 还是非常简单的,有非常成熟的库可以直接使用。

上面的代码我都实际跑过,有问题的话可以对照常见问题排查,或者在评论区留言交流!!!

版权声明

未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!

本文原文链接: https://fiveyoboy.com/articles/go-gin-websocket/

备用原文链接: https://blog.fiveyoboy.com/articles/go-gin-websocket/