Sse实现服务端长时间推送消息到客户端(H5)

Sse实现服务端长时间推送消息到客户端(H5)

介绍:

SseEmitter是SpringMVC(4.2+)提供的一种技术,它是基于Http协议的,相比WebSocket,它更轻量,但是它只能从服务端向客户端单向发送信息。而webscoket 是双通道, 在SpringBoot中我们无需引用其他jar就可以使用。

SSE 最大的特点,可以简单规划为两个

  • 长连接
  • 服务端可以向客户端推送信息

websocket 协议,使用相对复杂默认支持断线重连需要自己实现断线重连文本传输二进制传输支持自定义发送的消息类型

并且H5页面可以在服务端挂了的情况下, 自动不断尝试重连接。

客户端H5的实现

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Reactive Programming with Spring 5</title>
</head>
<body>
<div>
    <ul id="events"></ul>
</div>
<script type="application/javascript">
    function addMessage(message) {
        const el = document.createElement("li");
        el.innerHTML = message;
        document.getElementById("events").appendChild(el);
    }

    if (!!window.EventSource) {
        console.log('浏览器支持EventSource')
        // 建立连接
        const eventSource = new EventSource("http://localhost/map-stream");

        window.onbeforeunload = function () {
            console.log('关闭页面, 进行关闭连接')
            eventSource.close();
        };

        eventSource.onopen = function (event) {
            console.log('连接已经成功打开'+JSON.stringify(event))
        };
        eventSource.onmessage = function (event) {
            console.log(event)
            addMessage('接收到客户端json串信息: ' + event.data);
        };

        eventSource.onerror = function (event) { // 发生错误时
            if (e.readyState === EventSource.CONNECTING) {
                console.log('Connecting to server');
            } else if (event.readyState === EventSource.OPEN) {
                console.log('Connection opened');
            } else if (event.readyState === EventSource.CLOSING) {
                console.log('Connection closing');
            } else if (event.readyState === EventSource.CLOSED) {
                console.log('Connection closed');
            }
        };
    } else {
        addMessage('您的浏览器不支持服务器发送的事件!');
    }
</script>
</body>
</html>

Java服务端的实现

在 html5 的定义中,服务端 sse,一般需要遵循以下要求

请求头

开启长连接 + 流方式传递

@RestController
public class TemperatureController implements InitializingBean {

    static final long SSE_SESSION_TIMEOUT = 30 * 60 * 1000L;

    private static final Logger log = LoggerFactory.getLogger(TemperatureController.class);

    private final Set<SseEmitter> clients = new CopyOnWriteArraySet<>();

    private ScheduledExecutorService executorService = Executors.newScheduledThreadPool(4);

    @CrossOrigin
    @GetMapping(value = "/map-stream")
    public SseEmitter events(HttpServletRequest request) {
        log.info("SSE stream opened for client: (为客户端打开SSE流)" + request.getRemoteAddr());
        //默认为30000毫秒 0L为不超时 超过时间未完成会抛出异常:AsyncRequestTimeoutException
        SseEmitter emitter = new SseEmitter(SSE_SESSION_TIMEOUT); 
        clients.add(emitter);
        // Remove SseEmitter from active clients on error or client disconnect
        emitter.onTimeout(() -> { //  超时了回调删除连接
            log.error("SseEmitter-1, 客户端连接超时");
            clients.remove(emitter);
        });
        emitter.onCompletion(() -> { // 完成了断开连接回调
            log.error("SseEmitter-2, 已完成");
            clients.remove(emitter);
        });
        emitter.onError((throwable)->{ // 错误时
            log.error("SseEmitter-3", throwable);
            clients.remove(emitter);
        });
        return emitter;
    }
    
    @ExceptionHandler(value = AsyncRequestTimeoutException.class)
    public ModelAndView handleTimeout(HttpServletResponse rsp) throws IOException {
        if (!rsp.isCommitted()) {
            rsp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
        }
        return new ModelAndView();
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        final int[] i = {0};
        executorService.scheduleAtFixedRate(() -> { // 定时推送信息到客户端
            log.info("scheduleAtFixedRate-1, 执行了!"+ i[0]);
            for (SseEmitter sseEmitter : clients) {
                i[0]++;
                try {
                    Instant start = Instant.now();
                    HashMap<String, String> hashMap = MapUtil.of(String.valueOf(i[0]), ":当前推送次数" + IdUtil.fastSimpleUUID());
                    // 推送信息到客户端
                    sseEmitter.send(hashMap, MediaType.APPLICATION_JSON);
                    log.info("Sent to client, took: {}", Duration.between(start, Instant.now()));
                } catch (IOException e) {
                   log.error("scheduleAtFixedRate-2, 前端关闭了连接!");
                }
            }
        }, 15, 3, TimeUnit.SECONDS);
    }
}

效果:

在这里插入图片描述

使用场景

网站在线人数、 促销成交金额、 监控屏数据

1

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

懵懵懂懂程序员

如果节省了你的时间, 请鼓励

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值