Files
nl-im-service/internal/ws/worker.go
2025-12-02 21:00:26 +08:00

208 lines
5.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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)
}