我的项目架构和使用场景
- 前端:我的在线简历(服务端渲染),在线音乐(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是使用的比较基础的版本,需要自己维护链接池数组,还要自己删除已经断开的数据,就是很简陋,建议小伙伴使用封装比较完善的第三方插件,喜欢折腾的就随意啦,最后附一张图展示一下我捡漏的页面
