SSE

SSE(Server-Sent Events)是一种用于实现服务器主动向客户端推送数据的技术,它基于 HTTP 协议,利用了其长连接特性,在客户端与服务器之间建立一条持久化连接,并通过这条连接实现服务器向客户端的实时数据推送。

Server-Sent Events (SSE) 和 Sockets 都可以用于实现服务器向客户端推送消息的实时通信,差异对比:

SSE:

优点:
使用简单,只需发送 HTTP 流式响应。
自动处理网络中断和重连。
支持由浏览器原生实现的事件,如 "error" 和 "message"。 缺点:
单向通信,服务器只能发送消息给客户端。
每个连接需要服务器端的一个线程或进程。

Socket:

优点:
双向通信,客户端和服务器都可以发送或接收消息。
可以处理更复杂的应用场景,如双向对话、多人游戏等。
服务器可以更精细地管理连接,如使用长连接或短连接。 缺点:
需要处理网络中断和重连,相对复杂。
需要客户端和服务器端的代码都能处理 Socket 通信。
对开发者要求较高,需要对网络编程有深入了解。

SSE使用场景:

使用场景主要包括需要服务器主动向客户端推送数据的应用场景,‌如AI问答聊天、实时新闻、‌股票行情等。

案例

服务端基于springboot实现,默认支持SSE;

Android客户端基于OkHttp实现,同样也支SSE;

服务端接口开发

SSEController.java

package com.qxc.server.controller.sse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; @RestController
@RequestMapping("/sse")
public class SSEController {
Logger logger = LoggerFactory.getLogger(SSEController.class);
public static Map<String, SseEmitter> sseEmitters = new ConcurrentHashMap<>(); /**
* 接收sse请求,异步处理,分批次返回结果,然后关闭SseEmitter
* @return SseEmitter
*/
@GetMapping("/stream-sse")
public SseEmitter handleSse() {
SseEmitter emitter = new SseEmitter();
// 在新线程中发送消息,以避免阻塞主线程
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
Map<String, Object> event = new HashMap<>();
String mes = "Hello, SSE " + (i+1);
event.put("message", mes);
logger.debug("emitter.send: "+mes);
emitter.send(event);
Thread.sleep(200);
}
emitter.complete(); // 完成发送
} catch (IOException | InterruptedException e) {
emitter.completeWithError(e); // 发送错误
}
}).start();
return emitter;
} /**
* 接收sse请求,异步处理,分批次返回结果,并存储SseEmitter,可通过外界调用sendMsg接口,继续返回结果
* @param uid 客户唯一标识
* @return SseEmitter
*/
@GetMapping("/stream-sse1")
public SseEmitter handleSse1(@RequestParam("uid") String uid) {
SseEmitter emitter = new SseEmitter();
sseEmitters.put(uid, emitter);
// 在新线程中发送消息,以避免阻塞主线程
new Thread(() -> {
try {
for (int i = 10; i < 15; i++) {
Map<String, Object> event = new HashMap<>();
String mes = "Hello, SSE " + (i+1);
event.put("message", mes);
logger.debug("emitter.send: "+mes);
emitter.send(event);
Thread.sleep(200); // 每2秒发送一次
}
} catch (IOException | InterruptedException e) {
emitter.completeWithError(e); // 发送错误
}
}).start();
return emitter;
} /**
* 外界调用sendMsg接口,根据标识获取缓存的SseEmitter,继续返回结果
* @param uid 客户唯一标识
*/
@GetMapping("/sendMsg")
public void sendMsg(@RequestParam("uid") String uid) {
logger.debug("服务端发送消息 to " + uid);
SseEmitter emitter = sseEmitters.get(uid);
if(emitter != null){
new Thread(() -> {
try {
for (int i = 20; i < 30; i++) {
Map<String, Object> event = new HashMap<>();
String mes = "Hello, SSE " + (i+1);
event.put("message", mes);
logger.debug("emitter.send: "+mes);
emitter.send(event);
Thread.sleep(200); // 每2秒发送一次
}
emitter.send(SseEmitter.event().name("stop").data(""));
emitter.complete(); // close connection
logger.debug("服务端主动关闭了连接 to " + uid);
} catch (IOException | InterruptedException e) {
emitter.completeWithError(e); // error finish
}
}).start();
}
}
}

代码定义了3个接口,主要实现了两个功能:

stream-sse 接口

用于模拟一次请求,批次返回结果,然后结束SseEmitter;

stream-sse1接口 & sendMsg接口

用于模拟一次请求,批次返回结果,缓存SseEmitter,后续还可以通过sendMsg接口,通知服务端继续返回结果;

客户端功能开发

Android客户端依赖OkHttp:

implementation 'com.squareup.okhttp3:okhttp:4.9.1'
implementation("com.squareup.okhttp3:okhttp-sse:4.9.1")

