SpringBoot usa SSE para la interfaz de notificación en tiempo real

ilustrar

Un requisito del proyecto es notificar al front-end en tiempo real que la tarea está cargada. Luego pensé en dos soluciones, una es usar websocket para una conexión larga y la otra es usar SSE (Sever Send Event), que es uno de los protocolos HTTP. El tipo de contenido es texto/flujo de eventos, que Puede mantener una conexión larga.
Websocket es un front-end que puede enviar mensajes al back-end, y el back-end también puede enviar mensajes al front-end.
SSE solo puede enviar mensajes desde el backend al frontend.
Como solo necesito notificaciones de backend, elegí usar SSE aquí.
Tome nota aquí primero, no sea que olvide cómo usarlo más tarde.

dependencia experta

<?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>

Código de herramienta SSE

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

Código de prueba del controlador

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

}

Los resultados de la prueba son los siguientes:

Para probar la conexión SSE aquí, simplemente solicítela como una interfaz normal.
La interfaz de llamada local /sse/connect/1 es la siguiente:
Aquí conecto dos usuarios para simular el envío de información al ID de usuario especificado y el envío de mensajes en lotes a los usuarios conectados.
Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí
El servicio backend imprime de la siguiente manera:
Insertar descripción de la imagen aquí

La interfaz de llamada local /sse/send/1 es la siguiente:
Insertar descripción de la imagen aquí
El resultado del usuario 1 es el siguiente y se descubre que ha recibido el mensaje:
Insertar descripción de la imagen aquí
El usuario 2 no recibió el resultado, de la siguiente manera:
Insertar descripción de la imagen aquí

La interfaz de llamada local /sse/batchSend es la siguiente:
envía mensajes en lotes a todos los usuarios conectados.
Insertar descripción de la imagen aquí
Los resultados del usuario 1 son los siguientes y se encuentra que se ha recibido el mensaje:
Insertar descripción de la imagen aquí
Los resultados del usuario 2 son los siguientes y se encuentra que se ha recibido el mensaje:
Insertar descripción de la imagen aquí
Todos los resultados de la prueba están en línea con las expectativas.
Haga clic en el botón de cierre del cartero para cerrar la conexión:
Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí
se descubre que, aunque la conexión de front-end está cerrada, el back-end todavía está conectado y no aparece ningún mensaje para eliminar al usuario: por lo que aún debe escribir
Insertar descripción de la imagen aquí
manualmente la interfaz de cierre pruébelo usted mismo.
La interfaz de llamada local /sse/close/1 es la siguiente:
Insertar descripción de la imagen aquí
puede ver que el ID de usuario 1 se ha eliminado y solo el usuario 2 sigue conectado.
Insertar descripción de la imagen aquí
Todas las pruebas aquí se completan y los resultados son los esperados.

Aviso

Si cambia el tiempo de espera del original 0 a 30 milisegundos (L después del número entero significa que Long ocupa 8 bytes), se informará un error.

Insertar descripción de la imagen aquí
Los resultados de la prueba son los siguientes:
Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí
Se produjo una excepción directamente aquí: org.springframework.web.context.request.async.AsyncRequestTimeoutException
e incluso se desconectó la conexión.

Reduzca springboot a una versión inferior como 1.4.2.RELEASE.

Cuando usé cartero para realizar pruebas, descubrí que no siempre estaba en la solicitud: de la siguiente manera:
degradar Springboot a 1.4.2.RELEASE
Insertar descripción de la imagen aquí

La versión 1.4.2.RELEASE de springboot no tiene el método onError y debe comentarse. Insertar descripción de la imagen aquí
La prueba del cartero es la siguiente:
Al probar la versión inferior, se encontró que tiene una conexión que se puede ver directamente, sin embargo, al usar la versión 2.x de Springboot, se encontró que siempre está enviando solicitudes. envía un mensaje al frontend y muestra esto.
Resultados de la versión Springboot 1.4.2.RELEASE:
Insertar descripción de la imagen aquí
resultados de la versión Springboot 2.7.3:
Insertar descripción de la imagen aquí

Registremos esta situación primero y luego estudiémosla más tarde cuando tengamos tiempo. ¿Por qué la versión superior no puede devolver esta información de conexión como la versión inferior? Entonces, cuando la versión superior de SpringBoot usa SSE para conectarse, siempre está en la situación Enviando solicitud, ¿es esto normal? ¿Alguien puede hacérmelo saber? Gracias.

Supongo que te gusta

Origin blog.csdn.net/weixin_48040732/article/details/131000339
Recomendado
Clasificación