服务器端事件发送SSE
背景
近期有这么一个需求:
手机端需要展示一个比较大的pdf
基于手机端网络/流量/体验等考虑,希望不通过pdf下载然后展示
而是把pdf转成一张张的图片,然后再在手机上展示。
分析
pdf转图片,肯定是一个比较慢的过程,最好能转完一张就返回一张到前端。
So,此文要讲的是 请求异步多次返回的技术实现SSE
当然,WebSocket也能做到,它可以双向通信,比SSE(单向发送)强大且复杂,SSE好在比较简单
服务器端事件发送 SSE
全称:Server Send Event
其实严格地说,HTTP 协议无法做到服务器主动推送数据到客户端的。只不过可以变通一下,就是服务器向客户端声明,接下来要发送的是流数据(stream)。
此时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流。
SSE 就是利用这种机制,使用流信息向浏览器推送信息。它基于 HTTP 协议,目前除了 IE,其他浏览器都支持。
IE的话,也可以通过evensource.js来兼容起来。
代码实现
客户端
需要用到EventSource,并实现onmessage方法
if (!!window.EventSource) {
var source = new EventSource('push');
s = '';
source.addEventListener('message', function(e) {
s += e.data + "<br/>";
$("#msgFrompPush").html(s);
});
source.addEventListener('open', function(e) {
console.log("连接打开.");
}, false);
source.addEventListener('error', function(e) {
if (e.readyState == EventSource.CLOSED) {
console.log("连接关闭");
} else {
console.log(e);
source.close();
}
}, false);
} else {
console.log("你的浏览器不支持SSE");
}
服务端
需要设置类型为event-stream
@RequestMapping(value = "/pushV2", produces = "text/event-stream")
public void pushV2(HttpServletResponse response) {
response.setContentType("text/event-stream");
response.setCharacterEncoding("utf-8");
int count = 0;
while (true) {
Random r = new Random();
try {
Thread.sleep(1000);
PrintWriter pw = response.getWriter();
// 如果浏览器直接关闭,需要check一下
if (pw.checkError()) {
System.out.println("客户端主动断开连接");
return;
}
pw.write("data:Testing 1,2,3" + r.nextInt() + "\n\n");
pw.flush();
count++;
if(count>5){
return;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
以上客户端和服务端的代码示例基于http://blog.longjiazuo.com/archives/1489
做了如下修改:
1、原文示例代码中,每个请求只返回了一次数据,服务器每次发完数据断开了连接。
但SSE默认会自动重连,所以客户端不断地重连(重新发请求)。浏览器F12 network,可以看到刷了很多请求
这和ajax长轮询没什么区别了。
2、Controller端处理完return返回之后,前端页面会收到一个error事件。浏览器接收到error事件后,SSE又会自动重连,所以我加了一个source.close();
当然这里close不合理,后面再聊合理的做法
这里需要知道的是:return之后长连接就断开了,就不是我们想要的持续推送了。
修改后的代码见Github:
https://github.com/yejg/springMvc4.x-project/tree/master/springMvc4.x-serverSendEvent
基于SpringMvc实现
SpringMvc已经对这种异步响应做了很好的封装,我们可以直接返回Callable、DeferredResult或SseEmitter 来更优雅地实现我们的需求。
返回Callable的时候,Spring做了这些事情
- Controller返回一个Callable对象
- Spring MVC开始异步处理并且提交Callable到TaskExecutor在一个单独的线程中进行处理
- DispatcherServlet与所有的Filter的Servlet容器线程退出,但Response仍然开放
- Callable产生结果并且Spring MVC分发请求给Servlet容器继续处理
- DispatcherServlet再次被调用并且继续异步的处理由Callable产生的结果
DeferredResult的处理逻辑和Callable返回差不多,只不过DeferredResult的线程不由SpringMvc管理。
参考资料: https://docs.spring.io/spring/docs/4.3.16.RELEASE/spring-framework-reference/html/mvc.html#mvc-ann-async
Callable和DeferredResult一般用于异步返回单个结果;
SseEmitter则可以异步多次返回。
在使用SseEmitter写代码前,再解决以下前面提到的一个小问题 -- 合理地close掉EventSource。
前面的代码里面,为了避免Controller中return后,浏览器重连,我们直接在error里面把source给close掉了。
source.addEventListener('error', function(e) {
if (e.readyState == EventSource.CLOSED) {
console.log("连接关闭");
} else {
console.log(e);
source.close(); // <--- 就是这里
}
}, false);
SseEmitter有complete()方法,不过执行之后,浏览器也是会收到error事件,并重新请求链接;
那么,最好的做法是:
Controller处理返回完之后,通知请求端浏览器,告诉它数据都传完了,由浏览器端主动去close掉EventSource。
经过上面一系列的分析,可以开始愉快地写代码了:
服务端
返回一个自定义的event,type为finish,告知浏览器可以关闭连接了。
@RequestMapping("/sseEmitter")
@ResponseBody
public SseEmitter sseEmitterCall() {
// SseEmitter用于异步返回多个结果,直到调用sseEmitter.complete()结束返回
SseEmitter sseEmitter = new SseEmitter();
Thread t = new Thread(new TestRun(sseEmitter));
t.start();
return sseEmitter;
}
class TestRun implements Runnable {
private SseEmitter sseEmitter;
private int times = 0;
public TestRun(SseEmitter sseEmitter) {
this.sseEmitter = sseEmitter;
}
@Override
public void run() {
while (true) {
try {
System.out.println("当前times=" + times);
sseEmitter.send(System.currentTimeMillis());
times++;
Thread.sleep(1000);
if (times > 4) {
System.out.println("发送finish事件");
sseEmitter.send(SseEmitter.event().name("finish").id("6666").data("哈哈"));
System.out.println("调用complete");
sseEmitter.complete();
System.out.println("complete!times=" + times);
break;
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
}
客户端
增加处理finish事件的响应代码
if (!!window.EventSource) {
var source = new EventSource('sseEmitter');
s='';
source.addEventListener('message', function(e) {
s+=e.data+"<br/>";
$("#msgFrompPush").html(s);
});
source.addEventListener('open', function(e) {
console.log("连接打开.");
}, false);
// 响应finish事件,主动关闭EventSource
source.addEventListener('finish', function(e) {
console.log("数据接收完毕,关闭EventSource");
source.close();
console.log(e);
}, false);
source.addEventListener('error', function(e) {
if (e.readyState == EventSource.CLOSED) {
console.log("连接关闭");
} else {
console.log(e);
}
}, false);
} else {
console.log("你的浏览器不支持SSE");
}
完整代码见:
https://github.com/yejg/springMvc4.x-project/tree/master/springMvc4.x-servlet3/src/main
推荐阅读:
Server-Sent Events 教程 http://www.ruanyifeng.com/blog/2017/05/server-sent_events.html
服务器端事件发送SSE的更多相关文章
- ASP.NET服务器端事件利用MARQUEE实现正在处理效果
前言:ASP.NET同仁们应该都遇到过当触发一个比较耗时的服务器端事件时,页面会处在一个等待的状态(即假死状态),用户体验非常不好,很容易造成用户二次点击,造成重复提交.至于解决方案自然是有的(问go ...
- JavaEE开发之Spring中的事件发送与监听以及使用@Profile进行环境切换
本篇博客我们就来聊一下Spring框架中的观察者模式的应用,即事件的发送与监听机制.之前我们已经剖析过观察者模式的具体实现,以及使用Swift3.0自定义过通知机制.所以本篇博客对于事件发送与监听的底 ...
- 异常将上下文初始化事件发送到类的侦听器实例.[org.springframework.web.context.ContextLoaderListener] org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sessionFactory' defined in class p
严重: 异常将上下文初始化事件发送到类的侦听器实例.[org.springframework.web.context.ContextLoaderListener]org.springframework ...
- apiCloud事件发送与监听
apiCloud事件发送与监听 1.sendEvent 将任意一个自定义事件广播出去,该事件可在任意页面通过 addEventListener 监听收到. sendEvent({params}) 2. ...
- 如何使用DTM将App事件发送到Google Analytics
本文分享于华为开发者论坛<如何使用DTM将App事件发送到Google Analytics>,可观看视频具体集成指导. 作为一名开发者或App运营人员,实时获取用户在App中的行为数据是日 ...
- QT虚拟小键盘设计--qt事件循环,事件发送的理解
有人讲到QT5.7及其以后的版本才自带免费的小键盘插件. QT5.10中关于QKeyEvent类:点击打开链接 QT sendEvent和PostEvent, 点击打开链接 my god,我今天安装了 ...
- asp.net button浏览器端事件和服务器端事件
OnClientClick:触发浏览器端的响应,OnClick触发服务器端响应; 在服务器aspx.cs脚本中设置按钮属性: this.btnTest.Attributes["OnClick ...
- 通过java代码HttpRequestUtil(服务器端)发送HTTP请求并解析
关键代码:String jsonStr = HttpRequestUtil.sendGet(config.getAddress() + config.getPorts() + config.getFi ...
- 关于iphone消息推送把C#当服务器端来发送
看了苹果消息推送文档,感觉推送很简单的,但是还是按个人习惯把这些简单知识记录下来,在需要时候再查看一下! 在开发之前,要准备以下的资料 1.证书(包括产生证书和调试证书) 2.证书密码 3.唯一标识( ...
随机推荐
- Mapreduce atop Apache Phoenix (ScanPlan 初探)
利用Mapreduce/hive查询Phoenix数据时如何划分partition? PhoenixInputFormat的源码一看便知: public List<InputSplit> ...
- Go语言函数
目录 函数定义 函数返回多个值 函数参数 Go 语言函数值传递 Go语言函数引用传递 函数用法 函数作为值 匿名函数 闭包 方法 不定参数的函数 init函数 内建函数 函数调用机制 总结 函数定义 ...
- JavaScript实现LUHN算法验证银行卡号有效性
一般验证银行卡有效性用到一种叫做LUHN的算法,简介请参考这篇博客:基于Luhn算法的银行卡卡号的格式校验 注意: 1.LUHN算法只是能校验卡号是否有效,并不能校验卡号和用户名是否一致. 2.如果有 ...
- MySQL:事务的隔离性
[参考文章]:数据库的事务特性及隔离级别 1. 事务的四大特性 1.1 原子性(Atomicity) 原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用 ...
- Linux 定位网络不通问题
[参考文章]:ping命令入门详解 1. ipconfig / ifconfig 1. 作用: 检查本地的网络配置是否正确 2. ping 1. 作用: 一个十分好用的TCP/IP工具.它主要的功能是 ...
- Spring MVC 后端获取前端提交的json格式字符串并直接转换成control方法对应的参数对象
场景: 在web应用开发中,spring mvc凭借出现的性能和良好的可扩展性,导致使用日渐增多,成为事实标准,在日常的开发过程中,有一个很常见的场景:即前端通过ajax提交方式,提交参数为一个jso ...
- python中使用queue实现约瑟夫环(约瑟夫问题)求解
约瑟夫问题:是一个数学的应用问题:已知n个人(以编号1,2,3...n分别表示)围坐在一张圆桌周围. 从编号为k的人开始报数,数到m的那个人出列:他的下一个人又从1开始报数,数到m的那个人又出列: 依 ...
- Testing - 软件测试知识汇总
软件测试知识梳理 基础概念 : http://www.cnblogs.com/anliven/p/6070000.html 测试分类 : http://www.cnblogs.com/anliven/ ...
- Spring Boot读取配置的 5 种方式
读取application文件 在application.yml或者properties文件中添加: info.address=USA info.company=Spring info.degree= ...
- Java架构技术进阶之:从分布式到微服务,深挖Service Mesh
自从几十年前第一次引入分布式系统这个概念以来,出现了很多原来根本想象不到的分布式系统使用案例,但同时也引入了各种各样的新问题. 当这些系统还是比较少比较简单的时候,工程师可以通过减少远程交互的次数来解 ...