1. SSE 基础概念

什么是 SSE?

SSE(Server-Sent Events)是一种 Web 标准,允许服务器向客户端推送实时数据。

核心特点

  • 单向通信:服务器 → 客户端
  • 基于 HTTP 协议:使用 GET 请求
  • 长连接:连接建立后保持开放
  • 自动重连:网络中断时自动重连

2. SSE 协议详解

HTTP 请求格式

 

GET /events HTTP/1.1

Host: api.example.com

Accept: text/event-stream

Cache-Control: no-cache

服务器响应格式

 

HTTP/1.1 200 OK

Content-Type: text/event-stream

Cache-Control: no-cache

Connection: keep-alive

data: {"message": "Hello"}

data: {"message": "World"}

data: {"message": "Test"}

SSE 数据格式

 

id: 12345

event: message

data: {"content": "Hello World"}

data: {"content": "Another message"}

3. OkHttp SSE 三种实现方式对比

方式一:使用 OkHttp SSE 库(标准 SSE 实现)

依赖配置

 

implementation 'com.squareup.okhttp3:okhttp:4.9.3'implementation 'com.squareup.okhttp3:okhttp-sse:4.9.3'

基本实现

 

OkHttpClient client = new OkHttpClient.Builder().connectTimeout(30, TimeUnit.SECONDS).readTimeout(0, TimeUnit.SECONDS)  // 重要:无读取超时.build();Request request = new Request.Builder().url("https://api.example.com/events").addHeader("Accept", "text/event-stream").build();EventSource eventSource = EventSources.createEventSource(client, request, new EventSourceListener() {@Overridepublic void onOpen(EventSource eventSource, Response response) {System.out.println("SSE 连接建立成功");System.out.println("响应码: " + response.code());System.out.println("响应头: " + response.headers());}@Overridepublic void onEvent(EventSource eventSource, String id, String type, String data) {// 自动解析 SSE 格式,直接得到 dataSystem.out.println("收到数据: " + data);System.out.println("事件ID: " + id);System.out.println("事件类型: " + type);}@Overridepublic void onClosed(EventSource eventSource) {System.out.println("SSE 连接已关闭");}@Overridepublic void onFailure(EventSource eventSource, Throwable t, Response response) {System.err.println("SSE 连接失败: " + t.getMessage());if (response != null) {System.err.println("响应码: " + response.code());}}});

SSE 库的优势
  • ✅ 自动处理 SSE 协议:自动解析 data:、id:、event: 等字段
  • ✅ 内置重连机制:网络中断时自动重连
  • ✅ 标准 SSE 实现:完全符合 SSE 规范
  • ✅ 更高级的 API:提供 EventSource 和 EventSourceListener
  • ✅ 自动连接管理:自动处理连接生命周期

方式二:自定义 SSE 客户端(推荐用于 Kotlin 项目)

依赖配置

 

// 只需要基本的 OkHttp,不需要 SSE 库implementation 'com.squareup.okhttp3:okhttp:4.9.3'

自定义 SSE 客户端实现

先补一下知识点:

trySend、Channel 和 Flow 的工作原理-CSDN博客

再看代码:

