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.
El servicio backend imprime de la siguiente manera:
La interfaz de llamada local /sse/send/1 es la siguiente:
El resultado del usuario 1 es el siguiente y se descubre que ha recibido el mensaje:
El usuario 2 no recibió el resultado, de la siguiente manera:
La interfaz de llamada local /sse/batchSend es la siguiente:
envía mensajes en lotes a todos los usuarios conectados.
Los resultados del usuario 1 son los siguientes y se encuentra que se ha recibido el mensaje:
Los resultados del usuario 2 son los siguientes y se encuentra que se ha recibido el mensaje:
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:
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
manualmente la interfaz de cierre pruébelo usted mismo.
La interfaz de llamada local /sse/close/1 es la siguiente:
puede ver que el ID de usuario 1 se ha eliminado y solo el usuario 2 sigue conectado.
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.
Los resultados de la prueba son los siguientes:
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
La versión 1.4.2.RELEASE de springboot no tiene el método onError y debe comentarse.
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:
resultados de la versión Springboot 2.7.3:
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.