Go WebSocket 协程泄漏问题分析与解决方案

问题背景:

在开发基于 WebSocket 的实时通信服务时,遇到了严重的协程泄漏问题。每次客户端连接都会创建新的协程,但这些协程无法正常退出,导致内存使用量不断增长,最终可能导致程序崩溃。

问题分析:

原始代码存在的问题

  1. Manager 协程重复启动
Go 复制代码
//  问题代码:每次连接都启动新的 Manager 协程

func Start(c *gin.Context) {

    Run(c.Writer, c.Request)

    go Manager.Start()  // 每次连接都启动新的协程!

}
  1. 每个连接启动多个协程
Go 复制代码
//  问题代码:每个连接启动读写协程

go ClientSocket.Read()   // 读协程

go ClientSocket.Write()  // 写协程
  1. 缺乏协程退出机制
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")  // 连接结束时清理资源
  1. 添加连接状态管理

新增字段:

Go 复制代码
type ClientSession struct {

    // ... 原有字段 ...

    IsClosed      bool          // 连接是否已关闭

    CloseMutex    sync.Mutex    // 保护 IsClosed 字段的互斥锁

    WriteMutex    sync.Mutex    // 保护 WebSocket 写入操作的互斥锁

    CloseChan     chan struct{} // 连接关闭通知通道

}
  1. 实现协程退出机制
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)

    }

}
  1. 改进 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

            }

            // 处理消息逻辑...

        }

    }

}
  1. 改进消息发送机制
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 服务中。通过合理的架构设计和资源管理,可以有效避免协程泄漏问题,提高系统的稳定性和性能。

希望这个经验分享对大家有所帮助!

相关推荐
少废话h23 分钟前
解决Flink中ApacheCommonsCLI版本冲突
开发语言·python·pycharm
天命码喽c26 分钟前
GraphRAG-2.7.0整合Milvus-2.5.1
开发语言·python·milvus·graphrag
后端小张28 分钟前
【JAVA进阶】Spring Boot 核心知识点之自动配置:原理与实战
java·开发语言·spring boot·后端·spring·spring cloud·自动配置
Mr_Xuhhh6 小时前
YAML相关
开发语言·python
8***23556 小时前
【Golang】——Gin 框架中间件详解:从基础到实战
中间件·golang·gin
咖啡の猫6 小时前
Python中的变量与数据类型
开发语言·python
前端达人6 小时前
你的App消息推送为什么石沉大海?看Service Worker源码我终于懂了
java·开发语言
汤姆yu6 小时前
基于springboot的电子政务服务管理系统
开发语言·python
全栈师6 小时前
C#中控制权限的逻辑写法
开发语言·c#
S***q1926 小时前
Rust在系统工具中的内存安全给代码上了三道保险锁。但正是这种“编译期的严苛”,换来了运行时的安心。比如这段代码:
开发语言·后端·rust