tumrs简要流程分析(四)—Push 流程

模块四:Push 流程

Push 是「在线投递」:消息先落 MongoDB,再由 turms-service 经 RPC 把通知推到目标客户端所在的 gateway 节点、再下发到 session。本模块讲清整条链路、零拷贝设计、以及离线回收。

前置:session 模型与 UserStatusService(模块一、二);Message 实体(模块三)。


1. 总览:在线投递主链路

sequenceDiagram participant C as 客户端 participant G as turms-gateway(请求方) participant S as turms-service participant OMM as OutboundMessageManager participant USS as UserStatusService(Redis) participant G2 as 目标 gateway 节点 participant NS as NotificationService participant US as UserSession C->>G: TurmsRequest(如 CREATE_MESSAGE) G->>S: RPC 转发 S->>S: dispatch0: 解码 + handler -> RequestHandlerResult S-->>G: ServiceResponse(回请求方,不等推送) Note over S: 异步订阅 notifyRelatedUsersOfAction S->>S: 构建 TurmsNotification + 编码为 ByteBuf(一次) S->>OMM: forwardNotification(buffer, recipients) OMM->>USS: getUserSessionsStatus(每个 recipient) USS-->>OMM: 在线节点 IDs OMM->>OMM: 按 gateway 节点分组 recipients OMM->>G2: SendNotificationRequest RPC(每节点一个,并行) G2->>NS: callAsync -> sendNotificationToLocalClients NS->>US: sendNotification(buffer) US->>C: netConnection.send(duplicate, 零拷贝) G2-->>OMM: 离线 recipient IDs OMM-->>S: 离线 IDs 并集 S->>S: afterNotify 插件(离线推送钩子)

四个关键性质:

  1. at-most-once / fire-and-forget:推送无 per-message ack、无重传、无离线队列。
  2. 异步与响应解耦 :通知在 dispatch0 的 detached 路径上订阅,客户端响应不等推送完成
  3. 单 buffer 零拷贝全链路 :编码一次,沿途 composite/readRetainedSlice/duplicate/retain,不复制。
  4. 离线回收而非排队 :每个 RPC 返回该节点的离线 recipient IDs,并集后交 afterNotify 插件(APNs/FCM 钩子);turms 本身不存离线消息,靠 pull 恢复(模块六)。

2. 触发:ServiceRequestDispatcher

客户端请求经 gateway RPC 到达 ServiceRequestDispatcher.dispatch0ServiceRequestDispatcher.java:237):

  1. 解码 TurmsRequestProtoDecoder + mergeFrom,buffer 全读不阻塞)。
  2. 插件 ClientRequestTransformer(顺序转换)。
  3. KindCaseClientRequestHandler@ServiceRequestMapping 映射,启动时反射建立),插件 ClientRequestHandler 先于默认 handler(first-wins)。
  4. handler 返回 RequestHandlerResult

RequestHandlerResult:handler 与 dispatcher 的契约

RequestHandlerResult record(RequestHandlerResult.java:45):

字段 含义
code / reason 响应状态码与原因(回请求方)
response TurmsNotification.Data,回请求方的业务数据(如 messageId)
notifications List<Notification>,要推送给相关用户的通知列表

Notification(内嵌 record,:329)= (forwardToRequesterOtherOnlineSessions, recipients Set<Long>, notification TurmsRequest)。一个结果可含多条 Notification(如「推给接收方」+「推给请求方其它在线设备」两条)。

异步:响应不等待推送

dispatch0doOnEach:347)里:成功(code==OK)时 turmsRequestBuffer.retain()订阅 notifyRelatedUsersOfAction(...) 到一条 detached 路径,完成时 release。而 ServiceResponse:385 map)在另一条路径上独立返回。所以客户端拿到响应不等推送

注:turmsRequestBuffer 在 handler 后仍 retain,因为 NioByteString 字段直接引用该 buffer(性能),需保活到工作流结束(注释 :308-314)。


3. 构建通知与转发

