首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

Android Spingboot 实现SSE通信案例

编程知识
2024年07月24日 14:35

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接口,服务端继续返回结果:

From:https://www.cnblogs.com/qixingchao/p/18320799
本文地址: http://www.shuzixingkong.net/article/377
0评论
提交 加载更多评论
其他文章 Known框架实战演练——进销存业务单据
本文介绍如何实现进销存管理系统的业务单据模块,业务单据模块包括采购进货单、采购退货单、销售出货单、销售退货单4个菜单页面。由于进销单据字段大同小异,因此设计共用一个页面组件类。 项目代码:JxcLite 开源地址: https://gitee.com/known/JxcLite 1. 配置模块 运行
Linux 文本文件编辑相关命令简介【Linux 常用命令系列二】
本文介绍了如何通过 vim 命令,对文本文件进行打开、编辑、保存等相关操作,并通过简单的示例演示了常用用法。
Linux 文本文件编辑相关命令简介【Linux 常用命令系列二】 Linux 文本文件编辑相关命令简介【Linux 常用命令系列二】 Linux 文本文件编辑相关命令简介【Linux 常用命令系列二】
2024-07-24:用go语言,给定一个整数数组 nums,其中至少包含两个元素。 可以根据以下规则执行操作:选择最前面两个元素删除、选择最后两个元素删除,或选择第一个和最后一个元素删除。 每次操作
2024-07-24:用go语言,给定一个整数数组 nums,其中至少包含两个元素。 可以根据以下规则执行操作:选择最前面两个元素删除、选择最后两个元素删除,或选择第一个和最后一个元素删除。 每次操作的得分是被删除元素的和。 在每次操作后,所有操作得分需保持相同。 问题要求确定在这些前提下,最多可以
2024-07-24:用go语言,给定一个整数数组 nums,其中至少包含两个元素。 可以根据以下规则执行操作:选择最前面两个元素删除、选择最后两个元素删除,或选择第一个和最后一个元素删除。 每次操作
关于学习.NET的历程回顾与今后的探索实践方向
关于学习.NET的历程回顾 自从2023年9月11日注册公众号以来,这次还是第一次介绍自己。我今年24岁,双非本,211硕,非计算机相关专业。大学期间接触过计算机相关的课程可能就《大学生计算机基础》、《C语言程序设计》,并且也没掌握多好。 22年4月研究生复试结束,联系好导师后,由于导师研究方向的缘
关于学习.NET的历程回顾与今后的探索实践方向 关于学习.NET的历程回顾与今后的探索实践方向 关于学习.NET的历程回顾与今后的探索实践方向
DASCTF 2023 & 0X401七月暑期挑战赛【PWN】(FileEditor篇)
DASCTF 2023 &amp; 0X401七月暑期挑战赛【PWN】(FileEditor篇) 题目保护情况(保护全家桶) 64位ida逆向 模拟了一个类似vim的功能,有打开文件,打印内容,插入行,删除行,复制行,和编辑行,还有查找字符和替换字符的功能,然后就是保存退出 一个一个来分析吧 1.o
DASCTF 2023 & 0X401七月暑期挑战赛【PWN】(FileEditor篇) DASCTF 2023 & 0X401七月暑期挑战赛【PWN】(FileEditor篇) DASCTF 2023 & 0X401七月暑期挑战赛【PWN】(FileEditor篇)
RestSharp编写api接口测试,并实现异步调用(不卡顿)
首先,确保你已经安装了RestSharp NuGet包。如果没有安装,可以通过以下命令安装: bash Install-Package RestSharp 然后,在你的C#代码中,你可以按照以下步骤操作: 引用RestSharp命名空间。 创建一个RestClient实例。 创建一个RestRequ
博弈论
一、要素 局中人:在一场竞赛或博弈中,每一个有决策权的参与者成为一个局中人。只有两个局中人的博弈现象称为“两人博弈”,而多于两个局中人的博弈称为“多人博弈”。 策略:一局博弈中,每个局中人都有选择实际可行的完整的行动方案,即方案不是某阶段的行动方案,而是指导整个行动的一个方案,一个局中人的一个可行的
博弈论 博弈论 博弈论
2个月搞定计算机二级C语言——真题(1)解析
1. 前言 大家好,我是梁国庆。 这段时间将持续发布计算机二级 C 语言真题的解析,想要同步练习,需要资源包的朋友可以跳转免费获取——《3个月搞定计算机二级C语言——准备工作》。 现在恐怕要改为 2 个月搞定计算机二级 C 语言了,不过没有关系,干就完了! 本篇博客将解析计算机二级 C 语言考试真题
2个月搞定计算机二级C语言——真题(1)解析 2个月搞定计算机二级C语言——真题(1)解析 2个月搞定计算机二级C语言——真题(1)解析