Procházet zdrojové kódy

为 client 添加用户信息。为此动了大调整

shizhongqi před 11 měsíci
rodič
revize
35c4cd2f97
4 změnil soubory, kde provedl 266 přidání a 31 odebrání
  1. 27 10
      main.go
  2. 12 0
      wsClient/client.go
  3. 1 1
      wsClient/redis.go
  4. 226 20
      wsClient/room.go

+ 27 - 10
main.go

@@ -6,6 +6,7 @@ import (
 	"chatapp/templates"
 	"chatapp/wsClient"
 	"flag"
+	"fmt"
 	"log"
 	"net/http"
 	"os"
@@ -64,19 +65,35 @@ func main() {
 	http.Handle("/chat", &templates.TemplateHandler{Filename: "chat.html"})
 
 	// Handle all websocket connections for chat rooms dynamically
-	// 处理 WebSocket 连接的路由
-	// 支持动态创建和管理聊天室
 	http.HandleFunc("/room", func(w http.ResponseWriter, r *http.Request) {
-		// 获取房间名称,并处理URL中的特殊字符(如+号)
-		roomName := r.URL.Query().Get("room")
-		if roomName == "" {
-			http.Error(w, "Room name required", http.StatusBadRequest)
+		log.Println("Received request for room")
+		// 从WebSocket子协议中预解析房间名称
+		protocols := r.Header["Sec-Websocket-Protocol"]
+		if len(protocols) == 0 {
+			log.Println("No protocols found")
+			return
+		}
+
+		// 解析房间参数
+		roomParams, err := wsClient.ParseWebSocketProtocols(protocols)
+		if err != nil {
+			log.Printf("解析WebSocket协议参数失败: %v", err)
+			http.Error(w, "Invalid WebSocket protocols", http.StatusBadRequest)
 			return
 		}
-		// 获取或创建指定名称的房间,并启动房间的消息处理协程
+
+		// 组合房间名称
+		roomName := fmt.Sprintf("custom_id-%dANDshop_id-%d", roomParams.CustomId, roomParams.ShopId)
+		if roomName == "" {
+            http.Error(w, "Room name required in WebSocket protocols or URL parameter", http.StatusBadRequest)
+		}
+		roomParams.RoomName = roomName
+		log.Printf("--------------- 房间名称: %s\n\n", roomName)
+
+		// 获取(或创建)指定名称的房间,并启动房间的消息处理协程
 		room := wsClient.GetRoomAndRun(roomName)
-		// 将 WebSocket 连接交给房间处理
-		room.ServeHTTP(w, r)
+		// 将 WebSocket 连接交给房间处理,使用解析好的参数避免重复解析
+		room.HttpServe(w, r, roomParams)
 	})
 
 	// 启动 HTTP 服务器
@@ -85,4 +102,4 @@ func main() {
 	if err := http.ListenAndServe(*addr, nil); err != nil {
 		log.Fatal("服务器启动失败:", err)
 	}
-}
+}

+ 12 - 0
wsClient/client.go

@@ -30,6 +30,18 @@ type Client struct {
 	// 客户端的显示名称(用户名)
 	// 在消息中用于标识消息发送者
 	name string
+	
+	// 新增的用户参数
+	userInfo UserInfo
+}
+
+type UserInfo struct {
+	UserId   int32 `json:"userId"`
+	Username string `json:"username"`
+	CustomId int32 `json:"customId"`
+	ShopId   int32 `json:"shopId"`
+	Platform string `json:"platform"`
+	Type string `json:"type,omitempty"` // 客户端类型 -- shop:门店端, customer:客户端
 }
 
 // read 从 WebSocket 连接读取消息并转发给房间 (原作者说: Used to send messages)

+ 1 - 1
wsClient/redis.go

@@ -75,7 +75,7 @@ func StoreRawMessage(roomName string, rawMessage []byte) error {
 	// 设置过期时间(可选)- 7天后自动删除
 	redisClient.Expire(ctx, key, 7*24*time.Hour)
 
-	log.Printf("消息已存储到Redis - 房间: %s", roomName)
+	log.Printf("消息已存储到Redis - 房间: %s\n\n", roomName)
 	return nil
 }
 

+ 226 - 20
wsClient/room.go

@@ -2,10 +2,14 @@
 package wsClient
 
 import (
+	"encoding/base64"
+	"encoding/json"
 	"fmt"
 	"log"
 	"math/rand"
 	"net/http"
+	"net/url"
+	"strings"
 	"sync"
 
 	"github.com/gorilla/websocket"
@@ -44,6 +48,17 @@ type message struct {
 	sender  *Client // 发送者 -- // 用处:1.用于在转发时排除发送者自己
 }
 
+// RoomParams 表示从客户端 WebSocket子协议中解析出的房间参数
+type RoomParams struct {
+	RoomName  string `json:"roomName,omitempty"`
+	UserId    int32 `json:"u_id"` 	// 用户ID: 门店客户端是 staffId, 买家客户端是 userId  ---- 未确定,可修改
+	CustomId  int32 `json:"c_id"`
+	ShopId    int32 `json:"s_id"`
+	Username  string `json:"u_name"`
+	Platform  string `json:"platform"`
+	Type  string `json:"type,omitempty"` // 客户端类型:shop-门店端, customer-客户端
+}
+
 // 全局互斥锁,用于保护 rooms 字典的并发访问,确保多个 goroutine 同时访问 rooms 时的线程安全
 var mu sync.Mutex
 
@@ -75,9 +90,9 @@ func GetRoomAndRun(name string) *Room {
 	return r
 }
 
-// getRoom 获取指定名称的房间,如果不存在则创建新房间但不启动
+// GetRoom 获取指定名称的房间,如果不存在则创建新房间但不启动
 // 注意:这个函数只创建房间但不启动 run() 方法,可能导致房间无法处理消息
-// 建议使用 getRoomAndRun() 替代
+// 建议使用 GetRoomAndRun() 替代
 func GetRoom(roomName string) *Room {
 	mu.Lock()
 	defer mu.Unlock()
@@ -141,36 +156,227 @@ const (
 var upgrader = &websocket.Upgrader{
 	ReadBufferSize:  socketBufferSize, // 读缓冲区大小
 	WriteBufferSize: socketBufferSize, // 写缓冲区大小
+	CheckOrigin: func(r *http.Request) bool {
+		return true // 允许跨域连接
+	},
+}
+
+// parseWebSocketProtocols 解析WebSocket子协议中的参数
+// 客户端通过子协议传递多个Base64编码的JSON参数片段
+func ParseWebSocketProtocols(protocols []string) (*RoomParams, error) {
+	//log.Println("-------- ParseWebSocketProtocols ---------")
+	// 查找包含房间参数的子协议
+	for _, protocol := range protocols {
+		//log.Printf("Parsing protocol: %s", protocol)
+		if strings.HasPrefix(protocol, "chat,") {
+			// 初始化参数结构体
+			params := RoomParams{}
+
+			// 分割协议字符串,获取各个参数部分
+			parts := strings.Split(protocol, ",")
+			if len(parts) < 2 {
+				log.Printf("协议格式错误,参数不足: %s", protocol)
+				continue
+			}
+
+			// 遍历所有参数部分(跳过第一个"chat")
+			for i := 1; i < len(parts); i++ {
+				part := strings.TrimSpace(parts[i])
+				if err := parseProtocolPart(part, &params); err != nil {
+					log.Printf("解析协议部分失败 %s: %v", part, err)
+					continue
+				}
+			}
+
+			log.Printf("成功解析WebSocket协议参数: %+v\n", params)
+			return &params, nil
+		} else {
+			log.Printf("跳过非chat协议: %s", protocol)
+		}
+	}
+
+	return nil, fmt.Errorf("未找到有效的房间参数协议")
+}
+
+// parseProtocolPart 解析单个协议参数部分
+func parseProtocolPart(part string, params *RoomParams) error {
+	// 检查参数格式:p0-..., p1-..., p2-...
+	if len(part) < 3 || !strings.Contains(part, "-") {
+		return fmt.Errorf("参数格式错误: %s", part)
+	}
+
+	// 分割前缀和base64内容
+	sepIndex := strings.Index(part, "-")
+	if sepIndex == -1 {
+		return fmt.Errorf("缺少分隔符: %s", part)
+	}
+
+	prefix := part[:sepIndex]
+	encodedData := part[sepIndex+1:]
+
+	// Base64解码 - 使用RawStdEncoding来处理可能没有正确填充的base64字符串
+	decodedBytes, err := base64.RawStdEncoding.DecodeString(encodedData)
+	if err != nil {
+		// 如果RawStdEncoding失败,尝试标准的StdEncoding(带填充)
+		decodedBytes, err = base64.StdEncoding.DecodeString(encodedData)
+		if err != nil {
+			return fmt.Errorf("Base64解码失败: %v", err)
+		}
+	}
+
+	// URL解码
+	decodedStr, err := url.QueryUnescape(string(decodedBytes))
+	if err != nil {
+		return fmt.Errorf("URL解码失败: %v", err)
+	}
+
+	// 根据前缀类型解析不同的参数
+	switch prefix {
+	case "p0":
+		// 解析用户ID和自定义ID
+		var p0Data struct {
+			UserId   int32 `json:"u_id"`
+			CustomId int32 `json:"c_id"`
+		}
+		if err := json.Unmarshal([]byte(decodedStr), &p0Data); err != nil {
+			return fmt.Errorf("p0参数JSON解析失败: %v", err)
+		}
+		params.UserId = p0Data.UserId
+		params.CustomId = p0Data.CustomId
+
+	case "p1":
+		// 解析店铺ID和用户名
+		var p1Data struct {
+			ShopId   int32  `json:"s_id"`
+			Username string `json:"u_name"`
+		}
+		if err := json.Unmarshal([]byte(decodedStr), &p1Data); err != nil {
+			return fmt.Errorf("p1参数JSON解析失败: %v", err)
+		}
+		params.ShopId = p1Data.ShopId
+		params.Username = p1Data.Username
+
+	case "p2":
+		// 解析平台和类型
+		var p2Data struct {
+			Platform string `json:"platform"`
+			Type     string `json:"type"`
+		}
+		if err := json.Unmarshal([]byte(decodedStr), &p2Data); err != nil {
+			return fmt.Errorf("p2参数JSON解析失败: %v", err)
+		}
+		params.Platform = p2Data.Platform
+		params.Type = p2Data.Type
+
+	// 当有 p3 参数时,解析 p3 参数
+
+	// 当有 p4 参数时,解析 p4 参数
+
+	default:
+		log.Printf("未知的参数前缀: %s", prefix)
+	}
+
+	return nil
+}
+
+
+func (r *Room) HttpServe(w http.ResponseWriter, req *http.Request,roomParams *RoomParams) {
+	// 将 HTTP 连接升级为 WebSocket 连接,指定接受的子协议
+	responseHeader := http.Header{}
+	// 这行代码的作用是在 WebSocket握手响应中添加 Sec-WebSocket-Protocol头,值为"chat"。这表示服务器接受并选择了"chat"子协议作为通信协议。当客户端在请求中提供多个子协议选项时,服务器需要在响应中指定它选择使用哪一个。这行代码确认服务器将使用基础的"chat"协议与客户端通信,这样客户端就知道应该使用哪种协议格式来解释后续的WebSocket消息。
+	responseHeader.Add("Sec-WebSocket-Protocol", "chat") // 响应基础的 chat 协议
+	
+	socket, err := upgrader.Upgrade(w, req, responseHeader)
+	if err != nil {
+		log.Println("Upgrade error:", err)
+		return
+	}
+
+	// -----------------------------------    生成客户端对象     ----------------------------------
+	// 创建新的客户端对象,使用解析出的参数
+	client := &Client{
+		socket:   socket,                              // WebSocket 连接
+		receive:  make(chan []byte, messageBufferSize), // 接收消息的缓冲 channel
+		room:     r,                                   // 使用当前房间实例
+		name:     roomParams.Username,                 // 使用传递的用户名
+		userInfo: UserInfo{
+			UserId:   roomParams.UserId,
+			Username: roomParams.Username,
+			CustomId: roomParams.CustomId,
+			ShopId:   roomParams.ShopId,
+			Platform: roomParams.Platform,
+			Type: roomParams.Type,
+		},
+	}
+
+	// 如果用户名为空,生成一个默认用户名
+	if client.name == "" {
+		client.name = fmt.Sprintf("user_%d", rand.Intn(9000000))
+	}
+
+	// 将客户端加入当前房间
+	r.join <- client
+	defer func() { r.leave <- client }()
+
+	// 启动客户端的写消息 goroutine
+	go client.write()
+	// 在当前 goroutine 中处理客户端的读消息(阻塞直到连接断开)
+	client.read()
 }
 
 // ServeHTTP 处理 WebSocket 连接请求
-// 实现了 http.Handler 接口,负责将 HTTP 连接升级为 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)
+	// 解析WebSocket子协议中的参数
+	protocols := req.Header["Sec-Websocket-Protocol"]
+	roomParams, err := ParseWebSocketProtocols(protocols)
+	if err != nil {
+		log.Printf("解析WebSocket协议参数失败: %v", err)
+		http.Error(w, "Invalid WebSocket protocols", http.StatusBadRequest)
+		return
+	}
+
+	// 验证房间名称是否匹配
+	if roomParams.RoomName != r.name {
+		log.Printf("房间名称不匹配: 期望 %s, 收到 %s", r.name, roomParams.RoomName)
+		http.Error(w, "Room name mismatch", http.StatusBadRequest)
+		return
+	}
+
+	// 将 HTTP 连接升级为 WebSocket 连接,指定接受的子协议
+	responseHeader := http.Header{}
+	responseHeader.Add("Sec-WebSocket-Protocol", "chat") // 响应基础聊天协议
+	
+	socket, err := upgrader.Upgrade(w, req, responseHeader)
 	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)), // 生成随机用户名
+		socket:   socket,                              // WebSocket 连接
+		receive:  make(chan []byte, messageBufferSize), // 接收消息的缓冲 channel
+		room:     r,                                   // 使用当前房间实例
+		name:     roomParams.Username,                 // 使用传递的用户名
+		userInfo: UserInfo{
+			UserId:   roomParams.UserId,
+			Username: roomParams.Username,
+			CustomId: roomParams.CustomId,
+			ShopId:   roomParams.ShopId,
+			Platform: roomParams.Platform,
+		},
+	}
+
+	// 如果用户名为空,生成一个默认用户名
+	if client.name == "" {
+		client.name = fmt.Sprintf("user_%d", rand.Intn(1000))
 	}
 
+	log.Printf("新客户端连接: 用户名=%s, 用户ID=%d, 自定义ID=%d, 店铺ID=%d, 平台=%s, 客户端类型=%s\n\n",
+		client.userInfo.Username, client.userInfo.UserId, client.userInfo.CustomId, client.userInfo.ShopId, client.userInfo.Platform, client.userInfo.Type)
+
 	// 将客户端加入当前房间
 	r.join <- client
 	defer func() { r.leave <- client }()