服务端推送技术 Server-sent Events springBoot代码示例

简介: 服务端推送技术 Server-sent Events springBoot代码示例

 SSE推送技术

SSE全称Server-sent Events,是HTML 5 规范的一个组成部分,具体去MDN网站查看相关文档。该规范十分简单,

SSE推送技术是服务器端与浏览器端之间的通讯协议,通讯协议是基于纯文本的简单协议。

服务器端的响应的内容类型是“text/event-stream”。响应文本的内容可以看成是一个事件流,由不同的事件所组成。每个事件由类型和数据两部分组成,同时每个事件可以有一个可选的标识符。不同事件的内容之间通过仅包含回车符和换行符的空行(“rn”)来分隔。每个事件的数据可能由多行组成。

image.gif编辑

如上图所示,每个事件之间通过空行来分隔。每一行都是由键值对组成。如果键为空则表示该行为注释,会在处理时被忽略。例如第10行。第1行表示一个只包含数据的事件。会按照默认事件走(message事件)。第3-4代表一个附带eventID的事件。第6-8代表一个自定义事件。第10-14代表一个多行数据事件,多行数据由换行符链接

key定义有以下几种:

    • data,表示该行包含的是数据。以 data 开头的行可以出现多次。所有这些行都是该事件的数据。
    • 类型为 event,表示该行用来声明事件的类型。浏览器在收到数据时,会产生对应类型的事件。默认提供三个标准事件(当然你可以自定义):

    image.gif编辑

      • id,表示该行用来声明事件的标识符。服务器端返回的数据中包含了事件的标识符,浏览器会记录最近一次接收到的事件的标识符。如果与服务器端的连接中断,当浏览器端再次进行连接时,会通过 HTTP 头“Last-Event-ID”来声明最后一次接收到的事件的标识符。服务器端可以通过浏览器端发送的事件标识符来确定从哪个事件开始来继续连接。
      • retry,表示该行用来声明浏览器在连接断开之后进行再次连接之前的等待时间。

      SSE只适用于高级浏览器,但是注意IE不直接支持。IE上的XMLHttpRequest对象不支持获取部分的响应内容,所以不支持。每次总有IE怪不得快被淘汰了。

      SSE VS Websocket

        • SSE 只能Server到Client单项,而Websocket是双向通信。
        • SSE 比 Websocket 轻量。当然功能要简单的多。开发便利,不牵涉协议升级问题。
        • SSE 天然支持断线重连

        服务端代码示例

        import com.alibaba.fastjson.JSONObject;
        import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
        import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
        import com.hxtx.spacedata.common.domain.ResponseDTO;
        import com.hxtx.spacedata.domain.entity.task.TaskInfoEntity;
        import com.hxtx.spacedata.enums.task.TaskInfoStatusEnum;
        import com.hxtx.spacedata.mapper.task.TaskInfoDao;
        import lombok.extern.slf4j.Slf4j;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.scheduling.annotation.Scheduled;
        import org.springframework.web.bind.annotation.GetMapping;
        import org.springframework.web.bind.annotation.PathVariable;
        import org.springframework.web.bind.annotation.RestController;
        import javax.servlet.http.HttpServletRequest;
        import javax.servlet.http.HttpSession;
        import java.util.Iterator;
        import java.util.List;
        import java.util.Map;
        import java.util.concurrent.ConcurrentHashMap;
        import java.util.stream.Collectors;
        /**
         * 服务端推送技术 server-sent events
         * @description
         * @author tarzan Liu
         * @version 1.0.0
         * @date 2020/10/27
         */
        @RestController
        @Slf4j
        public class SSEController {
            @Autowired
            private TaskInfoDao taskInfoDao;
            private static ConcurrentHashMap<String,Long> ssePushUsers = new ConcurrentHashMap<>();
            /**
             *  如果没有客户端,则直接修改消息已发送 (2分钟执行一次)
             * @author sunboqiang
             * @date 2020/11/3
             */
            @Scheduled(cron = "0 0/2 * * * ?")
            public void finishSend() {
                if(ssePushUsers.size()==0){
                    QueryWrapper<TaskInfoEntity> queryWrapper = new QueryWrapper<>();
                    queryWrapper.lambda().eq(TaskInfoEntity::getStatus, TaskInfoStatusEnum.SUCCESS.getStatus());
                    queryWrapper.lambda().eq(TaskInfoEntity::getSendStatus,0);
                    List<TaskInfoEntity> list = taskInfoDao.selectList(queryWrapper);
                    if(CollectionUtils.isNotEmpty(list)){
                        taskInfoDao.updateSendStatusByIds(list.stream().map(TaskInfoEntity::getId).collect(Collectors.toList()), 2);
                    }
                }
            }
            /**
             *  剔除关闭的客户端
             * @author sunboqiang
             * @date 2020/11/3
             */
            @Scheduled(cron = "0/2 * * * * ?") // 2S执行一次
            public void clear() {
                //2秒执行一次,时间差>5S 说明客户端关闭了,直接剔除
                long now = System.currentTimeMillis();
                for (Iterator<Map.Entry<String, Long>> it = ssePushUsers.entrySet().iterator(); it.hasNext(); ) {
                    Map.Entry<String, Long> item = it.next();
                    long time = item.getValue();
                    //log.info(item.getKey()+"注册时间差:"+(now - time)/1000);
                    if(now - time > 5000){
                        //5 秒
                        it.remove();
                        log.info("剔除客户端:"+item.getKey());
                    }
                }
            }
            @GetMapping(value="/sse/push/version/get")
            public String getVersion(HttpServletRequest request){
                HttpSession session = request.getSession();
                if(null != session){
                    return session.getId();
                }
                return null;
            }
            /**
             *  推送C++ json文件编译情况信息
             * @author sunboqiang
             * @date 2020/10/29
             */
            @GetMapping(value="/sse/push/{version}",produces="text/event-stream;charset=utf-8")
            public String push(@PathVariable("version") String version) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                QueryWrapper<TaskInfoEntity> queryWrapper = new QueryWrapper<>();
                queryWrapper.lambda().eq(TaskInfoEntity::getStatus, TaskInfoStatusEnum.SUCCESS.getStatus());
                queryWrapper.lambda().eq(TaskInfoEntity::getSendStatus,0);
                List<TaskInfoEntity> list = taskInfoDao.selectList(queryWrapper);
                String data = "";
                if(CollectionUtils.isEmpty(list)){
                    //还没有消息,收集等待推送的客户端
                    ssePushUsers.put(version,System.currentTimeMillis());
                    //data = "data:没有编译消息,当前打开客户端数量:"+ ssePushUsers.size()+"个;" +"\n\n";
                } else {
                    List<Long> drawingIds = list.stream().map(TaskInfoEntity::getDrawingId).distinct().collect(Collectors.toList());
                    //编译成功,推送消息
                    if(ssePushUsers.size()>0){
                        //存在接收客户端
                        ResponseDTO result = new ResponseDTO();
                        result.setCode(1);
                        result.setMsg("有新的编译");
                        result.setSuccess(true);
                        result.setData("drawingIds="+drawingIds);
                        data = "data:"+ JSONObject.toJSONString(result) +"\n\n";
                        ssePushUsers.remove(version);
                        if(ssePushUsers.size() == 0){
                            //最后一个客户端推送完成
                            taskInfoDao.updateSendStatusByIds(list.stream().map(TaskInfoEntity::getId).collect(Collectors.toList()), 1);
                        }
                    } else {
                        //没有客户端,直接推送成功
                        taskInfoDao.updateSendStatusByIds(list.stream().map(TaskInfoEntity::getId).collect(Collectors.toList()), 1);
                    }
                }
                return data;
            }
        }

        image.gif


        相关文章
        |
        3月前
        |
        人工智能 自然语言处理 前端开发
        SpringBoot + 通义千问 + 自定义React组件:支持EventStream数据解析的技术实践
        【10月更文挑战第7天】在现代Web开发中,集成多种技术栈以实现复杂的功能需求已成为常态。本文将详细介绍如何使用SpringBoot作为后端框架,结合阿里巴巴的通义千问(一个强大的自然语言处理服务),并通过自定义React组件来支持服务器发送事件(SSE, Server-Sent Events)的EventStream数据解析。这一组合不仅能够实现高效的实时通信,还能利用AI技术提升用户体验。
        261 2
        |
        1月前
        |
        JavaScript 安全 Java
        java版药品不良反应智能监测系统源码,采用SpringBoot、Vue、MySQL技术开发
        基于B/S架构,采用Java、SpringBoot、Vue、MySQL等技术自主研发的ADR智能监测系统,适用于三甲医院,支持二次开发。该系统能自动监测全院患者药物不良反应,通过移动端和PC端实时反馈,提升用药安全。系统涵盖规则管理、监测报告、系统管理三大模块,确保精准、高效地处理ADR事件。
        |
        3月前
        |
        Java 数据库连接 Maven
        mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
        这篇文章介绍了如何在Spring Boot项目中整合MyBatis和MyBatis Generator,使用逆向工程来自动生成Java代码,包括实体类、Mapper文件和Example文件,以提高开发效率。
        165 2
        mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
        |
        2月前
        |
        缓存 监控 Java
        |
        2月前
        |
        缓存 监控 Java
        |
        3月前
        |
        安全 Java 编译器
        springboot 整合表达式计算引擎 Aviator 使用示例详解
        本文详细介绍了Google Aviator 这款高性能、轻量级的 Java 表达式求值引擎
        315 6
        |
        3月前
        |
        Java BI API
        spring boot 整合 itextpdf 导出 PDF,写入大文本,写入HTML代码,分析当下导出PDF的几个工具
        这篇文章介绍了如何在Spring Boot项目中整合iTextPDF库来导出PDF文件,包括写入大文本和HTML代码,并分析了几种常用的Java PDF导出工具。
        726 0
        spring boot 整合 itextpdf 导出 PDF,写入大文本,写入HTML代码,分析当下导出PDF的几个工具
        |
        3月前
        |
        JSON NoSQL Java
        springBoot:jwt&redis&文件操作&常见请求错误代码&参数注解 (九)
        该文档涵盖JWT(JSON Web Token)的组成、依赖、工具类创建及拦截器配置,并介绍了Redis的依赖配置与文件操作相关功能,包括文件上传、下载、删除及批量删除的方法。同时,文档还列举了常见的HTTP请求错误代码及其含义,并详细解释了@RequestParam与@PathVariable等参数注解的区别与用法。
        |
        3月前
        |
        存储 Java API
        简单两步,Spring Boot 写死的定时任务也能动态设置:技术干货分享
        【10月更文挑战第4天】在Spring Boot开发中,定时任务通常通过@Scheduled注解来实现,这种方式简单直接,但存在一个显著的限制:任务的执行时间或频率在编译时就已经确定,无法在运行时动态调整。然而,在实际工作中,我们往往需要根据业务需求或外部条件的变化来动态调整定时任务的执行计划。本文将分享一个简单两步的解决方案,让你的Spring Boot应用中的定时任务也能动态设置,从而满足更灵活的业务需求。
        214 4
        |
        3月前
        |
        消息中间件 Java 大数据
        大数据-56 Kafka SpringBoot与Kafka 基础简单配置和使用 Java代码 POM文件
        大数据-56 Kafka SpringBoot与Kafka 基础简单配置和使用 Java代码 POM文件
        82 2

        热门文章

        最新文章