关于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是使用的比较基础的版本,需要自己维护链接池数组,还要自己删除已经断开的数据,就是很简陋,建议小伙伴使用封装比较完善的第三方插件,喜欢折腾的就随意啦,最后附一张图展示一下我捡漏的页面

相关推荐
dllmayday1 分钟前
Bootstrap三开关样式
前端·javascript·bootstrap
chenzhou__2 分钟前
独立游戏开发日志 ①:从信号博弈到涂色对战——一次玩法重构始
笔记·后端·学习·unity·go·独立游戏
杨先生哦3 分钟前
【2026热端攻防系列 11/12】前端AI风控攻防实战:验证码缺陷、人机验证绕过、智能爬虫对抗与企业智能风控加固方案
前端·人工智能·笔记·爬虫·安全
CodeSheep4 分钟前
hvv员工家属爆料:老公22年入职的,普通员工,老是被误以为高薪多奖金,实际上还没配股,暂时钱没存多少,我倒是很心疼他工作辛苦。
前端·后端·程序员
IT_陈寒5 分钟前
Redis这个内存杀手坑惨了我,排查三小时发现是过期策略搞的鬼
前端·人工智能·后端
摇滚侠18 分钟前
《SpringBoot 3:入门与应用实战》第 15 章 生产级特性 使用监控体系 阅读笔记
spring boot·笔记·后端
倔强的石头10624 分钟前
Spring Boot 接入金仓数据库:配置分层、启动自检与常见错误
数据库·spring boot·后端
人间凡尔赛33 分钟前
2026 云原生后端架构演进:事件驱动、虚拟线程与 AI Agent 内嵌,三驾马车如何重塑技术栈
后端·云原生·架构
人间凡尔赛42 分钟前
React Compiler 正式落地一年:告别手动 useMemo/useCallback 的全栈实践
前端·性能优化·react
好好沉淀1 小时前
Spring @Validated和Validation注解 校验机制完全指南
java·数据库·后端