转载整理自https://www.zhihu.com/question/20215561
http://www.open-open.com/lib/view/open1435905714122.html
http://www.ibm.com/developerworks/cn/java/j-lo-WebSocket/
http://www.open-open.com/lib/view/open1413011727155.html
一、WebSocket是HTML5出的东西(协议),也就是说HTTP协议没有变化,或者说没关系,但HTTP是不支持持久连接的(长连接,循环连接的不算)
Tomcat:
J2EE下面用的最多的容器应该就是tomcat了。说到tomcat对WebSocket的支持,不得不先提一下,目前的WebSocket协议已经经过了好几代的演变,不同浏览器对此协议的支持程度也不同,因此,如果作为服务器,最理想的是支持尽可能多的WebSocket协议版本。
tomcat8真正支持jsr-356(包含对websocket的支持), tomcat7支持部分版本的websocket实现不兼容jsr-356。因此,能用tomcat8的话,还是尽量用。
代码实现相当简单,以下是一个列子,只需要tomcat8的基本库,不需要其他依赖。
import java.io.IOException; import javax.websocket.OnClose; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.ServerEndpoint; &#64;ServerEndpoint("/websocket") public class WebSocketTest { &#64;OnMessage public void onMessage(String message, Session session) throws IOException, InterruptedException { // Print the client message for testing purposes System.out.println("Received: " &#43; message); // Send the first message to the client session.getBasicRemote().sendText("This is the first server message"); // Send 3 messages to the client every 5 seconds int sentMessages &#61; 0; while (sentMessages <3) { Thread.sleep(5000); session.getBasicRemote().sendText("This is an intermediate server message. Count: " &#43; sentMessages); sentMessages&#43;&#43;; } // Send a final message to the client session.getBasicRemote().sendText("This is the last server message"); } &#64;OnOpen public void onOpen() { System.out.println("Client connected"); } &#64;OnClose public void onClose() { System.out.println("Connection closed"); } }
Jetty&#xff1a;
Jetty和Tomcat一样&#xff0c;也是一个Servlet的容器。如果说不同之处&#xff0c;那么最大的不同应该是Tomcat采用的是BIO处理方式&#xff0c;也就是说一个request会用一个线程去处理&#xff0c;即使是WebSocket这种长连接&#xff0c;也是会独立开一个线程。作为一个普通的Web服务器&#xff0c;tomcat可以轻松应对耗时比较短的Request/Response。但是如果换成是长连接的WebSocket&#xff0c;那麻烦就来了&#xff0c;对于上万用户的聊天和推送&#xff0c;总不能开上万个线程去处理吧。此时&#xff0c;Jetty的性能就体现出来了&#xff0c;Jetty采用的是NIO&#xff0c;一个线程可以处理多个WebSocket的长链接&#xff0c;如果你的需求是大量耗时比较长的request或者大量长连接&#xff0c;那么建议采用Jetty。
Jetty对WebSocket的实现有点绕&#xff0c;Servlet不再是继承原来的HttpServlet&#xff0c;而是继承WebSocketServlet。此处要注意导入jetty-util.jar和jetty-websocket.jar两个包&#xff0c;否则可能会有class not found错误。
ReverseAjaxServlet.java:
import java.io.IOException; import java.util.Date; import java.util.Random; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.codehaus.jettison.json.JSONArray; import org.eclipse.jetty.websocket.WebSocket; import org.eclipse.jetty.websocket.WebSocketServlet; /** * &#64;author Mathieu Carbou (mathieu.carbou&#64;gmail.com) */ public final class ReverseAjaxServlet extends WebSocketServlet { private final Endpoints endpoints &#61; new Endpoints(); private final Random random &#61; new Random(); private final Thread generator &#61; new Thread("Event generator") { &#64;Override public void run() { while (!Thread.currentThread().isInterrupted()) { try { Thread.sleep(random.nextInt(5000)); endpoints.broadcast(new JSONArray().put("At " &#43; new Date()).toString()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } }; &#64;Override public void init() throws ServletException { super.init(); generator.start(); } &#64;Override public void destroy() { generator.interrupt(); super.destroy(); } &#64;Override public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) { return endpoints.newEndpoint(); } &#64;Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().write("11111"); } }
package com.cn.test.chapter2.websocket; import org.eclipse.jetty.websocket.WebSocket; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; /** * &#64;author Mathieu Carbou (mathieu.carbou&#64;gmail.com) */ final class Endpoints { private final Queue
import java.io.IOException; import java.util.concurrent.ConcurrentLinkedQueue; import org.codehaus.jettison.json.JSONArray; import org.eclipse.jetty.websocket.WebSocket; /** * &#64;author Mathieu Carbou (mathieu.carbou&#64;gmail.com) */ class Endpoint implements WebSocket.OnTextMessage { protected Connection _connection; private Endpoints endpoints; private static int clientCounter &#61; 0; private int clientId &#61; clientCounter&#43;&#43;; public Endpoint(Endpoints endpoints) { this.setEndpoints(endpoints); } &#64;Override public void onClose(int code, String message) { System.out.println("Client disconnected"); this.endpoints.remove(this); } &#64;Override public void onOpen(Connection connection) { System.out.println("Client connected"); _connection &#61; connection; try { this._connection.sendMessage(new JSONArray().put("ClientID &#61; " &#43; clientId).toString()); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } endpoints.offer(this); } &#64;Override public void onMessage(final String data) { System.out.println("Received data: " &#43; data); this.endpoints.broadcast(data); } public Endpoints getEndpoints() { return endpoints; } public void setEndpoints(Endpoints endpoints) { this.endpoints &#61; endpoints; } }
辅助工具&#xff1a;
在编写服务器最麻烦的是要写对应的客户端来测试&#xff0c;还好Chrome为我们解决了这个问题。下载Chrome插件WebSocket Clinet可以轻松地和服务器建立连接&#xff0c;发送消息到服务器。
来自&#xff1a;http://blog.csdn.net/lrenjun/article/details/39934823
众所周知&#xff0c;Web 应用的交互过程通常是客户端通过浏览器发出一个请求&#xff0c;服务器端接收请求后进行处理并返回结果给客户端&#xff0c;客户端浏览器将信息呈现&#xff0c;这种机制对于信息变化不是特别频繁的应用尚可&#xff0c;但对于实时要求高、海量并发的应用来说显得捉襟见肘&#xff0c;尤其在当前业界移动互联网蓬勃发展的趋势下&#xff0c;高并发与用户实时响应是 Web 应用经常面临的问题&#xff0c;比如金融证券的实时信息&#xff0c;Web 导航应用中的地理位置获取&#xff0c;社交网络的实时消息推送等。
传统的请求-响应模式的 Web 开发在处理此类业务场景时&#xff0c;通常采用实时通讯方案&#xff0c;常见的是&#xff1a;
从上文可以看出&#xff0c;传统 Web 模式在处理高并发及实时性需求的时候&#xff0c;会遇到难以逾越的瓶颈&#xff0c;我们需要一种高效节能的双向通信机制来保证数据的实时传输。在此背景下&#xff0c;基于 HTML5 规范的、有 Web TCP 之称的 WebSocket 应运而生。
早期 HTML5 并没有形成业界统一的规范&#xff0c;各个浏览器和应用服务器厂商有着各异的类似实现&#xff0c;如 IBM 的 MQTT&#xff0c;Comet 开源框架等&#xff0c;直到 2014 年&#xff0c;HTML5 在 IBM、微软、Google 等巨头的推动和协作下终于尘埃落地&#xff0c;正式从草案落实为实际标准规范&#xff0c;各个应用服务器及浏览器厂商逐步开始统一&#xff0c;在 JavaEE7 中也实现了 WebSocket 协议&#xff0c;从而无论是客户端还是服务端的 WebSocket 都已完备&#xff0c;读者可以查阅HTML5 规范&#xff0c;熟悉新的 HTML 协议规范及 WebSocket 支持。
回页首
以下简要介绍一下 WebSocket 的原理及运行机制。
WebSocket 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信&#xff0c;能更好的节省服务器资源和带宽并达到实时通讯&#xff0c;它建立在 TCP 之上&#xff0c;同 HTTP 一样通过 TCP 来传输数据&#xff0c;但是它和 HTTP 最大不同是&#xff1a;
非 WebSocket 模式传统 HTTP 客户端与服务器的交互如下图所示&#xff1a;
使用 WebSocket 模式客户端与服务器的交互如下图&#xff1a;
上图对比可以看出&#xff0c;相对于传统 HTTP 每次请求-应答都需要客户端与服务端建立连接的模式&#xff0c;WebSocket 是类似 Socket 的 TCP 长连接的通讯模式&#xff0c;一旦 WebSocket 连接建立后&#xff0c;后续数据都以帧序列的形式传输。在客户端断开 WebSocket 连接或 Server 端断掉连接前&#xff0c;不需要客户端和服务端重新发起连接请求。在海量并发及客户端与服务器交互负载流量大的情况下&#xff0c;极大的节省了网络带宽资源的消耗&#xff0c;有明显的性能优势&#xff0c;且客户端发送和接受消息是在同一个持久连接上发起&#xff0c;实时性优势明显。
我们再通过客户端和服务端交互的报文看一下 WebSocket 通讯与传统 HTTP 的不同&#xff1a;
在客户端&#xff0c;new WebSocket 实例化一个新的 WebSocket 客户端对象&#xff0c;连接类似 ws://yourdomain:port/path 的服务端 WebSocket URL&#xff0c;WebSocket 客户端对象会自动解析并识别为 WebSocket 请求&#xff0c;从而连接服务端端口&#xff0c;执行双方握手过程&#xff0c;客户端发送数据格式类似&#xff1a;
GET /webfin/websocket/ HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: xqBt3ImNzJbYqRINxEFlkg&#61;&#61;
Origin: http://localhost
:8080
Sec-WebSocket-Version: 13
可以看到&#xff0c;客户端发起的 WebSocket 连接报文类似传统 HTTP 报文&#xff0c;”Upgrade&#xff1a;websocket”参数值表明这是 WebSocket 类型请求&#xff0c;“Sec-WebSocket-Key”是 WebSocket 客户端发送的一个 base64 编码的密文&#xff0c;要求服务端必须返回一个对应加密的“Sec-WebSocket-Accept”应答&#xff0c;否则客户端会抛出“Error during WebSocket handshake”错误&#xff0c;并关闭连接。
服务端收到报文后返回的数据格式类似&#xff1a;
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: K7DJLdLooIwIG/MOpvWFB3y3FE8&#61;
“Sec-WebSocket-Accept”的值是服务端采用与客户端一致的密钥计算出来后返回客户端的,“HTTP/1.1 101 Switching Protocols”表示服务端接受 WebSocket 协议的客户端连接&#xff0c;经过这样的请求-响应处理后&#xff0c;客户端服务端的 WebSocket 连接握手成功, 后续就可以进行 TCP 通讯了。读者可以查阅WebSocket 协议栈了解 WebSocket 客户端和服务端更详细的交互数据格式。
在开发方面&#xff0c;WebSocket API 也十分简单&#xff0c;我们只需要实例化 WebSocket&#xff0c;创建连接&#xff0c;然后服务端和客户端就可以相互发送和响应消息&#xff0c;在下文 WebSocket 实现及案例分析部分&#xff0c;可以看到详细的 WebSocket API 及代码实现。
回页首
如上文所述&#xff0c;WebSocket 的实现分为客户端和服务端两部分&#xff0c;客户端&#xff08;通常为浏览器&#xff09;发出 WebSocket 连接请求&#xff0c;服务端响应&#xff0c;实现类似 TCP 握手的动作&#xff0c;从而在浏览器客户端和 WebSocket 服务端之间形成一条 HTTP 长连接快速通道。两者之间后续进行直接的数据互相传送&#xff0c;不再需要发起连接和相应。
以下简要描述 WebSocket 服务端 API 及客户端 API。
WebSocket 服务端在各个主流应用服务器厂商中已基本获得符合 JEE JSR356 标准规范 API 的支持&#xff08;详见JSR356 WebSocket API 规范&#xff09;&#xff0c;以下列举了部分常见的商用及开源应用服务器对 WebSocket Server 端的支持情况&#xff1a;
厂商 | 应用服务器 | 备注 |
---|---|---|
IBM | WebSphere | WebSphere 8.0 以上版本支持&#xff0c;7.X 之前版本结合 MQTT 支持类似的 HTTP 长连接 |
甲骨文 | WebLogic | WebLogic 12c 支持&#xff0c;11g 及 10g 版本通过 HTTP Publish 支持类似的 HTTP 长连接 |
微软 | IIS | IIS 7.0&#43;支持 |
Apache | Tomcat | Tomcat 7.0.5&#xff0b;支持&#xff0c;7.0.2X 及 7.0.3X 通过自定义 API 支持 |
Jetty | Jetty 7.0&#xff0b;支持 |
以下我们使用 Tomcat7.0.5 版本的服务端示例代码说明 WebSocket 服务端的实现&#xff1a;
JSR356 的 WebSocket 规范使用 javax.websocket.*的 API&#xff0c;可以将一个普通 Java 对象&#xff08;POJO&#xff09;使用 &#64;ServerEndpoint 注释作为 WebSocket 服务器的端点&#xff0c;代码示例如下&#xff1a;
&#64;ServerEndpoint("/echo")public class EchoEndpoint {&#64;OnOpenpublic void onOpen(Session session) throws IOException {//以下代码省略...}&#64;OnMessagepublic String onMessage(String message) {//以下代码省略...}&#64;Message(maxMessageSize&#61;6)public void receiveMessage(String s) {//以下代码省略...} &#64;OnErrorpublic void onError(Throwable t) {//以下代码省略...}&#64;OnClosepublic void onClose(Session session, CloseReason reason) {//以下代码省略...} }
代码解释&#xff1a;
上文的简洁代码即建立了一个 WebSocket 的服务端&#xff0c;&#64;ServerEndpoint("/echo") 的 annotation 注释端点表示将 WebSocket 服务端运行在 ws://[Server 端 IP 或域名]:[Server 端口]/websockets/echo 的访问端点&#xff0c;客户端浏览器已经可以对 WebSocket 客户端 API 发起 HTTP 长连接了。
使用 ServerEndpoint 注释的类必须有一个公共的无参数构造函数&#xff0c;&#64;onMessage 注解的 Java 方法用于接收传入的 WebSocket 信息&#xff0c;这个信息可以是文本格式&#xff0c;也可以是二进制格式。
OnOpen 在这个端点一个新的连接建立时被调用。参数提供了连接的另一端的更多细节。Session 表明两个 WebSocket 端点对话连接的另一端&#xff0c;可以理解为类似 HTTPSession 的概念。
OnClose 在连接被终止时调用。参数 closeReason 可封装更多细节&#xff0c;如为什么一个 WebSocket 连接关闭。
更高级的定制如 &#64;Message 注释&#xff0c;MaxMessageSize 属性可以被用来定义消息字节最大限制&#xff0c;在示例程序中&#xff0c;如果超过 6 个字节的信息被接收&#xff0c;就报告错误和连接关闭。
注意&#xff1a;早期不同应用服务器支持的 WebSocket 方式不尽相同&#xff0c;即使同一厂商&#xff0c;不同版本也有细微差别&#xff0c;如 Tomcat 服务器 7.0.5 以上的版本都是标准 JSR356 规范实现&#xff0c;而 7.0.2x/7.0.3X 的版本使用自定义 API &#xff08;WebSocketServlet 和 StreamInbound&#xff0c; 前者是一个容器&#xff0c;用来初始化 WebSocket 环境&#xff1b;后者是用来具体处理 WebSocket 请求和响应&#xff0c;详见案例分析部分&#xff09;&#xff0c;且 Tomcat7.0.3x 与 7.0.2x 的 createWebSocketInbound 方法的定义不同&#xff0c;增加了一个 HttpServletRequest 参数&#xff0c;使得可以从 request 参数中获取更多 WebSocket 客户端的信息&#xff0c;如下代码所示&#xff1a;
public class EchoServlet extends WebSocketServlet {
&#64;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 端重点需要选择其版本&#xff0c;通常情况下&#xff0c;更新的版本对 WebSocket 的支持是标准 JSR 规范 API&#xff0c;但也要考虑开发易用性及老版本程序移植性等方面的问题&#xff0c;如下文所述的客户案例&#xff0c;就是因为客户要求统一应用服务器版本所以使用的 Tomcat 7.0.3X 版本的 WebSocketServlet 实现&#xff0c;而不是 JSR356 的 &#64;ServerEndpoint 注释端点。
对于 WebSocket 客户端&#xff0c;主流的浏览器&#xff08;包括 PC 和移动终端&#xff09;现已都支持标准的 HTML5 的 WebSocket API&#xff0c;这意味着客户端的 WebSocket JavaScirpt 脚本具备良好的一致性和跨平台特性&#xff0c;以下列举了常见的浏览器厂商对 WebSocket 的支持情况&#xff1a;
浏览器 | 支持情况 |
---|---|
Chrome | Chrome version 4&#43;支持 |
Firefox | Firefox version 5&#43;支持 |
IE | IE version 10&#43;支持 |
Safari | IOS 5&#43;支持 |
Android Brower | Android 4.5&#43;支持 |
客户端 WebSocket API 基本上已经在各个主流浏览器厂商中实现了统一&#xff0c;因此使用标准 HTML5 定义的 WebSocket 客户端的 Javascript API 即可&#xff0c;当然也可以使用业界满足 WebSocket 标准规范的开源框架&#xff0c;如 Socket.io。
以下以一段代码示例说明 WebSocket 的客户端实现&#xff1a;
var ws &#61; new WebSocket(“ws://echo.websocket.org”); ws.onopen &#61; function(){ws.send(“Test!”); }; ws.onmessage &#61; function(evt){console.log(evt.data);ws.close();}; ws.onclose &#61; function(evt){console.log(“WebSocketClosed!”);}; ws.onerror &#61; function(evt){console.log(“WebSocketError!”);};
第一行代码是在申请一个 WebSocket 对象&#xff0c;参数是需要连接的服务器端的地址&#xff0c;同 HTTP 协议开头一样&#xff0c;WebSocket 协议的 URL 使用 ws://开头&#xff0c;另外安全的 WebSocket 协议使用 wss://开头。
第二行到第五行为 WebSocket 对象注册消息的处理函数&#xff0c;WebSocket 对象一共支持四个消息 onopen, onmessage, onclose 和 onerror&#xff0c;有了这 4 个事件&#xff0c;我们就可以很容易很轻松的驾驭 WebSocket。
当 Browser 和 WebSocketServer 连接成功后&#xff0c;会触发 onopen 消息&#xff1b;如果连接失败&#xff0c;发送、接收数据失败或者处理数据出现错误&#xff0c;browser 会触发 onerror 消息&#xff1b;当 Browser 接收到 WebSocketServer 发送过来的数据时&#xff0c;就会触发 onmessage 消息&#xff0c;参数 evt 中包含 Server 传输过来的数据&#xff1b;当 Browser 接收到 WebSocketServer 端发送的关闭连接请求时&#xff0c;就会触发 onclose 消息。我们可以看出所有的操作都是采用异步回调的方式触发&#xff0c;这样不会阻塞 UI&#xff0c;可以获得更快的响应时间&#xff0c;更好的用户体验。
回页首
以下我们以一个真实的客户案例来分析说明 WebSocket 的优势及具体开发实现&#xff08;为保护客户隐私&#xff0c;以下描述省去客户名&#xff0c;具体涉及业务细节的代码在文中不再累述&#xff09;。
该客户为一个移动设备制造商&#xff0c;移动设备装载的是 Android/IOS 操作系统&#xff0c;设备分两类&#xff08;以下简称 A&#xff0c;B 两类&#xff09;&#xff0c;A 类设备随时处于移动状态中&#xff0c;B 类设备为 A 类设备的管理控制设备&#xff0c;客户需要随时在 B 类设备中看到所属 A 类设备的地理位置信息及状态信息。如 A 类设备上线&#xff0c;离线的时候&#xff0c;B 类设备需要立即获得消息通知&#xff0c;A 类设备上报时&#xff0c;B 类设备也需要实时获得该上报 A 类设备的地理位置信息。
为降低跨平台的难度及实施工作量&#xff0c;客户考虑轻量级的 Web App 的方式屏蔽 Android/IOS 平台的差异性&#xff0c;A 类设备数量众多&#xff0c;且在工作状态下 A 类设备处于不定时的移动状态&#xff0c;而 B 类设备对 A 类设备状态变化的感知实时性要求很高&#xff08;秒级&#xff09;。
根据以上需求&#xff0c;A/B 类设备信息存放在后台数据库中&#xff0c;A/B 类设备的交互涉及 Web 客户端/服务器频繁和高并发的请求-相应&#xff0c;如果使用传统的 HTTP 请求-响应模式&#xff0c;B 类设备的 Web App 上需要对服务进行轮询&#xff0c;势必会对服务器带来大的负载压力&#xff0c;且当 A 类设备没有上线或者上报等活动事件时&#xff0c;B 类设备的轮询严重浪费网络资源。
综上所述&#xff0c;项目采用 WebSocket 技术实现实时消息的通知及推送&#xff0c;每当 A 类设备/B 类设备上线登录成功即打开 WebSocket 的 HTTP 长连接&#xff0c;新的 A 类设备上线&#xff0c;位置变化&#xff0c;离线等状态变化通过 WebSocket 发送实时消息&#xff0c;WebSocket Server 端处理 A 类设备的实时消息&#xff0c;并向所从属的 B 类设备实时推送。
WebSocket 客户端使用 jQuery Mobile&#xff08;jQuery Mobile 移动端开发在本文中不再详细描述&#xff0c;感兴趣的读者可以参考jQuery Mobile 简介)&#xff0c;使用原生 WebSocket API 实现与服务端交互。
服务端沿用客户已有的应用服务器 Tomcat 7.0.33 版本&#xff0c;使用 Apache 自定义 API 实现 WebSocket Server 端&#xff0c;为一个上线的 A 类设备生成一个 WebSocket 的 HTTP 长连接&#xff0c;每当 A 类设备有上线&#xff0c;位置更新&#xff0c;离线等事件的时候&#xff0c;客户端发送文本消息&#xff0c;服务端识别并处理后&#xff0c;向所属 B 类设备发送实时消息&#xff0c;B 类设备客户端接收消息后&#xff0c;识别到 A 类设备的相应事件&#xff0c;完成对应的 A 类设备位置刷新以及其他业务操作。
其涉及的 A 类设备&#xff0c;B 类设备及后台服务器交互时序图如下&#xff1a;
A/B 类设备的 WebSocket 客户端封装在 websocket.js 的 Javascript 代码中&#xff0c;与 jQuery MobileApp 一同打包为移动端 apk/ipa 安装包&#xff1b;WebSocket 服务端实现主要为 WebSocketDeviceServlet.java, WebSocketDeviceInbound.java&#xff0c;WebSocketDeviceInboundPool.java 几个类。下文我们一一介绍其具体代码实现。
在下文中我们把本案例中的主要代码实现做解释说明&#xff0c;读者可以下载完整的代码清单做详细了解。
WebSocketDeviceServlet 类
A 类设备或者 B 类设备发起 WebSocket 长连接后&#xff0c;服务端接受请求的是 WebSocketDeviceServlet 类&#xff0c;跟传统 HttpServlet 不同的是&#xff0c;WebSocketDeviceServlet 类实现 createWebSocketInbound 方法&#xff0c;类似 SocketServer 的 accept 方法&#xff0c;新生产的 WebSocketInbound 实例对应客户端 HTTP 长连接&#xff0c;处理与客户端交互功能。
WebSocketDeviceServlet 服务端代码示例如下&#xff1a;
public class WebSocketDeviceServlet extends org.apache.catalina.websocket.WebSocketServlet {private static final long serialVersionUID &#61; 1L;&#64;Overrideprotected StreamInbound createWebSocketInbound(String subProtocol,HttpServletRequest request) {WebSocketDeviceInbound newClientConn &#61; new WebSocketDeviceInbound(request);WebSocketDeviceInboundPool.addMessageInbound(newClientConn);return newClientConn;}}
代码解释&#xff1a;
WebSocketServlet 是 WebSocket 协议的后台监听进程&#xff0c;和传统 HTTP 请求一样&#xff0c;WebSocketServlet 类似 Spring/Struct 中的 Servlet 监听进程&#xff0c;只不过通过客户端 ws 的前缀指定了其监听的协议为 WebSocket。
WebSocketDeviceInboundPool 实现了类似 JDBC 数据库连接池的客户端 WebSocket 连接池功能&#xff0c;并统一处理 WebSocket 服务端对单个客户端/多个客户端&#xff08;同组 A 类设备&#xff09;的消息推送&#xff0c;详见 WebSocketDeviceInboundPool 代码类解释。
WebSocketDeviceInboundl 类
WebSocketDeviceInbound 类为每个 A 类和 B 类设备验证登录后&#xff0c;客户端建立的 HTTP 长连接的对应后台服务类&#xff0c;类似 Socket 编程中的 SocketServer accept 后的 Socket 进程&#xff0c;在 WebSocketInbound 中接收客户端发送的实时位置信息等消息&#xff0c;并向客户端&#xff08;B 类设备&#xff09;发送下属 A 类设备实时位置信息及位置分析结果数据&#xff0c;输入流和输出流都是 WebSocket 协议定制的。WsOutbound 负责输出结果&#xff0c;StreamInbound 和 WsInputStream 负责接收数据&#xff1a;
public class WebSocketDeviceInbound extends MessageInbound {
private final HttpServletRequest request;
private DeviceAccount connectedDevice;public DeviceAccount getConnectedDevice() {
return connectedDevice;
}public void setConnectedDevice(DeviceAccount connectedDevice) {
this.connectedDevice &#61; connectedDevice;
}public HttpServletRequest getRequest() {
return request;
}public WebSocketDeviceInbound(HttpServletRequest request) {
this.request &#61; request;
DeviceAccount connectedDa &#61; (DeviceAccount)request.getSession(true).getAttribute("connectedDevice");
if(connectedDa&#61;&#61;null)
{
String deviceId &#61; request.getParameter("id");
DeviceAccountDao deviceDao &#61; new DeviceAccountDao();
connectedDa &#61; deviceDao.getDaById(Integer.parseInt(deviceId));
}
this.setConnectedDevice(connectedDa);
}&#64;Override
protected void onOpen(WsOutbound outbound) {/}&#64;Override
protected void onClose(int status) {
WebSocketDeviceInboundPool.removeMessageInbound(this);}&#64;Override
protected void onBinaryMessage(ByteBuffer message) throws IOException {
throw new UnsupportedOperationException("Binary message not supported.");
}&#64;Override
protected void onTextMessage(CharBuffer message) throws IOException {
WebSocketDeviceInboundPool.processTextMessage(this, message.toString());}public void sendMessage(BaseEvent event)
{
String eventStr &#61; JSON.toJSONString(event);
try {
this.getWsOutbound().writeTextMessage(CharBuffer.wrap(eventStr));
//…以下代码省略
} catch (IOException e) {
e.printStackTrace();
}
}
}
代码解释&#xff1a;
connectedDevice 是当前连接的 A/B 类客户端设备类实例&#xff0c;在这里做为成员变量以便后续处理交互。
sendMessage 函数向客户端发送数据&#xff0c;使用 Websocket WsOutbound 输出流向客户端推送数据&#xff0c;数据格式统一为 JSON。
onTextMessage 函数为客户端发送消息到服务器时触发事件&#xff0c;调用 WebSocketDeviceInboundPool 的 processTextMessage 统一处理 A 类设备的登入&#xff0c;更新位置&#xff0c;离线等消息。
onClose 函数触发关闭事件&#xff0c;在连接池中移除连接。
WebSocketDeviceInbound 构造函数为客户端建立连接后&#xff0c;WebSocketServlet 的 createWebSocketInbound 函数触发&#xff0c;查询 A 类/B 类设备在后台数据库的详细数据并实例化 connectedDevice 做为 WebSocketDeviceInbound 的成员变量&#xff0c;WebSocketServlet 类此时将新的 WebSocketInbound 实例加入自定义的 WebSocketDeviceInboundPool 连接池中&#xff0c;以便统一处理 A/B 设备组员关系及位置分布信息计算等业务逻辑。
WebSocketDeviceInboundPool 类
WebSocketInboundPool 类: 由于需要处理大量 A 类 B 类设备的实时消息&#xff0c;服务端会同时存在大量 HTTP 长连接&#xff0c;为统一管理和有效利用 HTTP 长连接资源&#xff0c;项目中使用了简单的 HashMap 实现内存连接池机制&#xff0c;每次设备登入新建的 WebSocketInbound 都放入 WebSocketInbound 实例的连接池中&#xff0c;当设备登出时&#xff0c;从连接池中 remove 对应的 WebSocketInbound 实例。
此外&#xff0c;WebSocketInboundPool 类还承担 WebSocket 客户端处理 A 类和 B 类设备间消息传递的作用&#xff0c;在客户端发送 A 类设备登入、登出及位置更新消息的时候&#xff0c;服务端 WebSocketInboundPool 进行位置分布信息的计算&#xff0c;并将计算完的结果向同时在线的 B 类设备推送。
public class WebSocketDeviceInboundPool {private static final ArrayList
new ArrayList
//添加连接
DeviceAccount da &#61; inbound.getConnectedDevice();
System.out.println("新上线设备 : " &#43; da.getDeviceNm());
connections.add(inbound);
}public static ArrayList
ArrayList
for(WebSocketDeviceInbound webClient:connections)
{
onlineDevices.add(webClient.getConnectedDevice());
}
return onlineDevices;
}public static WebSocketDeviceInbound getGroupBDevices(String group){
WebSocketDeviceInbound retWebClient &#61;null;
for(WebSocketDeviceInbound webClient:connections)
{
if(webClient.getConnectedDevice().getDeviceGroup().equals(group)&&
webClient.getConnectedDevice().getType().equals("B")){
retWebClient &#61; webClient;
}
}
return retWebClient;
}
public static void removeMessageInbound(WebSocketDeviceInbound inbound){
//移除连接
System.out.println("设备离线 : " &#43; inbound.getConnectedDevice());
connections.remove(inbound);
}public static void processTextMessage(WebSocketDeviceInbound inbound,String message){BaseEvent receiveEvent &#61; (BaseEvent)JSON.parseObject(message.toString(),BaseEvent.class);
DBEventHandleImpl dbEventHandle &#61; new DBEventHandleImpl();
dbEventHandle.setReceiveEvent(receiveEvent);
dbEventHandle.HandleEvent();
if(receiveEvent.getEventType()&#61;&#61;EventConst.EVENT_MATCHMATIC_RESULT||
receiveEvent.getEventType()&#61;&#61;EventConst.EVENT_GROUP_DEVICES_RESULT||
receiveEvent.getEventType()&#61;&#61;EventConst.EVENT_A_REPAIRE){
String clientDeviceGroup &#61; ((ArrayList
receiveEvent.getEventObjs()).get(0).getDeviceGroup();
WebSocketDeviceInbound bClient &#61; getGroupBDevices(clientDeviceGroup);
if(bClient!&#61;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();
}
}
}
代码解释&#xff1a;
addMessageInbound 函数向连接池中添加客户端建立好的连接。
getOnlineDevices 函数获取所有的连线的 A/B 类设备。
removeMessageInbound 函数实现 A 类设备或者 B 类设备离线退出&#xff08;服务端收到客户端关闭 WebSocket 连接事件&#xff0c;触发 WebSocketInbound 中的 onClose 方法&#xff09;&#xff0c;从连接池中删除连接设备客户端的连接实例。
processTextMessage 完成处理客户端消息&#xff0c;这里使用了消息处理的机制&#xff0c;包括解码客户端消息&#xff0c;根据消息构造 Event 事件&#xff0c;通过 EventHandle 多线程处理&#xff0c;处理完后向客户端返回&#xff0c;可以向该组 B 设备推送消息&#xff0c;也可以向发送消息的客户端推送消息。
sendMessageToAllDevices 函数实现发送数据给所有在线 A/B 类设备客户端。sendMessageToSingleClient 函数实现向某一 A/B 类设备客户端发送数据。
客户端代码 websocket.js&#xff0c;客户端使用标准 HTML5 定义的 WebSocket API&#xff0c;从而保证支持 IE9&#43;&#xff0c;Chrome&#xff0c;FireFox 等多种浏览器&#xff0c;并结合 jQueryJS 库 API 处理 JSON 数据的处理及发送。
var websocket&#61;window.WebSocket || window.MozWebSocket;
var isConnected &#61; false;function doOpen(){isConnected &#61; true;
if(deviceType&#61;&#61;&#39;B&#39;){mapArea&#61;&#39;mapB&#39;;doLoginB(mapArea);}else{mapArea&#61;&#39;mapA&#39;;doLoginA(mapArea);}}function doClose(){
showDiagMsg("infoField","已经断开连接", "infoDialog");
isConnected &#61; false;
}function doError() {
showDiagMsg("infoField","连接异常!", "infoDialog");
isConnected &#61; false;}function doMessage(message){
var event &#61; $.parseJSON(message.data);
doReciveEvent(event);
}function doSend(message) {
if (websocket !&#61; null) {
websocket.send(JSON.stringify(message));
} else {
showDiagMsg("infoField","您已经掉线&#xff0c;无法与服务器通信!", "infoDialog");
}
}//初始话 WebSocket
function initWebSocket(wcUrl) {
if (window.WebSocket) {
websocket &#61; new WebSocket(encodeURI(wcUrl));
websocket.onopen &#61; doOpen;
websocket.onerror &#61; doError;
websocket.onclose &#61; doClose;
websocket.onmessage &#61; doMessage;
}
else{
showDiagMsg("infoField","您的设备不支持 webSocket!", "infoDialog");}
};function doReciveEvent(event){
//设备不存在&#xff0c;客户端断开连接
if(event.eventType&#61;&#61;101){
showDiagMsg("infoField","设备不存在或设备号密码错!", "infoDialog");
websocket.close();
}
//返回组设备及计算目标位置信息&#xff0c;更新地图
else if(event.eventType&#61;&#61;104||event.eventType&#61;&#61;103){
clearGMapOverlays(mapB); $.each(event.eventObjs,function(idx,item){var deviceNm &#61; item.deviceNm;//google api
// var deviceLocale &#61; new google.maps.LatLng(item.lag,item.lat);
//baidu apivar deviceLocale &#61; new BMap.Point(item.lng,item.lat);var newMarker;if(item.status&#61;&#61;&#39;target&#39;){newMarker &#61; addMarkToMap(mapB,deviceLocale,deviceNm,true);//…以下代码省略}else{newMarker &#61; addMarkToMap(mapB,deviceLocale,deviceNm);} markArray.push(newMarker);});showDiagMsg("infoField","有新报修设备或设备离线, 地图已更新&#xff01;", "infoDialog");
}}
代码解释&#xff1a;
doOpen 回调函数处理打开 WebSocket&#xff0c;A 类设备或者 B 类设备连接上 WebSocket 服务端后&#xff0c;将初始化地图并显示默认位置&#xff0c;然后向服务端发送设备登入的消息。
doReciveEvent 函数处理关闭 WebSocket&#xff0c;A 类/B 类设备离线&#xff08;退出移动终端上的应用&#xff09;时&#xff0c;服务端关闭 HTTP 长连接&#xff0c;客户端 WebSocket 对象执行 onclose 回调句柄。
initWebSocket 初始化 WebSocket&#xff0c;连接 WebSocket 服务端&#xff0c;并设置处理回调句柄&#xff0c;如果浏览器版本过低而不支持 HTML5&#xff0c;提示客户设备不支持 WebSocket。
doSend 函数处理客户端向服务端发送消息&#xff0c;注意 message 是 JSON OBJ 对象&#xff0c;通过 JSON 标准 API 格式化字符串。
doMessage 函数处理 WebSocket 服务端返回的消息&#xff0c;后台返回的 message 为 JSON 字符串&#xff0c;通过 jQuery 的 parseJSON API 格式化为 JSON Object 以便客户端处理 doReciveEvent 函数时客户端收到服务端返回消息的具体处理&#xff0c;由于涉及大量业务逻辑在此不再赘述。
回页首
以上简要介绍了 WebSocket 的由来&#xff0c;原理机制以及服务端/客户端实现&#xff0c;并以实际客户案例指导并讲解了如何使用 WebSocket 解决实时响应及服务端消息推送方面的问题。本文适用于熟悉 HTML 协议规范和 J2EE Web 编程的读者&#xff0c;旨在帮助读者快速熟悉 HTML5 WebSocket 的原理和开发应用。文中的服务端及客户端项目代码可供下载&#xff0c;修改后可用于用户基于 WebSocket 的 HTTP 长连接的实际生产环境中。
使用四种框架分别实现1百万websocket常连接的服务器
参见&#xff1a;http://www.open-open.com/lib/view/open1435905714122.html