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

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

相关推荐
灯澜忆梦12 小时前
GO_并发编程---定时器
开发语言·后端·golang
-银雾鸢尾-13 小时前
C#中的StringBuilder相关方法
开发语言·c#
GitLqr13 小时前
玩转 WebSocket:从原理到 Flutter 实战
websocket·网络协议·flutter
-银雾鸢尾-13 小时前
C#中结构体与类的区别;抽象类与接口的区别
开发语言·c#
大模型码小白14 小时前
【Python零基础教程】继承、多态与魔法函数:面向对象编程三大核心特性详解
java·大数据·开发语言·人工智能·python·ai编程
段一凡-华北理工大学16 小时前
向量数据库实战:选型、调优与落地~系列文章12:文本分块策略实战:chunk_size 怎么选?重叠多少?
开发语言·数据库·后端·oracle·rust·工业智能体·高炉智能化
Ljwuhe16 小时前
C++——多态
开发语言·c++
心平气和量大福大17 小时前
C#-WPF-Window主窗体
开发语言·c#·wpf
从零开始的代码生活_18 小时前
C++ 继承详解:访问控制、对象模型、菱形继承与设计取舍
开发语言·c++·后端·学习·算法
云小逸18 小时前
【C++ 第七阶段:模板、泛型编程与工程综合详解】
开发语言·c++