feat(chat): implement sequence pre-allocation, versioned sync, and push worker
Introduce several performance and synchronization enhancements: - Implement `SeqBufferManager` to allow sequence number pre-allocation via Redis Lua scripts, reducing atomic increment overhead. - Add `PushWorker` to handle asynchronous message pushing using Redis Streams. - Implement incremental conversation synchronization via `ConversationVersionLog` to allow clients to fetch only recent changes. - Add support for Gzip compression in WebSocket communications to reduce bandwidth usage. - Update dependency injection and configuration to support these new components.
This commit is contained in:
@@ -585,6 +585,37 @@ func (h *MessageHandler) HandleGetSyncData(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// HandleGetSyncByVersion 增量同步:按版本号获取会话变更
|
||||
// GET /api/v1/conversations/sync?version=0&limit=100
|
||||
func (h *MessageHandler) HandleGetSyncByVersion(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
response.Unauthorized(c, "")
|
||||
return
|
||||
}
|
||||
|
||||
versionStr := c.DefaultQuery("version", "0")
|
||||
sinceVersion, err := strconv.ParseInt(versionStr, 10, 64)
|
||||
if err != nil || sinceVersion < 0 {
|
||||
response.BadRequest(c, "invalid version parameter")
|
||||
return
|
||||
}
|
||||
|
||||
limitStr := c.DefaultQuery("limit", "100")
|
||||
limit, err := strconv.Atoi(limitStr)
|
||||
if err != nil || limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
result, err := h.chatService.GetSyncByVersion(c.Request.Context(), userID, sinceVersion, limit)
|
||||
if err != nil {
|
||||
response.InternalServerError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, result)
|
||||
}
|
||||
|
||||
// GetConversationUnreadCount 获取单个会话的未读数
|
||||
// GET /api/conversations/:id/unread/count
|
||||
func (h *MessageHandler) GetConversationUnreadCount(c *gin.Context) {
|
||||
|
||||
@@ -151,12 +151,14 @@ func (h *WSHandler) HandleWebSocket(c *gin.Context) {
|
||||
|
||||
// 3. 创建客户端
|
||||
clientID := atomic.AddUint64(&h.clientSeq, 1)
|
||||
compressEnabled := c.Query("compress") == "1"
|
||||
client := &ws.Client{
|
||||
ID: clientID,
|
||||
UserID: userID,
|
||||
Send: make(chan []byte, defaultUserBufferSize),
|
||||
Quit: make(chan struct{}),
|
||||
PendingAcks: make(map[string]time.Time),
|
||||
ID: clientID,
|
||||
UserID: userID,
|
||||
Send: make(chan []byte, defaultUserBufferSize),
|
||||
Quit: make(chan struct{}),
|
||||
PendingAcks: make(map[string]time.Time),
|
||||
CompressEnabled: compressEnabled,
|
||||
}
|
||||
|
||||
// 4. 注册客户端
|
||||
@@ -243,9 +245,21 @@ func (h *WSHandler) readPump(conn *websocket.Conn, client *ws.Client) {
|
||||
break
|
||||
}
|
||||
|
||||
// 解析消息
|
||||
// 解析消息(支持 gzip 解压)
|
||||
var rawData []byte
|
||||
if client.CompressEnabled && ws.IsCompressed(message) {
|
||||
decompressed, dErr := ws.DefaultCompressor.Decompress(message)
|
||||
if dErr != nil {
|
||||
h.publisher.SendError(client, "decompress_error", "failed to decompress gzip message")
|
||||
continue
|
||||
}
|
||||
rawData = decompressed
|
||||
} else {
|
||||
rawData = message
|
||||
}
|
||||
|
||||
var msg ws.Message
|
||||
if err := json.Unmarshal(message, &msg); err != nil {
|
||||
if err := json.Unmarshal(rawData, &msg); err != nil {
|
||||
h.publisher.SendError(client, "parse_error", "invalid message format")
|
||||
continue
|
||||
}
|
||||
@@ -280,31 +294,60 @@ func (h *WSHandler) writePump(conn *websocket.Conn, client *ws.Client) {
|
||||
return
|
||||
}
|
||||
|
||||
w, err := conn.NextWriter(websocket.TextMessage)
|
||||
if err != nil {
|
||||
zap.L().Warn("WebSocket write error (NextWriter)",
|
||||
zap.String("user_id", client.UserID),
|
||||
zap.Uint64("client_id", client.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return
|
||||
}
|
||||
w.Write(message)
|
||||
if client.CompressEnabled {
|
||||
buf := make([]byte, 0, len(message)+64)
|
||||
buf = append(buf, message...)
|
||||
n := len(client.Send)
|
||||
for i := 0; i < n; i++ {
|
||||
buf = append(buf, '\n')
|
||||
buf = append(buf, <-client.Send...)
|
||||
}
|
||||
compressed, cErr := ws.DefaultCompressor.Compress(buf)
|
||||
if cErr != nil {
|
||||
zap.L().Warn("WebSocket compress error, sending raw",
|
||||
zap.String("user_id", client.UserID),
|
||||
zap.Error(cErr),
|
||||
)
|
||||
w, err := conn.NextWriter(websocket.TextMessage)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
w.Write(buf)
|
||||
w.Close()
|
||||
} else {
|
||||
w, err := conn.NextWriter(websocket.BinaryMessage)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
w.Write(compressed)
|
||||
w.Close()
|
||||
}
|
||||
} else {
|
||||
w, err := conn.NextWriter(websocket.TextMessage)
|
||||
if err != nil {
|
||||
zap.L().Warn("WebSocket write error (NextWriter)",
|
||||
zap.String("user_id", client.UserID),
|
||||
zap.Uint64("client_id", client.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return
|
||||
}
|
||||
w.Write(message)
|
||||
|
||||
// 批量发送缓冲区中的消息
|
||||
n := len(client.Send)
|
||||
for i := 0; i < n; i++ {
|
||||
w.Write([]byte{'\n'})
|
||||
w.Write(<-client.Send)
|
||||
}
|
||||
n := len(client.Send)
|
||||
for i := 0; i < n; i++ {
|
||||
w.Write([]byte{'\n'})
|
||||
w.Write(<-client.Send)
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
zap.L().Warn("WebSocket write error (Close)",
|
||||
zap.String("user_id", client.UserID),
|
||||
zap.Uint64("client_id", client.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return
|
||||
if err := w.Close(); err != nil {
|
||||
zap.L().Warn("WebSocket write error (Close)",
|
||||
zap.String("user_id", client.UserID),
|
||||
zap.Uint64("client_id", client.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
case <-ticker.C:
|
||||
conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
||||
|
||||
Reference in New Issue
Block a user