问题背景:
在开发基于 WebSocket 的实时通信服务时,遇到了严重的协程泄漏问题。每次客户端连接都会创建新的协程,但这些协程无法正常退出,导致内存使用量不断增长,最终可能导致程序崩溃。
问题分析:
原始代码存在的问题
- Manager 协程重复启动
Go
// 问题代码:每次连接都启动新的 Manager 协程
func Start(c *gin.Context) {
Run(c.Writer, c.Request)
go Manager.Start() // 每次连接都启动新的协程!
}
- 每个连接启动多个协程
Go
// 问题代码:每个连接启动读写协程
go ClientSocket.Read() // 读协程
go ClientSocket.Write() // 写协程
- 缺乏协程退出机制
Go
// 问题代码:Manager 无限循环,无法退出
func (manager *ClientManager) Start() {
for { // 无限循环,没有退出条件
select {
case client := <-manager.Connect:
manager.RegisterClient(client)
// ...
}
}
}
解决方案:
1.使用统一 Manager 管理
修改前:
Go
// 每次连接都启动 Manager
func Start(c *gin.Context) {
Run(c.Writer, c.Request)
go Manager.Start() // ❌ 重复启动
}
修改后:
Go
// main.go - 程序启动时只启动一次
func main() {
// ... 其他初始化代码 ...
go webSocket.Manager.Start() // ✅ 只启动一次
// ... 其他代码 ...
}
// server.go - 连接处理不再启动 Manager
func Start(c *gin.Context) {
Run(c)
// 移除 go Manager.Start() // ✅ 不再重复启动
}
修改前:
Go
// 每个连接启动两个协程
go ClientSocket.Read() // 读协程
go ClientSocket.Write() // 写协程
修改后:
Go
// 改为同步处理,避免协程泄漏
ClientSocket.Read() // 直接调用,不启动新协程
ClientSocket.setClosed("Run-main-exit") // 连接结束时清理资源
- 添加连接状态管理
新增字段:
Go
type ClientSession struct {
// ... 原有字段 ...
IsClosed bool // 连接是否已关闭
CloseMutex sync.Mutex // 保护 IsClosed 字段的互斥锁
WriteMutex sync.Mutex // 保护 WebSocket 写入操作的互斥锁
CloseChan chan struct{} // 连接关闭通知通道
}
- 实现协程退出机制
Go
// 检查连接是否已关闭
func (client *ClientSession) isClosed() bool {
client.CloseMutex.Lock()
defer client.CloseMutex.Unlock()
return client.IsClosed
}
// 设置连接为已关闭状态,统一处理资源清理
func (client *ClientSession) setClosed(caller string) {
client.CloseMutex.Lock()
defer client.CloseMutex.Unlock()
if client.IsClosed {
return
}
client.IsClosed = true
// 通知 Manager 处理断开连接
select {
case Manager.DisConnect <- client:
default:
// 防止通道阻塞
}
// 关闭 WebSocket 连接
if client.Conn != nil {
_ = client.Conn.Close()
}
// 安全关闭通知通道
select {
case <-client.CloseChan:
default:
close(client.CloseChan)
}
}
- 改进 Read 方法支持优雅退出
Go
func (client *ClientSession) Read() {
for {
select {
case <-client.CloseChan: // ✅ 支持退出信号
log.Printf("客户端 %s 的 Read 协程接收到连接关闭通知,正在退出", client.UserName)
return
default:
// 处理消息...
messageType, message, err := client.Conn.ReadMessage()
if err != nil {
return
}
// 处理消息逻辑...
}
}
}
- 改进消息发送机制
Go
func SendMsg(client *ClientSession, commandStruct constants.CommandStruct) {
// 首先检查连接是否已关闭
if client.isClosed() {
log.Printf("连接已关闭,无法发送消息给客户端: %s", client.UserName)
return
}
// 获取写锁,保护并发写操作
client.WriteMutex.Lock()
defer client.WriteMutex.Unlock()
// 安全发送消息
if err := client.Conn.WriteMessage(websocket.TextMessage, msg); err != nil {
log.Printf("向客户端 %s 发送消息失败: %v", client.UserName, err)
client.setClosed("SendMsg-error")
}
}
解决效果:
修改前的问题
-
❌ 每次连接都创建新的 Manager 协程
-
❌ 每个连接启动多个读写协程
-
❌ 协程无法正常退出
-
❌ 内存使用量不断增长
-
❌ 系统性能下降
修改后的改进
-
✅ Manager 只启动一次
-
✅ 连接处理改为同步方式
-
✅ 完善的协程退出机制
-
✅ 内存使用稳定
-
✅ 系统性能提升
总结
协程泄漏是 Go 并发编程中的常见问题,特别是在 WebSocket 服务中。通过合理的架构设计和资源管理,可以有效避免协程泄漏问题,提高系统的稳定性和性能。
希望这个经验分享对大家有所帮助!