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

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

相关推荐
Joy T2 小时前
Agent 确定性上下文设计:从 Prompt 参数注入到能力分层
java·开发语言·微服务·架构·prompt·agent context
专注仿真6 小时前
引擎扩展性与集成设计深度剖析:从 14 个扩展点到 DIS→HLA 演进之路
开发语言·算法·仿真引擎·推演引擎
Best-Wishes6 小时前
科来网络分析系统(技术交流版)使用说明书
开发语言·junit·php
代码不停7 小时前
自动化测试(java)
java·开发语言·selenium·自动化
鬼手点金8 小时前
opencode-rules 插件
java·开发语言·前端·人工智能·chrome·git·github
爱上网的小舟8 小时前
星际战士2启动错误怎么办?5种游戏报错解决方法
开发语言·php
阿里嘎多学长9 小时前
2026-10-12 GitHub 热点项目精选
开发语言·程序员·github·代码托管
wangchunyu11410 小时前
Python 数据可视化实战入门教程
开发语言·python·信息可视化
microrain11 小时前
读数是 1008,用电量是 8:SagooIoT 计算服务的增量口径与七种计算策略
物联网·golang·计算服务·sagooiot
学逆向的12 小时前
C 突破编译器限制
c语言·开发语言·网络安全