room.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. // 房间管理模块 -- 负责创建、管理聊天房间,处理客户端的加入、离开和消息转发
  2. package wsClient
  3. import (
  4. "encoding/base64"
  5. "encoding/json"
  6. "fmt"
  7. "math/rand"
  8. "net/http"
  9. "net/url"
  10. "strings"
  11. "sync"
  12. "github.com/gorilla/websocket"
  13. "github.com/rs/zerolog/log"
  14. )
  15. // 全局房间 map,存储所有创建的房间
  16. // key: 房间名称,value: 房间对象指针
  17. var rooms = make(map[string]*Room)
  18. // Room 表示一个聊天房间
  19. type Room struct {
  20. // holds all current clientConns in the room
  21. // 保存房间中所有当前活跃的客户端连接
  22. clientConns map[*Client]bool
  23. // join is a channel for all clients wishing to join the room
  24. // 用于接收希望加入房间的客户端。新客户端通过这个 channel 请求加入
  25. join chan *Client
  26. // leave is a channel for all clients wishing to leave the room
  27. // 用于接收希望离开房间的客户端。断开连接的客户端通过这个 channel 通知离开
  28. leave chan *Client
  29. // forward is a channel that holds incoming messages that should be forwarded to the other clients.
  30. // 用于接收需要转发给其他客户端的消息
  31. // 所有向此房间发送的消息都会通过这个 channel 进行分发
  32. forward chan *message
  33. // name 房间名称,用作Redis存储的键
  34. name string
  35. }
  36. // message 包含要转发的消息内容和发送者信息
  37. type message struct {
  38. content []byte // 消息内容
  39. sender *Client // 发送者 -- // 用处:1.用于在转发时排除发送者自己
  40. }
  41. // RoomParams 表示从客户端 WebSocket子协议中解析出的房间参数
  42. type RoomParams struct {
  43. RoomName string `json:"roomName,omitempty"`
  44. UserId int32 `json:"u_id"` // 用户ID: 只是表示买家客户端是 userId
  45. CustomId int32 `json:"c_id"`
  46. ShopId int32 `json:"s_id"`
  47. Username string `json:"u_name"`
  48. Platform string `json:"platform"`
  49. Type string `json:"type,omitempty"` // 客户端类型:shop-门店端, customer-客户端
  50. StaffId int32 `json:"staff_id"` // 只是表示门店客户端的员工Id
  51. }
  52. // 全局互斥锁,用于保护 rooms 字典的并发访问,确保多个 goroutine 同时访问 rooms 时的线程安全
  53. var mu sync.Mutex
  54. // newRoom 创建并返回一个新的房间实例
  55. // 初始化所有必要的 channel 和 map
  56. func newRoom(name string) *Room {
  57. return &Room{
  58. clientConns: make(map[*Client]bool),
  59. join: make(chan *Client),
  60. leave: make(chan *Client),
  61. forward: make(chan *message), // 现在传输 message 结构体
  62. name: name, // 设置房间名称
  63. }
  64. }
  65. // GetRoomAndRun 获取指定名称的房间,如果不存在则创建新房间并启动
  66. // 这是线程安全的函数,使用互斥锁保护全局房间字典
  67. func GetRoomAndRun(name string) *Room {
  68. mu.Lock()
  69. defer mu.Unlock()
  70. if r, ok := rooms[name]; ok {
  71. return r
  72. }
  73. r := newRoom(name) // 传入房间名称
  74. rooms[name] = r
  75. // 启动房间的消息处理循环(在新的 goroutine 中)
  76. go r.run()
  77. return r
  78. }
  79. // run 是房间的核心消息处理循环
  80. // 在独立的 goroutine 中运行,使用 select 语句处理三种类型的事件
  81. func (r *Room) run() {
  82. for {
  83. select {
  84. // 处理客户端加入事件
  85. case client := <-r.join:
  86. // 检查channel是否已关闭(收到nil值)
  87. if client == nil {
  88. return // channel已关闭,退出goroutine
  89. }
  90. // 将新客户端添加到房间的客户端集合中
  91. r.clientConns[client] = true // 使用 true 作为值,实际上只使用 key(client 指针)
  92. // log.Printf("客户端 %v 加入房间 %v", client.name, r.name)
  93. log.Info().Str("clientName", client.name).Str("roomName", r.name).Msg("客户端加入房间")
  94. // 清空未读消息数
  95. go ClearUnreadCount(client)
  96. // 处理客户端离开事件
  97. case client := <-r.leave:
  98. // 检查channel是否已关闭(收到nil值)
  99. if client == nil {
  100. return // channel已关闭,退出goroutine
  101. }
  102. // 从房间的客户端集合中删除客户端
  103. delete(r.clientConns, client)
  104. // 关闭客户端的接收 channel,通知客户端的 write() 方法退出
  105. close(client.receive)
  106. log.Info().Str("clientName", client.name).Str("roomName", r.name).Msg("客户端离开房间")
  107. if len(r.clientConns) == 0 {
  108. log.Info().Str("roomName", r.name).Msg("房间没有客户端,关闭房间")
  109. close(r.forward)
  110. close(r.join)
  111. close(r.leave)
  112. delete(rooms, r.name)
  113. return // 退出run() goroutine,避免继续从已关闭的channel读取数据
  114. }
  115. // 处理消息转发事件
  116. case msg := <-r.forward:
  117. // 检查channel是否已关闭(收到nil值)
  118. if msg == nil {
  119. return // channel已关闭,退出goroutine
  120. }
  121. // 打印消息内容与发送者信息
  122. log.Debug().Str("sender", msg.sender.name).Str("message", string(msg.content)).Msg("Received message")
  123. // 直接存储原始消息内容到Redis,使用房间名作为键
  124. go StoreRawMessage(r.name, msg.content) // 异步存储,不阻塞消息转发
  125. // 创建最近联系人列表 或 更新最近联系人列表
  126. go UpdateRecentContacts(msg.sender)
  127. online := false
  128. // 将消息发送给房间中的所有客户端,但排除发送者自己
  129. for client := range r.clientConns {
  130. if client != msg.sender { // 高效的指针比较,排除发送者 // && client.userInfo.Type != msg.sender.userInfo.Type -- 排除同一类型的客户端
  131. // 将消息发送到每个客户端的接收 channel
  132. client.receive <- msg.content // 只发送消息内容,不包含发送者信息
  133. online = true
  134. }
  135. }
  136. // 本消息转发,目的是让客户与门店互相发消息,当有另一方不在线时,要累计未读消息数
  137. if !online {
  138. go IncrementUnreadCount(msg.sender, 1)
  139. }
  140. }
  141. }
  142. }
  143. // 查询当前房间的客户端数量
  144. func (r *Room)clientCount() int {
  145. return len(r.clientConns)
  146. }
  147. // 统计所有房间里的客户端总数量
  148. func GetTotalClientCount() int {
  149. totalCount := 0
  150. for _, room := range rooms {
  151. totalCount += len(room.clientConns)
  152. }
  153. return totalCount
  154. }
  155. // 用 Client.name 来查找房间
  156. func GetRoomByClientName(clientName string) *Room {
  157. for _, room := range rooms {
  158. for client := range room.clientConns {
  159. if client.name == clientName {
  160. return room
  161. }
  162. }
  163. }
  164. return nil
  165. }
  166. // 用 Client.name 查询用户是否在线
  167. func IsClientOnline(clientName string) bool {
  168. room := GetRoomByClientName(clientName)
  169. return room != nil
  170. }
  171. // ======================================== webSocket服务相关 ========================================
  172. // WebSocket 相关常量定义
  173. const (
  174. // WebSocket 连接的读写缓冲区大小(字节)
  175. socketBufferSize = 1024
  176. // 每个客户端消息接收 channel 的缓冲区大小
  177. messageBufferSize = 256
  178. )
  179. // WebSocket 升级器,用于将 HTTP 连接升级为 WebSocket 连接
  180. // 设置读写缓冲区大小以优化性能
  181. var upgrader = &websocket.Upgrader{
  182. ReadBufferSize: socketBufferSize, // 读缓冲区大小
  183. WriteBufferSize: socketBufferSize, // 写缓冲区大小
  184. CheckOrigin: func(r *http.Request) bool {
  185. return true // 允许跨域连接
  186. },
  187. }
  188. // parseWebSocketProtocols 解析WebSocket子协议中的参数
  189. // 客户端通过子协议传递多个Base64编码的JSON参数片段
  190. func ParseWebSocketProtocols(protocols []string) (*RoomParams, error) {
  191. //log.Println("-------- ParseWebSocketProtocols ---------")
  192. // 查找包含房间参数的子协议
  193. for _, protocol := range protocols {
  194. //log.Printf("Parsing protocol: %s", protocol)
  195. if strings.HasPrefix(protocol, "chat,") {
  196. // 初始化参数结构体
  197. params := RoomParams{}
  198. // 分割协议字符串,获取各个参数部分
  199. parts := strings.Split(protocol, ",")
  200. if len(parts) < 2 {
  201. log.Warn().Str("protocol", protocol).Msg("协议格式错误,参数不足")
  202. continue
  203. }
  204. // 遍历所有参数部分(跳过第一个"chat")
  205. for i := 1; i < len(parts); i++ {
  206. part := strings.TrimSpace(parts[i])
  207. if err := parseProtocolPart(part, &params); err != nil {
  208. // log.Printf("解析协议部分失败 %s: %v", part, err)
  209. log.Error().Err(err).Str("part", part).Msg("解析协议部分失败")
  210. continue
  211. }
  212. }
  213. // log.Printf("成功解析WebSocket协议参数: %+v\n", params)
  214. log.Info().Any("params", params).Msg("成功解析WebSocket协议参数")
  215. return &params, nil
  216. } else {
  217. log.Debug().Str("protocol", protocol).Msg("跳过非chat协议")
  218. }
  219. }
  220. return nil, fmt.Errorf("未找到有效的房间参数协议")
  221. }
  222. // parseProtocolPart 解析单个协议参数部分
  223. func parseProtocolPart(part string, params *RoomParams) error {
  224. // 检查参数格式:p0-..., p1-..., p2-...
  225. if len(part) < 3 || !strings.Contains(part, "-") {
  226. return fmt.Errorf("参数格式错误: %s", part)
  227. }
  228. // 分割前缀和base64内容
  229. sepIndex := strings.Index(part, "-")
  230. if sepIndex == -1 {
  231. return fmt.Errorf("缺少分隔符: %s", part)
  232. }
  233. prefix := part[:sepIndex]
  234. encodedData := part[sepIndex+1:]
  235. // Base64解码 - 按优先级尝试不同的编码方式
  236. decodedBytes, err := base64.RawStdEncoding.DecodeString(encodedData)
  237. if err != nil {
  238. // 如果RawStdEncoding失败,尝试RawURLEncoding(URL安全的base64)
  239. decodedBytes, err = base64.RawURLEncoding.DecodeString(encodedData)
  240. if err != nil {
  241. // 如果RawURLEncoding也失败,尝试标准的StdEncoding(带填充)
  242. decodedBytes, err = base64.StdEncoding.DecodeString(encodedData)
  243. if err != nil {
  244. // 最后尝试URLEncoding
  245. decodedBytes, err = base64.URLEncoding.DecodeString(encodedData)
  246. if err != nil {
  247. return fmt.Errorf("Base64解码失败: %v", err)
  248. }
  249. }
  250. }
  251. }
  252. // URL解码
  253. decodedStr, err := url.QueryUnescape(string(decodedBytes))
  254. if err != nil {
  255. return fmt.Errorf("URL解码失败: %v", err)
  256. }
  257. // 根据前缀类型解析不同的参数
  258. switch prefix {
  259. case "p0":
  260. // 解析用户ID和自定义ID
  261. var p0Data struct {
  262. UserId int32 `json:"u_id"`
  263. CustomId int32 `json:"c_id"`
  264. }
  265. if err := json.Unmarshal([]byte(decodedStr), &p0Data); err != nil {
  266. return fmt.Errorf("p0参数JSON解析失败: %v", err)
  267. }
  268. params.UserId = p0Data.UserId
  269. params.CustomId = p0Data.CustomId
  270. case "p1":
  271. // 解析店铺ID和用户名
  272. var p1Data struct {
  273. ShopId int32 `json:"s_id"`
  274. Username string `json:"u_name"`
  275. }
  276. if err := json.Unmarshal([]byte(decodedStr), &p1Data); err != nil {
  277. return fmt.Errorf("p1参数JSON解析失败: %v", err)
  278. }
  279. params.ShopId = p1Data.ShopId
  280. params.Username = p1Data.Username
  281. case "p2":
  282. // 解析平台和类型
  283. var p2Data struct {
  284. Platform string `json:"platform"`
  285. Type string `json:"type"`
  286. }
  287. if err := json.Unmarshal([]byte(decodedStr), &p2Data); err != nil {
  288. return fmt.Errorf("p2参数JSON解析失败: %v", err)
  289. }
  290. params.Platform = p2Data.Platform
  291. params.Type = p2Data.Type
  292. case "p3":
  293. // 解析平台和类型
  294. var p3Data struct {
  295. StaffId int32 `json:"staff_id"`
  296. }
  297. if err := json.Unmarshal([]byte(decodedStr), &p3Data); err != nil {
  298. return fmt.Errorf("p2参数JSON解析失败: %v", err)
  299. }
  300. params.StaffId = p3Data.StaffId
  301. // 当有 p4 参数时,解析 p4 参数
  302. default:
  303. log.Warn().Str("prefix", prefix).Msg("未知的参数前缀")
  304. }
  305. return nil
  306. }
  307. func (r *Room) HttpServe(w http.ResponseWriter, req *http.Request,roomParams *RoomParams) {
  308. // 将 HTTP 连接升级为 WebSocket 连接,指定接受的子协议
  309. responseHeader := http.Header{}
  310. // 这行代码的作用是在 WebSocket握手响应中添加 Sec-WebSocket-Protocol头,值为"chat"。这表示服务器接受并选择了"chat"子协议作为通信协议。当客户端在请求中提供多个子协议选项时,服务器需要在响应中指定它选择使用哪一个。这行代码确认服务器将使用基础的"chat"协议与客户端通信,这样客户端就知道应该使用哪种协议格式来解释后续的WebSocket消息。
  311. responseHeader.Add("Sec-WebSocket-Protocol", "chat") // 响应基础的 chat 协议
  312. socket, err := upgrader.Upgrade(w, req, responseHeader)
  313. if err != nil {
  314. log.Error().Err(err).Msg("Upgrade error")
  315. return
  316. }
  317. // ----------------------------------- 生成客户端对象 ----------------------------------
  318. // 创建新的客户端对象,使用解析出的参数
  319. client := &Client{
  320. name: clientName(roomParams),
  321. socket: socket, // WebSocket 连接
  322. receive: make(chan []byte, messageBufferSize), // 接收消息的缓冲 channel
  323. room: r, // 使用当前房间实例
  324. userInfo: UserInfo{
  325. UserId: roomParams.UserId,
  326. Username: roomParams.Username,
  327. CustomId: roomParams.CustomId,
  328. ShopId: roomParams.ShopId,
  329. Platform: roomParams.Platform,
  330. Type: roomParams.Type,
  331. StaffId: roomParams.StaffId,
  332. },
  333. }
  334. // 如果用户名为空,生成一个默认用户名
  335. if client.name == "" {
  336. client.name = fmt.Sprintf("user_%d", rand.Intn(9000000))
  337. }
  338. // 将客户端加入当前房间
  339. r.join <- client
  340. defer func() { r.leave <- client }()
  341. // 启动客户端的写消息 goroutine
  342. go client.write()
  343. // 在当前 goroutine 中处理客户端的读消息(阻塞直到连接断开)
  344. client.read()
  345. }