notifyRelatedUsersOfAction0ServiceRequestDispatcher.java:447):

  1. 构建 TurmsNotification(通知形态,非响应形态):

    java 复制代码
    TurmsNotification notificationForRecipients = ClientMessagePool.getTurmsNotificationBuilder()
            .setTimestamp(System.currentTimeMillis())
            .setRelayedRequest(notification)   // 转发的原始 TurmsRequest
            .setRequesterId(requesterId)
            .build();
  2. 编码一次 为堆外 ByteBufProtoEncoder.getDirectByteBuffer(notificationForRecipients)

  3. forwardToRequesterOtherOnlineSessions :若为 true,额外把通知推给请求方的其它在线设备 (用 excludedDeviceType 排除发起设备)。此时 notificationByteBuf.retain(2),分别转发给请求方与接收方,Flux.mergeDelayError 合并、doFinally 释放。

  4. outboundMessageManager.forwardNotification(...),返回离线 recipient IDs。

  5. afterNotify 插件 :把离线 IDs 传给 RequestHandlerResultHandler.afterNotify--这是离线推送(APNs/FCM)的扩展点

插件扩展点时序:beforeNotify -> 转发 -> afterNotify(offlineRecipientIds)


4. 路由与扇出:OutboundMessageManager

OutboundMessageManager@ComponentOutboundMessageManager.java:67)负责「把 buffer 发给任意服务器上的任意用户」。类注释明确:所有发往外服务的操作须保证 buffer release 1 次;不支持取消(取消会内存泄漏)。

4.1 按 gateway 节点分组

forwardClientMessageByRecipientIds:207):

  • 对每个 recipient 调 userStatusService.getUserSessionsStatus(recipientId)(模块二的 Redis 状态);离线则跳过。
  • 在线者取 sessionsStatus.getActiveNodeIds()(用户在线设备所在的所有 gateway 节点)。
  • 把 recipients 按 nodeId 聚合成 Map<String, Set<Long>> nodeIdToUserIds
  • 短路 :若 node.getDiscoveryService().getActiveSortedGatewayMembers().size() == 0(无 gateway 节点),直接返回全部 recipients 为离线。

4.2 每节点一个 RPC(并行)

forwardClientMessageToNodes:301):

  • 1 个节点:单 RPC。
  • N 个节点:messageData.retain(nodeCount)每节点一个 SendNotificationRequest RPC 并行发出collectOfflineRecipientIdsFlux.merge + 并集)汇总离线 IDs,doFinally(messageData.release())

关键:按节点而非按 recipient 发 RPC。一个 gateway 节点上可能有多个 recipient,一次 RPC 携带全部 recipient IDs,摊薄 RPC 开销。

forwardClientMessageToNode:370):把 messageData + recipients + excludedUserSessionIds + excludedDeviceType 包成 SendNotificationRequest,调 node.getRpcService().requestResponse(nodeId, request),返回 Mono<Set<Long>>(该节点的离线 IDs)。

4.3 buffer 引用计数(易错点)

整条链路的 ByteBuf 引用计数显式管理:

  • OutboundMessageManager 入口 retain(nodeCount)(多节点)或单次传递。
  • 每节点 RPC 内部再按需 retain(如 forwardNotification(notification, buffer, requesterId, excludedDeviceType) 路径 :357)。
  • doFinally / 出错路径 release
  • 类注释警告:不支持取消 ,取消会泄漏(:57-62)。

单 recipient 与多 recipient 走不同方法(forwardClientMessageByRecipientId vs ...ByRecipientIds),1 个 recipient 时直接取 getActiveNodeIds(),避免分组开销。


5. RPC 载荷:SendNotificationRequest + 零拷贝编解码

SendNotificationRequest extends RpcRequest<Set<Long>>SendNotificationRequest.java:40):

字段 含义
notificationBuffer ByteBuf(TurmsNotification 的堆外 buffer)
recipientIds 该节点上的接收者
excludedUserSessionIds 排除的 session(如请求方发起设备)
excludedDeviceType 排除的设备类型
  • nodeTypeToRequest() = SERVICEnodeTypeToRespond() = GATEWAYisAsync() = true
  • 构造时 setBoundBuffer(notificationBuffer) 把 buffer 绑定到请求生命周期。
  • 在 gateway 侧 callAsync() 委托 notificationService.sendNotificationToLocalClients(...)

自定义紧凑编解码(非 protobuf)

SendNotificationRequestCodecSendNotificationRequestCodec.java:38)是 turms 节点间 RPC 的自定义 codec:

  • writeRequestData:只写元数据(recipientIds 写为 longs、excludedUserSessionIds 写为 userId+deviceType 字节对、excludedDeviceType 一字节)。
  • byteBufToComposite:114):直接返回原始 notificationBuffer--RPC 编解码器把它组合(composite)进 RPC 帧,零拷贝
  • readRequestData:70):in.readRetainedSlice(in.readableBytes()) 把剩余字节作为 retained slice 读出,零拷贝还原 notificationBuffer。
  • initialCapacityForRequest:精确预分配元数据容量,避免扩容。

