Android?Springboot?實(shí)現(xiàn)SSE通信案例詳解
SSE
SSE(Server-Sent Events)是一種用于實(shí)現(xiàn)服務(wù)器主動(dòng)向客戶端推送數(shù)據(jù)的技術(shù),它基于 HTTP 協(xié)議,利用了其長(zhǎng)連接特性,在客戶端與服務(wù)器之間建立一條持久化連接,并通過(guò)這條連接實(shí)現(xiàn)服務(wù)器向客戶端的實(shí)時(shí)數(shù)據(jù)推送。
Server-Sent Events (SSE) 和 Sockets 都可以用于實(shí)現(xiàn)服務(wù)器向客戶端推送消息的實(shí)時(shí)通信,差異對(duì)比:
SSE:
優(yōu)點(diǎn): 使用簡(jiǎn)單,只需發(fā)送 HTTP 流式響應(yīng)。 自動(dòng)處理網(wǎng)絡(luò)中斷和重連。 支持由瀏覽器原生實(shí)現(xiàn)的事件,如 "error" 和 "message"。 缺點(diǎn): 單向通信,服務(wù)器只能發(fā)送消息給客戶端。 每個(gè)連接需要服務(wù)器端的一個(gè)線程或進(jìn)程。
Socket:
優(yōu)點(diǎn): 雙向通信,客戶端和服務(wù)器都可以發(fā)送或接收消息。 可以處理更復(fù)雜的應(yīng)用場(chǎng)景,如雙向?qū)υ?、多人游戲等? 服務(wù)器可以更精細(xì)地管理連接,如使用長(zhǎng)連接或短連接。 缺點(diǎn): 需要處理網(wǎng)絡(luò)中斷和重連,相對(duì)復(fù)雜。 需要客戶端和服務(wù)器端的代碼都能處理 Socket 通信。 對(duì)開發(fā)者要求較高,需要對(duì)網(wǎng)絡(luò)編程有深入了解。
SSE使用場(chǎng)景:
使用場(chǎng)景主要包括需要服務(wù)器主動(dòng)向客戶端推送數(shù)據(jù)的應(yīng)用場(chǎng)景,?如AI問(wèn)答聊天、實(shí)時(shí)新聞、?股票行情等。
案例
服務(wù)端基于springboot實(shí)現(xiàn),默認(rèn)支持SSE;
Android客戶端基于OkHttp實(shí)現(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請(qǐng)求,異步處理,分批次返回結(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ā)送錯(cuò)誤 } }).start(); return emitter; } /** * 接收sse請(qǐng)求,異步處理,分批次返回結(jié)果,并存儲(chǔ)SseEmitter,可通過(guò)外界調(diào)用sendMsg接口,繼續(xù)返回結(jié)果 * @param uid 客戶唯一標(biāo)識(shí) * @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ā)送錯(cuò)誤 } }).start(); return emitter; } /** * 外界調(diào)用sendMsg接口,根據(jù)標(biāo)識(shí)獲取緩存的SseEmitter,繼續(xù)返回結(jié)果 * @param uid 客戶唯一標(biāo)識(shí) */ @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ù)端主動(dòng)關(guān)閉了連接 to " + uid); } catch (IOException | InterruptedException e) { emitter.completeWithError(e); // error finish } }).start(); } } }
代碼定義了3個(gè)接口,主要實(shí)現(xiàn)了兩個(gè)功能:
stream-sse 接口
用于模擬一次請(qǐng)求,批次返回結(jié)果,然后結(jié)束SseEmitter;
stream-sse1接口 & sendMsg接口
用于模擬一次請(qǐng)求,批次返回結(jié)果,緩存SseEmitter,后續(xù)還可以通過(guò)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="測(cè)試普通接口" 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) // 建立連接的超時(shí)時(shí)間 .readTimeout(10, TimeUnit.MINUTES) // 建立連接后讀取數(shù)據(jù)的超時(shí)時(shí)間 .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) // 建立連接的超時(shí)時(shí)間 .readTimeout(10, TimeUnit.MINUTES) // 建立連接后讀取數(shù)據(jù)的超時(shí)時(shí)間 .build(); RealEventSource realEventSource = new RealEventSource(request, new EventSourceListener() { @Override public void onEvent(EventSource eventSource, String id, String type, String data) { System.out.println(data); // 請(qǐng)求到的數(shù)據(jù) String text = tv.getText().toString(); tv.setText(data+"\n"+text); if ("finish".equals(type)) { // 消息類型,add 增量,finish 結(jié)束,error 錯(cuò)誤,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) // 建立連接的超時(shí)時(shí)間 .readTimeout(10, TimeUnit.MINUTES) // 建立連接后讀取數(shù)據(jù)的超時(shí)時(shí)間 .build(); RealEventSource realEventSource = new RealEventSource(request, new EventSourceListener() { @Override public void onEvent(EventSource eventSource, String id, String type, String data) { System.out.println(data); // 請(qǐng)求到的數(shù)據(jù) String text = tv.getText().toString(); tv.setText(data+"\n"+text); } }); realEventSource.connect(okHttpClient); } }
效果測(cè)試
調(diào)用stream-sse接口
服務(wù)器分批次返回了結(jié)果:
調(diào)用stream-sse1接口
服務(wù)器分批次返回了結(jié)果:
通過(guò)h5調(diào)用sendMsg接口,服務(wù)端繼續(xù)返回結(jié)果:
到此這篇關(guān)于Android Springboot 實(shí)現(xiàn)SSE通信案例的文章就介紹到這了,更多相關(guān)Android Springboot SSE通信內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot3中Spring?WebFlux?SSE服務(wù)器發(fā)送事件的實(shí)現(xiàn)步驟
- Spring?Boot整合Kafka+SSE實(shí)現(xiàn)實(shí)時(shí)數(shù)據(jù)展示
- Spring Boot中使用Server-Sent Events (SSE) 實(shí)現(xiàn)實(shí)時(shí)數(shù)據(jù)推送教程
- springboot-assembly自定義打包全過(guò)程
- Springboot集成SSE實(shí)現(xiàn)單工通信消息推送流程詳解
- Spring Boot整合SSE實(shí)時(shí)通信的問(wèn)題小結(jié)
相關(guān)文章
android仿即刻點(diǎn)贊文字部分的自定義View的示例代碼
本篇文章主要介紹了android仿即刻點(diǎn)贊文字部分的自定義View的示例代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-11-11Android TextView實(shí)現(xiàn)跑馬燈效果的方法
這篇文章主要介紹了Android TextView跑馬燈效果實(shí)現(xiàn)方法,涉及Android布局文件中相關(guān)屬性的設(shè)置技巧,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-01-01Android Application存取公共數(shù)據(jù)的實(shí)例詳解
這篇文章主要介紹了Android Application存取公共數(shù)據(jù)的實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下2017-07-07Android ViewFlipper的簡(jiǎn)單使用
這篇文章主要為大家詳細(xì)介紹了Android ViewFlipper的簡(jiǎn)單使用,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06Android recycleView的應(yīng)用和點(diǎn)擊事件實(shí)例詳解
這篇文章主要介紹了Android recycleView的應(yīng)用和點(diǎn)擊事件實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下2016-12-12談?wù)凙ndroid里的Context的使用實(shí)例
這篇文章主要介紹了談?wù)凙ndroid里的Context的使用實(shí)例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2016-11-11Android編程實(shí)現(xiàn)橫豎屏切換時(shí)不銷毀當(dāng)前activity和鎖定屏幕的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)橫豎屏切換時(shí)不銷毀當(dāng)前activity和鎖定屏幕的方法,涉及Android屬性設(shè)置及activity操作的相關(guān)技巧,需要的朋友可以參考下2015-11-11