client.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. package websocket
  2. import (
  3. "encoding/json"
  4. "log"
  5. "strings"
  6. "time"
  7. "github.com/gorilla/websocket"
  8. )
  9. type inboundWSMessage struct {
  10. Type string `json:"type"`
  11. Data map[string]interface{} `json:"data"`
  12. }
  13. const (
  14. // 客户端发送 ping 的最大等待时间
  15. writeWait = 10 * time.Second
  16. // 从客户端读取 pong 的最大等待时间
  17. pongWait = 60 * time.Second
  18. // 发送 ping 的频率(必须小于 pongWait)
  19. pingPeriod = (pongWait * 9) / 10
  20. // 最大消息大小
  21. maxMessageSize = 512 * 1024 // 512KB
  22. )
  23. // Client 是一个 WebSocket 客户端
  24. type Client struct {
  25. hub *Hub
  26. // WebSocket 连接
  27. conn *websocket.Conn
  28. // 发送消息的通道
  29. send chan *Message
  30. // 对话ID(这个客户端属于哪个对话)
  31. conversationID uint
  32. // 是否是访客(true 表示访客,false 表示客服)
  33. isVisitor bool
  34. // 客服ID(如果是客服连接,存储客服的用户ID)
  35. agentID uint
  36. }
  37. // NewClient 创建一个新的客户端
  38. func NewClient(hub *Hub, conn *websocket.Conn, conversationID uint, isVisitor bool, agentID uint) *Client {
  39. return &Client{
  40. hub: hub,
  41. conn: conn,
  42. send: make(chan *Message, 256),
  43. conversationID: conversationID,
  44. isVisitor: isVisitor,
  45. agentID: agentID,
  46. }
  47. }
  48. // ReadPump 从 WebSocket 连接读取消息
  49. func (c *Client) ReadPump() {
  50. defer func() {
  51. c.hub.unregister <- c
  52. c.conn.Close()
  53. }()
  54. // 设置读取限制和超时
  55. c.conn.SetReadDeadline(time.Now().Add(pongWait))
  56. c.conn.SetReadLimit(maxMessageSize)
  57. c.conn.SetPongHandler(func(string) error {
  58. c.conn.SetReadDeadline(time.Now().Add(pongWait))
  59. return nil
  60. })
  61. // 持续读取消息
  62. for {
  63. _, payload, err := c.conn.ReadMessage()
  64. if err != nil {
  65. if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
  66. log.Printf("⚠️ WebSocket 读取错误: 对话ID=%d, 错误=%v", c.conversationID, err)
  67. }
  68. break
  69. }
  70. c.handleIncoming(payload)
  71. }
  72. }
  73. // WritePump 向 WebSocket 连接写入消息
  74. func (c *Client) WritePump() {
  75. ticker := time.NewTicker(pingPeriod)
  76. defer func() {
  77. ticker.Stop()
  78. c.conn.Close()
  79. }()
  80. for {
  81. select {
  82. case message, ok := <-c.send:
  83. c.conn.SetWriteDeadline(time.Now().Add(writeWait))
  84. if !ok {
  85. // Hub 关闭了通道
  86. c.conn.WriteMessage(websocket.CloseMessage, []byte{})
  87. return
  88. }
  89. // 发送消息
  90. if err := c.conn.WriteJSON(message); err != nil {
  91. log.Printf("❌ WebSocket 写入错误: 对话ID=%d, 类型=%s, 错误=%v",
  92. c.conversationID, message.Type, err)
  93. return
  94. }
  95. case <-ticker.C:
  96. // 定期发送 ping 保持连接
  97. c.conn.SetWriteDeadline(time.Now().Add(writeWait))
  98. if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
  99. log.Printf("❌ 发送 ping 失败: 对话ID=%d, 错误=%v", c.conversationID, err)
  100. return
  101. }
  102. }
  103. }
  104. }
  105. // SendMessage 发送消息给客户端(用于测试)
  106. func (c *Client) SendMessage(messageType string, data interface{}) error {
  107. message := &Message{
  108. ConversationID: c.conversationID,
  109. Type: messageType,
  110. Data: data,
  111. }
  112. messageJSON, err := json.Marshal(message)
  113. if err != nil {
  114. return err
  115. }
  116. c.conn.SetWriteDeadline(time.Now().Add(writeWait))
  117. return c.conn.WriteMessage(websocket.TextMessage, messageJSON)
  118. }
  119. func (c *Client) handleIncoming(payload []byte) {
  120. var in inboundWSMessage
  121. if err := json.Unmarshal(payload, &in); err != nil {
  122. return
  123. }
  124. switch in.Type {
  125. case "typing_draft":
  126. text, _ := in.Data["text"].(string)
  127. text = strings.TrimSpace(text)
  128. if text == "" {
  129. c.hub.BroadcastMessage(c.conversationID, "typing_stop", map[string]interface{}{
  130. "sender_id": c.agentID,
  131. "sender_is_agent": !c.isVisitor,
  132. })
  133. return
  134. }
  135. // 控制草稿长度,避免超长输入导致 WS 事件过大。
  136. if len(text) > 300 {
  137. text = text[:300]
  138. }
  139. out := map[string]interface{}{
  140. "sender_id": c.agentID,
  141. "sender_is_agent": !c.isVisitor,
  142. "text": text,
  143. }
  144. if seq, ok := in.Data["seq"]; ok {
  145. out["seq"] = seq
  146. }
  147. c.hub.BroadcastMessage(c.conversationID, "typing_draft", out)
  148. case "typing_stop":
  149. c.hub.BroadcastMessage(c.conversationID, "typing_stop", map[string]interface{}{
  150. "sender_id": c.agentID,
  151. "sender_is_agent": !c.isVisitor,
  152. })
  153. default:
  154. // 忽略未知客户端事件,避免污染服务端日志。
  155. }
  156. }