节点间 RPC 用自定义紧凑编码而非 protobuf(模块一 CodecService)。通知 buffer 作为「裸字节」组合进帧,全程不复制。


6. 网关投递:NotificationService

NotificationService implements RpcNotificationServiceNotificationService.java:59)。sendNotificationToLocalClients:106):

  1. 对每个 recipientId:sessionService.getUserSessionsManager(recipientId)--本地内存查 (模块二的 userIdToSessionsManager)。null -> 离线。
  2. 否则遍历 getDeviceTypeToSession().values()
    • 跳过 excludedDeviceTypeexcludedUserSessionIds 中的 session。
    • notificationData.retain()(下游 send 完成或失败时释放)。
    • userSession.sendNotification(notificationData, tracingContext)onErrorResume 时把 recipient 加入 offlineRecipientIds
    • userSession.getConnection().tryNotifyClientToRecover() :若该 session 处于 UDP 休眠态(连接断但有 UDP 地址),发 OPEN_CONNECTION 信号唤醒客户端重建 TCP/WS(模块九)。
  3. Mono.whenDelayError(monos) 等全部发送完成,返回 offlineRecipientIdsdoFinally(notificationData.release())
  4. 插件 NotificationHandler.handle(notification, recipientIds, offlineRecipientIds);通知日志(按 relayedRequestType 过滤)。

网关侧只查本地内存 session,不查 Redis--RPC 路由已经把 recipients 分发到「拥有其 session 的节点」。所以 getUserSessionsManager 返回 null 即该用户在本节点无在线 session = 离线。


7. session 与 Netty 写出

  • UserSession.sendNotification(ByteBuf, TracingContext)(模块一 UserSession.java:192)-> notificationConsumer.apply(buf, ctx)

  • notificationConsumerUserSessionAssembler.bindConnectionWithSessionWrapper 中设置(模块一 :80-98):

    java 复制代码
    turmsNotificationBuffer = turmsNotificationBuffer.duplicate();  // 独立 readerIndex,零拷贝
    return netConnection.send(turmsNotificationBuffer)
            .doOnError(t -> handleConnectionError(...));

    duplicate() 共享内容但独立 reader index--同一 buffer 可发给多个客户端而不互相干扰,且不复制内容。

  • NetConnection.send

    • TcpConnection.sendTcpConnection.java:61):connection.sendObject(buffer).then()
    • WebSocketConnection.sendWebSocketConnection.java:67):out.sendObject(new BinaryWebSocketFrame(buffer)).then()
    • reactor-netty 的 sendObject 无论成功失败都会释放 buffer。

8. 零拷贝全链路(重点)

一条通知 buffer 从编码到客户端,全程零拷贝(SSL 关闭时直达 socket):

