简单的单向通信SSE

605 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情

SSE(Server Send Event)是一个spring mvc提供的轻量的服务端推送消息的框架.

与之对应的是耳熟能详的webSocket框架,两者的区别是:

  1. sse是单工通信,只能服务端给客户端推送消息,而websocket是双工通信,客户端和服务端可以双向通信
  2. websocket是单独的ws协议,而sse是基于http协议

两者的应用场景并不一致,如果仅仅是需要服务端给客户端推送消息的情况下,并不需要使用websocket,比如下载进度,提示消息等不需要客户端向服务端发送消息的功能,可以采用sse.

当然ws有着更强大的功能,例如网页聊天的场景就必须要借助ws了,强大灵活的功能带来的就是配置及使用复杂.所以需要结合具体的业务分析使用哪种技术.

sse的原理是在http协议上发送流式数据以达到长连接的效果,类似视频在线观看,需要设置响应头为contentType=text/event-stream,告诉浏览器收到响应后不要结束此次请求,而是继续等待,以便于数据的传输.

来看下sse的简单应用:

1. sse简单使用

1.1 创建连接

    public SseEmitter createConnect(String userId) {
        // 0L永不超时 否则会在配置时间后断开连接,而客户端会尝试重连
        SseEmitter sseEmitter = new SseEmitter(0L);

        cache.put(userId, sseEmitter);
        try {
            sseEmitter.send(SseEmitter.event().id("cust").data("test" + userId));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return sseEmitter;
    }

返回一个SseEmitter对象,springmvc会在返回处理器里对这个对象进行处理,设置响应头contentType=text/event-stream

上面的例子里还创建完连接后,还发送了一条消息.

在测试中发现,如果不发送消息,有几率无法推送消息,还没搞清楚是啥问题..

1.2 销毁连接

SseEmitter sseEmitter = cache.get(userId);
        if (sseEmitter != null) {
            sseEmitter.complete();
            cache.remove(userId);
        }

将sseEmitter对象销毁即可.

1.3 发送消息

public void sendMsgToClient(String userId, String id, Object data) {
        if (StringUtils.isEmpty(userId)) {
            cache.forEach((item, sseEmitter) -> {
                SseEmitter.SseEventBuilder eventBuilder = SseEmitter.event().id(id).data(data);
                sendMsg(sseEmitter, eventBuilder);
            });
        } else {
            SseEmitter sseEmitter = cache.get(userId);
            if (sseEmitter == null) {
                throw new RuntimeException("长连接为空");
            }
            SseEmitter.SseEventBuilder eventBuilder = SseEmitter.event().id(id).data(data);
            sendMsg(sseEmitter, eventBuilder);
        }
    }

    private void sendMsg(SseEmitter sseEmitter, SseEmitter.SseEventBuilder eventBuilder) {
        try {
            sseEmitter.send(eventBuilder);
        } catch (IOException e) {
            e.printStackTrace();
            log.error("推送消息客户端失败");
        }
    }

核心代码就是sseEmitter.send()方法.

这里使用的sseEventBuilder对象,可以设置发送数据的type(方便客户端区分不同类型的数据)

2. 网关gateway+消息队列RabbitMq实现群发消息

这个其实是具体的业务场景了

我遇到的业务经常需要服务器给所有的客户端推送消息,而这种场景有着如下问题:

  • 服务端是多节点,客户端的sse连接可能在某一个节点上.

这个时候就需要如下操作:

  1. 将要推送的消息推送到消息队列RabbitMq(广播模式)
  2. 服务端每个节点均启动一个消费者,消费消息队列的这条消息
  3. 消费消息时判断需要推送消息的客户端sse连接是不是在本节点,如果是,则调用sseEmitter对象的send方法推送消息

加入网关是为了模拟真实情况,实际业务里一般都是客户端通过网关连接到服务端的.

具体代码不在这里复制粘贴,详细代码均上传github: github.com/zhangyao-zy…