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

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

相关推荐
小羊没烦恼!1 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
伞伞悦读1 天前
【第38期】Python 模块与包详解:import、from、模块搜索路径、包结构和 __init__
开发语言·python
ttwuai1 天前
Go开源后台管理系统推荐:怎么按技术栈和边界比较4个官方仓库?
golang·gin
只睡四小时1 天前
Canvas 弹道联机实战:700 行 + 固定时间步长
python·websocket·html5·游戏开发·canvas
C语言小火车1 天前
C/C++ 为什么需要编译器?
开发语言·c++
codeejun1 天前
每日一Go·MySQL-5、锁机制全解析
云原生·golang
霍霍的袁1 天前
【C++】map 和 set 的使用 | 从用法到底层
开发语言·c++·学习·visual studio
孙启超1 天前
【AI开发之Rust】第 11 课:智能指针与内部可变性
开发语言·后端·rust
此生决int1 天前
深入理解C++系列(20)——C++11(下)
开发语言·c++
Achou.Wang1 天前
k8s中nginx worker process自动设置
后端·golang