Android SSE 流式接收:从手写到框架的进阶之路

前言

在移动开发中,实时推送数据的需求越来越常见。无论是 AI 对话的流式输出,还是股票行情的实时更新,Server-Sent Events(SSE)都是比 WebSocket 更轻量的选择。

本文将带你从零开始,先手写一个 SSE 客户端,理解底层原理,再使用官方框架简化开发,最终掌握生产级的 SSE 实现方案。


一、什么是 SSE?

SSE(Server-Sent Events)是一种基于 HTTP 的服务端推送技术,允许服务端主动向客户端推送数据。

SSE vs WebSocket 对比

特性 SSE WebSocket

通信方向 单向(服务端→客户端) 双向

协议 HTTP WS/WSS

自动重连 内置支持 需手动实现

复杂度 简单 较复杂

适用场景 实时通知、流式输出 聊天、游戏

SSE 的数据格式非常简单:

复制代码
event: message
data: {"text": "Hello"}

event: done
data: [DONE]

二、手写实现:深入理解 SSE 底层

2.1 为什么先手写?

手写实现能帮助我们:

· 理解 SSE 协议细节

· 掌握流式数据处理原理

· 在特殊场景下进行定制化改造

2.2 添加依赖

kotlin 复制代码
dependencies {
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
}

2.3 核心实现:从零解析 SSE

kotlin 复制代码
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flowOn
import okhttp3.*
import java.io.BufferedReader
import java.util.concurrent.TimeUnit

class SseClientManual {
    
    private val client = OkHttpClient.Builder()
        .connectTimeout(30, TimeUnit.SECONDS)
        .readTimeout(60, TimeUnit.SECONDS)  // 流式读取需要长超时
        .writeTimeout(30, TimeUnit.SECONDS)
        .build()
    
    /**
     * 手写实现 SSE 流式接收
     */
    fun connect(url: String, headers: Map<String, String> = emptyMap()): Flow<String> = callbackFlow {
        val requestBuilder = Request.Builder()
            .url(url)
            .addHeader("Accept", "text/event-stream")
            .addHeader("Cache-Control", "no-cache")
        
        // 添加自定义请求头
        headers.forEach { (key, value) ->
            requestBuilder.addHeader(key, value)
        }
        
        val request = requestBuilder.get().build()
        
        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                close(e)  // 连接失败,关闭 Flow
            }
            
            override fun onResponse(call: Call, response: Response) {
                response.body?.let { body ->
                    try {
                        val reader = body.charStream().buffered()
                        parseSseStream(reader) { eventData ->
                            trySend(eventData)  // 发送解析后的数据
                        }
                    } catch (e: Exception) {
                        close(e)
                    } finally {
                        response.close()
                        close()
                    }
                } ?: close(IOException("Response body is null"))
            }
        })
        
        awaitClose {
            client.dispatcher.cancelAll()
        }
    }.flowOn(Dispatchers.IO)
    
    /**
     * 核心解析逻辑:手动解析 SSE 协议
     */
    private fun parseSseStream(reader: BufferedReader, onEvent: (String) -> Unit) {
        var line: String?
        var eventType: String? = null
        val dataBuilder = StringBuilder()
        
        while (reader.readLine().also { line = it } != null) {
            when {
                // 事件类型行
                line!!.startsWith("event:") -> {
                    eventType = line!!.substring(6).trim()
                }
                
                // 数据行(可能跨多行)
                line!!.startsWith("data:") -> {
                    if (dataBuilder.isNotEmpty()) {
                        dataBuilder.append("\n")
                    }
                    dataBuilder.append(line!!.substring(5).trim())
                }
                
                // 重试时间
                line!!.startsWith("retry:") -> {
                    // 可以解析重试间隔
                }
                
                // ID 行
                line!!.startsWith("id:") -> {
                    // 可以解析消息 ID,用于断点续传
                }
                
                // 空行表示一个事件结束
                line!!.isEmpty() -> {
                    if (dataBuilder.isNotEmpty()) {
                        // 根据 eventType 可以分发不同的事件
                        onEvent(dataBuilder.toString())
                        dataBuilder.clear()
                    }
                    eventType = null
                }
            }
        }
    }
}

2.4 配合 ViewModel 使用

kotlin 复制代码
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.launch

class MainViewModelManual : ViewModel() {
    
    private val sseClient = SseClientManual()
    
    private val _messages = MutableStateFlow<List<String>>(emptyList())
    val messages: StateFlow<List<String>> = _messages
    
    private val _error = MutableStateFlow<String?>(null)
    val error: StateFlow<String?> = _error
    
    private var isConnected = false
    
