在开发类似ChatGPT的实时对话功能时,服务器响应方式的选择直接影响用户体验。传统HTTP请求是"一问一答"模式,用户需要等待整个响应完成才能看到内容。而SSE(Server-Sent Events)协议就像打开了一个数据水龙头,服务器可以持续不断地向客户端推送数据片段。
我去年在开发智能客服App时就面临这个选择。最初尝试用WebSocket,发现它更适合双向高频通信(比如在线游戏),而我们的场景主要是接收AI生成文本。SSE的三大优势最终说服了我们:
实测下来,在4G网络环境下,SSE的连接稳定性比长轮询高3倍以上。这里有个对比表格:
| 特性 | SSE | WebSocket | 长轮询 |
|---|---|---|---|
| 连接方向 | 单向(服务端推) | 全双工 | 半双工 |
| 协议复杂度 | 低 | 高 | 中 |
| 心跳包开销 | 无 | 需要 | 需要 |
| 浏览器支持 | 原生 | 需要JS封装 | 需要JS封装 |
| 适合场景 | 实时文本推送 | 即时通讯/游戏 | 简单实时更新 |
OkHttp从4.9.0版本开始原生支持SSE,我们不需要额外引入库。这里分享一个经过生产环境验证的连接器实现:
kotlin复制class SSEClient(private val callback: SSECallback) {
private val client = OkHttpClient.Builder()
.readTimeout(0, TimeUnit.MILLISECONDS) // 必须设置为0表示无限等待
.build()
fun connect(url: String) {
val request = Request.Builder()
.url(url)
.header("Accept", "text/event-stream")
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
callback.onError(e)
}
override fun onResponse(call: Call, response: Response) {
if (!response.isSuccessful) {
callback.onError(IOException("Unexpected code $response"))
return
}
val source = response.body?.source()
?: throw IOException("Response body is null")
while (true) {
val line = source.readUtf8Line() ?: break
when {
line.startsWith("data:") -> {
val data = line.substring(5).trim()
callback.onMessage(data)
}
line.startsWith("event:") -> {
val eventType = line.substring(6).trim()
callback.onEvent(eventType)
}
line.startsWith("retry:") -> {
// 可自定义重连时间
}
}
}
}
})
}
}
关键点说明:
readTimeout(0) 是SSE连接的核心配置onEvent回调中处理不同类型的消息(如思考中/最终结果)在地铁等弱网环境下,我们总结了几个稳定性优化方案:
Last-Event-ID头字段发送kotlin复制// 指数退避实现示例
private var retryDelay = 1000L
private fun scheduleReconnect() {
handler.postDelayed({
connect(serverUrl)
retryDelay = min(retryDelay * 2, 30000L)
}, retryDelay)
}
原始代码已经实现了基础的字幕效果,我们在实际项目中做了这些增强:
kotlin复制private val typewriterAnimator = ValueAnimator.ofInt(0, 100).apply {
duration = 500 // 每个字符动画时长
interpolator = LinearInterpolator()
addUpdateListener { anim ->
val progress = anim.animatedValue as Int
// 根据进度计算当前应显示的字符数
updateVisibleText(progress)
}
}
**加粗**、*斜体*等标记[链接](url)为可点击文本kotlin复制private fun autoScroll() {
val layout = layout ?: return
val scrollY = computeVerticalScrollRange() - height
if (scrollY - scrollY < 50.dp) { // 当接近底部时自动滚动
smoothScrollTo(0, scrollY)
}
}
当处理长文本时(比如AI生成的千字回答),需要注意:
SpannableStringBuilder.clearSpans()定期清理无用spankotlin复制override fun onMeasure(widthSpec: Int, heightSpec: Int) {
super.onMeasure(widthSpec, heightSpec)
// 预计算文本高度避免重复测量
if (textHeight == 0) {
textHeight = layout.getLineBottom(layout.lineCount - 1)
}
}
对于需要集成到现有项目的情况,推荐这个经过验证的架构:
code复制app/
├── sse/
│ ├── SSEClient.kt # 网络连接层
│ ├── EventParser.kt # 消息解析
│ └── ReconnectStrategy.kt # 重试策略
├── ui/
│ ├── TypeWriterView.kt # 自定义TextView
│ └── ChatAdapter.kt # 列表适配器
├── repository/
│ ├── ChatRepo.kt # 数据仓库
│ └── LocalCache.kt # 本地缓存
└── viewmodel/
└── ChatVM.kt # 业务逻辑
关键交互流程:
在电商客服项目中,这套架构每天处理超过20万条消息,平均响应延迟控制在800ms以内。一个容易踩的坑是忘记在onCleared()中释放SSE连接,会导致内存泄漏。