SpringBoot使用SSE向前端推送消息

2,125 阅读3分钟

SSE简介

SSE(Server Sent Event),直译为服务器发送事件,也就是服务器主动发送事件,客户端可以获取到服务器发送的事件。


SSE简单来说就是服务器主动向前端推送数据的一种技术,它是单向的。SSE适用于消息推送,监控等只需要服务器推送数据的场景中。比如:文件下载时,后端可以推送下载进度条信息。

SSE相关依赖

 <!--web依赖,内嵌入tomcat,SSE依赖于该jar包,只要有该依赖就能使用SSE-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

SSE工具类代码

@Slf4j
public class SseEmitterServer {

    /**
     * 当前连接数
     */
    private static AtomicInteger count = new AtomicInteger(0);

    private static Map<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();

    public static SseEmitter connect(String userId){
        //设置超时时间,0表示不过期,默认是30秒,超过时间未完成会抛出异常
        SseEmitter sseemitter = new SseEmitter(0L);
        //注册回调
        sseemitter.onCompletion(completionCallBack(userId));
        //这个onError在springbooot低版本没有这个方法,公司springboot1.4.2版本,没有这个方法,可以进行注释。
        sseemitter.onError(errorCallBack(userId));
        sseemitter.onTimeout(timeoutCallBack(userId));
        sseEmitterMap.put(userId,sseemitter);
        //数量+1
        count.getAndIncrement();
        log.info("create new sse connect ,current user:{}",userId);
        return sseemitter;
    }

    /**
     * 给指定用户发消息
     */
    public static void sendMessage(String userId, String message){
        if(sseEmitterMap.containsKey(userId)){
            try{
                sseEmitterMap.get(userId).send(message);
            }catch (IOException e){
                log.error("user id:{}, send message error:{}",userId,e.getMessage());
                e.printStackTrace();
            }
        }
    }

    /**
     * 想多人发送消息,组播
     */
    public static void groupSendMessage(String groupId, String message){
        if(sseEmitterMap!=null&&!sseEmitterMap.isEmpty()){
            sseEmitterMap.forEach((k,v) -> {
                try{
                    if(k.startsWith(groupId)){
                        v.send(message, MediaType.APPLICATION_JSON);
                    }
                }catch (IOException e){
                    log.error("user id:{}, send message error:{}",groupId,message);
                    removeUser(k);
                }
            });
        }
    }

    public static void batchSendMessage(String message) {
        sseEmitterMap.forEach((k,v)->{
            try{
                v.send(message,MediaType.APPLICATION_JSON);
            }catch (IOException e){
                log.error("user id:{}, send message error:{}",k,e.getMessage());
                removeUser(k);
            }
        });
    }

    /**
     * 群发消息
     */
    public static void batchSendMessage(String message, Set<String> userIds){
        userIds.forEach(userid->sendMessage(userid,message));
    }

    //移除用户
    public static void removeUser(String userid){
        sseEmitterMap.remove(userid);
        //数量-1
        count.getAndDecrement();
        log.info("remove user id:{}",userid);
    }

    public static List<String> getIds(){
        return new ArrayList<>(sseEmitterMap.keySet());
    }

    public static int getUserCount(){
        return count.intValue();
    }

    private static Runnable completionCallBack(String userId) {
        return () -> {
            log.info("结束连接,{}",userId);
            removeUser(userId);
        };
    }
    private static Runnable timeoutCallBack(String userId){
        return ()->{
            log.info("连接超时,{}",userId);
            removeUser(userId);
        };
    }
    private static Consumer<Throwable> errorCallBack(String userId){
        return throwable -> {
            log.error("连接异常,{}",userId);
            removeUser(userId);
        };
    }
}

Controller测试层

@RestController
@RequestMapping(value = "/test")
public class TestController {

    //sse连接接口
    @GetMapping (value = "/sse/connect/{id}")
    public SseEmitter connect(@PathVariable String id){
        SseEmitter connect = SseEmitterServer.connect(id);
        //连接成功之后通知前端连接成功
        SseEmitterServer.sendMessage(id,"链接成功");
        return connect;
    }

    //sse向指定用户发送消息接口
    @GetMapping (value = "/sse/send/{id}")
    public Map<String,Object> send(@PathVariable String id,@RequestParam(value = "message", required = false) String message){
        Map<String,Object> returnMap = new HashMap<>();
        //向指定用户发送信息
        SseEmitterServer.sendMessage(id,message);
        returnMap.put("message","向id为"+id+"的用户发送:"+message+"成功!");
        returnMap.put("status","200");
        returnMap.put("result",null);
        return returnMap;
    }

    //sse向所有已连接用户发送消息接口
    @GetMapping (value = "/sse/batchSend")
    public Map<String,Object> batchSend(@RequestParam(value = "message", required = false) String message){
        Map<String,Object> returnMap = new HashMap<>();
        //向指定用户发送信息
        SseEmitterServer.batchSendMessage(message);
        returnMap.put("message",message+"消息发送成功!");
        returnMap.put("status","200");
        returnMap.put("result",null);
        return returnMap;
    }

    //sse关闭接口
    @GetMapping (value = "/sse/close/{id}")
    public Map<String,Object> close(@PathVariable String id){
        Map<String,Object> returnMap = new HashMap<>();
        //移除id
        SseEmitterServer.removeUser(id);
        System.out.println("当前连接用户id:"+SseEmitterServer.getIds());
        returnMap.put("message","连接关闭成功!");
        returnMap.put("status","200");
        returnMap.put("result",null);
        return returnMap;
    }
}

Apifox测试效果
测试连接接口: 如下图客户端主动连接服务端成功!

image.png

服务端推送消息给客户端

image.png

测试群发功能(这时候我们在使用一个请求连接到SSE服务端,并指定不同的连接id)
后端打印当前连接成功的用户:

image.png 群发测试:

image.png 此时两个连接成功的SSE客户端都能收到服务端推送的消息内容。
关闭连接的接口测试

image.png 如果有什么问题欢迎大家私信,也可以互相交流,私信必回