一般来说,Web端即时通讯技术因受限于浏览器的设计限制,一直以来实现起来并不容易,主流的Web端即时通讯方案大致有4种:传统Ajax短轮询、Comet技术、WebSocket技术、SSE(Server-sent Events)
1,Ajax短轮询:脚本发送的http请求
短连接:在HTTP/1.0中默认使用短连接。也就是说,客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接。当客户端浏览器访问的某个HTML或其他类型的web页中包含有其他的Web资源(如JavaScript文件、图像文件、CSS文件等),每遇到这样一个Web资源,浏览器就会重新建立一个HTTP会话。
http 短轮询:其实就是普通的轮询。指在特定的时间间隔(如每1秒),由浏览器对服务器发出HTTP request,然后由服务器返回最新的数据给客户端的浏览器。
- 特点:http端轮询是服务器收到请求不管是否有数据都直接响应 http 请求; 浏览器受到 http 响应隔一段时间在发送同样的http 请求查询是否有数据;
- 应用场景:传统的web通信模式。后台处理数据,需要一定时间,前端想要知道后端的处理结果,就要不定时的向后端发出请求以获得最新情况。
- 优点:前后端程序编写比较容易。
- 缺点:请求中有大半是无用,难于维护,浪费带宽和服务器资源;响应的结果没有顺序(因为是异步请求,当发送的请求没有返回结果的时候,后面的请求又被发送。而此时如果后面的请求比前面的请 求要先返回结果,那么当前面的请求返回结果数据时已经是过时无效的数据了)。
- 实例:适于小型应用。
2,Comet:一种hack技术
传统模式的 Web 系统以客户端发出请求、服务器端响应的方式工作。这种方式并不能满足很多现实应用的需求,譬如:
- 监控系统:后台硬件热插拔、LED、温度、电压发生变化;
- 即时通信系统:其它用户登录、发送信息;
- 即时报价系统:后台数据库内容发生变化。
这些应用都需要服务器能实时地将更新的信息传送到客户端,而无须客户端发出请求。“服务器推”技术在现实应用中有一些解决方案,主要分为两类:一类需要在浏览器端安装插件,基于套接口传送信息,或是使用 RMI、CORBA 进行远程调用;而另一类则无须浏览器安装任何插件、基于 HTTP 长连接。
2.1,基于客户端套接字接口的“服务器推”技术
Flash XMLSocket:如果 Web 应用的用户接受应用只有在安装了 Flash 播放器才能正常运行, 那么使用 Flash 的 XMLSocket 也是一个可行的方案。
这种方案实现的基础是:Flash 提供了 XMLSocket 类。JavaScript 和 Flash 的紧密结合:在 JavaScript 可以直接调用 Flash 程序提供的接口。
具体实现方法:在 HTML 页面中内嵌入一个使用了 XMLSocket 类的 Flash 程序。JavaScript 通过调用此 Flash 程序提供的套接口接口与服务器端的套接口进行通信。JavaScript 在收到服务器端以 XML 格式传送的信息后可以很容易地控制 HTML 页面的内容显示。
Javascript 与 Flash 的紧密结合,极大增强了客户端的处理能力。从 Flash 播放器 V7.0.19 开始,已经取消了 XMLSocket 的端口必须大于 1023 的限制。Linux 平台也支持 Flash XMLSocket 方案。但此方案的缺点在于:
- 客户端必须安装 Flash 播放器;
- 因为 XMLSocket 没有 HTTP 隧道功能,XMLSocket 类不能自动穿过防火墙;
- 因为是使用套接口,需要设置一个通信端口,防火墙、代理服务器也可能对非 HTTP 通道端口进行限制。
不过这种方案在一些网络聊天室,网络互动游戏中已得到广泛使用。
2.2,Java Applet套接口
在客户端使用 Java Applet,通过 java.net.Socket 或 java.net.DatagramSocket 或 java.net.MulticastSocket 建立与服务器端的套接口连接,从而实现“服务器推”。
这种方案最大的不足在于 Java applet 在收到服务器端返回的信息后,无法通过 JavaScript 去更新 HTML 页面的内容。
2.3,基于 HTTP 长连接的“服务器推”技术:Comet技术
Comet简介:因为 AJAX 技术的普及,以及把 IFrame 嵌在“htmlfile“的 ActiveX 组件中可以解决 IE 的加载显示问题,一些受欢迎的应用如 meebo,gmail+gtalk 在实现中使用了这些新技术;同时“服务器推”在现实应用中确实存在很多需求。因为这些原因,基于纯浏览器的“服务器推”技术开始受到较多关注,Alex Russell(Dojo Toolkit 的项目 Lead)称这种基于 HTTP 长连接、无须在浏览器端安装插件的“服务器推”技术为“Comet”。目前已经出现了一些成熟的 Comet 应用以及各种开源框架;一些 Web 服务器如 Jetty 也在为支持大量并发的长连接进行了很多改进。
长轮询和短轮询最大的区别:短轮询去服务端查询的时候,不管库存量有没有变化,服务器就立即返回结果了。 而长轮询则不是,在长轮询中,服务器如果检测到库存量没有变化的话,将会把当前请求挂起一段时间(这个时间也叫作超时时间,一般是几十秒)。在这个时间里,服务器会去检测库存量有没有变化,检测到变化就立即返回,否则就一直等到超时为止。
Comet技术实现模型1:基于 AJAX 的长轮询(long-polling)方式
定义:客户端向服务器发送Ajax请求,服务器接到请求后hold住连接,直到有新消息才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求。
- 服务器端会阻塞请求直到有数据传递或超时才返回。
- 客户端 JavaScript 响应处理函数会在处理完服务器返回的信息后,再次发出请求,重新建立连接。
- 当客户端处理接收的数据、重新建立连接时,服务器端可能有新的数据到达;这些信息会被服务器端保存直到客户端重新建立连接,客户端会一次把当前服务器端所有的信息取回。
优点:在无消息的情况下不会频繁的请求,耗费资源小。
缺点:服务器hold连接会消耗资源,返回数据顺序无保证,难于管理维护。
实例:WebQQ、Hi网页版、Facebook IM。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <base href="<%=basePath%>"> <title>My JSP 'index.jsp' starting page</title> <meta http-equiv="pragma" content="no-cache"> <meta http-equiv="cache-control" content="no-cache"> <meta http-equiv="expires" content="0"> <meta http-equiv="keywords" content="keyword1,keyword2,keyword3"> <meta http-equiv="description" content="This is my page"> <link rel="stylesheet" href="http://apps.bdimg.com/libs/jquerymobile/1.4.5/jquery.mobile-1.4.5.min.css"> <!-- 引入 jQuery 库 --> <script src="http://apps.bdimg.com/libs/jquery/1.10.2/jquery.min.js"></script> <!-- 引入 jQuery Mobile 库 --> <script src="http://apps.bdimg.com/libs/jquerymobile/1.4.5/jquery.mobile-1.4.5.min.js"></script> <script type="text/javascript"> $(function() { getMsgNum(); }); function getMsgNum() { $.ajax({ url : 'JsLongPollingMsgServlet', type : 'post', dataType : 'json', data : { "pageMsgNum" : $("#pageMsgNum").val() }, timeout : 5000, success : function(data, textStatus) { if (data && data.msgNum) { //请求成功,刷新数据 $("#msgNum").html(data.msgNum); //这个是用来和后台数据作对比判断是否发生了改变 $("#pageMsgNum").val(data.msgNum); } if (textStatus == "success") { //成功之后,再发送请求,递归调用 getMsgNum(); } }, error : function(XMLHttpRequest, textStatus, errorThrown) { if (textStatus == "timeout") { //有效时间内没有响应,请求超时,重新发请求 getMsgNum(); } else { // 其他的错误,如网络错误等 getMsgNum(); } } }); } </script> </head> <body> <div id="page1" data-role="page"> <div data-role="header"> <h1>AJAX长轮询</h1> </div> <div data-role="content"> <input id="pageMsgNum" name="pageMsgNum" type="hidden" /> 您有<span id="msgNum" style="color: red;">0</span>条消息! </div> </div> </body> </html>
import java.io.IOException; import java.io.PrintWriter; import java.util.List; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet("/JsLongPollingMsgServlet") public class JsLongPollingMsgServlet extends HttpServlet { private static final long serialVersionUID = 1L; public JsLongPollingMsgServlet() { super(); // TODO Auto-generated constructor stub } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ServletContext application = this.getServletContext(); List msglist= (List)application.getAttribute("msg"); request.setCharacterEncoding("utf-8"); PrintWriter out = response.getWriter(); String pageMsgNumStr = request.getParameter("pageMsgNum"); if(pageMsgNumStr==null || "".equals(pageMsgNumStr)){ pageMsgNumStr = "0"; } int pageMsgNum = Integer.parseInt(pageMsgNumStr); int num = 0; StringBuffer json = null; while(true){ num = msglist.size(); //数据发生改变 将数据响应客户端 if(num != pageMsgNum){ json = new StringBuffer("{"); json.append("\"msgNum\":"+num); json.append("}"); break; }else{ //没有新的数据 保持住连接 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } out.write(json.toString()); out.close(); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }
Comet技术实现模型2:基于 Iframe 及 htmlfile 的流(streaming)方式
iframe 是很早就存在的一种 HTML 标记, 通过在 HTML 页面里嵌入一个隐蔵帧,然后将这个隐蔵帧的 SRC 属性设为对一个长连接的请求,服务器端就能源源不断地往客户端输入数据。
在 iframe 方案的客户端,iframe 服务器端并不返回直接显示在页面的数据,而是返回对客户端 Javascript 函数的调用,如“<script type="text/javascript">js_func(“data from server ”)</script>”。服务器端将返回的数据作为客户端 JavaScript 函数的参数传递;客户端浏览器的 Javascript 引擎在收到服务器返回的 JavaScript 调用时就会去执行代码。
- 每次数据传送不会关闭连接,连接只会在通信出现错误时,或是连接重建时关闭(一些防火墙常被设置为丢弃过长的连接, 服务器端可以设置一个超时时间, 超时后通知客户端重新建立连接,并关闭原来的连接)。
- 使用 iframe 请求一个长连接有一个很明显的不足之处:IE、Morzilla Firefox 下端的进度栏都会显示加载没有完成,而且 IE 上方的图标会不停的转动,表示加载正在进行。Google 的天才们使用一个称为“htmlfile”的 ActiveX 解决了在 IE 中的加载显示问题,并将这种方法用到了 gmail+gtalk 产品中。。
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title></title> </head> <body> <ul id="content"></ul> <form class="form"> <input type="text" placeholder="请输入发送的消息" class="message" id="message"/> <input type="button" value="发送" id="send" class="connect"/> <input type="button" value="连接" id="connect" class="connect"/> </form> <script> var oUl=document.getElementById('content'); var oConnect=document.getElementById('connect'); var oSend=document.getElementById('send'); var oInput=document.getElementById('message'); var ws=null; oConnect.onclick=function(){ ws=new WebSocket('ws://localhost:3000'); ws.onopen=function(){ oUl.innerHTML+="<li>客户端已连接</li>"; } ws.onmessage=function(evt){ oUl.innerHTML+="<li>"+evt.data+"</li>"; } ws.onclose=function(){ oUl.innerHTML+="<li>客户端已断开连接</li>"; }; ws.onerror=function(evt){ oUl.innerHTML+="<li>"+evt.data+"</li>"; }; }; oSend.onclick=function(){ if(ws){ ws.send(oInput.value); } } </script> </body> </html>
import java.io.IOException; import java.util.ArrayList; import java.util.List; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet("/SetMsg") public class SetMsg extends HttpServlet { private static final long serialVersionUID = 1L; public SetMsg() { super(); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub ServletContext application = this.getServletContext(); List msglist= new ArrayList(); if(application.getAttribute("msg")!=null){ msglist=(List)application.getAttribute("msg"); } msglist.add(request.getParameter("msgstr")); application.setAttribute("msg", msglist); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub doGet(request, response); } }
2.4,使用Comet模型开发自己的应用
(1)不要在同一客户端同时使用超过两个的 HTTP 长连接:
- 我们使用 IE 下载文件时会有这样的体验,从同一个 Web 服务器下载文件,最多只能有两个文件同时被下载。第三个文件的下载会被阻塞,直到前面下载的文件下载完毕。这是因为 HTTP 1.1 规范中规定,客户端不应该与服务器端建立超过两个的 HTTP 连接, 新的连接会被阻塞。而 IE 在实现中严格遵守了这种规定。
- HTTP 1.1 对两个长连接的限制,会对使用了长连接的 Web 应用带来如下现象:在客户端如果打开超过两个的 IE 窗口去访问同一个使用了长连接的 Web 服务器,第三个 IE 窗口的 HTTP 请求被前两个窗口的长连接阻塞。
- 所以在开发长连接的应用时, 必须注意在使用了多个 frame 的页面中,不要为每个 frame 的页面都建立一个 HTTP 长连接,这样会阻塞其它的 HTTP 请求,在设计上考虑让多个 frame 的更新共用一个长连接。
(2)服务器端的性能和可扩展性:
- 一般 Web 服务器会为每个连接创建一个线程,如果在大型的商业应用中使用 Comet,服务器端需要维护大量并发的长连接。在这种应用背景下,服务器端需要考虑负载均衡和集群技术;或是在服务器端为长连接作一些改进。
- 应用和技术的发展总是带来新的需求,从而推动新技术的发展。HTTP 1.1 与 1.0 规范有一个很大的不同:1.0 规范下服务器在处理完每个 Get/Post 请求后会关闭套接口连接; 而 1.1 规范下服务器会保持这个连接,在处理两个请求的间隔时间里,这个连接处于空闲状态。 Java 1.4 引入了支持异步 IO 的 java.nio 包。当连接处于空闲时,为这个连接分配的线程资源会返还到线程池,可以供新的连接使用;当原来处于空闲的连接的客户发出新的请求,会从线程池里分配一个线程资源处理这个请求。 这种技术在连接处于空闲的机率较高、并发连接数目很多的场景下对于降低服务器的资源负载非常有效。
- 但是 AJAX 的应用使请求的出现变得频繁,而 Comet 则会长时间占用一个连接,上述的服务器模型在新的应用背景下会变得非常低效,线程池里有限的线程数甚至可能会阻塞新的连接。Jetty 6 Web 服务器针对 AJAX、Comet 应用的特点进行了很多创新的改进,请参考文章“AJAX,Comet and Jetty”。
(3)控制信息与数据信息使用不同的 HTTP 连接:
- 使用长连接时,存在一个很常见的场景:客户端网页需要关闭,而服务器端还处在读取数据的堵塞状态,客户端需要及时通知服务器端关闭数据连接。服务器在收到关闭请求后首先要从读取数据的阻塞状态唤醒,然后释放为这个客户端分配的资源,再关闭连接。
- 所以在设计上,我们需要使客户端的控制请求和数据请求使用不同的 HTTP 连接,才能使控制请求不会被阻塞。
- 在实现上,如果是基于 iframe 流方式的长连接,客户端页面需要使用两个 iframe,一个是控制帧,用于往服务器端发送控制请求,控制请求能很快收到响应,不会被堵塞;一个是显示帧,用于往服务器端发送长连接请求。如果是基于 AJAX 的长轮询方式,客户端可以异步地发出一个 XMLHttpRequest 请求,通知服务器端关闭数据连接。
(4)在客户和服务器之间保持“心跳”信息:
- 在浏览器与服务器之间维持一个长连接会为通信带来一些不确定性:因为数据传输是随机的,客户端不知道何时服务器才有数据传送。服务器端需要确保当客户端不再工作时,释放为这个客户端分配的资源,防止内存泄漏。因此需要一种机制使双方知道大家都在正常运行。在实现上:服务器端在阻塞读时会设置一个时限,超时后阻塞读调用会返回,同时发给客户端没有新数据到达的心跳信息。此时如果客户端已经关闭,服务器往通道写数据会出现异常,服务器端就会及时释放为这个客户端分配的资源。
- 如果客户端使用的是基于 AJAX 的长轮询方式;服务器端返回数据、关闭连接后,经过某个时限没有收到客户端的再次请求,会认为客户端不能正常工作,会释放为这个客户端分配、维护的资源。
- 当服务器处理信息出现异常情况,需要发送错误信息通知客户端,同时释放资源、关闭连接。
3,SSE
3.1,基本概念
服务器推送事件(Server-sent Events),简称SSE,是 HTML 5 规范中的一个组成部分,可以用来从服务端实时推送数据到浏览器端。相对于与之类似的 COMET 和 WebSocket 技术来说,服务器推送事件的使用更简单,对服务器端的改动也比较小。对于某些类型的应用来说,服务器推送事件是最佳的选择。
SSE本质:严格地说,HTTP 协议无法做到服务器主动推送信息。但是,有一种变通方法,就是服务器向客户端声明,接下来要发送的是流信息(streaming)。也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,视频播放就是这样的例子。本质上,这种通信就是以流信息的方式,完成一次用时很长的下载。SSE 就是利用这种机制,使用流信息向浏览器推送信息。它基于 HTTP 协议,目前除了 IE/Edge,其他浏览器都支持。
SSE的特点:WebSocket的比较 SSE 与 WebSocket 作用相似,都是建立浏览器与服务器之间的通信渠道,然后服务器向浏览器推送信息。总体来说,WebSocket 更强大和灵活。因为它是全双工通道,可以双向通信;SSE 是单向通道,只能服务器向浏览器发送,因为流信息本质上就是下载。如果浏览器向服务器发送信息,就变成了另一次 HTTP 请求。
SSE的优点:
- SSE 使用 HTTP 协议,现有的服务器软件都支持。WebSocket 是一个独立协议。
- SSE 属于轻量级,使用简单;WebSocket 协议相对复杂。
- SSE 默认支持断线重连,WebSocket 需要自己实现。
- SSE 一般只用来传送文本,二进制数据需要编码后传送,WebSocket 默认支持传送二进制数据。
- SSE 支持自定义发送的消息类型。
- SSE适用于更新频繁、低延迟并且数据都是从服务端到客户端。
3.2,SSE在HTML5中的技术规范和定义
Server-sent Events 规范是 HTML 5 规范的一个组成部分,该规范比较简单,主要由两个部分组成:
- 第一个部分是服务器端与浏览器端之间的通讯协议。
- 第二部分则是在浏览器端可供 JavaScript 使用的 EventSource 对象。
【服务器端】通讯协议是基于纯文本的简单协议。 服务器向浏览器发送的 SSE 数据,必须是 UTF-8 编码的文本,具有如下的 HTTP 头信息:
Content-Type: text/event-stream Cache-Control: no-cache Connection: keep-alive
Content-Type必须指定 MIME 类型为event-steam。每一次发送的信息,由若干个message组成,每个message之间用\n\n分隔。每个message内部由若干行组成,每一行都是如下格式。
[field]: value\n
field可以取四个值:data、event、id、retry。此外,还可以有冒号开头的行,表示注释。通常,服务器每隔一段时间就会向浏览器发送一个注释,保持连接不中断。
data: first event data: second event id: 100 event: myevent data: third event id: 101 : this is a comment data: fourth event data: fourth event continue
如上所示,每个事件之间通过空行来分隔。对于每一行来说,冒号(“:”)前面表示的是该行的类型,冒号后面则是对应的值。可能的类型包括:
- 类型为空白,表示该行是注释,会在处理时被忽略。
- 类型为 data,表示该行包含的是数据。以 data 开头的行可以出现多次。所有这些行都是该事件的数据。如果数据很长,可以分成多行,最后一行用\n\n结尾,前面行都用\n结尾。
data: begin message\n data: continue message\n\n
一个发送 JSON 数据的例子:
data: {\n data: "foo": "bar",\n data: "baz", 555\n data: }\n\n
- 类型为 id,表示该行用来声明事件的标识符,相当于每一条数据的编号。
id: msg1\n data: message\n\n
如果服务器端返回的数据中包含了事件的标识符,浏览器会记录最近一次接收到的事件的标识符。 浏览器用lastEventId属性读取这个值。一旦连接断线,浏览器会发送一个 HTTP 头,里面包含一个特殊的Last-Event-ID头信息,将这个值发送回来,用来帮助服务器端重建连接。服务器端可以通过浏览器端发送的事件标识符来确定从哪个事件开始来继续连接。因此,这个头信息可以被视为一种同步机制。
- 类型为 event,表示该行用来声明事件的类型。浏览器在收到数据时,会产生对应类型的事件。默认是message事件。浏览器可以用addEventListener()监听该事件。
event: foo\n data: a foo event\n\n data: an unnamed event\n\n event: bar\n data: a bar event\n\n
上面的代码创造了三条信息。第一条的名字是foo,触发浏览器的foo事件;第二条未取名,表示默认类型,触发浏览器的message事件;第三条是bar,触发浏览器的bar事件。
- 类型为 retry,表示该行用来声明浏览器在连接断开之后进行再次连接之前的等待时间。
retry: 10000\n
两种情况会导致浏览器重新发起连接:一种是时间间隔到期,二是由于网络错误等原因,导致连接出错。
【客户端】对于服务器端返回的响应,浏览器端需要在 JavaScript 中使用 EventSource 对象来进行处理。EventSource 使用的是标准的事件监听器方式,只需要在对象上添加相应的事件处理方法即可。EventSource 提供了三个标准事件:
- open:当成功与服务器建立连接时产生,使用onopen处理。
- message:当收到服务器发送的事件时产生,使用onmessage处理。
- error:当出现错误时产生,使用onerror处理。
服务器端可以返回自定义类型的事件。对于这些事件,可以使用 addEventListener 方法来添加相应的事件处理方法。如下代码给出了 EventSource 对象的使用示例。
var es = new EventSource('events'); es.onmessage = function(e) { console.log(e.data); }; es.addEventListener('myevent', function(e) { console.log(e.data); });
在指定 URL 创建出 EventSource 对象之后,可以通过 onmessage 和 addEventListener 方法来添加事件处理方法。当服务器端有新的事件产生,相应的事件处理方法会被调用。EventSource 对象的 onmessage 属性的作用类似于 addEventListener( ‘ message ’ ),不过 onmessage 属性只支持一个事件处理方法。
- SSE 的客户端 API 部署在EventSource对象上。
//检测浏览器是否支持 SSE if ('EventSource' in window) { // ... }
- 使用 SSE 时,浏览器首先生成一个EventSource实例,向服务器发起连接。
var source = new EventSource(url); 上面的url可以与当前网址同域,也可以跨域。 跨域时,可以指定第二个参数,打开withCredentials属性,表示是否一起发送 Cookie。 var source = new EventSource(url, { withCredentials: true });
- EventSource实例的readyState属性,表明连接的当前状态。该属性只读,可以取以下值。
0:相当于常量EventSource.CONNECTING,表示连接还未建立,或者断线正在重连。 1:相当于常量EventSource.OPEN,表示连接已经建立,可以接受数据。 2:相当于常量EventSource.CLOSED,表示连接已断,且不会重连。
- 连接一旦建立,就会触发open事件,可以在onopen属性定义回调函数。
source.onopen = function (event) { // ... }; // 另一种写法 source.addEventListener('open', function (event) { // ... }, false);
- 客户端收到服务器发来的数据,就会触发message事件,可以在onmessage属性的回调函数。
source.onmessage = function (event) { var data = event.data; // handle message }; // 另一种写法 source.addEventListener('message', function (event) { var data = event.data; // handle message }, false); 上面代码中,事件对象的data属性就是服务器端传回的数据(文本格式)。
- 如果发生通信错误(比如连接中断),就会触发error事件,可以在onerror属性定义回调函数。
source.onerror = function (event) { // handle error event }; // 另一种写法 source.addEventListener('error', function (event) { // handle error event }, false);
- close方法用于关闭 SSE 连接。
source.close();
自定义事件:默认情况下,服务器发来的数据,总是触发浏览器EventSource实例的message事件。开发者还可以自定义 SSE 事件,这种情况下,发送回来的数据不会触发message事件。
source.addEventListener('foo', function (event) { var data = event.data; // handle message }, false);
3.3,案例
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> <script> var eventSource; function start() { eventSource = new EventSource("HelloServlet"); eventSource.onmessage = function(event) { document.getElementById("foo").innerHTML = event.data; }; eventSource.addEventListener("ms",function(){}) } function close(){ eventSource.close(); } </script> </head> <body> Time: <span id="foo"></span> <br><br> <button onclick="start()">Start</button> <button onclick="close()">Close</button> </body> </html>
import java.io.IOException; import java.io.PrintWriter; import java.util.Date; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet("/HelloServlet") public class HelloServlet extends HttpServlet { private static final long serialVersionUID = 1L; public HelloServlet() { super(); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // ContentType 必须指定为 text/event-stream response.setContentType("text/event-stream"); // CharacterEncoding 必须指定为 UTF-8 response.setCharacterEncoding("UTF-8"); PrintWriter pw = response.getWriter(); // 每次发送的消息必须以\n\n结束 pw.write("event:ms\n data: " + new Date() + " 这是第1次测试\n\n"); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } pw.close(); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }
4,WebSocket
4.1,基本原理
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。 在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
https://shao12138.blog.csdn.net/article/details/112789716#t5
https://shao12138.blog.csdn.net/article/details/112789716#t5【通信原理和机制】既然是基于浏览器端的web技术,那么它的通信肯定少不了http,websocket本身虽然也是一种新的应用层协议,但是它也不能够脱离http而单独存在。具体来讲,我们在客户端构建一个websocket实例,并且为它绑定一个需要连接到的服务器地址,当客户端连接服务端的时候,会向服务端发送一个类似下面的http报文:
可以看到,这是一个http get请求报文,注意该报文中有一个upgrade首部,它的作用是告诉服务端需要将通信协议切换到websocket,如果服务端支持websocket协议,那么它就会将自己的通信协议切换到websocket,同时发给客户端类似于以下的一个响应报文头:
返回的状态码为101,表示同意客户端协议转换请求,并将它转换为websocket协议。以上过程都是利用http通信完成的,称之为websocket协议握手(websocket Protocol handshake),进过这握手之后,客户端和服务端就建立了websocket连接,以后的通信走的都是websocket协议了。所以总结为websocket握手需要借助于http协议,建立连接后通信过程使用websocket协议。同时需要了解的是,该websocket连接还是基于我们刚才发起http连接的那个TCP连接。一旦建立连接之后,我们就可以进行数据传输了,websocket提供两种数据传输:文本数据和二进制数据。
4.2,函数定义
服务端:javax.websocket
客户端:
- 构造函数:通过WebSocket构造函数创建一个WebSocket实例,接收一个请求地址作为参数,此时就已经向服务端发起请求了。
var Socket = new WebSocket(url, [protocol] );
- readyState:获取该实例对象当前的状态,有四种返回值:
var wss = new WebSocket.Server({ port: 8181 }); console.log(wss.readyState); //0 0:正在连接 1:连接成功,可以进行通信 2:正在关闭连接 3、已经关闭连接,或者打开连接失败
- onopen:onopen属性用来指定连接成功之后的回调函数。这里如果要是指定多个回调函数,需要使用addEventListener方法。
ws.addEventListener("open", function(event) { console.log("client:打开连接"); }); ws.addEventListener("open", function() { ws.send("我在另外一个回调中发送消息"); });
- onclose:和onopen一样的使用,用来指定关闭连接的回调。
- onmessage:指定接收到服务器数据后的回调,可以在回调中通过参数.data获取到返回的数据。
- onerror:指定发生错误时的回调。
- send:用来发送数据,不仅仅是普通字符串文本,也可以是其他类型的数据(比如ArrayBuffer )。
- bufferedAmount:可以获取当前还有多少数据没有发出去,用来判断是否发送结束。
if(ws.bufferedAmount === 0){ console.log("发送完毕"); }else{ console.log("还有", ws.bufferedAmount, "数据没有发送"); }
4.3,案例
<!DOCTYPE HTML> <html> <head> <title>Java后端WebSocket的Tomcat实现</title> <script type="text/javascript"> var websocket = null; var host = document.location.host; function connect() { //判断当前浏览器是否支持WebSocket if ('WebSocket' in window) { var value = document.getElementById("b").value; websocket = new WebSocket("ws://" + host + "/exam5/websocket/" + value); //连接发生错误的回调方法 websocket.onerror = function() { setMessageInnerHTML("WebSocket连接发生错误"); }; //连接成功建立的回调方法 websocket.onopen = function() { setMessageInnerHTML("WebSocket连接成功"); } //接收到消息的回调方法 websocket.onmessage = function(event) { setMessageInnerHTML(event.data); } //连接关闭的回调方法 websocket.onclose = function() { setMessageInnerHTML("WebSocket连接关闭"); } //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。 window.onbeforeunload = function() { closeWebSocket(); } } else { alert('当前浏览器 Not support websocket') } } //将消息显示在网页上 function setMessageInnerHTML(innerHTML) { document.getElementById('message').innerHTML += innerHTML + '<br/>'; } //关闭WebSocket连接 function closeWebSocket() { websocket.close(); } //发送消息 function send() { var message = document.getElementById('text').value; websocket.send(message); } </script> </head> <body> Welcome <br /> <input id="text" type="text" /> <input type="button" onclick="send()" value="发送消息"/> <br /> <input id="b" type="text" /> <!-- 这里用于注册不同的clientId, 多个webSocket客户端只能同步收到相同clientId的消息 --> <input type="button" onclick="connect()" value="连接"/> <hr /> <input type="button" onclick="closeWebSocket()" value="关闭WebSocket连接"/> <hr /> <div id="message"></div> </body> </html>
import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; /** * @ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端, * 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端 */ @ServerEndpoint("/websocket/{clientId}") public class WebSocket { // 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。 private static AtomicInteger onlineCount = new AtomicInteger(0); // concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。 //若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识 // private static CopyOnWriteArraySet<WebSocket> webSocketSet = new // CopyOnWriteArraySet<WebSocket>(); // 与某个客户端的连接会话,需要通过它来给客户端发送数据 //记录每个客户端的实例变量, 现在拿下面的全局map记录 //private Session session; private static Map<String, Session> webSocketMap = new ConcurrentHashMap<String, Session>(); /** * 连接建立成功调用的方法 * * @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据 */ @OnOpen public void onOpen(@PathParam("clientId") String clientId, Session session) { // 用登录用户编号和sessionId的拼接来做webSocket通信的唯一标识 String key = getWebSocketMapKey(clientId, session); webSocketMap.put(key, session); addOnlineCount(); // 在线数加1 System.out.println("WebSocket有新连接加入!当前在线人数为" + getOnlineCount()); } /** * 连接关闭调用的方法 */ @OnClose public void onClose(@PathParam("clientId") String clientId, Session session, CloseReason closeReason) { String key = getWebSocketMapKey(clientId, session); webSocketMap.remove(key, session); subOnlineCount(); // 在线数减1 System.out.println("WebSocket有一连接关闭!当前在线人数为" + getOnlineCount()); } /** * 收到客户端消息后调用的方法 * * @param message 客户端发送过来的消息 * @param session 可选的参数 */ @OnMessage public void onMessage(@PathParam("clientId") String clientId, String message, Session session) { System.out.println("WebSocket收到来自客户端的消息:" + message); sendMessageByClientId(clientId, message); } /** * 获取webSocketMap集合的Key * * @param clientId 用户编号 * @param session webSocket的Session * @return */ private String getWebSocketMapKey(String clientId, Session session) { if (clientId==null) { return session.getId(); } else { return clientId + "_" + session.getId(); } } /** * 发生错误时调用 * * @param session * @param error */ @OnError public void onError(Session session, Throwable error) { System.out.println("WebSocket发生错误"); } // 群发消息 public static void doSend(String message) { if (webSocketMap.size() > 0) { for (Map.Entry<String, Session> entry : webSocketMap.entrySet()) { try { sendMessage(entry.getValue(), message); } catch (IOException e) { System.out.println("WebSocket doSend is error:"); continue; } } } } public static void sendMessage(Session session, String message) throws IOException { session.getBasicRemote().sendText(message); } public static int sendMessageByClientIdList(List<String> clientIdList, String message) { int status = 0; for (String clientId : clientIdList) { status = sendMessageByClientId(clientId, message); } return status; } /** * 通过用户的编号来发送webSocket消息 * * @param clientId * @param message */ public static int sendMessageByClientId(String clientId, String message) { int status = 0; if (webSocketMap.size() > 0) { for (Map.Entry<String, Session> entry : webSocketMap.entrySet()) { try { String key = entry.getKey(); // 判断webSocketMap中的clientId和发送的clientId是否相同 // 若相同则进行发送消息 String key1 = key.substring(0, key.lastIndexOf("_")); if (key1.equals(clientId)) { sendMessage(entry.getValue(), message); status = 200; } } catch (IOException e) { System.out.println("WebSocket doSend is error:"); continue; } } } return status; } public static void sendSpeechMessageByClientId(String clientId, String message) { if (webSocketMap.size() > 0) { for (Map.Entry<String, Session> entry : webSocketMap.entrySet()) { try { String key = entry.getKey(); // 判断webSocketMap中的clientId和发送的clientId是否相同 // 若相同则进行发送消息 String key1 = key.substring(0, key.lastIndexOf("_")); if (key1.equals(clientId)) { sendMessage(entry.getValue(), message); } } catch (IOException e) { System.out.println("WebSocket doSend is error:"); continue; } } } } public static synchronized AtomicInteger getOnlineCount() { return onlineCount; } public static synchronized void addOnlineCount() { WebSocket.onlineCount.getAndIncrement(); } public static synchronized void subOnlineCount() { WebSocket.onlineCount.getAndDecrement(); } }