From 7b94177b8360969e1b620f5862957090c30722b5 Mon Sep 17 00:00:00 2001 From: RichZDS <3388214266@qq.com> Date: Wed, 4 Feb 2026 13:06:11 +0800 Subject: [PATCH] =?UTF-8?q?dockerfile=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/server/main.go | 16 +- docker-compose.yml | 36 +++- go.mod | 6 + go.sum | 10 ++ internal/models/message.go | 28 ++-- internal/redis/client.go | 56 +++++++ internal/ws/connection.go | 62 +++++-- web/index.html | 332 ++++++++++++++++++++++++++++++++----- 8 files changed, 475 insertions(+), 71 deletions(-) create mode 100644 internal/redis/client.go diff --git a/cmd/server/main.go b/cmd/server/main.go index 5da5a37..c3188e6 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -2,6 +2,7 @@ package main import ( "ChatRoom/internal/rabbitmq" + "ChatRoom/internal/redis" "ChatRoom/internal/ws" "log" "net/http" @@ -14,16 +15,27 @@ func main() { rabbitmqURL := os.Getenv("RABBITMQ_URL") if rabbitmqURL == "" { rabbitmqURL = "amqp://guest:guest@localhost:5672/" - } + } rmq, err := rabbitmq.NewClient(rabbitmqURL) if err != nil { log.Fatalf("RabbitMQ 连接失败: %v", err) } defer rmq.Close() + // 1.5 初始化 Redis + redisAddr := os.Getenv("REDIS_ADDR") + if redisAddr == "" { + redisAddr = "localhost:6379" + } + redisClient, err := redis.NewClient(redisAddr, "", 0) + if err != nil { + log.Fatalf("Redis 连接失败: %v", err) + } + defer redisClient.Close() + // 2. WebSocket 路由 http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { - ws.NewConnection(w, r, rmq) // 传入 RabbitMQ 客户端 + ws.NewConnection(w, r, rmq, redisClient) // 传入 RabbitMQ 和 Redis 客户端 }) // 3. 静态文件服务(用于测试前端) diff --git a/docker-compose.yml b/docker-compose.yml index ee5e598..0561639 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,8 @@ version: '3.8' services: app: - image: zhengdushi/chatroom:latest # 直接使用你推送的镜像 + build: . # 强制使用当前代码构建镜像 + image: zhengdushi/chatroom:latest container_name: chatroom-app ports: - "2779:2779" @@ -10,11 +11,14 @@ services: - PORT=2779 - RABBITMQ_HOST=rabbitmq - RABBITMQ_USER=admin - - RABBITMQ_PASS=1218Zhengyaqi # ← 请务必修改为你的密码! + - RABBITMQ_PASS=1218Zhengyaqi - RABBITMQ_URL=amqp://admin:1218Zhengyaqi@rabbitmq:5672/ - + - REDIS_ADDR=redis:6379 # 确保环境变量正确传递 depends_on: - - rabbitmq + rabbitmq: + condition: service_healthy # 等待 RabbitMQ 健康 + redis: + condition: service_healthy # 等待 Redis 健康 restart: unless-stopped networks: - chatroom-net @@ -28,15 +32,37 @@ services: environment: RABBITMQ_DEFAULT_USER: admin RABBITMQ_DEFAULT_PASS: 1218Zhengyaqi # ← 必须和上面一致! + healthcheck: + test: ["CMD", "rabbitmq-diagnostics", "-q", "check_running"] + interval: 10s + timeout: 5s + retries: 5 volumes: - rabbitmq_data:/var/lib/rabbitmq restart: unless-stopped networks: - chatroom-net + redis: + image: redis:alpine + container_name: chatroom-redis + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + restart: unless-stopped + networks: + - chatroom-net + networks: chatroom-net: driver: bridge volumes: - rabbitmq_data: \ No newline at end of file + rabbitmq_data: + redis_data: \ No newline at end of file diff --git a/go.mod b/go.mod index 0c5035b..aeda41a 100644 --- a/go.mod +++ b/go.mod @@ -5,4 +5,10 @@ go 1.24.2 require ( github.com/gorilla/websocket v1.5.3 github.com/rabbitmq/amqp091-go v1.10.0 + github.com/redis/go-redis/v9 v9.17.3 +) + +require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect ) diff --git a/go.sum b/go.sum index 0b2bb22..6e84f0d 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,16 @@ +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= +github.com/redis/go-redis/v9 v9.17.3 h1:fN29NdNrE17KttK5Ndf20buqfDZwGNgoUr9qjl1DQx4= +github.com/redis/go-redis/v9 v9.17.3/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= diff --git a/internal/models/message.go b/internal/models/message.go index 61f9547..fcddf5b 100644 --- a/internal/models/message.go +++ b/internal/models/message.go @@ -6,23 +6,25 @@ import ( // Message 核心变更:增加 Room 字段,移除歧义字段 type Message struct { - Type string `json:"type"` // 消息类型(严格按常量) - User string `json:"user"` // 发送者(客户端填,服务端可校验) - Content string `json:"content"` // 消息内容 - Time string `json:"time"` // 服务端统一覆盖为 RFC3339(防客户端篡改) - To string `json:"to,omitempty"` // 私聊目标(仅 type=private 时有效) - Room string `json:"room,omitempty"` // 房间ID(仅 type=room 时有效) + Type string `json:"type"` // 消息类型(严格按常量) + User string `json:"user"` // 发送者(客户端填,服务端可校验) + Content string `json:"content"` // 消息内容 + Time string `json:"time"` // 服务端统一覆盖为 RFC3339(防客户端篡改) + To string `json:"to,omitempty"` // 私聊目标(仅 type=private 时有效) + Room string `json:"room,omitempty"` // 房间ID(仅 type=room 时有效) + Count int64 `json:"count,omitempty"` // 在线人数(仅 type=user_count 时有效) } // 消息类型常量 const ( - MsgTypeLogin = "login" // 服务端生成:用户上线事件 - MsgTypeLogout = "logout" // 服务端生成:用户下线事件 - MsgTypeBroadcast = "broadcast" // 全体用户广播(无视房间) - MsgTypeRoom = "room" // 房间消息(必须带 Room 字段) - MsgTypePrivate = "private" // 私聊(必须带 To 字段) - MsgTypeSystem = "system" // 服务端生成:系统通知 - MsgTypeError = "error" // 服务端生成:定向错误提示 + MsgTypeLogin = "login" // 服务端生成:用户上线事件 + MsgTypeLogout = "logout" // 服务端生成:用户下线事件 + MsgTypeBroadcast = "broadcast" // 全体用户广播(无视房间) + MsgTypeRoom = "room" // 房间消息(必须带 Room 字段) + MsgTypePrivate = "private" // 私聊(必须带 To 字段) + MsgTypeSystem = "system" // 服务端生成:系统通知 + MsgTypeError = "error" // 服务端生成:定向错误提示 + MsgTypeUserCount = "user_count" // 服务端生成:在线人数更新 ) // 发送广播消息 diff --git a/internal/redis/client.go b/internal/redis/client.go new file mode 100644 index 0000000..74329f2 --- /dev/null +++ b/internal/redis/client.go @@ -0,0 +1,56 @@ +package redis + +import ( + "context" + "fmt" + "time" + + "github.com/redis/go-redis/v9" +) + +const UserZSet = "chat:users" + +type Client struct { + rdb *redis.Client +} + +// NewClient 创建 Redis 客户端 +func NewClient(addr string, password string, db int) (*Client, error) { + rdb := redis.NewClient(&redis.Options{ + Addr: addr, + Password: password, + DB: db, + }) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := rdb.Ping(ctx).Err(); err != nil { + return nil, fmt.Errorf("redis ping error: %w", err) + } + + return &Client{rdb: rdb}, nil +} + +// Close 关闭连接 +func (c *Client) Close() error { + return c.rdb.Close() +} + +// AddUserToZSet 添加用户到 ZSet +func (c *Client) AddUserToZSet(key string, user string, score int64) error { + return c.rdb.ZAdd(context.Background(), key, redis.Z{ + Score: float64(score), + Member: user, + }).Err() +} + +// RemoveUserFromZSet 从 ZSet 移除用户 +func (c *Client) RemoveUserFromZSet(key string, user string) error { + return c.rdb.ZRem(context.Background(), key, user).Err() +} + +// CountUsers 获取在线用户数 +func (c *Client) CountUsers(key string) (int64, error) { + return c.rdb.ZCard(context.Background(), key).Result() +} diff --git a/internal/ws/connection.go b/internal/ws/connection.go index b8e9acf..051b120 100644 --- a/internal/ws/connection.go +++ b/internal/ws/connection.go @@ -3,6 +3,7 @@ package ws import ( "ChatRoom/internal/models" "ChatRoom/internal/rabbitmq" + "ChatRoom/internal/redis" "encoding/json" "log" "net/http" @@ -30,14 +31,15 @@ var upgrader = websocket.Upgrader{ // 连接 type Connection struct { - wsConn *websocket.Conn //websocket连接 - rmqClient *rabbitmq.Client //rabbitmq客户端 - queueName string //队列名称 - userID string //用户ID - send chan []byte //发送通道 + wsConn *websocket.Conn //websocket连接 + rmqClient *rabbitmq.Client //rabbitmq客户端 + redisClient *redis.Client //redis客户端 + queueName string //队列名称 + userID string //用户ID + send chan []byte //发送通道 } -func NewConnection(w http.ResponseWriter, r *http.Request, rmq *rabbitmq.Client) { +func NewConnection(w http.ResponseWriter, r *http.Request, rmq *rabbitmq.Client, redisClient *redis.Client) { // 1. 升级 HTTP 到 WebSocket wsConn, err := upgrader.Upgrade(w, r, nil) if err != nil { @@ -51,6 +53,13 @@ func NewConnection(w http.ResponseWriter, r *http.Request, rmq *rabbitmq.Client) wsConn.Close() return } + //redis记录用户 + if err := redisClient.AddUserToZSet(redis.UserZSet, userID, time.Now().Unix()); err != nil { + log.Printf("Redis 用户记录失败: %v", err) + wsConn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(5000, "redis error")) + wsConn.Close() + return + } // 3. 为用户创建 RabbitMQ 队列 queueName, err := rmq.DeclareQueue() if err != nil { @@ -75,11 +84,12 @@ func NewConnection(w http.ResponseWriter, r *http.Request, rmq *rabbitmq.Client) // 5. 创建 Connection 对象 conn := &Connection{ - wsConn: wsConn, - rmqClient: rmq, - queueName: queueName, - userID: userID, - send: make(chan []byte, 256), + wsConn: wsConn, + rmqClient: rmq, + redisClient: redisClient, + queueName: queueName, + userID: userID, + send: make(chan []byte, 256), } // 发送登录成功消息 @@ -93,13 +103,20 @@ func NewConnection(w http.ResponseWriter, r *http.Request, rmq *rabbitmq.Client) conn.send <- data } + // 广播在线人数 + conn.broadcastUserCount() + go conn.writePump() go conn.readPump() go conn.consumeFromRabbitMQ() } func (c *Connection) readPump() { - defer c.wsConn.Close() + defer func() { + c.redisClient.RemoveUserFromZSet(redis.UserZSet, c.userID) + c.broadcastUserCount() + c.wsConn.Close() + }() c.wsConn.SetReadLimit(maxMessageSize) c.wsConn.SetReadDeadline(time.Now().Add(pongWait)) @@ -231,3 +248,24 @@ func (c *Connection) consumeFromRabbitMQ() { } } } + +// 广播在线人数 +func (c *Connection) broadcastUserCount() { + count, err := c.redisClient.CountUsers(redis.UserZSet) + if err != nil { + log.Printf("获取在线人数失败: %v", err) + return + } + + msg := &models.Message{ + Type: models.MsgTypeUserCount, + User: "system", + Count: count, + Time: time.Now().UTC().Format(time.RFC3339), + } + + body, _ := json.Marshal(msg) + if err := c.rmqClient.Publish(c.rmqClient.ExchangeName, "chat.global", body); err != nil { + log.Printf("广播在线人数失败: %v", err) + } +} diff --git a/web/index.html b/web/index.html index da16dea..29335bc 100644 --- a/web/index.html +++ b/web/index.html @@ -1,38 +1,253 @@ - + - Chat Test + + + 混沌聊天室 - 散修浅谈代码 -

多人聊天测试

- -
- - -
+
+
+

混沌聊天室

+
当前道友: 0
+
+ 道号: + + +
+
-
+
+
神识未开,请先开启神识以感应诸位道友...
+
-
- - -
+
+ +
+ + +
-
- - - -
+ +
+ + + +
-
- - - + +
+ + + +
+
- \ No newline at end of file +