前言
在移动开发中,实时推送数据的需求越来越常见。无论是 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 手写实现的核心要点
- 协议解析:需要处理 data:、event:、id:、retry: 等多种字段
- 多行数据:使用 StringBuilder 拼接跨行的数据
- 事件分隔:通过空行判断一个事件的结束
- 错误处理:完善的异常捕获和资源释放
- 线程管理: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 客户端,深刻理解了数据流处理、协议解析等核心概念。随后引入官方框架,体验了封装带来的开发效率提升。
核心要点:
- SSE 基于 HTTP,实现简单,适合单向推送
- 手写实现能帮助我们深入理解协议细节
- 官方框架适用于大多数生产场景
- 合理使用 Flow 和协程能优雅管理生命周期
在实际项目中,建议优先使用官方框架,但在学习阶段,不妨亲手实现一遍,这会让你的技术功底更加扎实。