博客
关于我
服务器端事件发送SSE
阅读量:737 次
发布时间:2019-03-22

本文共 4555 字,大约阅读时间需要 15 分钟。

背景

近期有这么一个需求:

手机端需要展示一个比较大的pdf基于手机端网络/流量/体验等考虑,希望不通过pdf下载然后展示而是把pdf转成一张张的图片,然后再在手机上展示。

分析

pdf转图片,肯定是一个比较慢的过程,最好能转完一张就返回一张到前端。

So,此文要讲的是 请求异步多次返回的技术实现SSE
当然,WebSocket也能做到,它可以双向通信,比SSE(单向发送)强大且复杂,SSE好在比较简单

服务器端事件发送 SSE

全称:Server Send Event

其实严格地说,HTTP 协议无法做到服务器主动推送数据到客户端的。只不过可以变通一下,就是服务器向客户端声明,接下来要发送的是流数据(stream)。
此时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流。
SSE 就是利用这种机制,使用流信息向浏览器推送信息。它基于 HTTP 协议,目前除了 IE,其他浏览器都支持。
IE的话,也可以通过来兼容起来。

代码实现

客户端

需要用到EventSource,并实现onmessage方法

if (!!window.EventSource) {	var source = new EventSource('push');	s = '';	source.addEventListener('message', function(e) {		s += e.data + "
"; $("#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();		}	}}

以上客户端和服务端的代码示例基于

做了如下修改:

1、原文示例代码中,每个请求只返回了一次数据,服务器每次发完数据断开了连接。   但SSE默认会自动重连,所以客户端不断地重连(重新发请求)。浏览器F12 network,可以看到刷了很多请求   这和ajax长轮询没什么区别了。2、Controller端处理完return返回之后,前端页面会收到一个error事件。浏览器接收到error事件后,SSE又会自动重连,所以我加了一个source.close();   当然这里close不合理,后面再聊合理的做法

这里需要知道的是:return之后长连接就断开了,就不是我们想要的持续推送了。

修改后的代码见Github:

基于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管理。

参考资料:

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")@ResponseBodypublic 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+"
"; $("#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");}

完整代码见:

推荐阅读:

Server-Sent Events 教程

转载地址:http://xvywk.baihongyu.com/

你可能感兴趣的文章
mysql复制表结构和数据
查看>>
mysql复杂查询,优质题目
查看>>
MySQL外键约束
查看>>
MySQL多表关联on和where速度对比实测谁更快
查看>>
MySQL多表左右连接查询
查看>>
mysql大批量删除(修改)The total number of locks exceeds the lock table size 错误的解决办法
查看>>
mysql如何做到存在就更新不存就插入_MySQL 索引及优化实战(二)
查看>>
mysql如何删除数据表,被关联的数据表如何删除呢
查看>>
MySQL如何实现ACID ?
查看>>
mysql如何记录数据库响应时间
查看>>
MySQL子查询
查看>>
Mysql字段、索引操作
查看>>
mysql字段的细节(查询自定义的字段[意义-行列转置];UNION ALL;case-when)
查看>>
mysql字段类型不一致导致的索引失效
查看>>
mysql字段类型介绍
查看>>
mysql字段解析逗号分割_MySQL逗号分割字段的行列转换技巧
查看>>
MySQL字符集与排序规则
查看>>
MySQL字符集乱码
查看>>
mysql字符集设置
查看>>
mysql存储IP地址的数据类型
查看>>