布局文件:activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"> <TextView
android:id="@+id/tv"
android:layout_above="@id/btn"
android:layout_centerHorizontal="true"
android:text="--"
android:lines="15"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp"/> <Button
android:layout_width="200dp"
android:layout_height="50dp"
android:id="@+id/btn"
android:text="测试普通接口"
android:layout_centerInParent="true"/> <Button
android:layout_width="200dp"
android:layout_height="50dp"
android:id="@+id/btn1"
android:layout_below="@id/btn"
android:text="sse连接"
android:layout_centerInParent="true"/> <Button
android:layout_width="200dp"
android:layout_height="50dp"
android:id="@+id/btn2"
android:layout_below="@id/btn1"
android:text="sse连接,携带参数"
android:layout_centerInParent="true"/>
</RelativeLayout>

MainActivity.java

package com.cb.testsd;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView; import java.util.concurrent.TimeUnit; import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.internal.sse.RealEventSource;
import okhttp3.sse.EventSource;
import okhttp3.sse.EventSourceListener; public class MainActivity extends Activity {
Button btn;
Button btn1;
Button btn2;
TextView tv; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn = findViewById(R.id.btn);
btn1 = findViewById(R.id.btn1);
btn2 = findViewById(R.id.btn2);
tv = findViewById(R.id.tv); btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
testDate();
}
}).start();
}
});
btn1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
sse();
}
}).start();
}
});
btn2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
sseWithParams();
}
}).start();
}
});
} private void testDate(){
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS) // 建立连接的超时时间
.readTimeout(10, TimeUnit.MINUTES) // 建立连接后读取数据的超时时间
.build();
Request request = new Request.Builder()
.url("http://192.168.43.102:58888/common/getCurDate")
.build();
okhttp3.Call call = client.newCall(request);
try {
Response response = call.execute(); // 同步方法
if (response.isSuccessful()) {
String responseBody = response.body().string(); // 获取响应体
System.out.println(responseBody);
tv.setText(responseBody);
}
} catch (Exception e) {
e.printStackTrace();
}
} void sse(){
Request request = new Request.Builder()
.url("http://192.168.43.102:58888/sse/stream-sse")
.addHeader("Authorization", "Bearer ")
.addHeader("Accept", "text/event-stream")
.build(); OkHttpClient okHttpClient = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS) // 建立连接的超时时间
.readTimeout(10, TimeUnit.MINUTES) // 建立连接后读取数据的超时时间
.build(); RealEventSource realEventSource = new RealEventSource(request, new EventSourceListener() {
@Override
public void onEvent(EventSource eventSource, String id, String type, String data) {
System.out.println(data); // 请求到的数据
String text = tv.getText().toString();
tv.setText(data+"\n"+text);
if ("finish".equals(type)) { // 消息类型,add 增量,finish 结束,error 错误,interrupted 中断 }
}
});
realEventSource.connect(okHttpClient);
} void sseWithParams(){
Request request = new Request.Builder()
.url("http://192.168.43.102:58888/sse/stream-sse1?uid=1")
.addHeader("Authorization", "Bearer ")
.addHeader("Accept", "text/event-stream")
.build(); OkHttpClient okHttpClient = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS) // 建立连接的超时时间
.readTimeout(10, TimeUnit.MINUTES) // 建立连接后读取数据的超时时间
.build(); RealEventSource realEventSource = new RealEventSource(request, new EventSourceListener() {
@Override
public void onEvent(EventSource eventSource, String id, String type, String data) {
System.out.println(data); // 请求到的数据
String text = tv.getText().toString();
tv.setText(data+"\n"+text);
}
});
realEventSource.connect(okHttpClient);
}
}

效果测试

调用stream-sse接口

服务器分批次返回了结果:

调用stream-sse1接口

服务器分批次返回了结果:



通过h5调用sendMsg接口,服务端继续返回结果:



