|
|
@@ -0,0 +1,185 @@
|
|
|
+// 房间管理模块 -- 负责创建、管理聊天房间,处理客户端的加入、离开和消息转发
|
|
|
+package wsClient
|
|
|
+
|
|
|
+import (
|
|
|
+ "fmt"
|
|
|
+ "log"
|
|
|
+ "math/rand"
|
|
|
+ "net/http"
|
|
|
+ "sync"
|
|
|
+
|
|
|
+ "github.com/gorilla/websocket"
|
|
|
+)
|
|
|
+
|
|
|
+// message 包含要转发的消息内容和发送者信息
|
|
|
+// 用于在转发时排除发送者自己
|
|
|
+type message struct {
|
|
|
+ content []byte // 消息内容
|
|
|
+ sender *Client // 发送者
|
|
|
+}
|
|
|
+
|
|
|
+// Room 表示一个聊天房间
|
|
|
+type Room struct {
|
|
|
+ // holds all current clientCons in the room
|
|
|
+ // 保存房间中所有当前活跃的客户端连接
|
|
|
+ clientCons map[*Client]bool
|
|
|
+
|
|
|
+ // join is a channel for all clients wishing to join the room
|
|
|
+ // 用于接收希望加入房间的客户端。新客户端通过这个 channel 请求加入
|
|
|
+ join chan *Client
|
|
|
+
|
|
|
+ // leave is a channel for all clients wishing to leave the room
|
|
|
+ // 用于接收希望离开房间的客户端。断开连接的客户端通过这个 channel 通知离开
|
|
|
+ leave chan *Client
|
|
|
+
|
|
|
+ // forward is a channel that holds incoming messages that should be forwarded to the other clients.
|
|
|
+ // 用于接收需要转发给其他客户端的消息
|
|
|
+ // 所有向此房间发送的消息都会通过这个 channel 进行分发
|
|
|
+ // 现在包含消息内容和发送者信息,以便排除发送者自己
|
|
|
+ forward chan *message
|
|
|
+
|
|
|
+ // name 房间名称,用作Redis存储的键
|
|
|
+ name string
|
|
|
+}
|
|
|
+
|
|
|
+// 全局房间 map,存储所有创建的房间
|
|
|
+// key: 房间名称,value: 房间对象指针
|
|
|
+var rooms = make(map[string]*Room)
|
|
|
+// 全局互斥锁,用于保护 rooms 字典的并发访问,确保多个 goroutine 同时访问 rooms 时的线程安全
|
|
|
+var mu sync.Mutex
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+// newRoom 创建并返回一个新的房间实例
|
|
|
+// 初始化所有必要的 channel 和 map
|
|
|
+func newRoom(name string) *Room {
|
|
|
+ return &Room{
|
|
|
+ clientCons: make(map[*Client]bool),
|
|
|
+ join: make(chan *Client),
|
|
|
+ leave: make(chan *Client),
|
|
|
+ forward: make(chan *message), // 现在传输 message 结构体
|
|
|
+ name: name, // 设置房间名称
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// GetRoomAndRun 获取指定名称的房间,如果不存在则创建新房间并启动
|
|
|
+// 这是线程安全的函数,使用互斥锁保护全局房间字典
|
|
|
+func GetRoomAndRun(name string) *Room {
|
|
|
+ mu.Lock()
|
|
|
+ defer mu.Unlock()
|
|
|
+
|
|
|
+ if r, ok := rooms[name]; ok {
|
|
|
+ return r
|
|
|
+ }
|
|
|
+ r := newRoom(name) // 传入房间名称
|
|
|
+ rooms[name] = r
|
|
|
+ // 启动房间的消息处理循环(在新的 goroutine 中)
|
|
|
+ go r.run()
|
|
|
+ return r
|
|
|
+}
|
|
|
+
|
|
|
+// getRoom 获取指定名称的房间,如果不存在则创建新房间但不启动
|
|
|
+// 注意:这个函数只创建房间但不启动 run() 方法,可能导致房间无法处理消息
|
|
|
+// 建议使用 getRoomAndRun() 替代
|
|
|
+func GetRoom(roomName string) *Room {
|
|
|
+ mu.Lock()
|
|
|
+ defer mu.Unlock()
|
|
|
+
|
|
|
+ if r, ok := rooms[roomName]; ok {
|
|
|
+ return r
|
|
|
+ }
|
|
|
+ nr := newRoom(roomName) // 传入房间名称
|
|
|
+ rooms[roomName] = nr
|
|
|
+ // 注意:这里没有调用 go nr.run(),房间不会处理消息
|
|
|
+ return nr
|
|
|
+}
|
|
|
+
|
|
|
+// run 是房间的核心消息处理循环
|
|
|
+// 在独立的 goroutine 中运行,使用 select 语句处理三种类型的事件
|
|
|
+func (r *Room) run() {
|
|
|
+ for {
|
|
|
+ select {
|
|
|
+ // 处理客户端加入事件
|
|
|
+ case client := <-r.join:
|
|
|
+ // 将新客户端添加到房间的客户端集合中
|
|
|
+ r.clientCons[client] = true // 使用 true 作为值,实际上只使用 key(client 指针)
|
|
|
+
|
|
|
+ // 处理客户端离开事件
|
|
|
+ case client := <-r.leave:
|
|
|
+ // 从房间的客户端集合中删除客户端
|
|
|
+ delete(r.clientCons, client)
|
|
|
+ // 关闭客户端的接收 channel,通知客户端的 write() 方法退出
|
|
|
+ close(client.receive)
|
|
|
+
|
|
|
+ // 处理消息转发事件
|
|
|
+ case msg := <-r.forward:
|
|
|
+ // 打印消息内容与发送者信息
|
|
|
+ log.Printf("Received message from %v: --- %s", msg.sender, string(msg.content))
|
|
|
+
|
|
|
+ // 直接存储原始消息内容到Redis,使用房间名作为键
|
|
|
+ go StoreRawMessage(r.name, msg.content) // 异步存储,不阻塞消息转发
|
|
|
+
|
|
|
+ // 将消息发送给房间中的所有客户端,但排除发送者自己
|
|
|
+ // 使用指针比较是最高效的方法,因为每个客户端都有唯一的内存地址
|
|
|
+ for client := range r.clientCons {
|
|
|
+ if client != msg.sender { // 高效的指针比较,排除发送者
|
|
|
+ // 将消息发送到每个客户端的接收 channel
|
|
|
+ client.receive <- msg.content // 只发送消息内容,不包含发送者信息
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// WebSocket 相关常量定义
|
|
|
+const (
|
|
|
+ // WebSocket 连接的读写缓冲区大小(字节)
|
|
|
+ socketBufferSize = 1024
|
|
|
+ // 每个客户端消息接收 channel 的缓冲区大小
|
|
|
+ messageBufferSize = 256
|
|
|
+)
|
|
|
+
|
|
|
+// WebSocket 升级器,用于将 HTTP 连接升级为 WebSocket 连接
|
|
|
+// 设置读写缓冲区大小以优化性能
|
|
|
+var upgrader = &websocket.Upgrader{
|
|
|
+ ReadBufferSize: socketBufferSize, // 读缓冲区大小
|
|
|
+ WriteBufferSize: socketBufferSize, // 写缓冲区大小
|
|
|
+}
|
|
|
+
|
|
|
+// ServeHTTP 处理 WebSocket 连接请求
|
|
|
+// 实现了 http.Handler 接口,负责将 HTTP 连接升级为 WebSocket 连接
|
|
|
+// 并创建新的客户端加入当前房间
|
|
|
+func (r *Room) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
|
+ // -------- 使用当前房间实例(接收者 r),而不是重新获取房间 -------- 这是才正确的面向对象设计
|
|
|
+ // 从 URL 查询参数中获取房间名称
|
|
|
+ // roomName := req.URL.Query().Get("room")
|
|
|
+ // if roomName == "" {
|
|
|
+ // http.Error(w, "Room name required", http.StatusBadRequest)
|
|
|
+ // return
|
|
|
+ // }
|
|
|
+ // room := GetRoomAndRun(roomName)
|
|
|
+
|
|
|
+ // 将 HTTP 连接升级为 WebSocket 连接
|
|
|
+ socket, err := upgrader.Upgrade(w, req, nil)
|
|
|
+ if err != nil {
|
|
|
+ log.Println("Upgrade error:", err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建新的客户端对象
|
|
|
+ client := &Client{
|
|
|
+ socket: socket, // WebSocket 连接
|
|
|
+ receive: make(chan []byte, messageBufferSize), // 接收消息的缓冲 channel
|
|
|
+ room: r, // 使用当前房间实例
|
|
|
+ name: fmt.Sprintf("user_%d", rand.Intn(1000)), // 生成随机用户名
|
|
|
+ }
|
|
|
+
|
|
|
+ // 将客户端加入当前房间
|
|
|
+ r.join <- client
|
|
|
+ defer func() { r.leave <- client }()
|
|
|
+
|
|
|
+ // 启动客户端的写消息 goroutine
|
|
|
+ go client.write()
|
|
|
+ // 在当前 goroutine 中处理客户端的读消息(阻塞直到连接断开)
|
|
|
+ client.read()
|
|
|
+}
|