关于websocket,真实场景踩坑经验

我的项目架构和使用场景

  • 前端:我的在线简历(服务端渲染),在线音乐(nginx客户端渲染)
  • 管理端:vue3 elementuiplus echarts
  • 服务端:gin框架gorm+mysql+redis(个人不建议gin使用mysql,因为postgresql足够优秀,性能高使用简单)

我的简历和音乐都有浏览记录提交功能,有用户访问时会用websocket推送消息给管理端的首页数据可视化,让echarts图表实时变化,前段时间做了整体优化,之前博客提过,让nginx网关扛下所有攻击,由于没有日志系统,网站被攻击,删了大多数数据,凌晨时候还有很多莫名其妙的攻击,所以做了升级

之前的websocket是直连的服务端ip,也没有做wesocket断开自动重连,偶尔会断开,要手动刷新前端页面重新连接,毕竟是自己用,麻烦点也无所谓

升级后隐藏了服务端,ws链接改为nginx代理,这时候就出现了链接后很快就断了,

1,阶段1的解决方案

修改nginx配置,延长nginx连接时长

ini 复制代码
location /ws {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 300s;
    proxy_send_timeout 300s;
}

修改后确实连接的时间长了,但是考虑到性能,也不能无限长

2,第2阶段给后端加断开自动重连功能
go 复制代码
func sedMessage(scene string) {
    l := len(conns)
    for i := 0; i < len(conns); i++ { // 这个循环有问题后续说
        if index := strings.Index(scene, "jianli"); index != -1 {
            e := conns[i].WriteMessage(websocket.TextMessage, []byte("jianli"))
            if e == nil {
                return
            }
            time.AfterFunc(time.Second*3, func() {
                e2 := conns[i].WriteMessage(websocket.TextMessage, []byte("jianli"))
                if e2 != nil {
                    conns = removeIndex(conns, i)
                    i--
                }
            })
    } else if index := strings.Index(scene, "music"); index != -1 {
        e := conns[i].WriteMessage(websocket.TextMessage, []byte("music"))
        if e == nil {
            return
        }
        time.AfterFunc(time.Second*3, func() {
            e2 := conns[i].WriteMessage(websocket.TextMessage, []byte("music"))
            if e2 != nil {
                conns = removeIndex(conns, i)
                i--
            }
        })
    } else {
        logger.Info("发心跳信息")
            e := conns[i].WriteMessage(websocket.TextMessage, []byte(""))
            if e != nil {
                conns = removeIndex(conns, i)
                i--
            }
        }
    }
}

加了自动重连后前端5分钟断开一次,3秒后自动重新连上,服务端推送消息的时候第一次推送失败的话,3秒钟后再推送一次,第二次再失败就认定这个链接挂了,删除这个链接,这个方案怎么说呢!你就盯着前端5分钟断一次,重连一次,焦虑症都看发作了,而且循环发消息那里,发送失败就删除该索引的链接,肯定有问题,因为删除指定索引的元素后,数组几变短了,后面删除的数据就不是指定索引的数据。

3,第3阶段方案,服务端发心跳数据,保持nginx链接不超时,使用携程和从后往前循环发

循环从后往前循环,即使删除断开的链接也不影响前边的数据索引变化

go 复制代码
func sedMessage(scene string) {
    l := len(conns)
    logger.Info("ws链接的数组长度", zap.Int("长度为:", l))

    // 确定要发送的消息内容
    var message string
    if strings.Contains(scene, "jianli") {
            message = "jianli"
    } else if strings.Contains(scene, "music") {
            message = "music"
    } else {
            message = ""
    }

    // 遍历所有连接,每个连接独立处理
    // 从后往前循环,避免删除元素后数组索引改变
    for i := len(conns) - 1; i >= 0; i-- {
        // 保存当前连接的副本
        currentConn := conns[i]
        currentIndex := i

        // 使用协程异步发送,不阻塞其他连接
        go func(conn *websocket.Conn, idx int, msg string) {
            // 第一次发送
            err := conn.WriteMessage(websocket.TextMessage, []byte(msg))
            if err != nil {
                logger.Warn("第一次发送失败,3秒后重试", zap.Int("索引", idx), zap.Error(err))

                // 3秒后重试
                time.Sleep(3 * time.Second)
                err2 := conn.WriteMessage(websocket.TextMessage, []byte(msg))
                if err2 != nil {
                    // 重试失败,移除连接
                    logger.Error("重试发送失败,移除连接", zap.Int("索引", idx), zap.Error(err2))
                    conns = removeIndex(conns, idx)
                } else {
                    logger.Info("重试发送成功", zap.Int("索引", idx))
                }
            } else {
                logger.Info("发送成功", zap.Int("索引", idx))
            }
        }(currentConn, currentIndex, message)
    }
}
func removeIndex(slice []*websocket.Conn, index int) []*websocket.Conn {
    if index < 0 || index >= len(slice) {
        return slice
    }
    return append(slice[:index], slice[index+1:]...)
}

目前的情况,从更新后到发稿几个小时过去了,没有再断过,我给前端加了显示断开次数的数据,一直显示断开0次,我使用websocket是使用的比较基础的版本,需要自己维护链接池数组,还要自己删除已经断开的数据,就是很简陋,建议小伙伴使用封装比较完善的第三方插件,喜欢折腾的就随意啦,最后附一张图展示一下我捡漏的页面

相关推荐
PinkSun1 小时前
我用Spring AI做了个简历优化工具(1):Structured Output实战,让AI返回Java对象
后端
Asize1 小时前
重生之我在 Vibe Coding 时代当程序员:第十二课,Prompt 不是咒语,是可以沉淀的业务接口
前端·人工智能·python
东风微鸣1 小时前
Argo CD 用户管理:本地用户配置与权限分离实践
git·后端
Yeats_Liao1 小时前
Java网络编程(五):Selector选择器与高并发实现
java·后端·架构
小小龙学IT2 小时前
Go语言后端开发入门指南
开发语言·后端·golang
布兰妮甜2 小时前
Vue 项目 `localhost:3000` 打不开?404 常见原因排查指南
前端·javascript·vue.js·vuecli·4040排查
森林的尽头是阳光2 小时前
前端使用postman快速造数据
前端·javascript·vue·postman·造数·本地测试
土星碎冰机2 小时前
实现飞书群推送报错接口,critical复现curl
后端·飞书
小猿备忘录2 小时前
Spring Security OAuth2 双Token机制精讲:原理、配置与常见坑点全解析
java·前端·spring·中间件