init
This commit is contained in:
61
Backend/ws/client.go
Normal file
61
Backend/ws/client.go
Normal 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
26
Backend/ws/dos/readme.md
Normal 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
7
Backend/ws/errors.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package ws
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrSendQueueFull = errors.New("客户端队列堵塞")
|
||||
)
|
||||
132
Backend/ws/hub.go
Normal file
132
Backend/ws/hub.go
Normal 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)
|
||||
}
|
||||
34
Backend/ws/model/chat_message.go
Normal file
34
Backend/ws/model/chat_message.go
Normal 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 即可)
|
||||
}
|
||||
Reference in New Issue
Block a user