    fun startStream() {
        if (isConnected) return
        isConnected = true
        
        viewModelScope.launch {
            sseClient.connect(
                url = "https://api.example.com/stream",
                headers = mapOf(
                    "Authorization" to "Bearer your-token"
                )
            )
            .catch { e ->
                _error.value = "连接错误: ${e.message}"
                isConnected = false
            }
            .collect { data ->
                // 更新 UI 状态
                _messages.value = _messages.value + data
            }
        }
    }
    
    fun stopStream() {
        viewModelScope.coroutineContext.cancelChildren()
        isConnected = false
    }
}

2.5 手写实现的核心要点

  1. 协议解析:需要处理 data:、event:、id:、retry: 等多种字段
  2. 多行数据:使用 StringBuilder 拼接跨行的数据
  3. 事件分隔:通过空行判断一个事件的结束
  4. 错误处理:完善的异常捕获和资源释放
  5. 线程管理:IO 线程读数据,主线程更新 UI

手写实现虽然灵活,但代码量较大,且容易遗漏边缘情况。接下来我们看看官方框架如何简化这一切。


三、框架实现:使用 okhttp-sse

3.1 添加依赖

kotlin 复制代码
dependencies {
    // 官方 SSE 扩展库
    implementation("com.squareup.okhttp3:okhttp-sse:4.12.0")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
}

3.2 使用官方框架实现

kotlin 复制代码
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import okhttp3.*
import okhttp3.sse.EventSource
import okhttp3.sse.EventSourceListener
import okhttp3.sse.EventSources

class SseClientOfficial {
    
    private val client = OkHttpClient.Builder()
        .connectTimeout(30, TimeUnit.SECONDS)
        .readTimeout(60, TimeUnit.SECONDS)
        .writeTimeout(30, TimeUnit.SECONDS)
        .build()
    
    /**
     * 使用官方框架实现 SSE
     */
    fun connect(url: String, headers: Map<String, String> = emptyMap()): Flow<SseEvent> = callbackFlow {
        val requestBuilder = Request.Builder()
            .url(url)
            .addHeader("Accept", "text/event-stream")
            .addHeader("Cache-Control", "no-cache")
        
        headers.forEach { (key, value) ->
            requestBuilder.addHeader(key, value)
        }
        
        val request = requestBuilder.get().build()
        val factory = EventSources.createFactory(client)
        
        // 创建事件监听器
        val listener = object : EventSourceListener() {
            override fun onOpen(eventSource: EventSource, response: Response) {
                // 连接打开
            }
            
            override fun onEvent(
                eventSource: EventSource,
                id: String?,
                type: String?,
                data: String
            ) {
                // 收到事件 - 官方已经解析好数据!
                trySend(SseEvent(
                    id = id,
                    type = type,
                    data = data
                ))
            }
            
            override fun onClosed(eventSource: EventSource) {
                close()  // 连接关闭
            }
            
            override fun onFailure(
                eventSource: EventSource,
                t: Throwable?,
                response: Response?
            ) {
                close(t)  // 连接失败
            }
        }
        
        // 建立连接
        val eventSource = factory.newEventSource(request, listener)
        
        // 当 Flow 取消时关闭连接
        awaitClose {
            eventSource.cancel()
        }
    }.flowOn(Dispatchers.IO)
}

// SSE 事件数据类
data class SseEvent(
    val id: String?,
    val type: String?,
    val data: String
)

3.3 ViewModel 使用官方实现

kotlin 复制代码
class MainViewModelOfficial : ViewModel() {
    
    private val sseClient = SseClientOfficial()
    
    private val _messages = MutableStateFlow<List<String>>(emptyList())
    val messages: StateFlow<List<String>> = _messages
    
    private val _connectionStatus = MutableStateFlow(ConnectionStatus.DISCONNECTED)
    val connectionStatus: StateFlow<ConnectionStatus> = _connectionStatus
    
    fun startStream() {
        viewModelScope.launch {
            _connectionStatus.value = ConnectionStatus.CONNECTING
            
            sseClient.connect(
                url = "https://api.example.com/stream",
                headers = mapOf(
                    "Authorization" to "Bearer your-token"
                )
            )
            .catch { e ->
                _connectionStatus.value = ConnectionStatus.ERROR
                _messages.value = _messages.value + "错误: ${e.message}"
            }
            .collect { event ->
                _connectionStatus.value = ConnectionStatus.CONNECTED
                
                when (event.type) {
                    "message" -> {
                        // 处理普通消息
                        _messages.value = _messages.value + event.data
                    }
                    "done" -> {
                        // 流结束
                        _connectionStatus.value = ConnectionStatus.DISCONNECTED
                    }
                    else -> {
                        // 其他类型事件
                    }
                }
            }
        }
    }
    
