背景

近期有这么一个需求:

手机端需要展示一个比较大的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;}@Overridepublic 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事件,主动关闭EventSourcesource.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

转载于:https://www.cnblogs.com/yejg1212/p/9603544.html

服务器端事件发送SSE相关推荐

  1. 耗时很长的服务器端事件中让客户端得到中间过程信息的合理解决方案

    需求:B/S结构的系统里,用户点一个按钮系统开始发送上千封邮件,要求把发送信息(发送成功数,失败数,剩余数量...)动态实时的反馈给客户. 分析和实施过程当中遇到的问题: 一:最低级的问题 由于客户催 ...

  2. 页面回传与js调用服务器端事件(转)

    第一章. Asp.net中服务端控件事件是如何触发的 Asp.net 中在客户端触发服务器端事件分为两种情况: 一.   WebControls中的Button 和HtmlControls中的Type ...

  3. 8.QT的事件循环与事件发送相关类

    一.QT的事件发送类QCoreApplication QT使用QCoreApplication类为Qt程序提供了事件循环机制.该类继承QObject.QCoreApplication包含主事件循环,来 ...

  4. 【EventBus】EventBus 源码解析 ( 事件发送 | postToSubscription 方法 | EventBus 线程模式处理细节 )

    文章目录 一.事件发送 postSingleEventForEventType 方法 二.事件发送 postToSubscription 方法 三.事件发送 invokeSubscriber 方法 一 ...

  5. QT中事件发送函数sendEvent()、postEvent()详解

    Qt发送事件分为两种 -阻塞型事件发送 需要重写接收对象的event()事件处理函数 当事件发送后,将会立即进入event()事件处理函数进行事件处理 通过sendEvent()静态函数实现阻塞发送: ...

  6. 严重: 异常将上下文初始化事件发送到类的侦听器实例.[org.springframework.web.co

    原文 BeanFactory创建Bean实例错误,原因可能是项目的builderpath中的JDK版本莫名被调成默认的了,如javase1.5,重新移除添加系统的jdk即可. 2022.2.14 补充 ...

  7. unity网络实战开发(丛林战争)-前期知识准备(003-开发服务器端的发送数据和接收数据)

    使用工具:VS2015 使用语言:c# 作者:Gemini_xujian 参考:siki老师-<丛林战争>视频教程 继上一篇文章内容,这节课讲解一下服务器端的发送数据和接收数据. 上篇文章 ...

  8. 严重:异常将上下文初始化事件发送到类的侦听器实例.[org.springframework.web.context.ContextLoaderListener] 以解决

    严重: 异常将上下文初始化事件发送到类的侦听器实例.[org.springframework.web.context.ContextLoaderListener] org.springframewor ...

  9. Qt中的消息通知和事件发送

    Qt 中的信号和槽机制.事件机制是其具有特色的两大机制.利用这两种机制可以轻松地实现需要的消息通知和事件通知. 1.信号和槽机制 充分使用 Qt 库中已经定义和实现好的对象的信号和槽函数,如按钮的 c ...

最新文章

  1. 【Java集合源码剖析】Java集合框架
  2. 【机器学习基础】通俗易懂无监督学习K-Means聚类算法及代码实践
  3. python 知识点总结
  4. 【DP】K星人的语言(2020特长生 T3)
  5. 深拷贝与浅拷贝、值语义与引用语义对象语义 ——以C++和Python为例
  6. java 反射初探(1)
  7. 如何查看docker的内核版本_查看Linux内核版本的方法有几个?你也是这样操作吗?...
  8. 成都KTV点歌类小程序开发公司,微信小程序开发
  9. 猫扑网娱乐门户的猫派营销
  10. 夜天之书 #19 The ZeroMQ Community
  11. WordPress 安全漏洞
  12. C#获取汉字首字母,多音拼音
  13. 2023二建学天案例突破101问
  14. 计算机应用大赛宣传稿,江苏开放大学计算机应用基础中国名城宣传片
  15. 个人HTML期末大作业~ 个人网页(HTML+CSS)6页面带下拉特效~简单带表格带设计说明 ~学生网页设计作业源码
  16. uniapp 微信小程序发布
  17. 细数云计算产品和技术-Google App Engine
  18. 对计算机学院祝福语,祝福学校发展的祝福语(精选60句)
  19. 苹果结构体系不匹配_iPhone 5C下载安装微信时提示结构不匹配怎么办及 iPhoneX怎么强制重启...
  20. 微型计算机软件主要包括什么,系统软件主要包括哪几个部分?

热门文章

  1. jira导出HTML,JIRA Software 7.4.x 版本说明
  2. php主控,IcePHP框架中的快速后台中的通用CRUD功能框架(五) SCrud 主控类
  3. python中remove用法_python中remove的一些坑
  4. jdba访问mysql_Java中JDBC操作数据库的步骤
  5. 服务器系统无法使用gdt,《自己动手写操作系统》第三章第一个程序的问题。bochs加载后出现no bootable device.我在网上查了下,说是因为第一扇区最后2字节...
  6. 计算机c语言知识点txt,计算机二级C语言(重要知识点)
  7. python上机实验报告读取文件_Python程序设计实验八:文件
  8. java 我爱你_Java初级教程-课程笔记
  9. 20210217:力扣第228周周赛(上)
  10. php获取总共内存_PHP获取内存使用情况详解