sse后端推送消息到前端.18594295

933 阅读3分钟

SSE(Server-Sent Events)后端推送消息到前端

该方式仅适用于后端单向向前端推流,例如实时数据推送,

如果需要双向请使websocket

使用方法

依赖

使用的是springmvc的功能,所以只需要导入springmvc就可以了

<!-- SpringBoot Web容器 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
controller

与正常controller接口方法类似,但是返回值是SseEmitter,不同前端访问该接口都会生成一个SseEmitter,我们可以用Map保存它,在数据发生变化后,使用SseEmitter对象的send方法

@ApiOperation(value = "sse登录连接")
@GetMapping("/web/sseOpen")
public SseEmitter handleSse() {
    // 设置默认的超时时间60秒,超时之后服务端主动关闭连接。
    SseEmitter emitter = new SseEmitter(sseTimeout * 1000L);
    Long userId = SecurityUtils.getUserId();
    SseMap.sseEmitters.put(userId,emitter);

    emitter.onCompletion(() -> SseMap.sseEmitters.remove(userId));
    emitter.onTimeout(() -> SseMap.sseEmitters.remove(userId));
    AtomicLong counter = new AtomicLong();
    new Thread(() -> {
        try {
            emitter.send(SseEmitter.event()
                         .id(String.valueOf(counter.incrementAndGet()))
                         .name("message")
                         .data("连接成功"));
            for (int i = 0; i < 5000; i++) {
                emitter.send(SseEmitter.event()
                             .id(String.valueOf(counter.incrementAndGet()))
                             .name("message")
                             .data("This is message " + i));
                Thread.sleep(10000);
            }
            // emitter.complete();
        } catch (IOException | InterruptedException e) {
            emitter.completeWithError(e);
        }
    }).start();

    return emitter;
}
SseEmitter容器

emitter.send方法需要的对象参数中,

  • id可以按需填写,uuid应该也可以
  • name需要注意,前端需要监听这一事件,eventSource.addEventListener中就需要填写对应name
  • data 就是你传输的数据,可以传json字符串,方便解析

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

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

@Slf4j
public class SseMap {
    public static Map<Long, SseEmitter> sseEmitters = new ConcurrentHashMap<>();
    
    
    public static void notifyExit(Long userId,String msg){
        new Thread(() -> {
            //     sseMap退出通知
            SseEmitter sseEmitter = SseMap.sseEmitters.get(userId);
            if(sseEmitter==null){
                return;
            }
            
            try {
                sseEmitter.send(SseEmitter.event().name("finish").id(userId.toString()).data(msg));
                sseEmitter.complete();
            } catch (IOException e) {
                sseEmitter.completeWithError(e);
                // throw new RuntimeException(e);
                log.info("sse异常",e);
            }
        }).start();
    }
    
}
使用
// 如果用户不允许多终端同时登录,清除缓存信息
String userIdKey = Constants.LOGIN_USERID_KEY + loginUser.getUser().getUserId();
LoginIdInfo idInfo = redisUtils.getCacheObject(userIdKey);
if(idInfo!=null){
    String userKey=idInfo.getLoginUserRedisKey();
    if (StringUtils.isNotEmpty(userKey))
    {
        redisUtils.deleteObject(userIdKey);
        redisUtils.deleteObject(userKey);
    }
    // 通知前端用户退出系统,避免占用资源
    SseMap.notifyExit(loginUser.getUserId(),"账号已在别处登录");
}

前端调试

可以使用window.EventSource或者event-source-polyfill(支持携带header,鉴权)

<!DOCTYPE html>
<html>

<head>
    <title>SSE Example</title>
</head>