Android Spingboot 实现SSE通信案例的更多相关文章

  1. Android BLE与终端通信(五)——Google API BLE4.0低功耗蓝牙文档解读之案例初探

    Android BLE与终端通信(五)--Google API BLE4.0低功耗蓝牙文档解读之案例初探 算下来很久没有写BLE的博文了,上家的技术都快忘记了,所以赶紧读了一遍Google的API顺便 ...

  2. Android中的HTTP通信

    前言:近期在慕课网学习了慕课网课程Android中的HTTP通信,就自己总结了一下,其中参考了不少博文,感谢大家的分享. 文章内容包括:1.HTTP简介2.HTTP/1.0和HTTP/1.1之间的区别 ...

  3. Android进程间的通信之AIDL

    Android服务被设计用来执行很多操作,比如说,可以执行运行时间长的耗时操作,比较耗时的网络操作,甚至是在一个单独进程中的永不会结束的操作.实现这些操作之一是通过Android接口定义语言(AIDL ...

  4. NFC:Arduino、Android与PhoneGap近场通信

    NFC:Arduino.Android与PhoneGap近场通信(第一本全面讲解NFC应用开发的技术著作移动智能设备近距离通信编程实战入门) [美]Tom Igoe(汤姆.伊戈),Don Colema ...

  5. android 与usb 设备通信(二)

    再次遇到android  mUsbManager.getDevicelist() 得不到usb 设备的问题.于是深入去探讨android 与usb 外围设备通信的问题.第一篇文章写的有点乱,本质就是需 ...

  6. 使用Broadcast实现android组件之间的通信 分类: android 学习笔记 2015-07-09 14:16 110人阅读 评论(0) 收藏

    android组件之间的通信有多种实现方式,Broadcast就是其中一种.在activity和fragment之间的通信,broadcast用的更多本文以一个activity为例. 效果如图: 布局 ...

  7. 移动支付之智能IC卡与Android手机进行NFC通信

    本文来自http://blog.csdn.net/hellogv/ .引用必须注明出处.        眼下常见的智能IC卡执行着JavaCard虚拟机.智能IC卡上能够执行由精简后的Java语言编写 ...

  8. 使用Broadcast实现android组件之间的通信

    android组件之间的通信有多种实现方式,Broadcast就是其中一种.在activity和fragment之间的通信,broadcast用的更多本文以一个activity为例. 效果如图: 布局 ...

  9. Android BLE与终端通信(四)——实现服务器与客户端即时通讯功能

    Android BLE与终端通信(四)--实现服务器与客户端即时通讯功能 前面几篇一直在讲一些基础,其实说实话,蓝牙主要为多的还是一些概念性的东西,当你把概念都熟悉了之后,你会很简单的就可以实现一些逻 ...

  10. Android BLE与终端通信(三)——客户端与服务端通信过程以及实现数据通信

    Android BLE与终端通信(三)--客户端与服务端通信过程以及实现数据通信 前面的终究只是小知识点,上不了台面,也只能算是起到一个科普的作用,而同步到实际的开发上去,今天就来延续前两篇实现蓝牙主 ...

随机推荐

  1. Ubuntu20.04安装MySQL5.7

    ubuntu 20.04系统自带源直接安装的是MySQL 8.0,要安装MySQL 5.7的话,需要先换源. 1.换源 sudo cp /etc/apt/sources.list /etc/apt/s ...

  2. Android 13 - Media框架(21)- ACodec(三)

    关注公众号免费阅读全文,进入音视频开发技术分享群! 这一节我们一起来了解 ACodec 是如何通过 configureCodec 方法配置 OMX 组件的,因为 configureCodec 代码比较 ...

  3. 将任意程序安装成windows服务

    某些时候,一个程序控制台服务程序需要在系统启动的时候自动运行,这时候我们会想到采用windows服务的方式来实现 但是,如果程序本来不支持安装成服务的话,我就需要采用其他方案来实现 之前博主也遇到了类 ...

  4. 挨个配置资源组太麻烦?ROS伪参数一步搞定!

    介绍 伪参数 伪参数是资源编排服务ROS的编排引擎提供的固定参数,即在编写模板时可以使用的一系列预定义的参数,它们为模板提供了资源部署过程中的环境和执行上下文信息. 更多伪参数介绍请查看:ROS伪参数 ...

  5. flutter 打包web应用指定上下文

    使用flutter build web命令打包的应用不包含上下文,只能部署在根目录.如何指定上下文,部署在子目录下呢? 有两种办法: 1.修改web/index.html文件 修改 <base ...

  6. C# || 批量翻译工具 || 百度翻译api || 读取.cs文件内容 || 正则表达式筛选文件

    背景: 我们项目一开始的所有提示都是中文,后来要做国际化.发现项目中的带双引号的中文居然有 2.3 w 多条!!!简直让人欲哭无泪... 如果使用人工改的话,首先不说正确率了.光是效率都是难难难.所以 ...

  7. npm包管理器 切换npm源 使用nrm来管理npm镜像源

    npm是NodeJs项目模块管理工具,它已经集成了nodejs安装包中,在npm从从5.2版开始,增加了 npx 命令(解决的主要问题,就是调用项目内部安装的模块,很多工具不再需要npm -g 全局来 ...

  8. win10 chrome 百分浏览器 centbrowser 收藏夹栏字体突然变小

    win10 chrome 百分浏览器 centbrowser 收藏夹栏字体突然变小 解决方法: 在"开始" >"设置" >"轻松使用&qu ...

  9. 有点东西,template可以直接使用setup语法糖中的变量原来是因为这个

    前言 我们每天写vue3代码的时候都会使用到setup语法糖,那你知道为什么setup语法糖中的顶层绑定可以在template中直接使用的呢?setup语法糖是如何编译成setup函数的呢?本文将围绕 ...

  10. python 文件查找及截取字符串 (替换,分割) demo

    #"F:\\test.txt" ''' # 例1:字符串截取 str = '12345678' print str[0:1] # 例2:字符串替换 str = 'akakak' s ...