class SSEClient {private val okHttpClient by lazy {OkHttpClient.Builder().readTimeout(0, TimeUnit.SECONDS) // 禁用读取超时.build()}private var call: Call? = nullsealed class SSEEvent {data class Message(val event: String, val data: String) : SSEEvent()data class Error(val throwable: Throwable) : SSEEvent()object Closed : SSEEvent()}/*** 连接到 SSE 服务器* @param url SSE 服务器地址* @return Flow 流式返回事件*/fun connect(url: String, token :String): Flow<SSEEvent> = callbackFlow {val request = Request.Builder().url(url).addHeader("Accept", "text/event-stream").addHeader("Cache-Control", "no-cache").addHeader("X-Access-Token", token).build()call = okHttpClient.newCall(request)call?.enqueue(object : Callback {override fun onFailure(call: Call, e: IOException) {trySend(SSEEvent.Error(e))close(e)}override fun onResponse(call: Call, response: Response) {if (!response.isSuccessful) {trySend(SSEEvent.Error(IOException("Unexpected code: ${response.code}")))close()return}val body = response.body ?: returntry {var currentEvent = "message"val dataBuffer = StringBuilder()while (true) {val line = body.source().readUtf8Line() ?: breakwhen {line.startsWith("event:") -> currentEvent = line.substring(6).trim()line.startsWith("data:") -> dataBuffer.append(line.substring(5).trim()).append("\n")line.isEmpty() -> { // 空行表示事件结束if (dataBuffer.isNotEmpty()) {val data = dataBuffer.toString().trim()trySend(SSEEvent.Message(currentEvent, data))dataBuffer.clear()}}}}trySend(SSEEvent.Closed)} catch (e: Exception) {trySend(SSEEvent.Error(e))} finally {response.close()close()}}})awaitClose { disconnect() }}/*** 断开连接*/fun disconnect() {call?.cancel()call = null}/*** 重试连接到 SSE 服务器* @param url SSE 服务器地址* @param token 认证Token* @param maxRetries 最大重试次数* @param retryDelay 重试延迟时间(毫秒)* @return Flow 流式返回事件*/fun connectWithRetry(url: String, token: String, maxRetries: Int = Int.MAX_VALUE, retryDelay: Long = 3000): Flow<SSEEvent> = flow {var retryCount = 0while (retryCount < maxRetries) {connect(url, token).collect { event ->when (event) {is SSEClient.SSEEvent.Closed,is SSEClient.SSEEvent.Error -> {emit(event)if (retryCount < maxRetries) {delay(retryDelay)retryCount++}}else -> emit(event)}}}}
}

使用方式示例

fun initSSE(token: String) {sseClient = SSEClient()val sseUrl = BASE_URL// 启动 SSE 连接sseClient.connect(sseUrl, token).onEach { event ->when (event) {is SSEClient.SSEEvent.Message -> {// 处理事件消息runOnUiThread {mBinding.eventMag.append("Event: ${event.event}\nData: ${event.data}\n\n")}}is SSEClient.SSEEvent.Error -> {// 处理错误Log.e("SSE", "Error: ${event.throwable.message}")}SSEClient.SSEEvent.Closed -> {// 连接关闭Log.i("SSE", "Connection closed")}}}.launchIn(lifecycleScope) // 自动在生命周期结束时取消}

自定义 SSE 客户端的优势
  • ✅ 与 Kotlin Flow 深度集成,支持协程和生命周期自动管理
  • ✅ 事件结构清晰,便于扩展和维护
  • ✅ 可自定义重连、错误处理等高级功能
  • ✅ 只依赖 OkHttp,包体积小

方式三:手动处理 HTTP 流(适合极简和特殊场景)

依赖配置 
implementation 'com.squareup.okhttp3:okhttp:4.9.3'

实现方式

 

suspend fun sendAiQuestionStream(question: String): Flow<String> = flow {var response: ResponseBody? = nulltry {response = ApiManager.apiLogin.sendAiQuestionStream(QuestionRequest(question))val source = response.source()while (!source.exhausted()) {val line = source.readUtf8Line() ?: breakif (line.startsWith("data:")) {val data = line.substring(5).trim()if (data.isNotEmpty() && data != "[DONE]") {emit(data)} else if (data == "[DONE]") {break}}}} finally {response?.close()}}

手动处理方式的优势
  • ✅ 代码极简,完全自定义
  • ✅ 适合只需处理 data: 字段的场景
  • ✅ 适合特殊业务需求(如 AI 流式响应)

4. 三种方式的对比与适用场景

特性/方式OkHttp SSE 库自定义 SSEClient手动处理
依赖OkHttp+SSE库仅OkHttp仅OkHttp
事件结构标准ListenerKotlin Flow/自定义无结构/自定义
自动重连内置可自定义需手动
代码复杂度
灵活性一般最高
适合场景标准SSE推送Kotlin项目/自定义极简/特殊需求

推荐选择:

  • 标准SSE推送、Java项目:OkHttp SSE库
  • Kotlin项目、需要流式/自定义事件/重连:自定义SSEClient
  • 极简、特殊业务、AI流式响应:手动处理

5. 与 WebSocket、HTTP轮询的对比

特性SSEWebSocketHTTP轮询
通信方向单向(服务端→客户端)双向单向/伪双向
协议HTTPWebSocketHTTP
实时性最高
实现复杂度
适用场景实时推送、通知聊天、游戏、协作简单更新
浏览器支持原生原生原生

6. 最佳实践与总结

  • 标准SSE场景:优先用 OkHttp SSE 库,省心省力。
  • Kotlin/协程/流式场景:自定义 SSEClient,灵活扩展,易于维护。
  • 特殊/极简需求:手动处理,代码最少,完全自控。
  • AI流式响应:推荐手动处理,便于处理特殊标记和自定义逻辑。

你的项目已经灵活结合了自定义 SSEClient 和手动处理两种方式,既保证了结构化、可维护性,又能满足特殊业务需求,是非常现代且实用的做法。


结论:

OkHttp SSE 在实际开发中有多种实现方式。你可以根据项目需求选择标准库、自定义客户端或手动处理,三者各有优劣。对于现代 Kotlin 项目,推荐自定义 SSEClient 结合 Flow,既灵活又易于集成和维护。对于极简或特殊场景,手动处理同样高效可靠。

你的实现方式,正是当前最佳实践之一!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/914490.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/914490.shtml
英文地址,请注明出处:http://en.pswp.cn/news/914490.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

聚宽sql数据库传递

自建数据库从聚宽到Q-MT自动化交易实战 从接触聚宽以来一直都是手动跟单&#xff0c;在网上看到许多大佬的自动交易文章&#xff0c;心里也不禁十分痒痒。百说不如一练&#xff0c;千讲不如实干。经过一番努力&#xff0c;终于成功实盘了&#xff0c;效果还可以&#xff0c;几…

es里为什么node和shard不是一对一的关系

提问&#xff1a; 既然多个shard会被分配到同一个node上&#xff0c;那么为什么不把多个shard合并成一个然后存在当前node上呢&#xff0c;简而言之也就是让node和shard形成一对一的关系呢 &#xff1f;非常好的问题&#xff0c;这正是理解Elasticsearch分片&#xff08;shard…

浅谈npm,cnpm,pnpm,npx,nvm,yarn之间的区别

首先做一个基本的分类 名称描述npm,cnpm,yarn,pnpm都是Javascript包管理器nvm是Node.js版本控制器npx命令行工具 I.npm,cnpm,yarn,pnpm npm (Node Package Manager) npm是Node.js默认的包管理器&#xff0c;随Node.js的安装会一起安装。使用npm可以安装&#xff0c;发布&…

滑动窗口-76.最小覆盖子串-力扣(LeetCode)

一、题目解析1.不符合要求则返回空串("")2.子串中重复字符的数量要不少于t中该字符的数量二、算法原理解法1&#xff1a;暴力枚举哈希表这里的暴力枚举也可以优化&#xff0c;即在包含t中元素处枚举&#xff0c;如在A、B和C处开始枚举&#xff0c;减少不必要的枚举 解…

从零构建搜索引擎 build demo search engine from scratch

从零构建搜索引擎 build demo search engine from scratch 我们每天都会使用搜索引擎&#xff1a;打开google等搜索引擎&#xff0c;输入关键词&#xff0c;检索出结果&#xff0c;这是一次搜索&#xff1b;当打开历史记录旁边的&#x1f50d;按钮&#xff0c;输入关键词&#…

pytorch小记(二十九):深入解析 PyTorch 中的 `torch.clip`(及其别名 `torch.clamp`)

pytorch小记&#xff08;二十九&#xff09;&#xff1a;深入解析 PyTorch 中的 torch.clip&#xff08;及其别名 torch.clamp&#xff09;深入解析 PyTorch 中的 torch.clip&#xff08;及其别名 torch.clamp&#xff09;一、函数签名二、简单示例三、广播支持四、与 Autograd…

快速分页wpf

/*没有在xaml设置上下文window.context是因为 命名空间一直对应不上 所以在xaml.cs 里面绑定*/ <Window x:Class"DataGrid.views.MainWindow"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"http://schemas.microsoft…

如何彻底禁用 Chrome 自动更新

如何彻底禁用 Chrome 自动更新 随着谷歌将 Chrome 浏览器版本升级至 138&#xff0c;它即将彻底抛弃对 Manifest V2 扩展的支持。许多用户希望将浏览器版本锁定在 138&#xff0c;以继续使用 uBlock Origin、Tampermonkey 等常用扩展。 本文总结了四种有效方法&#xff0c;帮助…

流批一体的“奥卡姆剃刀”:Apache Cloudberry 增量物化视图应用解析

引言&#xff1a;流批一体&#xff0c;理想与现实的鸿沟 在数据驱动的今天&#xff0c;“实时”二字仿佛拥有魔力&#xff0c;驱使着无数企业投身于流批一体架构的建设浪潮中。我们渴望实时洞察业务变化&#xff0c;实时响应用户需求。以 Apache Flink 为代表的流处理引擎&…

C# 入门教程(三):详解字段、属性、索引器及各类参数与扩展方法

文章目录一、字段、属性、索引器、常量1.字段2.属性2.1 什么是属性2.2 属性的声明2.3 属性与字段的关系3 索引器4. 常量二、传值 输出 引用 数组 具名 可选参数&#xff0c;扩展方法2.1 传值参数2.1.1 值类型 传参2.1.2 引用类型 传参2.2 引用参数2.2.1 引用参数-值类型 传参2.…

《美术教育研究》是什么级别的期刊?是正规期刊吗?能评职称吗?

​问题解答&#xff1a;问&#xff1a;《美术教育研究》是不是核心期刊&#xff1f;答&#xff1a;不是&#xff0c;是知网收录的第一批认定学术期刊。问&#xff1a;《美术教育研究》级别&#xff1f;答&#xff1a;省级。主管单位&#xff1a; 安徽出版集团有限责任公司 主办…

每日算法刷题Day47:7.13:leetcode 复习完滑动窗口一章,用时2h30min

思考: 遇到子数组/子字符串可以考虑能不能用滑动窗口&#xff0c; 定长:逆向思维,答案不定 最大长度/最小长度:一般求长度 越长越合法/越短越合法/恰好:一般求数量 主要思考窗口条件成立&#xff0c; 判断条件是符合窗口条件(最小长度/越长越合法还是不符合(最大长度/越短越合法…

电流驱动和电压驱动的区别

理解电流驱动和电压驱动的区别对电路设计至关重要&#xff0c;尤其在高速、高抗噪要求的场景&#xff08;如LVDS&#xff09;。以下是两者的核心对比&#xff1a;一、电压驱动 (Voltage Drive) 核心原理&#xff1a; 驱动器输出一个受控的电压&#xff08;与负载阻抗无关&#…

宿舍电费查询——以ZUA为例

宿舍电费查询——以ZUA为例0. 安装抓包环境手机端桌面端1. 登录1.1 开启抓包后进入缴费页面&#xff1a;1.2 分析请求1.3 编写登录代码2. 获取楼栋及房间ID2.1 获取楼栋ID2.2 编写获取楼栋ID代码2.3 获取房间ID2.4 编写获取房间ID代码3. 获取剩余电费&#xff1a;3.1 选择房间号…

vue中计算属性的介绍

Vue.js 中的计算属性是基于它的响应式系统来实现的&#xff0c;它可以根据 Vue 实例的数据状态来动态计算出新的属性值。在 Vue 组件中&#xff0c;计算属性常用于对数据进行处理和转换&#xff0c;以及动态生成一些需要的数据。一、使用方式1.定义计算属性&#xff1a; 在Vue组…

MFC UI控件CheckBox从专家到小白

文章目录CheckBox勾选框控件控件与变量绑定控件点击消息映射互斥CheckBox勾选框控件 控件与变量绑定 方案一&#xff1a; BOOL m_bEnable1; BOOL m_bEnable2; void A::DoDataExchange(CDataExchange* pDX) {DDX_Check(pDX, IDC_CK_1, m_bEnable1);DDX_Check(pDX, IDC_CK_2, …

阿尔卡特ACT 250 ATP 150 AND ATP 400 分子泵控制器TURBOMOLECULAR PUMP CONTROLLER ALCATEL

阿尔卡特ACT 250 ATP 150 AND ATP 400 分子泵控制器TURBOMOLECULAR PUMP CONTROLLER ALCATEL

python的小学课外综合管理系统

前端开发框架:vue.js 数据库 mysql 版本不限 后端语言框架支持&#xff1a; 1 java(SSM/springboot)-idea/eclipse 2.NodejsVue.js -vscode 3.python(flask/django)–pycharm/vscode 4.php(thinkphp/laravel)-hbuilderx 数据库工具&#xff1a;Navicat/SQLyog等都可以 摘要 随着…

实用技巧 Excel 与 XML互转

一 概述 在android多语言适配中&#xff0c;可能提供的是excel格式的多语言翻译&#xff0c;而且翻译数量非常庞大。那手动一个一个往xml里面添加效率非常低&#xff0c;这时候就需要把excel快速转为android可以直接用的资源文件string.xml二 转换流程2.1 第一步任意文件夹或者…

云原生技术与应用-Containerd容器技术详解

目录 一.Containerd概述 1.什么是containerd 2.Containerd的起源与背景 二.Containerd架构 1.Containerd架构概述 2.核心组件解析 三.安装配置Containerd 1.安装Containerd 2.配置Containerd 四.Containerd基本操作 1.镜像类操作 2.容器类操作 3.任务类操作 4.其他操作 一.…