Android?Springboot?實現(xiàn)SSE通信案例詳解
SSE
SSE(Server-Sent Events)是一種用于實現(xiàn)服務(wù)器主動向客戶端推送數(shù)據(jù)的技術(shù),它基于 HTTP 協(xié)議,利用了其長連接特性,在客戶端與服務(wù)器之間建立一條持久化連接,并通過這條連接實現(xiàn)服務(wù)器向客戶端的實時數(shù)據(jù)推送。
Server-Sent Events (SSE) 和 Sockets 都可以用于實現(xiàn)服務(wù)器向客戶端推送消息的實時通信,差異對比:
SSE:
優(yōu)點: 使用簡單,只需發(fā)送 HTTP 流式響應(yīng)。 自動處理網(wǎng)絡(luò)中斷和重連。 支持由瀏覽器原生實現(xiàn)的事件,如 "error" 和 "message"。 缺點: 單向通信,服務(wù)器只能發(fā)送消息給客戶端。 每個連接需要服務(wù)器端的一個線程或進(jìn)程。
Socket:
優(yōu)點: 雙向通信,客戶端和服務(wù)器都可以發(fā)送或接收消息。 可以處理更復(fù)雜的應(yīng)用場景,如雙向?qū)υ?、多人游戲等? 服務(wù)器可以更精細(xì)地管理連接,如使用長連接或短連接。 缺點: 需要處理網(wǎng)絡(luò)中斷和重連,相對復(fù)雜。 需要客戶端和服務(wù)器端的代碼都能處理 Socket 通信。 對開發(fā)者要求較高,需要對網(wǎng)絡(luò)編程有深入了解。
SSE使用場景:
使用場景主要包括需要服務(wù)器主動向客戶端推送數(shù)據(jù)的應(yīng)用場景,?如AI問答聊天、實時新聞、?股票行情等。
案例
服務(wù)端基于springboot實現(xiàn),默認(rèn)支持SSE;
Android客戶端基于OkHttp實現(xiàn),同樣也支SSE;
服務(wù)端接口開發(fā)
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請求,異步處理,分批次返回結(jié)果,然后關(guān)閉SseEmitter
* @return SseEmitter
*/
@GetMapping("/stream-sse")
public SseEmitter handleSse() {
SseEmitter emitter = new SseEmitter();
// 在新線程中發(fā)送消息,以避免阻塞主線程
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(); // 完成發(fā)送
} catch (IOException | InterruptedException e) {
emitter.completeWithError(e); // 發(fā)送錯誤
}
}).start();
return emitter;
}
/**
* 接收sse請求,異步處理,分批次返回結(jié)果,并存儲SseEmitter,可通過外界調(diào)用sendMsg接口,繼續(xù)返回結(jié)果
* @param uid 客戶唯一標(biāo)識
* @return SseEmitter
*/
@GetMapping("/stream-sse1")
public SseEmitter handleSse1(@RequestParam("uid") String uid) {
SseEmitter emitter = new SseEmitter();
sseEmitters.put(uid, emitter);
// 在新線程中發(fā)送消息,以避免阻塞主線程
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秒發(fā)送一次
}
} catch (IOException | InterruptedException e) {
emitter.completeWithError(e); // 發(fā)送錯誤
}
}).start();
return emitter;
}
/**
* 外界調(diào)用sendMsg接口,根據(jù)標(biāo)識獲取緩存的SseEmitter,繼續(xù)返回結(jié)果
* @param uid 客戶唯一標(biāo)識
*/
@GetMapping("/sendMsg")
public void sendMsg(@RequestParam("uid") String uid) {
logger.debug("服務(wù)端發(fā)送消息 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秒發(fā)送一次
}
emitter.send(SseEmitter.event().name("stop").data(""));
emitter.complete(); // close connection
logger.debug("服務(wù)端主動關(guān)閉了連接 to " + uid);
} catch (IOException | InterruptedException e) {
emitter.completeWithError(e); // error finish
}
}).start();
}
}
}代碼定義了3個接口,主要實現(xiàn)了兩個功能:
stream-sse 接口
用于模擬一次請求,批次返回結(jié)果,然后結(jié)束SseEmitter;
stream-sse1接口 & sendMsg接口
用于模擬一次請求,批次返回結(jié)果,緩存SseEmitter,后續(xù)還可以通過sendMsg接口,通知服務(wù)端繼續(xù)返回結(jié)果;
客戶端功能開發(fā)
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連接,攜帶參數(shù)"
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) // 建立連接后讀取數(shù)據(jù)的超時時間
.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(); // 獲取響應(yīng)體
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) // 建立連接后讀取數(shù)據(jù)的超時時間
.build();
RealEventSource realEventSource = new RealEventSource(request, new EventSourceListener() {
@Override
public void onEvent(EventSource eventSource, String id, String type, String data) {
System.out.println(data); // 請求到的數(shù)據(jù)
String text = tv.getText().toString();
tv.setText(data+"\n"+text);
if ("finish".equals(type)) { // 消息類型,add 增量,finish 結(jié)束,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) // 建立連接后讀取數(shù)據(jù)的超時時間
.build();
RealEventSource realEventSource = new RealEventSource(request, new EventSourceListener() {
@Override
public void onEvent(EventSource eventSource, String id, String type, String data) {
System.out.println(data); // 請求到的數(shù)據(jù)
String text = tv.getText().toString();
tv.setText(data+"\n"+text);
}
});
realEventSource.connect(okHttpClient);
}
}效果測試
調(diào)用stream-sse接口
服務(wù)器分批次返回了結(jié)果:

調(diào)用stream-sse1接口
服務(wù)器分批次返回了結(jié)果:

通過h5調(diào)用sendMsg接口,服務(wù)端繼續(xù)返回結(jié)果:


到此這篇關(guān)于Android Springboot 實現(xiàn)SSE通信案例的文章就介紹到這了,更多相關(guān)Android Springboot SSE通信內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android TextView實現(xiàn)跑馬燈效果的方法
這篇文章主要介紹了Android TextView跑馬燈效果實現(xiàn)方法,涉及Android布局文件中相關(guān)屬性的設(shè)置技巧,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-01-01
Android Application存取公共數(shù)據(jù)的實例詳解
這篇文章主要介紹了Android Application存取公共數(shù)據(jù)的實例詳解的相關(guān)資料,需要的朋友可以參考下2017-07-07
Android recycleView的應(yīng)用和點擊事件實例詳解
這篇文章主要介紹了Android recycleView的應(yīng)用和點擊事件實例詳解的相關(guān)資料,需要的朋友可以參考下2016-12-12
Android編程實現(xiàn)橫豎屏切換時不銷毀當(dāng)前activity和鎖定屏幕的方法
這篇文章主要介紹了Android編程實現(xiàn)橫豎屏切換時不銷毀當(dāng)前activity和鎖定屏幕的方法,涉及Android屬性設(shè)置及activity操作的相關(guān)技巧,需要的朋友可以參考下2015-11-11

