This commit is contained in:
2026-01-18 18:20:40 +08:00
commit 20ed44aa74
178 changed files with 13789 additions and 0 deletions

61
Backend/ws/client.go Normal file
View File

@@ -0,0 +1,61 @@
package ws
import (
"encoding/json"
chatmodel "leke/ws/model"
"github.com/gogf/gf/v2/net/ghttp"
)
// Client 表示一个在线用户的 WebSocket 连接
// 这里只是框架:先把字段和发送逻辑搭好,读写循环可以后面慢慢加。
type Client struct {
UserId uint64 // 当前用户ID
Nickname string // 昵称(可选)
Conn *ghttp.Request // 底层 WebSocket 连接
// Send 是一个发送队列:其他地方往这里丢 []byte这个 client 的写协程负责发出去
Send chan []byte
// Rooms 记录当前用户加入的房间(简单用 map 做集合)
Rooms map[string]bool
}
// NewClient 创建一个新的客户端连接对象
func NewClient(userId uint64, nickname string, conn *ghttp.Request) *Client {
return &Client{
UserId: userId,
Nickname: nickname,
Conn: conn,
Send: make(chan []byte, 256), // 简单先给一个缓冲,防止轻微阻塞
Rooms: make(map[string]bool),
}
}
// EnqueueMessage 将 ChatMessage 编码为 JSON 丢到 Send 队列
// 真正 WriteMessage 的动作建议在一个单独的 writeLoop 里做。
func (c *Client) EnqueueMessage(msg *chatmodel.ChatMessage) error {
// 如果需要,补一些默认字段
if msg.FromUserId == 0 {
msg.FromUserId = c.UserId
}
if msg.FromNickname == "" {
msg.FromNickname = c.Nickname
}
data, err := json.Marshal(msg)
if err != nil {
return err
}
select {
case c.Send <- data:
// 正常入队
default:
// 队列满了,可以考虑:丢弃 / 断开连接 / 打日志
// 这里简单选择丢弃,并返回错误
return ErrSendQueueFull
}
return nil
}

26
Backend/ws/dos/readme.md Normal file
View File