    fun stopStream() {
        viewModelScope.coroutineContext.cancelChildren()
        _connectionStatus.value = ConnectionStatus.DISCONNECTED
    }
}

enum class ConnectionStatus {
    CONNECTING, CONNECTED, DISCONNECTED, ERROR
}

3.4 UI 层实现(Jetpack Compose)

kotlin 复制代码
@Composable
fun SseScreenManual(viewModel: MainViewModelManual = viewModel()) {
    val messages by viewModel.messages.collectAsState()
    val error by viewModel.error.collectAsState()
    
    SseScreenContent(
        messages = messages,
        error = error,
        onStart = { viewModel.startStream() },
        onStop = { viewModel.stopStream() }
    )
}

@Composable
fun SseScreenOfficial(viewModel: MainViewModelOfficial = viewModel()) {
    val messages by viewModel.messages.collectAsState()
    val connectionStatus by viewModel.connectionStatus.collectAsState()
    
    SseScreenContent(
        messages = messages,
        connectionStatus = connectionStatus,
        onStart = { viewModel.startStream() },
        onStop = { viewModel.stopStream() }
    )
}

@Composable
fun SseScreenContent(
    messages: List<String>,
    error: String? = null,
    connectionStatus: ConnectionStatus = ConnectionStatus.DISCONNECTED,
    onStart: () -> Unit,
    onStop: () -> Unit
) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        // 状态显示
        ConnectionStatusBar(status = connectionStatus, error = error)
        
        // 消息列表
        MessageList(messages = messages)
        
        // 控制按钮
        ControlButtons(
            isConnected = connectionStatus == ConnectionStatus.CONNECTED,
            onStart = onStart,
            onStop = onStop
        )
    }
}

@Composable
fun ConnectionStatusBar(status: ConnectionStatus, error: String?) {
    val statusText = when (status) {
        ConnectionStatus.CONNECTING -> "连接中..."
        ConnectionStatus.CONNECTED -> "已连接"
        ConnectionStatus.DISCONNECTED -> "未连接"
        ConnectionStatus.ERROR -> "错误"
    }
    
    val statusColor = when (status) {
        ConnectionStatus.CONNECTING -> Color.Yellow
        ConnectionStatus.CONNECTED -> Color.Green
        else -> Color.Gray
    }
    
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp)
    ) {
        Text(
            text = statusText,
            color = statusColor
        )
        
        error?.let {
            Text(
                text = it,
                color = Color.Red,
                modifier = Modifier.padding(start = 8.dp)
            )
        }
    }
}

@Composable
fun MessageList(messages: List<String>) {
    LazyColumn(
        modifier = Modifier
            .fillMaxWidth()
            .weight(1f)
            .padding(vertical = 8.dp)
    ) {
        items(messages) { message ->
            Card(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(vertical = 4.dp)
            ) {
                Text(
                    text = message,
                    modifier = Modifier.padding(12.dp)
                )
            }
        }
    }
}

@Composable
fun ControlButtons(isConnected: Boolean, onStart: () -> Unit, onStop: () -> Unit) {
    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        Button(
            onClick = onStart,
            enabled = !isConnected,
            modifier = Modifier.weight(1f)
        ) {
            Text("开始接收")
        }
        
        Button(
            onClick = onStop,
            enabled = isConnected,
            modifier = Modifier.weight(1f),
            colors = ButtonDefaults.outlinedButtonColors()
        ) {
            Text("停止接收")
        }
    }
}

四、两种方案对比总结

4.1 代码量对比

项目 手写实现 官方框架

核心解析代码 ~80 行 ~30 行

错误处理 手动实现 框架封装

协议兼容性 基础支持 完整支持

4.2 优缺点分析

手写实现:

· ✅ 深入理解 SSE 协议

· ✅ 完全可控,可定制

· ✅ 无额外依赖

· ❌ 代码量大,易出错

· ❌ 需处理各种边界情况

· ❌ 维护成本高

官方框架:

· ✅ 代码简洁,开箱即用

· ✅ 完整支持 SSE 规范

· ✅ 官方维护,稳定可靠

· ✅ 自动处理多行数据、ID、重连等

· ❌ 多一个依赖包(体积很小)

4.3 选择建议

场景 推荐方案

学习研究 手写实现

生产项目 官方框架

特殊定制需求 手写实现

快速开发 官方框架

包体积敏感 手写实现(但差距很小)


五、高级优化技巧

5.1 自动重连机制

