跳转至

SSE 与 WebSocket

network-realtime 提供有界 SSE 协议解析、SSE/WebSocket 托管重连,以及 Android OkHttp / iOS NSURLSession WebSocket 实现。

SSE 增量解析

Transport 只需提供任意分片边界的文本 Flow:

val events: Flow<ServerSentEvent> = parseServerSentEvents(textChunks)

events.collect { event ->
    when (event.event) {
        "message" -> renderMessage(event.data)
        "done" -> finish()
    }
}

Parser 支持:

  • data: 多行合并。
  • event:、id:、retry:。
  • \r\n 标准化。
  • 网络分片落在任意字符块边界。
  • 以 : 开头的注释行。
  • 最大行长与最大事件长度保护,异常输入不会无限增长内存。

ServerSentEvent:

data class ServerSentEvent(
    val data: String,
    val event: String? = null,
    val id: String? = null,
    val retryMillis: Long? = null,
)

只需要解释已有文本流时,直接使用 Parser。需要连接生命周期管理时,注入一个轻量 Transport:

val events = observeServerSentEvents(
    transport = NetworkSseTransport { url, headers ->
        eventStreamTransport.openTextStream(url, headers)
    },
    url = "https://events.example.com/v1/stream",
    headersProvider = { mapOf("Authorization" to "Bearer ${tokenStore.current()}") },
    reconnectPolicy = RealtimeReconnectPolicy(maxReconnectAttempts = 5),
)

托管 SSE 会在每次连接前重新调用 headersProvider,以免缓存过期令牌;重连自动携带 Last-Event-ID,包括正确处理仅含控制字段的事件块和空 id: 清除语义。服务端 retry: 会覆盖 下一次指数退避时间,但仍受最大延迟约束。协议越界属于不可恢复输入,不会反复重连。

WebSocket

interface NetworkSocketConnection {
    val incoming: Flow<NetworkSocketFrame>
    suspend fun send(frame: NetworkSocketFrame): Boolean
    suspend fun close(code: Int = 1_000, reason: String = "")
}

fun interface NetworkSocketTransport {
    suspend fun connect(
        url: String,
        headers: Map<String, String>,
    ): NetworkSocketConnection
}

Frame 包括 Text、Binary 和 Closing。createPlatformNetworkSocketTransport() 在 Android 返回 OkHttp 实现, 在 iOS 返回 NSURLSession WebSocketTask 实现;也可以注入 Harmony NetworkKit 或已有业务 Socket SDK。

class ChatRepository(
    private val transport: NetworkSocketTransport = createPlatformNetworkSocketTransport(),
) {
    suspend fun connect(token: String): NetworkSocketConnection = transport.connect(
        url = "wss://chat.example.com/v1/socket",
        headers = mapOf("Authorization" to "Bearer $token"),
    )
}

只关心入站帧时,可以让模块托管重连和应用层心跳:

val frames = observeNetworkSocketFrames(
    transport = createPlatformNetworkSocketTransport(),
    url = "wss://chat.example.com/v1/socket",
    headersProvider = { mapOf("Authorization" to "Bearer ${tokenStore.current()}") },
    reconnectPolicy = RealtimeReconnectPolicy(
        maxReconnectAttempts = 5,
        initialDelayMillis = 500,
        maxDelayMillis = 30_000,
        jitterRatio = 0.2,
    ),
    heartbeat = NetworkSocketHeartbeat(
        intervalMillis = 30_000,
        frame = NetworkSocketFrame.Text("ping"),
    ),
    onConnected = { connection ->
        connection.send(NetworkSocketFrame.Text("subscribe:orders"))
    },
)

每次重连都会重新执行 onConnected,适合恢复订阅。1000 正常关闭会结束 Flow;传输失败、流意外结束和 异常关闭会按上限重连。需要持续发送任意业务消息时,仍可直接持有原始 NetworkSocketConnection。

连接生命周期

  • Socket 应由 Repository/Application Scope 持有,不要无条件绑到单个页面。
  • 页面只收集 incoming,取消页面收集不一定意味着关闭共享连接。
  • 明确提供 close(),并让平台 Transport 释放底层任务。
  • 托管 API 已提供有上限的指数退避和抖动;业务可在收集 Flow 前结合网络状态暂停或恢复。
  • 托管连接默认只允许公开的 HTTPS/WSS 主机,并拒绝 Header 换行注入;可信内网需显式传入放行策略。

选择协议

需求 推荐
服务端到客户端单向文本事件 SSE
双向消息/二进制帧 WebSocket
短任务状态、服务端无推送能力 pollNetwork
普通一次性请求 NetworkCall

Realtime 模块负责通用的心跳和重连机制;业务确认、消息持久化、出站队列和协议级保活响应仍留在 Repository 层。