SpringBoot uses SSE for real-time notification front-end

illustrate

One requirement of the project is to notify the front-end in real time that the task is loaded. Then I thought of two solutions, one is to use websocket for a long connection, and the other is to use SSE (Sever Send Event), which is one of the HTTP protocols. The Content-Type is text/event-stream, which can maintain a long connection.
Websocket is a front-end that can send messages to the back-end, and the back-end can also send messages to the front-end.
SSE can only send messages from the backend to the frontend.
Because I only need backend notifications, I chose to use SSE here.
Make a note here first, lest you forget how to use it later.

maven dependency

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.project</groupId>
    <artifactId>test</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>test</name>
    <description>test</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!--web依赖,内嵌入tomcat,SSE依赖于该jar包,只要有该依赖就能使用SSE-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--lombok依赖,用来对象省略写set、get方法-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

SSE tool code

package com.etone.project.utils;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;

@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 test code

package com.project.test.controller;

import com.hjl.test.util.SseEmitterServer;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.util.HashMap;
import java.util.Map;

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

    //sse连接接口
    @GetMapping (value = "/sse/connect/{id}")
    public SseEmitter connect(@PathVariable String id){
    
    
        return SseEmitterServer.connect(id);
    }

    //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;
    }

}

The test results are as follows:

To test the SSE connection here, just request it like a normal interface.
The local calling interface /sse/connect/1 is as follows:
Here I connect two users to simulate sending information to the specified user ID and sending messages in batches to connected users.
Insert image description here
Insert image description here
The backend service prints as follows:
Insert image description here

The local call interface /sse/send/1 is as follows:
Insert image description here
The result of user 1 is as follows, and it is found that it has received the message:
Insert image description here
User 2 did not receive the result, as follows:
Insert image description here

The local calling interface /sse/batchSend is as follows:
send messages in batches to all connected users.
Insert image description here
The results of user 1 are as follows, and it is found that the message has been received:
Insert image description here
The results of user 2 are as follows, and it is found that the message has been received:
Insert image description here
The test results are all in line with expectations.
Click postman's close button to close the connection:
Insert image description here
Insert image description here
It is found that although the front-end connection is closed, the back-end is actually still connected, and there is no prompt to remove the user at all:
Insert image description here
so you still need to manually write the closing interface test yourself.
The local calling interface /sse/close/1 is as follows:
Insert image description here
you can see that the user ID 1 has been removed, and only user 2 is still connected.
Insert image description here
All tests here are completed and the results are as expected.

Notice

If you change the timeout from the original 0 to 30 milliseconds (L after the integer means that Long occupies 8 bytes), an error will be reported.

Insert image description here
The test results are as follows:
Insert image description here
Insert image description here
An exception occurred directly here: org.springframework.web.context.request.async.AsyncRequestTimeoutException
and even the connection was disconnected.

Reduce springboot to a lower version such as 1.4.2.RELEASE.

When using postman for testing, I found that it was not always in the request: as follows:
downgrade Springboot to 1.4.2.RELEASE
Insert image description here

The 1.4.2.RELEASE version of springboot does not have the onError method and needs to be commented out. Insert image description here
The postman test is as follows:
When testing the lower version, it was found that it has a connection that can be directly seen. However, when using springboot version 2.x, it was found that it is always sending requests. When the backend sends a message to the frontend, it Show this.
Springboot 1.4.2.RELEASE version results:
Insert image description here
springboot 2.7.3 version results:
Insert image description here

Let’s record this situation first, and then study it later when we have time. Why can't the higher version return this connection information like the lower version? So when the higher version of SpringBoot uses SSE to connect, it is always in the Sending request situation. Is this normal? Can anyone please let me know, thank you.

Guess you like

Origin blog.csdn.net/weixin_48040732/article/details/131000339