@@ -0,0 +1,26 @@
### ChatMessage (服务端 客户端)
```json
{
"type": "world | room | private | system",
"subType": "message | user_join | user_leave | room_created",
"fromUserId": 123,
"fromNickname": "某某",
"roomId": "xxx",
"content": "xxx",
"time": "2025-12-08T12:34:56"
}
**这里的“放在哪里”** 👉 放在你的项目文档里,用来说明“聊天协议”。
---
## 总结一句人话版
这个 JSON **不是一个要单独存起来的文件**,而是你聊天系统里「**一条消息长什么样**」的**约定**
- 在前端:定义成一个 TypeScript interface / JSDoc 类型,用来写 WebSocket 的 `send``onmessage`
- 在后端:定义成一个 Go struct用来 `json.Unmarshal` / `json.Marshal`
- 在文档写在一个协议文档里提醒自己和队友所有聊天消息都按这个格式来
你下一步如果愿意我可以帮你把前端消息类型定义 + 后端 struct + 一条从前端发到后端再广播出去的完整流程图给你画成一个数据流思路方便你对着实现

7
Backend/ws/errors.go Normal file
View File

@@ -0,0 +1,7 @@
package ws
import "errors"
var (
ErrSendQueueFull = errors.New("客户端队列堵塞")
)

132
Backend/ws/hub.go Normal file
View File

@@ -0,0 +1,132 @@
package ws
import (
"sync"
chatmodel "leke/ws/model" // 按实际路径修改
)
// Hub 聊天中枢:管理所有连接和房间
type Hub struct {
mu sync.RWMutex
// 所有在线用户userId -> Client
clients map[uint64]*Client
// 房间成员roomId -> (userId -> Client)
rooms map[string]map[uint64]*Client
}
// 全局唯一 Hub简单起见用一个全局变量
var ChatHub = NewHub()
// NewHub 创建一个新的 Hub 实例
func NewHub() *Hub {
return &Hub{
clients: make(map[uint64]*Client),
rooms: make(map[string]map[uint64]*Client),
}
}
// Register 注册新连接
func (h *Hub) Register(c *Client) {
h.mu.Lock()
defer h.mu.Unlock()
h.clients[c.UserId] = c
// 默认认为所有在线用户都在世界频道,这里你可以根据需要扩展
}
// Unregister 移除连接(断线/退出)
func (h *Hub) Unregister(c *Client) {
h.mu.Lock()
defer h.mu.Unlock()
delete(h.clients, c.UserId)
// 同时把他从所有房间移除
for roomId, members := range h.rooms {
if _, ok := members[c.UserId]; ok {
delete(members, c.UserId)
if len(members) == 0 {
delete(h.rooms, roomId)
}
}
}
}
// JoinRoom 加入房间
func (h *Hub) JoinRoom(userId uint64, roomId string) {
h.mu.Lock()
defer h.mu.Unlock()
client, ok := h.clients[userId]
if !ok {
return
}
members, ok := h.rooms[roomId]
if !ok {
members = make(map[uint64]*Client)
h.rooms[roomId] = members
}
members[userId] = client
client.Rooms[roomId] = true
}
// LeaveRoom 离开房间
func (h *Hub) LeaveRoom(userId uint64, roomId string) {
h.mu.Lock()
defer h.mu.Unlock()
client, ok := h.clients[userId]
if !ok {
return
}
if members, ok := h.rooms[roomId]; ok {
delete(members, userId)
if len(members) == 0 {
delete(h.rooms, roomId)
}
}
delete(client.Rooms, roomId)
}
// BroadcastWorld 向所有在线用户发送世界频道消息
func (h *Hub) BroadcastWorld(msg *chatmodel.ChatMessage) {
h.mu.RLock()
defer h.mu.RUnlock()
for _, client := range h.clients {
_ = client.EnqueueMessage(msg)
}
}
// BroadcastRoom 向房间内所有用户发送消息
func (h *Hub) BroadcastRoom(roomId string, msg *chatmodel.ChatMessage) {
h.mu.RLock()
defer h.mu.RUnlock()
members, ok := h.rooms[roomId]
if !ok {
return
}
for _, client := range members {
_ = client.EnqueueMessage(msg)
}
}
// SendPrivate 发送私聊消息
func (h *Hub) SendPrivate(fromUserId, toUserId uint64, msg *chatmodel.ChatMessage) {
h.mu.RLock()
defer h.mu.RUnlock()
toClient, ok := h.clients[toUserId]
if !ok {
return // 对方不在线可以后面扩展离线消息存DB
}
msg.FromUserId = fromUserId
_ = toClient.EnqueueMessage(msg)
}

View File

@@ -0,0 +1,34 @@
package model
// MessageType 消息的大类:发到哪里
type MessageType string
const (
TypeWorld MessageType = "world" // 世界频道
TypeRoom MessageType = "room" // 房间
TypePrivate MessageType = "private" // 私聊
TypeSystem MessageType = "system" // 系统消息(如通知、提示等)
)
// MessageAction 消息的动作:干什么事
type MessageAction string
const (
ActionSend MessageAction = "send" // 发送聊天消息
ActionJoinRoom MessageAction = "join_room" // 加入房间
ActionLeaveRoom MessageAction = "leave_room" // 离开房间
ActionCreateRoom MessageAction = "create_room" // 创建房间(也可以走 HTTP
)
// ChatMessage WebSocket 收/发的统一结构
// 前端和后端都按这个结构来编码/解码 JSON。
type ChatMessage struct {
Type MessageType `json:"type"` // 消息类型world/room/private/system
Action MessageAction `json:"action"` // 动作send/join_room/leave_room...
FromUserId uint64 `json:"fromUserId,omitempty"` // 发送者用户ID
FromNickname string `json:"fromNickname,omitempty"` // 发送者昵称
RoomId string `json:"roomId,omitempty"` // 房间ID房间消息时使用
ToUserId uint64 `json:"toUserId,omitempty"` // 目标用户ID私聊时使用
Content string `json:"content,omitempty"` // 文本内容
Time string `json:"time,omitempty"` // 时间ISO字符串前期用 string 即可)
}