SSE服务--服务器发送事件

162 阅读2分钟

1. 概念介绍

sse(Server Sent Event),直译为服务器发送事件,顾名思义,也就是客户端可以获取到服务器发送的事件

我们常见的 http 交互方式是客户端发起请求,服务端响应,然后一次请求完毕;但是在 sse 的场景下,客户端发起请求,连接一直保持,服务端有数据就可以返回数据给客户端,这个返回可以是多次间隔的方式

2. 特点分析

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

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

3. 应用场景

从 sse 的特点出发,我们可以大致的判断出它的应用场景,需要轮询获取服务端最新数据的 case 下,多半是可以用它的

比如显示当前网站在线的实时人数,法币汇率显示当前实时汇率,电商大促的实时成交额等等...

4. SseEmitter

上面只是简单实现了 sse 的长连接 + 后端推送消息,但是与标准的 SSE 还是有区别的,sse 有自己的规范,而我们上面的实现,实际上并没有管这个,导致的问题是前端按照 sse 的玩法来请求数据,可能并不能正常工作

实现

控制层

package com.whj.servicessepush.controller;

import com.whj.internalcommon.utils.SsmPrefixUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * @Auther: wanghaijun
 * @Date: 2023/2/23 - 02 - 23 - 15:08 
 * @Description: com.whj.ssedriverclientweb.controller
 */

@RestController
@Slf4j
public class SseController {

    public static Map<String, SseEmitter> sseEmitterMap = new HashMap<>();

    /**
     * 建立连接
     *
     * @param userId 建立连接的id
     * @param identity 身份类型
     * @return
     */
    @GetMapping("/connect")
    public SseEmitter connect(@RequestParam Long userId, @RequestParam String identity) {
        log.info("用户的Id = " + userId, ",身份类型是:" + identity);
        SseEmitter sseEmitter = new SseEmitter(0l);
        String sseMapKey = SsmPrefixUtils.generatorSseKey(userId, identity);
        //将收到的消息存放到静态的map中
        sseEmitterMap.put(sseMapKey, sseEmitter);
        return sseEmitter;
    }

    /**
     * 发送连接
     *
     * @param userId 连接的用户
     * @param identity 身份类型
     * @param content  消息的类容
     * @return
     */
    @GetMapping("/push")
    public String push(@RequestParam Long userId, @RequestParam String identity, @RequestParam String content) {

        String sseMapKey = SsmPrefixUtils.generatorSseKey(userId, identity);
        try {
            if (sseEmitterMap.containsKey(sseMapKey)) {
                sseEmitterMap.get(sseMapKey).send(content);
            } else {
                return "推送失败";
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "给用户:" + sseMapKey + "发送了消息:" + content;
    }

    /**
     * 移除sseEmitterMap发来的值 关闭连接
     *
     * @param userId 关闭连接的用户
     * @param identity 身份类型
     * @return
     */
    @GetMapping("/close")
    public String close(@RequestParam Long userId, @RequestParam String identity) {
        String sseMapKey = SsmPrefixUtils.generatorSseKey(userId, identity);
        log.info("关闭连接:" + sseMapKey);
        if (sseEmitterMap.containsKey(sseMapKey)) {
            sseEmitterMap.remove(sseMapKey);
        }
        return "close 成功!";
    }
}

司机:监听页面测试

<html lang="en" xmlns="http://www.w3.org/1999/html">
<head>
    <meta charset="UTF-8">
</head>

<body>
<h1>司机:监听页面测试-模拟监听客户端</h1>
<button onclick="setMessageContent('鼠标点我了')">测试message</button>
<div id="message">展示服务端推送过来的消息</br></div>
<button onclick="sourceClose()">关闭连接</button>

<script>
	// 连接传递的值
    userId="1631130415052435458";
    identity = 1;
    source = null;

    if(window.EventSource){
        console.info("此浏览器支持SSE");
        // 连接的建立
        source = new EventSource("http://localhost:9000/connect?userId="+userId+"&identity="+identity);

        // 监听服务的推送的消息
        source.addEventListener("message",function (e){
            content = e.data;
            console.info("消息内容:"+content);

            setMessageContent(content);
        });

    }else {
        setMessageContent("此浏览器不支持");
    }

    function setMessageContent(content){
        document.getElementById("message").innerHTML+= (content+'</br>');
    }

    function sourceClose(){
        console.info("close方法执行");
        // 客户端source的关闭
        source.close();
        // 服务端map的移除
        httpRequest = new XMLHttpRequest();
        httpRequest.open("get","http://localhost:9000/close?userId="+userId+"&identity="+identity);
        httpRequest.send();
    }
</script>
</body>
</html>

乘客:监听测试页面

<html lang="en">
<head>
    <meta charset="UTF-8">
</head>

<body>
<h1>乘客:监听测试页面-模拟监听客户端</h1>
<button onclick="setMessageContent('鼠标点我了')">测试message展示</button>
<div id="message">展示服务的推送过来消息的地方</br></div>

<button onclick="sourceClose()">关闭连接</button>

<script>
    userId="1631471212960489474";
    identity = 1;
    source = null;

    if(window.EventSource){
        console.info("此浏览器支持SSE");
        // 连接的建立
        source = new EventSource("http://localhost:9000/connect?userId="+userId+"&identity="+identity);

        // 监听服务的推送的消息
        source.addEventListener("message",function (e){
            content = e.data;
            console.info("消息内容:"+content);

            setMessageContent(content);
        });

    }else {
        setMessageContent("此浏览器不支持");
    }

    function setMessageContent(content){
        document.getElementById("message").innerHTML+= (content+'</br>');
    }

    function sourceClose(){
        console.info("close方法执行");
        // 客户端source的关闭
        source.close();
        // 服务端map的移除
        httpRequest = new XMLHttpRequest();
        httpRequest.open("get","http://localhost:9000/close?userId="+userId+"&identity="+identity);
        httpRequest.send();
    }
</script>
</body>
</html>