Files
nl-im-service/internal/ws/worker.go

208 lines
5.7 KiB
Go
Raw Normal View History

2025-12-02 21:00:26 +08:00
/**
* package ws
* 作用处理 WebSocket 协议层逻辑并管理基于 ants 的协程工作池
* 核心 I/O 读取与业务逻辑分离防止海量连接导致 Goroutine 爆炸
*/
package ws
import (
"encoding/json"
"fmt"
"log"
"runtime/debug"
"xk-websocket-v2/internal/manager"
"xk-websocket-v2/internal/model"
"xk-websocket-v2/internal/service"
"github.com/panjf2000/ants/v2"
)
// ==========================================
// 1. 配置常量与全局变量
// ==========================================
const (
// MaxWorkerPoolSize 定义最大的业务处理协程数
// 即使有 100万 个连接,同时也只有 5000 个协程在处理具体的业务逻辑(发消息、存库等)
// 其余连接处于空闲或等待 I/O 状态,极大节省内存。
MaxWorkerPoolSize = 5000
)
// GlobalWorkerPool 全局协程池实例
var GlobalWorkerPool *ants.PoolWithFunc
// ==========================================
// 2. 任务结构定义
// ==========================================
/**
* Job
* 结构体代表一个具体的待处理任务
* WebSocket 读到的原始 bytes 和对应的 Client 对象打包在一起
*/
type Job struct {
Client *manager.Client // 消息来源客户端
Payload []byte // 原始消息数据
}
// ==========================================
// 3. 初始化与启动
// ==========================================
/**
* StartWorkerPool
* 功能初始化 ants 协程池
* 注意必须在 main.go 中尽早调用
*/
func StartWorkerPool() {
var err error
// NewPoolWithFunc 创建一个带任务处理函数的池子
// 参数1: 池容量
// 参数2: 具体的任务处理函数 (消费者逻辑)
GlobalWorkerPool, err = ants.NewPoolWithFunc(MaxWorkerPoolSize, func(i interface{}) {
// 类型断言:将接口转换为具体的 Job 结构体
job, ok := i.(Job)
if !ok {
log.Printf("❌ [Worker] 类型断言失败")
return
}
// 执行业务逻辑
handleMessage(job.Client, job.Payload)
})
if err != nil {
log.Fatalf("❌ [Worker] 启动协程池失败: %v", err)
}
log.Printf("⚙️ [WorkerPool] 启动成功 | 容量: %d", MaxWorkerPoolSize)
}
/**
* StopWorkerPool
* 功能优雅关闭协程池
*/
func StopWorkerPool() {
if GlobalWorkerPool != nil {
GlobalWorkerPool.Release()
log.Println("🛑 [WorkerPool] 已释放")
}
}
// ==========================================
// 4. 业务处理逻辑 (消费者)
// ==========================================
/**
* handleMessage
* 功能核心业务路由解析 JSON 并调用 Service
* 特性包含 Panic Recover防止单个请求崩溃导致整个进程退出
*/
func handleMessage(client *manager.Client, message []byte) {
// 🛡️ Panic 恢复机制:这是生产环境必须的!
defer func() {
if r := recover(); r != nil {
log.Printf("❌ [Worker] Panic 捕获: %v\n堆栈信息: %s", r, debug.Stack())
// 可选:发送错误日志到监控系统 (Sentry/Prometheus)
}
}()
// 1. 基础格式校验
if len(message) == 0 {
return
}
// 2. 解析通用载荷 (RequestType)
var req model.WsPayload
if err := json.Unmarshal(message, &req); err != nil {
log.Printf("⚠️ [Worker] JSON解析失败: %v | 来源: %s", err, client.ID)
return
}
// 3. 业务路由分发
switch req.RequestType {
case "bind":
// 处理用户绑定 (bind)
// 格式: {"request_type": "bind", "data": {"user_id": "1001"}}
var bindData model.BindReq
if err := json.Unmarshal(jsonBytes(req.Data), &bindData); err == nil {
// 调用 Service 层逻辑
service.ChatSvc.BindUser(client, bindData.UserID)
} else {
log.Printf("⚠️ [Worker] 绑定参数错误: %v", err)
}
case "send_message":
// 处理消息发送 (send_message)
// 包含:文本聊天、图片、视频通话信令等
var msgData model.SendMessageReq
if err := json.Unmarshal(jsonBytes(req.Data), &msgData); err == nil {
// 核心:调用 ChatService 处理消息(持久化、转发、多端同步)
// 注意:这里传入的是 client 指针,以便 Service 层获取发送者的 UserID 和 ClientID
service.ChatSvc.HandleUserMessage(client, &msgData)
} else {
log.Printf("⚠️ [Worker] 消息参数错误: %v", err)
}
case "heartbeat":
// 处理心跳 (heartbeat)
// 快速响应,不需要走复杂业务逻辑
// 直接往客户端写回 Pong 消息
pong := []byte(`{"request_type":"heartbeat_ack"}`)
manager.Manager.SendToClient(client.ID, pong)
default:
log.Printf("❓ [Worker] 未知请求类型: %s", req.RequestType)
}
}
// ==========================================
// 5. 辅助工具
// ==========================================
/**
* jsonBytes
* 功能辅助函数 map[string]interface{} 或其他类型转回 []byte
* 场景因为 WsPayload.Data interface{} Unmarshal 时可能被解析为 map需要二次转换
*/
func jsonBytes(v interface{}) []byte {
if v == nil {
return []byte("{}")
}
// 如果已经是 string直接转换
if str, ok := v.(string); ok {
return []byte(str)
}
// 否则重新序列化
b, _ := json.Marshal(v)
return b
}
/**
* PushTask
* 功能对外暴露的投递函数 client_manager main.go 调用
* 作用非阻塞地将任务扔进池子
* @param client 消息来源客户端
* @param payload 原始消息
*/
func PushTask(client *manager.Client, payload []byte) error {
if GlobalWorkerPool == nil {
return fmt.Errorf("worker pool not initialized")
}
// 包装任务
job := Job{
Client: client,
Payload: payload,
}
// Invoke 会从池里取一个 worker 来执行 NewPoolWithFunc 注册的函数
// 如果池满了Invoke 会阻塞,直到有 worker 空闲 (或者返回错误,取决于配置)
return GlobalWorkerPool.Invoke(job)
}