room.go 13 KB

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