作为下一代的 Web标准,HTML5 拥有许多引人注目的新特性,如 Canvas、本地存储、多媒体编程接口、WebSocket等等。这其中有“Web的 TCP ”之称的 WebSocket格外吸引开发人员的注意。WebSocket的出现使得浏览器提供对 Socket的支持成为可能,从而在浏览器和服务器之间提供了一个基于 TCP连接的双向通道。WebSocket提供了一个受欢迎的技术,以替代我们过去几年一直在用的Ajax技术。Web开发人员可以非常方便地使用 WebSocket构建实时 web 应用,开发人员的手中从此又多了一柄神兵利器。本文介绍了 HTML5 WebSocket的由来,运作机制及客户端和服务端的 API实现,重点介绍服务端(基于 Tomcat7)及客户端(基于浏览器原生 HTML5 API)实现的详细步骤;并通过实际客户案例描述了客户端如何在 WebSocket 架构下使用 HTTP长连接与服务器实现实时通信及消息推送的功能,读者通过阅读本文中案例示例代码的实现,能够更深刻理解 WebSocket框架的技术原理和开发方法。
Web 应用的信息交互过程通常是客户端通过浏览器发出一个请求,服务器端接收和审核完请求后进行处理并返回结果给客户端,然后客户端浏览器将信息呈现出来,这种机制对于信息变化不是特别频繁的应用尚能相安无事,但是对于那些实时要求比较高的应用来说,比如说在线游戏、在线证券、设备监控、新闻在线播报、RSS订阅推送等等,当客户端浏览器准备呈现这些信息的时候,这些信息在服务器端可能已经过时了。所以保持客户端和服务器端的信息同步是实时 Web应用的关键要素,对 Web开发人员来说也是一个难题。在 WebSocket规范出来之前,开发人员想实现这些实时的 Web应用,不得不采用一些折衷的方案,其中最常用的就是轮询 (Polling)和 Comet 技术,而 Comet 技术实际上是轮询技术的改进,又可细分为两种实现方式,一种是长轮询机制,一种称为流技术。下面我们简单介绍一下这几种技术:
轮询:
这是最早的一种实现实时 Web应用的方案。客户端以一定的时间间隔向服务端发出请求,以频繁请求的方式来保持客户端和服务器端的同步。这种同步方案的最大问题是,当客户端以固定频率向服务器发起请求的时候,服务器端的数据可能并没有更新,这样会带来很多无谓的网络传输,所以这是一种非常低效的实时方案。
长轮询:
长轮询是对定时轮询的改进和提高,目地是为了降低无效的网络传输。当服务器端没有数据更新的时候,连接会保持一段时间周期直到数据或状态改变或者时间过期,通过这种机制来减少无效的客户端和服务器间的交互。当然,如果服务端的数据变更非常频繁的话,这种机制和定时轮询比较起来没有本质上的性能的提高。
流:
流技术方案通常就是在客户端的页面使用一个隐藏的窗口向服务端发出一个长连接的请求。服务器端接到这个请求后作出回应并不断更新连接状态以保证客户端和服务器端的连接不过期。通过这种机制可以将服务器端的信息源源不断地推向客户端。这种机制在用户体验上有一点问题,需要针对不同的浏览器设计不同的方案来改进用户体验,同时这种机制在并发比较大的情况下,对服务器端的资源是一个极大的考验。
综合这几种方案,您会发现这些目前我们所使用的所谓的实时技术并不是真正的实时技术,它们只是在用 Ajax方式来模拟实时的效果,在每次客户端和服务器端交互的时候都是一次 HTTP的请求和应答的过程,而每一次的 HTTP请求和应答都带有完整的 HTTP头信息,这就增加了每次传输的数据量,而且这些方案中客户端和服务器端的编程实现都比较复杂,在实际的应用中,为了模拟比较真实的实时效果,开发人员往往需要构造两个 HTTP连接来模拟客户端和服务器之间的双向通讯,一个连接用来处理客户端到服务器端的数据传输,一个连接用来处理服务器端到客户端的数据传输,这不可避免地增加了编程实现的复杂度,也增加了服务器端的负载,制约了应用系统的扩展性。
HTML5 WebSocket设计出来的目的就是要取代轮询和 Comet技术,使客户端浏览器具备像 C/S架构下桌面系统的实时通讯能力。浏览器通过 JavaScript向服务器发出建立 WebSocket连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP连接直接交换数据。因为 WebSocket连接本质上就是一个 TCP连接,所以在数据传输的稳定性和数据传输量的大小方面,和轮询以及 Comet技术比较,具有很大的性能优势。
WebSocket 协议本质上是一个基于 TCP的协议。为了建立一个 WebSocket连接,客户端浏览器首先要向服务器发起一个 HTTP请求,这个请求和通常的 HTTP请求不同,包含了一些附加头信息,其中附加头信息”Upgrade: WebSocket”表明这是一个申请协议升级的 HTTP请求,服务器端解析这些附加的头信息然后产生应答信息返回给客户端,客户端和服务器端的 WebSocket连接就建立起来了,双方就可以通过这个连接通道自由的传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接。下面是一个简单的建立握手的时序图:
这里简单说明一下WebSocket握手的过程。
当Web应用程序调用new WebSocket(url)接口时,Browser就开始了与地址为url的WebServer建立握手连接的过程。
1) Browser与WebSocket服务器通过TCP三次握手建立连接,如果这个建立连接失败,那么后面的过程就不会执行,Web应用程序将收到错误消息通知。
2) 在TCP建立连接成功后,Browser/UA通过http协议传送WebSocket支持的版本号,协议的字版本号,原始地址,主机地址等等一些列字段给服务器端。这很是有些类似于http的头信息,同样每行都是以”\r\n”结尾的,这段格式无需我们去构造,WebSocket对象会自动发送,对客户端这是透明的。
例如:
GET /chatHTTP/1.1
Host:server.example.com
Upgrade:websocket
Connection:Upgrade
Sec-WebSocket-Key:dGhlIHNhbXBsZSBub25jZQ==
Origin:http://example.com
Sec-WebSocket-Protocol:chat,superchat
Sec-WebSocket-Version:13
3) WebSocket服务器收到Browser/UA发送来的握手请求后,如果数据包数据和格式正确,客户端和服务器端的协议版本号匹配等等,就接受本次握手连接,并给出相应的数据回复,同样回复的数据包也是采用http协议传输。从这里我们太容易看出来,websocket协议的握手部分根本就是个类http的协议,所不同的是http每次都会有这样子的头信息交互,这在某些时候不得不显得很糟糕。而websocket只会执行一次这个过程,之后的传输信息就变得异常简洁了。
HTTP/1.1 101Switching Protocols
Upgrade:websocket
Connection:Upgrade
Sec-WebSocket-Accept:s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol:chat
客户端会发送一个“Sec-WebSocket-Key”的base64编码的密钥,要求服务端必须返回一个“Sec-WebSocket-Accept”,否则客户端会抛出一个“Error during WebSocket handshake: Sec-WebSocket-Acceptmismatch”错误之后,关闭连接,当然,这个Sec-WebSocket-Accept的值是计算出来的,胡乱的返回也是要遭到历史唾弃的。Sec-WebSocket-Accept的算法很简单:将客户端提交的Sec-WebSocket-Key+”258EAFA5-E914-47DA-95CA-C5AB0DC85B11″,然后sha1,然后base64_encode,以下列出我用nodejs写的代码片段:
sha1 = crypto.createHash('sha1'); sha1.update(headers["Sec-WebSocket-Key"]+"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); ws_accept=sha1.digest('base64'); |
4) Browser收到服务器回复的数据包后,如果数据包内容、格式都没有问题的话,就表示本次连接成功,触发onopen消息,此时Web开发者就可以在此时通过send接口想服务器发送数据。否则,握手连接失败,Web应用程序会收到onerror消息,并且能知道连接失败的原因。客户端的WebSocket对象一共绑定了四个事件:1、onopen:连接建立时触发;2、onmessage:收到服务端消息时触发;3、onerror:连接出错时触发;4、onclose:连接关闭时触发;有了这4个事件,我们就可以很容易很轻松的驾驭websocket,并且需要说明的是websocket支持二进制数据的传输.
WebSocket与http协议一样都是基于TCP的,所以他们都是可靠的协议,Web开发者调用的WebSocket的send函数在browser的实现中最终都是通过TCP的系统接口进行传输的。WebSocket和Http协议一样都属于应用层的协议,那么他们之间有没有什么关系呢?答案是肯定的,WebSocket在建立握手连接时,数据是通过http协议传输的,正如我们上一节所看到的“GET/chatHTTP/1.1”,这里面用到的只是http协议一些简单的字段。但是在建立连接之后,真正的数据传输阶段是不需要http协议参与的。具体关系可以参考下图:
如上文所述,WebSocket的实现分为客户端和服务端两部分,客户端(通常为浏览器)发出 WebSocket连接请求,服务端响应,实现类似 TCP握手的动作,从而在浏览器客户端和 WebSocket服务端之间形成一条 HTTP长连接快速通道。两者之间后续进行直接的数据互相传送,不再需要发起连接和相应。
以下简要描述 WebSocket服务端 API 及客户端 API。
WebSocket 服务端在各个主流应用服务器厂商中已基本获得符合 JEE JSR356标准规范 API 的支持(详见JSR356 WebSocket API规范),以下列举了部分常见的商用及开源应用服务器对 WebSocket Server端的支持情况:
表 1.WebSocket 服务端支持
厂商 |
应用服务器 |
备注 |
IBM |
WebSphere |
WebSphere 8.0 以上版本支持,7.X之前版本结合 MQTT支持类似的 HTTP 长连接 |
甲骨文 |
WebLogic |
WebLogic 12c 支持,11g及 10g 版本通过 HTTP Publish 支持类似的 HTTP 长连接 |
微软 |
IIS |
IIS 7.0+支持 |
Apache |
Tomcat |
Tomcat 7.0.5+支持,7.0.2X及 7.0.3X 通过自定义 API 支持 |
Jetty |
Jetty 7.0+支持 |
以下我们使用 Tomcat7.0.5版本的服务端示例代码说明 WebSocket服务端的实现:
JSR356 的 WebSocket规范使用javax.websocket.*的 API,可以将一个普通 Java对象(POJO)使用 @ServerEndpoint注释作为 WebSocket服务器的端点,代码示例如下:
清单 3.WebSocket 服务端 API 示例
@ServerEndpoint("/echo")
public class EchoEndpoint {
@OnOpen
public void onOpen(Session session) throws IOException {
//以下代码省略...
}
@OnMessage
public String onMessage(String message) {
//以下代码省略...
}
@Message(maxMessageSize=6)
public void receiveMessage(String s) {
//以下代码省略...
}
@OnError
public void onError(Throwable t) {
//以下代码省略...
}
@OnClose
public void onClose(Session session, CloseReason reason) {
//以下代码省略...
}
}
代码解释:
上文的简洁代码即建立了一个 WebSocket的服务端,@ServerEndpoint("/echo")的 annotation注释端点表示将 WebSocket服务端运行在 ws://[Server端 IP 或域名]:[Server 端口]/websockets/echo的访问端点,客户端浏览器已经可以对 WebSocket客户端 API 发起 HTTP 长连接了。
使用 ServerEndpoint注释的类必须有一个公共的无参数构造函数,@onMessage注解的 Java 方法用于接收传入的 WebSocket信息,这个信息可以是文本格式,也可以是二进制格式。
OnOpen 在这个端点一个新的连接建立时被调用。参数提供了连接的另一端的更多细节。Session表明两个 WebSocket端点对话连接的另一端,可以理解为类似 HTTPSession的概念。
OnClose 在连接被终止时调用。参数 closeReason可封装更多细节,如为什么一个 WebSocket连接关闭。
更高级的定制如 @Message注释,MaxMessageSize属性可以被用来定义消息字节最大限制,在示例程序中,如果超过 6个字节的信息被接收,就报告错误和连接关闭。
注意:早期不同应用服务器支持的 WebSocket方式不尽相同,即使同一厂商,不同版本也有细微差别,如 Tomcat服务器 7.0.5 以上的版本都是标准 JSR356 规范实现,而 7.0.2x/7.0.3X的版本使用自定义 API(WebSocketServlet和 StreamInbound,前者是一个容器,用来初始化 WebSocket环境;后者是用来具体处理 WebSocket请求和响应,详见案例分析部分),且 Tomcat7.0.3x与 7.0.2x 的createWebSocketInbound方法的定义不同,增加了一个HttpServletRequest参数,使得可以从 request参数中获取更多 WebSocket客户端的信息,如下代码所示:
清单 4.Tomcat7.0.3X 版本 WebSocket API
public class EchoServlet extends WebSocketServlet {
@Override
protected StreamInbound createWebSocketInbound(String subProtocol,
HttpServletRequest request) {
//以下代码省略....
return new MessageInbound() {
//以下代码省略....
}
protected void onBinaryMessage(ByteBuffer buffer)
throws IOException {
//以下代码省略...
}
protected void onTextMessage(CharBuffer buffer) throws IOException {
getWsOutbound().writeTextMessage(buffer);
//以下代码省略...
}
};
}
}
因此选择 WebSocket 的 Server 端重点需要选择其版本,通常情况下,更新的版本对 WebSocket的支持是标准 JSR 规范 API,但也要考虑开发易用性及老版本程序移植性等方面的问题,如下文所述的客户案例,就是因为客户要求统一应用服务器版本所以使用的 Tomcat 7.0.3X版本的 WebSocketServlet实现,而不是 JSR356的 @ServerEndpoint注释端点。
对于 WebSocket 客户端,主流的浏览器(包括 PC 和移动终端)现已都支持标准的 HTML5的 WebSocket API,这意味着客户端的 WebSocket JavaScirpt脚本具备良好的一致性和跨平台特性,以下列举了常见的浏览器厂商对 WebSocket的支持情况:
浏览器 |
支持情况 |
Chrome |
Chrome version 4+支持 |
Firefox |
Firefox version 5+支持 |
IE |
IE version 10+支持 |
Safari |
IOS 5+支持 |
Android Brower |
Android 4.5+支持 |
客户端 WebSocket API基本上已经在各个主流浏览器厂商中实现了统一,因此使用标准 HTML5定义的 WebSocket客户端的 JavaScript API即可,当然也可以使用业界满足 WebSocket标准规范的开源框架,如 Socket.io。
以下以一段代码示例说明 WebSocket的客户端实现:
var ws = new WebSocket(“ws://echo.websocket.org”);
ws.onopen = function(){ws.send(“Test!”); };
ws.onmessage = function(evt){console.log(evt.data);ws.close();};
ws.onclose = function(evt){console.log(“WebSocketClosed!”);};
ws.onerror = function(evt){console.log(“WebSocketError!”);};
第一行代码是在申请一个 WebSocket对象,参数是需要连接的服务器端的地址,同 HTTP协议开头一样,WebSocket协议的 URL 使用 ws://开头,另外安全的 WebSocket协议使用 wss://开头。
第二行到第五行为 WebSocket对象注册消息的处理函数,WebSocket对象一共支持四个消息 onopen,onmessage, onclose和 onerror,有了这 4个事件,我们就可以很容易很轻松的驾驭 WebSocket。
当 Browser 和 WebSocketServer 连接成功后,会触发 onopen 消息;如果连接失败,发送、接收数据失败或者处理数据出现错误,browser会触发 onerror消息;当 Browser接收到 WebSocketServer发送过来的数据时,就会触发 onmessage消息,参数 evt 中包含 Server 传输过来的数据;当 Browser 接收到WebSocketServer端发送的关闭连接请求时,就会触发 onclose消息。我们可以看出所有的操作都是采用异步回调的方式触发,这样不会阻塞 UI,可以获得更快的响应时间,更好的用户体验。
以下我们以一个真实的客户案例来分析说明 WebSocket的优势及具体开发实现(为保护客户隐私,以下描述省去客户名,具体涉及业务细节的代码在文中不再累述)。
该客户为一个移动设备制造商,移动设备装载的是 Android/IOS操作系统,设备分两类(以下简称 A,B两类),A 类设备随时处于移动状态中,B 类设备为 A 类设备的管理控制设备,客户需要随时在 B类设备中看到所属 A 类设备的地理位置信息及状态信息。如 A类设备上线,离线的时候,B类设备需要立即获得消息通知,A类设备上报时,B 类设备也需要实时获得该上报 A 类设备的地理位置信息。
为降低跨平台的难度及实施工作量,客户考虑轻量级的 Web App的方式屏蔽 Android/IOS平台的差异性,A 类设备数量众多,且在工作状态下 A 类设备处于不定时的移动状态,而 B 类设备对 A 类设备状态变化的感知实时性要求很高(秒级)。
根据以上需求,A/B 类设备信息存放在后台数据库中,A/B类设备的交互涉及 Web客户端/服务器频繁和高并发的请求-相应,如果使用传统的 HTTP请求-响应模式,B类设备的 Web App上需要对服务进行轮询,势必会对服务器带来大的负载压力,且当 A类设备没有上线或者上报等活动事件时,B类设备的轮询严重浪费网络资源。
综上所述,项目采用 WebSocket技术实现实时消息的通知及推送,每当 A类设备/B 类设备上线登录成功即打开 WebSocket的 HTTP 长连接,新的 A 类设备上线,位置变化,离线等状态变化通过 WebSocket发送实时消息,WebSocket Server端处理 A 类设备的实时消息,并向所从属的 B 类设备实时推送。
WebSocket 客户端使用 jQuery Mobile(jQuery Mobile移动端开发在本文中不再详细描述,感兴趣的读者可以参考jQuery Mobile简介),使用原生 WebSocket API实现与服务端交互。
服务端沿用客户已有的应用服务器 Tomcat 7.0.33版本,使用 Apache自定义 API 实现 WebSocket Server端,为一个上线的 A 类设备生成一个 WebSocket 的 HTTP 长连接,每当 A类设备有上线,位置更新,离线等事件的时候,客户端发送文本消息,服务端识别并处理后,向所属 B类设备发送实时消息,B类设备客户端接收消息后,识别到 A类设备的相应事件,完成对应的 A类设备位置刷新以及其他业务操作。
其涉及的 A 类设备,B类设备及后台服务器交互时序图如下:
图 3:A/B 类设备 WebSocket 交互图
A/B 类设备的 WebSocket客户端封装在 websocket.js的 JavaScript代码中,与 jQuery MobileApp一同打包为移动端 apk/ipa安装包;WebSocket服务端实现主要为WebSocketDeviceServlet.java, WebSocketDeviceInbound.java,WebSocketDeviceInboundPool.java几个类。下文我们一一介绍其具体代码实现。
在下文中我们把本案例中的主要代码实现做解释说明,读者可以下载完整的代码清单做详细了解。
WebSocketDeviceServlet 类
A 类设备或者 B类设备发起 WebSocket长连接后,服务端接受请求的是 WebSocketDeviceServlet类,跟传统 HttpServlet不同的是,WebSocketDeviceServlet类实现 createWebSocketInbound方法,类似 SocketServer的 accept 方法,新生产的 WebSocketInbound实例对应客户端 HTTP长连接,处理与客户端交互功能。
WebSocketDeviceServlet 服务端代码示例如下:
清单 6.WebSocketDeviceServlet.java 代码示例
public class WebSocketDeviceServlet extends org.apache.catalina.websocket.WebSocketServlet {
private static final long serialVersionUID = 1L;
@Override
protected StreamInbound createWebSocketInbound(String subProtocol,HttpServletRequest request) {
WebSocketDeviceInbound newClientConn = new WebSocketDeviceInbound(request);
WebSocketDeviceInboundPool.addMessageInbound(newClientConn);
return newClientConn;
}
}
代码解释:
WebSocketServlet 是 WebSocket协议的后台监听进程,和传统 HTTP请求一样,WebSocketServlet类似 Spring/Struct中的 Servlet 监听进程,只不过通过客户端 ws 的前缀指定了其监听的协议为 WebSocket。
WebSocketDeviceInboundPool实现了类似 JDBC数据库连接池的客户端 WebSocket连接池功能,并统一处理 WebSocket服务端对单个客户端/多个客户端(同组 A类设备)的消息推送,详见 WebSocketDeviceInboundPool代码类解释。
WebSocketDeviceInboundl 类
WebSocketDeviceInbound 类为每个 A类和 B 类设备验证登录后,客户端建立的 HTTP长连接的对应后台服务类,类似 Socket编程中的 SocketServer accept后的 Socket 进程,在 WebSocketInbound中接收客户端发送的实时位置信息等消息,并向客户端(B类设备)发送下属 A 类设备实时位置信息及位置分析结果数据,输入流和输出流都是 WebSocket协议定制的。WsOutbound负责输出结果,StreamInbound和 WsInputStream负责接收数据:
清单 7.WebSocketDeviceInbound.java 类代码示例
public class WebSocketDeviceInbound extends MessageInbound {
private final HttpServletRequest request;
private DeviceAccount connectedDevice;
public DeviceAccount getConnectedDevice() {
return connectedDevice;
}
public void setConnectedDevice(DeviceAccount connectedDevice) {
this.connectedDevice = connectedDevice;
}
public HttpServletRequest getRequest() {
return request;
}
public WebSocketDeviceInbound(HttpServletRequest request) {
this.request = request;
DeviceAccount connectedDa = (DeviceAccount)request.getSession(true).getAttribute("connectedDevice");
if(connectedDa==null)
{
String deviceId = request.getParameter("id");
DeviceAccountDao deviceDao = new DeviceAccountDao();
connectedDa = deviceDao.getDaById(Integer.parseInt(deviceId));
}
this.setConnectedDevice(connectedDa);
}
@Override
protected void onOpen(WsOutbound outbound) {
/
}
@Override
protected void onClose(int status) {
WebSocketDeviceInboundPool.removeMessageInbound(this);
}
@Override
protected void onBinaryMessage(ByteBuffer message) throws IOException {
throw new UnsupportedOperationException("Binary message not supported.");
}
@Override
protected void onTextMessage(CharBuffer message) throws IOException {
WebSocketDeviceInboundPool.processTextMessage(this, message.toString());
}
public void sendMessage(BaseEvent event)
{
String eventStr = JSON.toJSONString(event);
try {
this.getWsOutbound().writeTextMessage(CharBuffer.wrap(eventStr));
//…以下代码省略
} catch (IOException e) {
e.printStackTrace();
}
}
}
代码解释:
connectedDevice 是当前连接的 A/B类客户端设备类实例,在这里做为成员变量以便后续处理交互。
sendMessage 函数向客户端发送数据,使用 Websocket WsOutbound输出流向客户端推送数据,数据格式统一为 JSON。
onTextMessage 函数为客户端发送消息到服务器时触发事件,调用WebSocketDeviceInboundPool的processTextMessage统一处理 A 类设备的登入,更新位置,离线等消息。
onClose 函数触发关闭事件,在连接池中移除连接。
WebSocketDeviceInbound 构造函数为客户端建立连接后,WebSocketServlet的 createWebSocketInbound函数触发,查询 A 类/B 类设备在后台数据库的详细数据并实例化connectedDevice做为 WebSocketDeviceInbound的成员变量,WebSocketServlet类此时将新的WebSocketInbound实例加入自定义的WebSocketDeviceInboundPool连接池中,以便统一处理 A/B设备组员关系及位置分布信息计算等业务逻辑。
WebSocketDeviceInboundPool 类
WebSocketInboundPool 类:由于需要处理大量 A 类 B 类设备的实时消息,服务端会同时存在大量 HTTP长连接,为统一管理和有效利用 HTTP长连接资源,项目中使用了简单的 HashMap实现内存连接池机制,每次设备登入新建的 WebSocketInbound都放入 WebSocketInbound实例的连接池中,当设备登出时,从连接池中 remove对应的 WebSocketInbound实例。
此外,WebSocketInboundPool类还承担 WebSocket客户端处理 A 类和 B 类设备间消息传递的作用,在客户端发送 A类设备登入、登出及位置更新消息的时候,服务端 WebSocketInboundPool进行位置分布信息的计算,并将计算完的结果向同时在线的 B类设备推送。
清单 8.WebSocketDeviceInboundPool.java 代码示例
public class WebSocketDeviceInboundPool {
private static final ArrayListconnections =
new ArrayList();
public static void addMessageInbound(WebSocketDeviceInbound inbound){
//添加连接
DeviceAccount da = inbound.getConnectedDevice();
System.out.println("新上线设备 : " + da.getDeviceNm());
connections.add(inbound);
}
public static ArrayListgetOnlineDevices(){
ArrayListonlineDevices = new ArrayList ();
for(WebSocketDeviceInbound webClient:connections)
{
onlineDevices.add(webClient.getConnectedDevice());
}
return onlineDevices;
}
public static WebSocketDeviceInbound getGroupBDevices(String group){
WebSocketDeviceInbound retWebClient =null;
for(WebSocketDeviceInbound webClient:connections)
{
if(webClient.getConnectedDevice().getDeviceGroup().equals(group)&&
webClient.getConnectedDevice().getType().equals("B")){
retWebClient = webClient;
}
}
return retWebClient;
}
public static void removeMessageInbound(WebSocketDeviceInbound inbound){
//移除连接
System.out.println("设备离线 : " + inbound.getConnectedDevice());
connections.remove(inbound);
}
public static void processTextMessage(WebSocketDeviceInbound inbound,String message){
BaseEvent receiveEvent = (BaseEvent)JSON.parseObject(message.toString(),BaseEvent.class);
DBEventHandleImpl dbEventHandle = new DBEventHandleImpl();
dbEventHandle.setReceiveEvent(receiveEvent);
dbEventHandle.HandleEvent();
if(receiveEvent.getEventType()==EventConst.EVENT_MATCHMATIC_RESULT||
receiveEvent.getEventType()==EventConst.EVENT_GROUP_DEVICES_RESULT||
receiveEvent.getEventType()==EventConst.EVENT_A_REPAIRE){
String clientDeviceGroup = ((ArrayList)
receiveEvent.getEventObjs()).get(0).getDeviceGroup();
WebSocketDeviceInbound bClient = getGroupBDevices(clientDeviceGroup);
if(bClient!=null){
sendMessageToSingleClient(bClient,dbEventHandle.getReceiveEvent());
}
}
}
}
public static void sendMessageToAllDevices(BaseEvent event){
try {
for (WebSocketDeviceInbound webClient : connections) {
webClient.sendMessage(event);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void sendMessageToSingleClient(WebSocketDeviceInbound webClient,BaseEvent event){
try {
webClient.sendMessage(event);
}
catch (Exception e) {
e.printStackTrace();
}
}
}
代码解释:
addMessageInbound 函数向连接池中添加客户端建立好的连接。
getOnlineDevices 函数获取所有的连线的 A/B类设备。
removeMessageInbound 函数实现 A类设备或者 B 类设备离线退出(服务端收到客户端关闭 WebSocket连接事件,触发 WebSocketInbound中的 onClose 方法),从连接池中删除连接设备客户端的连接实例。
processTextMessage 完成处理客户端消息,这里使用了消息处理的机制,包括解码客户端消息,根据消息构造 Event事件,通过 EventHandle多线程处理,处理完后向客户端返回,可以向该组 B设备推送消息,也可以向发送消息的客户端推送消息。
sendMessageToAllDevices 函数实现发送数据给所有在线 A/B类设备客户端。sendMessageToSingleClient函数实现向某一 A/B类设备客户端发送数据。
客户端代码 websocket.js,客户端使用标准 HTML5定义的 WebSocket API,从而保证支持 IE9+,Chrome,FireFox等多种浏览器,并结合 jQueryJS库 API 处理 JSON 数据的处理及发送。
var websocket=window.WebSocket || window.MozWebSocket;
var isConnected = false;
function doOpen(){
isConnected = true;
if(deviceType=='B'){
mapArea='mapB';
doLoginB(mapArea);
}
else{
mapArea='mapA';
doLoginA(mapArea);
}
}
function doClose(){
showDiagMsg("infoField","已经断开连接", "infoDialog");
isConnected = false;
}
function doError() {
showDiagMsg("infoField","连接异常!", "infoDialog");
isConnected = false;
}
function doMessage(message){
var event = $.parseJSON(message.data);
doReciveEvent(event);
}
function doSend(message) {
if (websocket != null) {
websocket.send(JSON.stringify(message));
} else {
showDiagMsg("infoField","您已经掉线,无法与服务器通信!", "infoDialog");
}
}
//初始话 WebSocket
function initWebSocket(wcUrl) {
if (window.WebSocket) {
websocket = new WebSocket(encodeURI(wcUrl));
websocket.onopen = doOpen;
websocket.onerror = doError;
websocket.onclose = doClose;
websocket.onmessage = doMessage;
}
else{
showDiagMsg("infoField","您的设备不支持 webSocket!", "infoDialog");
}
};
function doReciveEvent(event){
//设备不存在,客户端断开连接
if(event.eventType==101){
showDiagMsg("infoField","设备不存在或设备号密码错!", "infoDialog");
websocket.close();
}
//返回组设备及计算目标位置信息,更新地图
else if(event.eventType==104||event.eventType==103){
clearGMapOverlays(mapB);
$.each(event.eventObjs,function(idx,item){
var deviceNm = item.deviceNm;
//google api
// var deviceLocale = new google.maps.LatLng(item.lag,item.lat);
//baidu api
var deviceLocale = new BMap.Point(item.lng,item.lat);
var newMarker;
if(item.status=='target'){
newMarker = addMarkToMap(mapB,deviceLocale,deviceNm,true);
//…以下代码省略
}
else{
newMarker = addMarkToMap(mapB,deviceLocale,deviceNm);
}
markArray.push(newMarker);
});
showDiagMsg("infoField","有新报修设备或设备离线, 地图已更新!", "infoDialog");
}
}
代码解释:
doOpen 回调函数处理打开 WebSocket,A类设备或者 B 类设备连接上 WebSocket 服务端后,将初始化地图并显示默认位置,然后向服务端发送设备登入的消息。
doReciveEvent 函数处理关闭 WebSocket,A类/B 类设备离线(退出移动终端上的应用)时,服务端关闭 HTTP长连接,客户端 WebSocket对象执行 onclose回调句柄。
initWebSocket 初始化 WebSocket,连接 WebSocket服务端,并设置处理回调句柄,如果浏览器版本过低而不支持 HTML5,提示客户设备不支持 WebSocket。
doSend 函数处理客户端向服务端发送消息,注意 message是 JSON OBJ 对象,通过 JSON 标准 API 格式化字符串。
doMessage 函数处理 WebSocket服务端返回的消息,后台返回的 message为 JSON 字符串,通过 jQuery 的 parseJSON API 格式化为 JSON Object 以便客户端处理 doReciveEvent函数时客户端收到服务端返回消息的具体处理,由于涉及大量业务逻辑在此不再赘述。
以上简要介绍了 WebSocket的由来,原理机制以及服务端/客户端实现,并以实际客户案例指导并讲解了如何使用 WebSocket解决实时响应及服务端消息推送方面的问题。本文适用于熟悉 HTML协议规范和 J2EE Web编程的读者,旨在帮助读者快速熟悉 HTML5WebSocket的原理和开发应用。文中的服务端及客户端项目代码可供下载,修改后可用于用户基于 WebSocket的 HTTP 长连接的实际生产环境中。
文中实例代码下载链接:http://download.csdn.net/detail/liuyez123/9402340