本文选自孙卫琴的《精通Spring:Java Web开发技术详解》清华大学出版社出版
技术支持网址为: www.javathinker.net/spring.jsp
本书对应的直播和录播课: www.javathinker.net/zhibo.jsp
孙卫琴的QQ学习答疑群:915851077
在浏览器与Web服务器之间的传统的通信过程中,都是由用户首先主动在浏览器端的网页上选择一个链接或者提交一个表单,浏览器就会生成一个相应的HTTP请求,把它发送到服务器端,服务器端再返回相应的响应结果。
如果用户希望每隔3秒就向服务器端询问当前的时间,用户必须每隔三秒就在浏览器端的网页上选择查询时间的链接。这样的操作方式对用户来说是非常繁琐的。
假如不用麻烦用户每隔三秒就主动查询时间,服务器也会自动向浏览器端定时发送当前的时间信息,那么就能使用户与Web应用之间的交互过程变得更加简捷方便。
为了解决上述问题,SSE(Sever-SentEvent ,服务器端发送事件)技术应运而生,它依靠服务器与浏览器的紧密配合,使得服务器端看上去能够主动向浏览器端推送数据:
(1)服务器端以数据流的方式来发送响应数据,响应正文的类型和字符编码为:“text/event-stream;charset=UTF-8”。
(2)浏览器端采用事件处理机制来读取服务器端推送过来的数据。
之所以说服务器端仅仅是“看上去”主动向浏览器端推送数据,这是因为在浏览器与服务器的HTTP通信过程中,始终是由浏览器先发出请求,再接收服务器端的响应结果。只不过浏览器会自动每隔一段时间(比如1秒钟),就会向服务器发出一个请求,再接收服务器端返回的最新响应结果,随后自动把响应结果输出到网页上。这样,对浏览器的用户而言,“看上去”好像是服务器端的数据主动推送到客户端。浏览器自动每隔一段时间就向服务器发出请求的过程也叫做轮询。
在多个TCP连接中推送数据
在以下例程1的PushController类中,push1()请求处理方法返回当前的时间,它的@RequestMapping注解指定响应正文的数据类型为“text/event-stream;charset=UTF-
8”,因此,push1()方法会以SSE的方式来和浏览器端通信。
例程1 PushController.java
@Controller
public class PushController {
@RequestMapping("showtime")
publicString showtime() {
return"time";
}
@RequestMapping(value="push1",
produces="text/event-stream;charset=UTF-8")
@ResponseBody
public String push1(){
System.out.println("push message......");
//return"retry:5000n"+"data:current time: "
+getCurrentDate()+"nn";
return "data:current time:
"+getCurrentDate()+"nn";
}
privateString getCurrentDate(){
returnnew SimpleDateFormat(
"YYYY-MM-dd hh:mm:ss").format(new Date());
}
@RequestMapping(value = "/push2")
@ResponseBody
publicString push2(HttpServletResponse response) {
response.setContentType("text/event-stream");
response.setCharacterEncoding("UTF-8");
Strings=null;
while(true) {
try {
PrintWriter pw=response.getWriter();
Thread.sleep(1000L);
s="data:current time: "+getCurrentDate()+"nn";
pw.write(s);
pw.flush();
if(pw.checkError()){
System.out.println("客户端断开连接");
return "" ;
}
} catch(IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
}
以下例程2的time.jsp类在Javascript脚本中创建了一个EventSource对象,这个EventSource对象会读取服务器端推送过来的数据。
例程2 time.jsp
<%@ page language="java"contentType="text/html; charset=UTF-8" %>SSE方式消息推送 if(!!window.EventSource){ var evtSource = new EventSource('push1'); s = ''; //处理接收到数据事件 evtSource.addEventListener('message',function(e){ console.log("get message>"+e.data); s+=e.data+"
"; $("#msgFromPush").html(s); }); evtSource.onopen = function () { //处理打开连接事件 console.log("连接打开"); } evtSource.onerror= function (e) { if(e.readyState == EventSource.CLOSE){ // 处理关闭连接事件 console.log("连接关闭"); }else{ console.log(e.readyState); } }; }else{ console.log("SSE is not supported."); }functioncloseConnection() { console.log('断开连接,停止接收服务器端的推送消息'); evtSource.close(); }
以上time.jsp引入了jquery-3.2.1.min.js文件,它是JQuery的类库文件之一,time.jsp中用于输出当前时间的“” 代码依赖于这个类库文件。但是,这个类库文件并没有提供对SSE的支持。
许多浏览器(如Chrome)对SSE都提供了默认的支持。而IE/Edge浏览器默认情况下不支持SSE,如果希望在IE/Edge浏览器中访问time.jsp,那么还需要在time.jsp中引入对SSE的支持类库:
eventsource.js文件的下载网址为:
https://github.com/EventSource/eventsource/blob/master/lib/eventsource.js
time.jsp的Javascript脚本创建了一个EventSource对象:
var evtSource = new EventSource('push1');
以上EventSource对象请求访问PushController类的push1()方法,会读取push1()方法推送的数据。
在EventSource对象与PushController类通信的过程中,会产生以下事件:
- open: 浏览器与服务器之间打开连接。
- message:浏览器读取到服务器端推送过来的数据。
- error: 通信中出现错误,包括浏览器与服务器之间关闭连接的情况。
time.jsp的Javascript脚本会捕获以上三种事件,并作出相应的处理。处理事件可以采用以下两种方式来编写代码,它们是等价的:
方式一:
evtSource.addEventListener('open', //处理打开连接事件
function(e){console.log("connect isopen"); },
false);
方式二:
evtSource.onopen = function () { //处理打开连接事件
console.log("
连接打开");
}
通过Chrome浏览器访问http://localhost:8080/helloapp/showtime,会出现time.jsp生成
的网页,参见图1。用户在浏览器端会看到,每隔一段时间,浏览器上的网页就会被自动刷新,显示当前的时间。
图1 time.jsp生成的网页
在Chrome浏览器的控制台中,会显示time.jsp的Javascript脚本输出的打印信息,参见图2。
图2 time.jsp的Javascript脚本在浏览器的控制台的打印信息
从以上图2可以看出,浏览器端每次调用PushController类的push1()方法,都会先打开连接,读取push1()方法返回的数据,接下来就关闭连接。默认情况下,当前连接关闭后,每隔一段时间(Chrome浏览器的默认间隔时间为3秒钟),浏览器端会再次建立与服务器端的连接,请求访问push1()方法,读取push1()方法返回的数据,再关闭连接。如此不断重复下去。
向浏览器端推送的数据的固定格式为“retry:${毫秒数}ndata:${返回数据}nn”:
- retry:指定每隔多少毫秒再次请求访问服务器。这个retry参数不是必须的,如果没有提供,会采用浏览器设置的默认间隔时间。Chrome浏览器的默认间隔时间是3000毫秒,即3秒钟。
- data:指定服务器端所推送的数据。
在PushController类的push1()方法中,以下代码返回采用上述格式的推送数据:
//指定连接服务器的间隔时间为5秒 return "retry:5000n"+"data:current time: "+getCurrentDate()+"nn";
或者:
//采用默认的连接服务器的间隔时间 return "data:current time: "+getCurrentDate()+"nn";
在time.jsp中还提供了一个“停止接收消息”按钮,按下这个按钮,浏览器会执行closeConnection()函数:
以上closeConnection()函数调用evtSource.close()方法,断开当前连接,并且不会再重新建立与服务器端的连接,也不会再试图去读取push1()方法返回的推送数据。