kotlin 复制代码
class AutoReconnectSseClient {
    private var retryCount = 0
    private val maxRetries = 3
    
    fun connectWithRetry(url: String): Flow<String> = callbackFlow {
        var currentRetry = 0
        var success = false
        
        while (!success && currentRetry < maxRetries) {
            try {
                // 尝试连接
                val sseClient = SseClientOfficial()
                sseClient.connect(url).collect { data ->
                    trySend(data)
                    success = true
                }
            } catch (e: Exception) {
                currentRetry++
                if (currentRetry >= maxRetries) {
                    close(e)
                } else {
                    delay(1000L * currentRetry)  // 指数退避
                }
            }
        }
        
        awaitClose()
    }
}

5.2 心跳检测

kotlin 复制代码
class HeartbeatAwareSseClient {
    private var lastHeartbeat = System.currentTimeMillis()
    
    fun connectWithHeartbeat(url: String): Flow<String> = callbackFlow {
        val heartbeatJob = launch {
            while (true) {
                delay(30_000)  // 30秒检测一次
                val timeSinceLastHeartbeat = System.currentTimeMillis() - lastHeartbeat
                if (timeSinceLastHeartbeat > 60_000) {
                    close(IOException("心跳超时"))
                }
            }
        }
        
        // 在收到数据时更新时间
        SseClientOfficial().connect(url).collect { event ->
            if (event.type == "heartbeat") {
                lastHeartbeat = System.currentTimeMillis()
            }
            trySend(event.data)
        }
        
        awaitClose {
            heartbeatJob.cancel()
        }
    }
}

5.3 断点续传

kotlin 复制代码
class ResumeableSseClient(private val prefs: SharedPreferences) {
    
    fun connectWithResume(url: String): Flow<String> = callbackFlow {
        val lastEventId = prefs.getString("last_event_id", null)
        
        val request = Request.Builder()
            .url(url)
            .addHeader("Accept", "text/event-stream")
            .addHeader("Last-Event-ID", lastEventId ?: "")
            .build()
        
        SseClientOfficial().connectWithRequest(request).collect { event ->
            // 保存最后收到的 ID
            event.id?.let { id ->
                prefs.edit().putString("last_event_id", id).apply()
            }
            trySend(event.data)
        }
        
        awaitClose()
    }
}

六、常见问题与解决方案

Q1:连接频繁断开怎么办?

· 检查服务端心跳配置

· 增加 readTimeout 时间

· 实现客户端重连机制

Q2:数据解析乱码?

kotlin 复制代码
// 确保使用正确的字符编码
val reader = body.charStream().buffered()  // 默认 UTF-8

Q3:如何在 Android 9+ 使用 HTTPS?

xml 复制代码
<!-- 添加网络安全配置 -->
<application
    android:networkSecurityConfig="@xml/network_security_config">
</application>

Q4:内存泄漏如何避免?

· 使用 viewModelScope 管理生命周期

· 在 onCleared 中取消协程

· 确保 close() 被正确调用


七、总结

通过本文的学习,我们从底层协议开始,手写实现了 SSE 客户端,深刻理解了数据流处理、协议解析等核心概念。随后引入官方框架,体验了封装带来的开发效率提升。

核心要点:

  1. SSE 基于 HTTP,实现简单,适合单向推送
  2. 手写实现能帮助我们深入理解协议细节
  3. 官方框架适用于大多数生产场景
  4. 合理使用 Flow 和协程能优雅管理生命周期

在实际项目中,建议优先使用官方框架,但在学习阶段,不妨亲手实现一遍,这会让你的技术功底更加扎实。

相关推荐
大尚来也2 小时前
PHP 反序列化漏洞深度解析:从原理利用到 allowed_classes 防御实战
android·开发语言·php
sp42a3 小时前
通过 RootEncoder 进行安卓直播 RTSP 推流
android·推流·rtsp
SY.ZHOU3 小时前
移动端架构体系(一):组件化
android·ios·架构·系统架构
恋猫de小郭4 小时前
Android 17 新适配要求,各大权限进一步收紧,适配难度提升
android·前端·flutter
流星白龙5 小时前
【MySQL】9.MySQL内置函数
android·数据库·mysql
进击的cc5 小时前
Android Kotlin:扩展函数如何优雅封装Android API
android·kotlin
进击的cc5 小时前
Android Kotlin:空安全机制在Android中的实战应用
android·kotlin
没有了遇见7 小时前
Android 实现天猫/京东/抖音/咸鱼/拼多多等商品详情页面智能跳转APP
android
乾坤一气杀7 小时前
Kotlin 协程线程切换原理 —— 以 Dispatchers.IO 为例
android