<body>
    <h1>SSE Example</h1>
    <div id="sse-data"></div>

    <button onclick="aaa()">aaa</button>
    <button onclick="bbb()">bbb</button>
    <button onclick="ccc()">ccc</button>
    <script src="eventsource.js"></script>
    <script>
        // import { EventSourcePolyfill } from 'event-source-polyfill';

        const sseDataElement = document.getElementById('sse-data');

        function aaa() {
            const eventSource = new EventSource('http://127.0.0.1:8080/sse');

            eventSource.onmessage = function (event) {
                // const data = JSON.parse(event.data);
                const data = event.data;
                sseDataElement.innerHTML += data + '<br>';
            };

            eventSource.onopen = function (event) {
                sseDataElement.innerHTML = 'Connection opened<br>';
            };

            eventSource.onerror = function (event) {
                console.log(event)
                if (event.target.readyState === EventSource.CLOSED) {
                    sseDataElement.innerHTML += 'Connection closed<br>';
                } else {
                    sseDataElement.innerHTML += 'Error occurred<br>';
                }
                eventSource.close()
            };
        }

        function bbb() {

            const eventSource = new EventSourcePolyfill('http://127.0.0.1:8080/sse', {
                headers: {
                    'X-Custom-Header': 'value'
                }
            });
            /*
              * open:订阅成功(和后端连接成功)
              */
            eventSource.addEventListener("open", function (e) {
                console.log('open successfully')
            })
            /*
            * message:后端返回信息,格式可以和后端协商
            */
            eventSource.addEventListener("message", function (e) {
                console.log(e.data)
            })

            // eventSource.onmessage = function (event) {
            //     console.log(event);
            //     // const data = JSON.parse(event.data);
            //     const data = event.data;
            //     sseDataElement.innerHTML += data + '<br>';
            // };

            // eventSource.onopen = function (event) {
            //     console.log(event);
            //     sseDataElement.innerHTML = 'Connection opened<br>';
            // };
            /*
            * error:错误(可能是断开,可能是后端返回的信息)
            */
            eventSource.addEventListener("error", function (err) {
                console.log(err)
                // 类似的返回信息验证,这里是实例
                err && err.status === 401 && console.log('not authorized')
                eventSource.close()
            })

            //自定义finish事件,主动关闭EventSource
            eventSource.addEventListener('finish', function (e) {
                console.log(e)
                // source.close();
                let a = "<br>" + "哈哈" + "<br/>";
                sseDataElement.innerHTML = a;
            }, false);
        }

        function ccc() {

            const eventSource = new EventSourcePolyfill(    
                'http://127.0.0.1:8080/api/web/sseOpen'
            {
                headers: {
                    'X-Custom-Header': 'value',
                    'Authorization': 'xxxxxxxx'
                },
                heartbeatTimeout: 15000,
            });
            /*
              * open:订阅成功(和后端连接成功)
              */
            eventSource.addEventListener("open", function (e) {
                console.log(e);
                console.log('open successfully')
            })
            /*
            * message:后端返回信息,格式可以和后端协商
            */
            eventSource.addEventListener("message", function (e) {
                console.log(e.data)
            })

            // eventSource.onmessage = function (event) {
            //     console.log(event);
            //     // const data = JSON.parse(event.data);
            //     const data = event.data;
            //     sseDataElement.innerHTML += data + '<br>';
            // };

            // eventSource.onopen = function (event) {
            //     console.log(event);
            //     sseDataElement.innerHTML = 'Connection opened<br>';
            // };
            /*
            * error:错误(可能是断开,可能是后端返回的信息)
            */
            eventSource.addEventListener("error", function (err) {
                console.log(err)
                // 类似的返回信息验证,这里是实例
                // err && err.status === 401 && console.log('not authorized')
                // eventSource.close()
            })

            //自定义finish事件,主动关闭EventSource
            eventSource.addEventListener('finish', function (e) {
                console.log(e)
                // source.close();
                let a = "<br>" + "哈哈" + "<br/>";
                sseDataElement.innerHTML = a;
                eventSource.close()
            }, false);
        }

    </script>
</body>

</html>
注意事项
  • 电脑使用kx上网的情况,有概率前后端连接失败
  • window.EventSource在浏览器中f12能够看到请求报文, event-source-polyfill不可以,建议使用apifox进行测试
  • EventSource在断连后,会自动重连,需要使用close才能关闭

参考: