SpringBoot 实现SSE 服务器发送事件

SSE 全称Server Sent Event,直译一下就是服务器发送事件,一般的项目开发中,用到的机会不多,可能很多小伙伴不太清楚这个东西,到底是干啥的,有啥用

本文主要知识点如下:

  • SSE 扫盲,应用场景分析
  • 借助异步请求实现 sse 功能,加深概念理解
  • 使用SseEmitter实现一个简单的推送示例

I. SSE 扫盲

1. 概念介绍

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

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

2. 特点分析

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

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

了解 websocket 的小伙伴,可能也知道它也是长连接,可以推送信息,但是它们有一个明显的区别

sse 是单通道,只能服务端向客户端发消息;而 webscoket 是双通道

那么为什么有了 webscoket 还要搞出一个 sse 呢?既然存在,必然有着它的优越之处

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

3. 应用场景

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

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

II. 手动实现 sse 功能

sse 本身是有自己的一套玩法的,后面会进行说明,这一小节,则主要针对 sse 的两个特点长连接 + 后端推送数据,如果让我们自己来实现这样的一个接口,可以怎么做?

1. 项目创建

借助 SpringBoot 2.2.1.RELEASE来创建一个用于演示的工程项目,核心的 xml 依赖如下

<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.1.RELEASE</version><relativePath/> <!-- lookup parent from repository -->
</parent><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><java.version>1.8</java.version>
</properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
</dependencies><build><pluginManagement><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></pluginManagement>
</build>
<repositories><repository><id>spring-snapshots</id><name>Spring Snapshots</name><url>https://repo.spring.io/libs-snapshot-local</url><snapshots><enabled>true</enabled></snapshots></repository><repository><id>spring-milestones</id><name>Spring Milestones</name><url>https://repo.spring.io/libs-milestone-local</url><snapshots><enabled>false</enabled></snapshots></repository><repository><id>spring-releases</id><name>Spring Releases</name><url>https://repo.spring.io/libs-release-local</url><snapshots><enabled>false</enabled></snapshots></repository>
</repositories>

2. 功能实现

在 Http1.1 支持了长连接,请求头添加一个Connection: keep-alive即可

在这里我们借助异步请求来实现 sse 功能,至于什么是异步请求,推荐查看博文: 【WEB 系列】异步请求知识点与使用姿势小结

因为后端可以不定时返回数据,所以我们需要注意的就是需要保持连接,不要返回一次数据之后就断开了;其次就是需要设置请求头Content-Type: text/event-stream;charset=UTF-8 (如果不是流的话会怎样?)

// 新建一个容器,保存连接,用于输出返回
private Map<String, PrintWriter> responseMap = new ConcurrentHashMap<>();// 发送数据给客户端
private void writeData(String id, String msg, boolean over) throws IOException {PrintWriter writer = responseMap.get(id);if (writer == null) {return;}writer.println(msg);writer.flush();if (over) {responseMap.remove(id);}
}// 推送
@ResponseBody
@GetMapping(path = "subscribe")
public WebAsyncTask<Void> subscribe(String id, HttpServletResponse response) {Callable<Void> callable = () -> {response.setHeader("Content-Type", "text/event-stream;charset=UTF-8");responseMap.put(id, response.getWriter());writeData(id, "订阅成功", false);while (true) {Thread.sleep(1000);if (!responseMap.containsKey(id)) {break;}}return null;};// 采用WebAsyncTask 返回 这样可以处理超时和错误 同时也可以指定使用的Excutor名称WebAsyncTask<Void> webAsyncTask = new WebAsyncTask<>(30000, callable);// 注意:onCompletion表示完成,不管你是否超时、是否抛出异常,这个函数都会执行的webAsyncTask.onCompletion(() -> System.out.println("程序[正常执行]完成的回调"));// 这两个返回的内容,最终都会放进response里面去===========webAsyncTask.onTimeout(() -> {responseMap.remove(id);System.out.println("超时了!!!");return null;});// 备注:这个是Spring5新增的webAsyncTask.onError(() -> {System.out.println("出现异常!!!");return null;});return webAsyncTask;
}

看一下上面的实现,基本上还是异步请求的那一套逻辑,请仔细看一下callable中的逻辑,有一个 while 循环,来保证长连接不中断

接下来我们新增两个接口,用来模拟后端给客户端发送消息,关闭连接的场景

@ResponseBody
@GetMapping(path = "push")
public String pushData(String id, String content) throws IOException {writeData(id, content, false);return "over!";
}@ResponseBody
@GetMapping(path = "over")
public String over(String id) throws IOException {writeData(id, "over", true);return "over!";
}

我们简单的来演示下操作过程

img

III. SseEmitter

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

1. sse 规范

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

请求头

开启长连接 + 流方式传递

Content-Type: text/event-stream;charset=UTF-8
Cache-Control: no-cache
Connection: keep-alive

数据格式

服务端发送的消息,由 message 组成,其格式如下:

field:value\n\n

其中 field 有五种可能

  • 空: 即以:开头,表示注释,可以理解为服务端向客户端发送的心跳,确保连接不中断
  • data:数据
  • event: 事件,默认值
  • id: 数据标识符用 id 字段表示,相当于每一条数据的编号
  • retry: 重连时间

2. 实现

SpringBoot 利用 SseEmitter 来支持 sse,可以说非常简单了,直接返回SseEmitter对象即可;重写一下上面的逻辑

@RestController
@RequestMapping(path = "sse")
public class SseRest {private static Map<String, SseEmitter> sseCache = new ConcurrentHashMap<>();@GetMapping(path = "subscribe")public SseEmitter push(String id) {// 超时时间设置为1小时SseEmitter sseEmitter = new SseEmitter(3600_000L);sseCache.put(id, sseEmitter);sseEmitter.onTimeout(() -> sseCache.remove(id));sseEmitter.onCompletion(() -> System.out.println("完成!!!"));return sseEmitter;}@GetMapping(path = "push")public String push(String id, String content) throws IOException {SseEmitter sseEmitter = sseCache.get(id);if (sseEmitter != null) {sseEmitter.send(content);}return "over";}@GetMapping(path = "over")public String over(String id) {SseEmitter sseEmitter = sseCache.get(id);if (sseEmitter != null) {sseEmitter.complete();sseCache.remove(id);}return "over";}
}

上面的实现,用到了 SseEmitter 的几个方法,解释如下

  • send(): 发送数据,如果传入的是一个非SseEventBuilder对象,那么传递参数会被封装到 data 中
  • complete(): 表示执行完毕,会断开连接
  • onTimeout(): 超时回调触发
  • onCompletion(): 结束之后的回调触发

上图总的效果和前面的效果差不多,而且输出还待上了前缀,接下来我们写一个简单的 html 消费端,用来演示一下完整的 sse 的更多特性

<!doctype html>
<html lang="en">
<head><title>Sse测试文档</title>
</head>
<body>
<div>sse测试</div>
<div id="result"></div>
</body>
</html>
<script>var source = new EventSource('http://localhost:8080/sse/subscribe?id=yihuihui');source.onmessage = function (event) {text = document.getElementById('result').innerText;text += '\n' + event.data;document.getElementById('result').innerText = text;};<!-- 添加一个开启回调 -->source.onopen = function (event) {text = document.getElementById('result').innerText;text += '\n 开启: ';console.log(event);document.getElementById('result').innerText = text;};
</script>

将上面的 html 文件放在项目的resources/static目录下;然后修改一下前面的SseRest

@Controller
@RequestMapping(path = "sse")
public class SseRest {@GetMapping(path = "")public String index() {return "index.html";}@ResponseBody@GetMapping(path = "subscribe", produces = {MediaType.TEXT_EVENT_STREAM_VALUE})public SseEmitter push(String id) {// 超时时间设置为3s,用于演示客户端自动重连SseEmitter sseEmitter = new SseEmitter(1_000L);// 设置前端的重试时间为1ssseEmitter.send(SseEmitter.event().reconnectTime(1000).data("连接成功"));sseCache.put(id, sseEmitter);System.out.println("add " + id);sseEmitter.onTimeout(() -> {System.out.println(id + "超时");sseCache.remove(id);});sseEmitter.onCompletion(() -> System.out.println("完成!!!"));return sseEmitter;}
}

我们上面超时时间设置的比较短,用来测试下客户端的自动重连,如下,开启的日志不断增加

img

其次将 SseEmitter 的超时时间设长一点,再试一下数据推送功能

请注意上面的演示,当后端结束了长连接之后,客户端会自动重新再次连接,不用写额外的重试逻辑了,就这么神奇

3. 小结

本篇文章介绍了 SSE 的相关知识点,并对比 websocket 给出了 sse 的优点(至于啥优点请往上翻)

请注意,本文虽然介绍了两种 sse 的方式,第一种借助异步请求来实现,如果需要完成 sse 的规范要求,需要自己做一些适配,如果需要了解 sse 底层实现原理的话,可以参考一下;在实际的业务开发中,推荐使用SseEmitter

IV. 其他

0. 项目

系列博文

  • 200329-SpringBoot 系列教程 web 篇之异步请求知识点与使用姿势小结
  • 200105-SpringBoot 系列教程 web 篇之自定义返回 Http-Code 的 n 种姿势
  • 191222-SpringBoot 系列教程 web 篇之自定义请求匹配条件 RequestCondition
  • 191206-SpringBoot 系列教程 web 篇 Listener 四种注册姿势
  • 191122-SpringBoot 系列教程 web 篇 Servlet 注册的四种姿势
  • 191120-SpringBoot 系列教程 Web 篇之开启 GZIP 数据压缩
  • 191018-SpringBoot 系列教程 web 篇之过滤器 Filter 使用指南扩展篇
  • 191016-SpringBoot 系列教程 web 篇之过滤器 Filter 使用指南
  • 191012-SpringBoot 系列教程 web 篇之自定义异常处理 HandlerExceptionResolver
  • 191010-SpringBoot 系列教程 web 篇之全局异常处理
  • 190930-SpringBoot 系列教程 web 篇之 404、500 异常页面配置
  • 190929-SpringBoot 系列教程 web 篇之重定向
  • 190913-SpringBoot 系列教程 web 篇之返回文本、网页、图片的操作姿势
  • 190905-SpringBoot 系列教程 web 篇之中文乱码问题解决
  • 190831-SpringBoot 系列教程 web 篇之如何自定义参数解析器
  • 190828-SpringBoot 系列教程 web 篇之 Post 请求参数解析姿势汇总
  • 190824-SpringBoot 系列教程 web 篇之 Get 请求参数解析姿势汇总
  • 190822-SpringBoot 系列教程 web 篇之 Beetl 环境搭建
  • 190820-SpringBoot 系列教程 web 篇之 Thymeleaf 环境搭建
  • 190816-SpringBoot 系列教程 web 篇之 Freemaker 环境搭建
  • 190421-SpringBoot 高级篇 WEB 之 websocket 的使用说明
  • 190327-Spring-RestTemplate 之 urlencode 参数解析异常全程分析
  • 190317-Spring MVC 之基于 java config 无 xml 配置的 web 应用构建
  • 190316-Spring MVC 之基于 xml 配置的 web 应用构建
  • 190213-SpringBoot 文件上传异常之提示 The temporary upload location xxx is not valid

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/554922.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

php多表递归查询,使用公用表表达式的递归查询

微软从SQL2005起引入了CTE(Common Table Expression)以强化T-SQL。公用表表达式 (CTE) 具有一个重要的优点&#xff0c;那就是能够引用其自身&#xff0c;从而创建递归 CTE。递归 CTE 是一个重复执行初始 CTE 以返回数据子集直到获取完整结果集的公用表表达式。当某个查询引用递…

Springboot之整合SSE实现消息推送

Springboot之整合SSE实现消息推送 前言 项目中涉及到部分请求&#xff0c;后端处理时间较长&#xff0c;使用常规Http请求&#xff0c;页面等待时间太长&#xff0c;对用户不友好&#xff0c;故考虑使用长链接进行消息推送&#xff0c;可选方案有WebSocket、SSE&#xff0c;We…

Vue中npm run dev 和 npm run serve区别

在运行vue文件时&#xff0c;需要进行npm操作&#xff0c;但我们发现&#xff0c;有时候用的是npm run serve&#xff0c;而有的时候用的是npm run dev&#xff0c;二者有什么区别 在我们运行一些 vue 项目的时候&#xff0c;输入npm run serve或者 npm run dev的其中一个时&a…

SpringBoot导出数据为PDF

一、SpringBoot导出数据为PDF 1、添加所需依赖 <dependency><groupId>com.itextpdf</groupId><artifactId>itextpdf</artifactId><version>5.5.11</version> </dependency> <dependency><groupId>com.itextpdf.…

php安装mem+cache扩展,安装memcached及php扩展

用的是centos系统1、安装memcachedyum -y install memcached安装完成后&#xff0c;memcached -h应该会出现memcached 参数说明2、memcached配置文件vi /etc/sysconfig/memcachedPORT"11210"USER"memcached"MAXCONN"1024"CACHESIZE"64"…

Springboot集成支付宝沙箱支付(完整版)

开发前准备 easy支付官方文档&#xff1a;https://opendocs.alipay.com/open/009ys9 通用版文档&#xff1a;https://opendocs.alipay.com/open/02np94 支付宝沙箱的配置 注册支付宝开发者账户&#xff0c;进入开发者控制台 https://openhome.alipay.com/platform/developer…

Springboot集成支付宝沙箱支付(退款功能)

包括&#xff1a; 支付宝沙箱 支付 异步通知 退款功能 正式版本的sdk 通用版本SDK文档&#xff1a;https://opendocs.alipay.com/open/02np94 <dependency><groupId>com.alipay.sdk</groupId><artifactId>alipay-sdk-java</artifactId><…

Java递归生成树

1.建菜单表 CREATE TABLE t_menu (id int(11) NOT NULL AUTO_INCREMENT,pid int(11) NOT NULL,name varchar(255) DEFAULT NULL,PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT11 DEFAULT CHARSETutf8mb4;2.造一些数据 注意&#xff1a;根节点的pid0&#xff0c;其他节点的p…

Springboot获取公网IP和当前所在城市(非常简单)

最近我们发现各大社交平台都出现了一个新的功能&#xff1a;IP属地。 比如某乎&#xff1a; 这个IP属地是怎么做到的呢&#xff1f;今天我来教教你&#xff0c;保证你看完直呼Easy~ 百度搜索 打开百度&#xff0c;搜索IP&#xff0c;你就能看到你当前的IP地址&#xff0c;类…

线程死锁——死锁产生的条件

什么是线程死锁 线程死锁是指由于两个或者多个线程互相持有对方所需要的资源&#xff0c;导致这些线程处于相互等待状态&#xff0c;若无外力作用&#xff0c;它们将无法继续执行下去。 造成死锁的原因可以概括成三句话&#xff1a; 当前线程拥有其他线程需要的资源当前线程…

TortoiseGit的使用详解

Git是什么&#xff0c;相信大家都很清楚。Git不就是分布式版本控制系统嘛&#xff1f;那你知道TortoiseGit是什么吗&#xff1f;下面我们就介绍一下TortoiseGit它是什么&#xff1f;如何使用&#xff1f;   TortoiseGit其实是一款开源的git的版本控制系统&#xff0c;也叫海龟…

将项目上传到Gitee上(命令方式使用TortoiseGit方式)

如何将项目上传到Gitee上&#xff08;命令方式&#xff09; 目录 将项目上传到Gitee是我们经常需要使用到的操作&#xff0c;因此我们要熟悉这些步骤 一、首先保证本机已经安装了Git git官网安装完成之后&#xff0c;鼠标右键会出现Git GUI Here和Git Bash Here 二、上传代…

BufferedImage类、Image类、Graphics类

BufferedImage Image是一个抽象类&#xff0c;BufferedImage是其实现类&#xff0c;是一个带缓冲区图像类&#xff0c;主要作用是将一幅图片加载到内存中&#xff08;BufferedImage生成的图片在内存里有一个图像缓冲区&#xff0c;利用这个缓冲区我们可以很方便地操作这个图片&…

Vue this.$refs的作用

案例一、ref 写在标签上时 <!-- ref 写在标签上时&#xff1a;this.$refs.名字 获取的是标签对应的dom元素ref 写在组件上时&#xff1a;这时候获取到的是 子组件&#xff08;比如counter&#xff09;的引用--><div id"root"><!-- ref hello&#…

linux电脑合盖后卡住了,解决ubuntu合盖后无法唤醒

解决办法&#xff1a;安装laptop-mode-tools工具包。1.检查是否安装了grep laptop-mode-tools 工具包$ dpkg -l | grep laptop-mode-tools如果执行命令无结果输出&#xff0c;表示未安装(如果已安装&#xff0c;忽略第2步)2.安装laptop-mode执行命令&#xff1a;$ sudo apt-get…

三列布局 css

实现如下图的三列布局&#xff1a; .box {width:1400px;margin:0 auto;padding-bottom:40px;> .left {float:left;width:180px;margin-top:100px;text-align:center;}> .center {float:left;margin-top:100px;margin-left:130px;item-box {float:left;text-align:left;…

axios和ajax的区别是什么

axios和ajax的区别&#xff1a; 1、axios是一个基于Promise的HTTP库&#xff0c;而ajax是对原生XHR的封装&#xff1b; 2、ajax技术实现了局部数据的刷新&#xff0c;而axios实现了对ajax的封装。 axios和ajax的区别是什么? axios和ajax的区别及优缺点: ajax&#xff1a; 1…

VUE学习笔记详细

VUE学习笔记 本文章以vue3来记录的&#xff0c;但并非记录vue3所有变化&#xff01; 1、ES6语法 1.1、let变量声明 let用于声明变量有局部作用域let声明的变量不会提升&#xff08;只能先定义后使用&#xff09; 1.2、const变量声明 const用于声明常量const声明的常量也不会…

Centos7配置gitlab服务器

Centos7配置gitlab服务器 1、安装SSH yum install -y curl policycoreutils-pythonopenssh-server设置开机自启 sudo systemctl enable sshd启动服务 sudo systemctl start sshd2、安装postfix 邮件服务 sudo yum install postfix设置开机自启 sudo systemctl enable po…

Jenkins学习笔记详细

最近接触了jenkins这个东西&#xff0c;所以花点时间了解了下。它可以在代码上传仓库&#xff08;如github,gitee&#xff0c;gitlab&#xff09;后&#xff0c;在jenkins&#xff08;一个网站界面&#xff09;中通过获取代码仓库中最新代码&#xff0c;进行自动化部署&#xf…