Server-Sent Events(SSE)——一种服务器向浏览器推送信息的方法
| Java
评论 0 | 点赞 0 | 浏览 666

从chatgpt的流式传输获得启发

起初以为,chatgpt的回答是通过websocket分段传输过来的,后来在浏览器控制台上看到并不是,研究了一下,发现使用的是流式传输。

使用流式传输非常简单,服务端的响应头中只要添加以下信息即可:


Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

content-type必须是text/event-stream,表示流式传输,no-cache表示不使用缓存,keep-alive表示不断开http连接

下面写一个示例

后端定一个get接口,每隔100ms向流中写入一个字符串


    @GetMapping("/sse")
    public void sse(HttpServletResponse response) {
        String str = "       春江花月夜 \n" +
                "春江潮水连海平,海上明月共潮生。\n" +
                "滟滟随波千万里,何处春江无月明!\n" +
                "\n";
        // 响应流
        response.setHeader("Content-Type", "text/event-stream");
        response.setContentType("text/event-stream");
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Pragma", "no-cache");
        ServletOutputStream out = null;
        try {
            out = response.getOutputStream();
            for (int i = 0; i < str.length(); i++) {
                out.write(String.valueOf(str.charAt(i)).getBytes());
                // 更新数据流
                out.flush();
                Thread.sleep(100);
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        } finally {
            try {
                if (out != null) {
                    out.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

前端代码:


    // 创建 XMLHttpRequest 对象
    const xhr = new XMLHttpRequest();
    // 设置请求的 URL
    xhr.open(
        "GET",
        `http://localhost:9093/openai/sse`
    );
    // 设置响应类型为 text/event-stream
    xhr.setRequestHeader("Content-Type", "text/event-stream");
    // 监听 readyStateChange 事件
    xhr.onreadystatechange = () => {
        // 如果 readyState 是 3,表示正在接收数据
        if (xhr.readyState === 3) {
            // 将数据添加到文本框中
            console.log('xhr.responseText :>> ', xhr);
            // reply("images/touxiang.png", xhr.responseText, randomStr)
            $('#content').text(xhr.responseText);
            var height = $("#content").height();
            $("html").scrollTop(height)
        }
    };
    // 发送请求
    xhr.send();

至此,就实现了类似chatgpt一样的打印机效果。本来以为到这里就结束了,后来查了一下资料,发现Server-Sent Events(SSE)就是基于这种流式传输封装成的。

SSE介绍

一般来说,实现服务端主动向浏览器推送消息,一般有以下几个方案:

1、前端轮询(最常用的场景:扫码登录)

2、websocket(最常用的场景:即时通讯)

3、MQTT(物联网常用,浏览器中还是依托websocket协议)

上面提到的chatgpt使用的event-stream传输方式,本质是就是下载,把数据分批传输到客户端,如果封装一下,让前后端一直保持连接,那么后端就可以随时向前端推送信息,这项技术就叫Server-Sent Events,简称SSE,他已经被列入html5标准之中,因此大部分的浏览器都对SSE做了支持(IE除外)。Spring也对SSE做了相关的封装。

代码实现

spring封装了SseEmitter可以直接使用,代码如下:


    //存储所有的SSE连接
    private static final ConcurrentHashMap<String, SseEmitter> SSE_POOL = new ConcurrentHashMap<>();



    //创建SSE连接
    @GetMapping("createSse")
    public SseEmitter createSse(String clientId) {
        //不过期
        SseEmitter sseEmitter = new SseEmitterUTF8(0L);
        SSE_POOL.put(clientId, sseEmitter);
        sseEmitter.onCompletion(() -> {
            log.info("onCompletion!");
            SSE_POOL.remove(clientId);
        });
        sseEmitter.onError((e) -> {
            log.error("onError!", e);
            SSE_POOL.remove(clientId);
        });
        sseEmitter.onTimeout(() -> {
            log.info("onTimeout!");
            SSE_POOL.remove(clientId);
        });
        return sseEmitter;
    }


    //触发消息推送
    @GetMapping("getMsg")
    public void getMsg(String clientId) throws IOException {
        SSE_POOL.get(clientId).send("这是一条来自SseEmitter的消息");
    }



    //自定义SSE客户端,实现utf-8编码传输
    public static class SseEmitterUTF8 extends SseEmitter {

        public SseEmitterUTF8(Long timeout) {
            super(timeout);
        }

        @Override
        protected void extendResponse(ServerHttpResponse outputMessage) {
            super.extendResponse(outputMessage);

            HttpHeaders headers = outputMessage.getHeaders();
            headers.setContentType(new MediaType(MediaType.TEXT_EVENT_STREAM, StandardCharsets.UTF_8));
        }
    }

这里有个注意事项,spring提供的SseEmitter默认不是使用的utf-8编码,传输到浏览器会中文乱码,所以需要自定义一个

浏览器提供了EventSource对象


    var source = new EventSource('http://localhost:9093/openai/createSse?clientId=zjh');
    source.onopen = function (event) {
        $('#status').append(`建立连接成功!</br>`)
    };
    source.onmessage = function (event) {
        var data = event.data;
        $('#msg').append(data + '</br>')
    };
    source.onerror = function (event) {
        console.log(event)
        $('#status').append(`出错了!</br>`)
    };

    $('#btn').click(() => {
        // 创建 XMLHttpRequest 对象
        const xhr = new XMLHttpRequest();
        // 设置请求的 URL
        xhr.open(
            "GET",
            `http://localhost:9093/openai/getMsg?clientId=zjh`
        );
        // 发送请求
        xhr.send();
    })

其实也没什么好讲的,和websocket一样,都是封装了onopen,onmessage,onerror等生命周期函数,用法很类似

SSE对比websocket

1、传输协议:SSE还是普通的http协议,不受平台限制;而websocket是基于TCP的独立协议,不是所有的客户端都支持

2、使用难度:SSE比websocket简单

3、性能:websocket更胜一筹

4、单双工:SSE是单工,只能服务端向客户端发送消息;websocket是双工,服务器和客户端可以互相收发消息;

5、数据传输:SSE只能传文本,二进制数据需要base64编码后传输;websocket可以直接传输二进制数据

6、事件:SSE可以自定义事件,即除了onopen、onmessage、onerror外,还可以自定义其他事件;websocket不支持

SSE使用场景

1、实时数据更新但客户端又不多的场景:例如监控大屏。

2、响应速度比较慢的接口:可以让接口先返回一部分数据,前端拿到数据后立即渲染页面,用户友好。

本文作者:不是好驴
本文链接:https://www.baddonkey.cn/detail/52
版权声明:原创文章,允许转载,转载请注明出处

高谈阔论

留言列表