flowchart LR A[&#34;ProtoEncoder<br/>编码一次为 direct ByteBuf&#34;] --> B[&#34;SendNotificationRequestCodec<br/>byteBufToComposite 组合进 RPC 帧&#34;] B --> C[&#34;节点间网络传输&#34;] C --> D[&#34;readRetainedSlice<br/>网关侧 retained slice 还原&#34;] D --> E[&#34;notificationData.duplicate()<br/>每 session 独立 readerIndex&#34;] E --> F[&#34;TcpConnection.sendObject<br/>direct buffer 直达 socket&#34;] A -.retain(nodeCount).-> G[&#34;同一 buffer 发往 N 个 gateway 节点&#34;] A -.retain(per session).-> H[&#34;同一 buffer 发给 N 个客户端&#34;]

要点:

  • 编码一次ProtoEncoder.getDirectByteBuffer 只调一次,后续全程是同一个堆外 buffer。
  • 跨节点retain(nodeCount),同一 buffer 发往 N 个节点不复制。
  • 跨客户端duplicate(),同一 buffer 发给一个节点上 N 个客户端不复制(仅独立 reader index)。
  • 跨 RPCcomposite + readRetainedSlice,节点间传输不复制。
  • 代价:引用计数必须精确 ,任何路径漏 release 即泄漏;故类注释强调不支持取消。

9. 响应 vs 通知:TurmsNotification 的双重角色

TurmsNotification 同一个 proto 承担两种用途:

用途 用的字段 路径
响应(给请求方) request_id / code / reason / data handler 结果 -> ServiceResponse -> 经请求 RPC 回 gateway -> 客户端
通知(给相关用户) requester_id / relayed_request / timestamp notifyRelatedUsersOfAction0 构建 -> OutboundMessageManager -> 目标客户端

例:发消息时,请求方收到响应 (含 messageId),接收方收到通知 (含转发来的 CreateMessageRequest)。两者是同一 proto 的不同字段组合。


10. 离线回收

turms 不存离线消息、不存储转发

  • 每个 SendNotificationRequest 返回该 gateway 节点的离线 recipient IDs。
  • OutboundMessageManager.collectOfflineRecipientIds 取并集。
  • ServiceRequestDispatcher.notifyRelatedUsersOfAction0 把离线 IDs 传给 RequestHandlerResultHandler.afterNotify 插件--APNs/FCM 等离线推送在此接入
  • 离线用户重新上线后,通过 QUERY_MESSAGES_REQUEST 主动拉取漏掉的消息(模块六)--这是可靠性主手段,不是重传。

因此 push 是 at-most-once:在线尽力推,离线不排队,靠 pull 兜底。可靠性模型在模块七总结。


11. 关键设计要点(Takeaways)

  1. 异步解耦:通知在 detached 路径订阅,客户端响应不等推送。
  2. 按节点扇出:recipients 按目标 gateway 节点分组,每节点一个 RPC(非每 recipient),摊薄 RPC 开销。
  3. 单 buffer 零拷贝全链路 :编码一次,composite/readRetainedSlice/duplicate/retain,SSL 关时直达 socket。
  4. 显式引用计数retain(nodeCount)doFinally release,不支持取消(否则泄漏)。
  5. TurmsNotification 双重角色 :响应用 requestId/code/data,通知用 requesterId/relayedRequest/timestamp
  6. at-most-once + 离线回收:无 ack/重传/离线队列;离线 IDs 交插件(APNs/FCM),pull 兜底(模块六/七)。
  7. 网关只查本地内存 :RPC 已路由到归属节点,getUserSessionsManager 返回 null 即离线。

12. 关键文件索引

关注点 文件
service 端分发/触发 turms-service/src/main/java/im/turms/service/access/servicerequest/dispatcher/ServiceRequestDispatcher.java
handler 结果契约 turms-service/src/main/java/im/turms/service/access/servicerequest/dto/RequestHandlerResult.java
扇出/路由 turms-server-common/src/main/java/im/turms/server/common/infra/message/OutboundMessageManager.java
RPC 载荷 turms-server-common/src/main/java/im/turms/server/common/domain/notification/rpc/dto/SendNotificationRequest.java
RPC 零拷贝编解码 turms-server-common/src/main/java/im/turms/server/common/domain/notification/rpc/dto/SendNotificationRequestCodec.java
网关投递 turms-gateway/src/main/java/im/turms/gateway/domain/notification/service/NotificationService.java
session 推送 turms-gateway/src/main/java/im/turms/gateway/access/client/common/UserSession.javaUserSessionAssembler.java
连接写出 turms-gateway/src/main/java/im/turms/gateway/access/client/tcp/TcpConnection.java.../websocket/WebSocketConnection.java
在线状态查询 turms-server-common/src/main/java/im/turms/server/common/domain/session/service/UserStatusService.java(模块二)

下一模块05-group-message-push.md 群消息推送(成员查询 + 扇出到多接收方 + 群通知)。

相关推荐
9527出列1 小时前
tumrs简要流程分析(二)—Session 会话的结构、存储与生命周期
开源
智_永无止境1 小时前
开源免费+AI运维,Netcatty全功能解析与上手教程
开源·netcatty
TunerT_TQ3 小时前
Valhalla 静态工程审阅 #024|蚂蚁集团Ant Design 源码证据驱动评测【大厂开源基础设施特辑】
开源·github·蚂蚁集团·#推荐系统·#typescript·#antdesign
魔镜前的帅比3 小时前
(开源项目)x-claw (设计)
python·ai·rust·开源
奈斯先生Vector3 小时前
2026 大模型 API 路由与基础设施效能深度评测:从开源中继方案到企业级智能调度选型指南
开源
9527出列4 小时前
tumrs简要流程分析(一)—Gateway 启动流程
开源
ClouGence6 小时前
CloudDM:开源免费!一站式数据库访问、SQL审核、权限与脱敏管控平台
数据库·sql·开源
tedcloud1237 小时前
Kimi-K3 部署指南:大模型应用开发环境搭建实践
linux·运维·服务器·开源·音视频