作者:黄 晓安, 何 亮, 和 许 宁 来源:https://www.ibm.com/developerworks/cn/web/1112_huangxa_websocket/
WebSocket产生背景:实时Web应用的窘境
应用的信息交互过程通常是客户端通过浏览器发出一个请求,服务器端接收和审核完请求后进行处理并返回结果给客户端,然后客户端浏览器将信息呈现出来,这种机制对于信息变化不是特别频繁的应用尚能相安无事,但是对于那些实时要求比较高的应用来说,比如说实时报表统计、在线游戏、在线证券、设备监控、新闻在线播报、RSS 订阅推送等等,当客户端浏览器准备呈现这些信息的时候,这些信息在服务器端可能已经过时了。所以保持客户端和服务器端的信息同步是实时 Web 应用的关键要素,对 Web 开发人员来说也是一个难题。
在 WebSocket 出来之前,开发人员想实现这些实时的 Web 应用,不得不采用一些折衷的方案,其中最常用的就是轮询 (Polling) 和 Comet 技术,而 Comet 技术实际上是轮询技术的改进,又可细分为两种实现方式,一种是长轮询机制,一种称为流技术。下面简单介绍一下这几种技术:
轮询
这是最早的一种实现实时 Web 应用的方案。客户端以一定的时间间隔向服务端发出请求,以频繁请求的方式来保持客户端和服务器端的同步。这种同步方案的最大问题是:当客户端以固定频率向服务器发起请求的时候,服务器端的数据可能并没有更新,这样会带来很多无谓的网络传输,所以这是一种非常低效的实时方案。
长轮询:
长轮询是对定时轮询的改进和提高,目地是为了降低无效的网络传输。当服务器端没有数据更新的时候,连接会保持一段时间周期直到数据或状态改变或者时间过期,通过这种机制来减少无效的客户端和服务器间的交互。当然,如果服务端的数据变更非常频繁的话,这种机制和定时轮询比较起来没有本质上的性能的提高。
流:
流技术方案通常就是在客户端的页面使用一个隐藏的窗口向服务端发出一个长连接的请求。服务器端接到这个请求后作出回应并不断更新连接状态以保证客户端和服务器端的连接不过期。通过这种机制可以将服务器端的信息源源不断地推向客户端。这种机制在用户体验上有一点问题,需要针对不同的浏览器设计不同的方案来改进用户体验,同时这种机制在并发比较大的情况下,对服务器端的资源是一个极大的考验。
综合这几种方案,您会发现这些目前我们所使用的所谓的实时技术并不是真正的实时技术,它们只是在用 Ajax 方式来模拟实时的效果,在每次客户端和服务器端交互的时候都是一次 HTTP 的请求和应答的过程,而每一次的 HTTP 请求和应答都带有完整的 HTTP 头信息,这就增加了每次传输的数据量,而且这些方案中客户端和服务器端的编程实现都比较复杂,在实际的应用中,为了模拟比较真实的实时效果,开发人员往往需要构造两个 HTTP 连接来模拟客户端和服务器之间的双向通讯,一个连接用来处理客户端到服务器端的数据传输,一个连接用来处理服务器端到客户端的数据传输,这不可避免地增加了编程实现的复杂度,也增加了服务器端的负载,制约了应用系统的扩展性。
##什么是WebSocket?
WebSocket是html5的新特性之一,其设计出来的目的就是要取代轮询和 Comet 技术,使客户端浏览器具备像 C/S 架构下桌面系统的实时通讯能力。
那WebSocket究竟是什么?首先我们需要清楚,WebSocket本质上就是一种计算机网络应用层的协议(HTTP就是一种网络应用层协议),用来弥补HTTP协议在持久通信能力上的不足。我们知道HTTP协议本身是无状态协议,每一个新的HTTP请求,只能通过客户端主动发起,通过建立连接-->传输数据-->断开连接的方式来传输数据,传送完连接就断开了,也就是此次HTTP请求已经完全结束了(虽然HTTP1.1增加了keep-alive请求头可以通过一条通道请求多次,但本质上还是一样的)。并且服务器是不能主动给客户端发送数据的(因为之前的请求得到响应后连接就断开了,之后服务器根本不知道谁请求过),客户端也不会知道之前请求的任何信息,所以HTTP协议本身是没有持久通信能力的,正因为这样,也就出现了上述实时Web应用的窘境。
WebSocket协议实现了浏览器与服务器的全双工通信(指在通信的任意时刻,线路上存在A到B和B到A的双向信号传输,简单说就如同打电话一样,浏览器和服务器任何一方随时都能够主动给对方说话)。并且在HTML5标准中增加了有关WebSocket协议的相关API,所以只要实现了HTML5标准的客户端,就可以与支持WebSocket协议的服务器进行全双工的持久通信了。
与HTTP协议一样,WebSocket协议也需要通过已建立的TCP连接来传输数据。具体实现上是通过HTTP协议建立通道,然后在此基础上用真正的WebSocket协议进行通信,所以WebSocket协议和Http协议是有一定的交叉关系的。Websocket是应用层第七层上的一个应用层协议,它必须依赖 HTTP 协议进行一次握手 ,握手成功后,数据就直接从TCP通道传输,与HTTP无关了。
为什么需要WebSocket?
浏览器通过JAVAScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。因为 WebSocket 连接本质上就是一个 TCP 连接,所以在数据传输的稳定性和数据传输量的大小方面,和轮询以及 Comet 技术比较,具有很大的性能优势。Websocket.org 网站对传统的轮询方式和 WebSocket 调用方式作了一个详细的测试和比较,将一个简单的 Web 应用分别用轮询方式和 WebSocket 方式来实现,在这里引用一下他们的测试结果图:
通过这张图可以清楚的看出,在流量和负载增大的情况下,WebSocket 方案相比传统的 Ajax 轮询方案有极大的性能优势。这也是为什么我们认为 WebSocket 是未来实时 Web 应用的首选方案的原因。
WebSocket使用场景
什么时候使用WebSocket,你需要考虑如下两个因素
你的应用是否提供多个用户之间的相互交流?
你的应用是展示服务器端经常变动的数据吗?
如果上述两个问题你的回答是肯定的,那么请考虑使用WebSocket。如果你还不确定,那有一些经典场景,你可以参考并激发一下自己的灵感。
实时统计(图表)
你需要你的统计数据(或者图表)实时更新,类似于淘宝双11大屏那样的效果
系统即时提醒
实时地图位置
弹幕
社交订阅
社交类的应用的一个裨益之处就是能够即时的知道你的朋友正在做什么。虽然听起来有点可怕,但是我们都喜欢这样做。你不会想要在数分钟之后才知道微信朋友圈朋友发布的更新动态。你是在线的,所以你的订阅的更新应该是实时的。
金融界瞬息万变——几乎是每毫秒都在变化。我们人类的大脑不能持续以那样的速度处理那么多的数据,所以我们写了一些算法来帮我们处理这些事情。虽然你不一定是在处理高频的交易,但是,过时的信息也只能导致损失。当你有一个显示盘来跟踪你感兴趣的公司时,你肯定想要随时知道他们的价值,而不是10秒前的数据。使用WebSocket可以流式更新这些数据变化而不需要等待。
体育实况更新
如果你在你的网站应用中包含了体育新闻,WebSocket能够助力你的用户获得实时的更新。
基于位置的应用
越来越多的开发者借用移动设备的GPS功能来实现他们基于位置的应用。如果你收集到了用户的位置数据(比如记录运动轨迹)。如果你想实时的更新网络数据仪表盘(可以说是一个监视运动员的教练),HTTP协议显得有些笨拙。借用WebSocket TCP链接可以让数据飞起来。
在线教育
上学花费越来越贵了,但互联网变得更快和更便宜。在线教育是一种不错的学习方式,尤其是你可以和老师以及其他同学一起交流。此时,用WebSocket来实现是个不错的选择,可以多媒体聊天、文字聊天以及其它优势如与别人合作一起在公共数字黑板上画画等。
如何使用WebSocket?
使用WebSocket需要客户端和服务器端,客户端通常就是浏览器(基于浏览器进行的JavaScript调用),服务器端通常就是支持WebSocket的中间件。
支持 WebSocket 的服务器
服务器端的实现不受平台和开发语言的限制,只需要遵从 WebSocket 规范即可,目前已经出现了一些比较成熟的 WebSocket 服务器端实现,比如Kaazing WebSocket Gateway(一个 Java 实现的 WebSocket Server)、mod_pywebsocket(一个 Python 实现的 WebSocket Server)、Netty(一个 Java 实现的网络框架其中包括了对 WebSocket 的支持)、Node.js(一个 Server 端的 JavaScript 框架提供了对 WebSocket 的支持),当然也可以使用Tomcat(需要为Tomcat7.0.47以上,且Tomcat7.x和Tomcat8.x的使用方式还不一样)。
WebSocket JavaScript API接口
针对 Web 开发人员的 WebSocket JavaScript 客户端接口是非常简单的,以下是 WebSocket JavaScript 接口的定义:
其中 URL 属性代表 WebSocket 服务器的网络地址,协议通常是”ws”,send 方法就是发送数据到服务器端,close 方法就是关闭连接。除了这些方法,还有一些很重要的事件:onopen,onmessage,onerror 以及 onclose。
##WebSocket实战—系统即时提醒
在上一节中我们说到使用WebSocket需要客户端和服务器端,客户端通常就是浏览器(基于浏览器进行的JavaScript调用),服务器端通常就是支持WebSocket的中间件。本节中我们将进行一个WebSocket实战—系统即时提醒(使用Chrome + Tomcat7.0.70,在此基础上进行前后端代码开发),后端模拟业务变化,向前端实时推送提醒消息,前端页面进行消息提醒的实时展示。
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <script src="js/jquery-1.7.2.min.js"></script> <script type="text/javascript" src="js/noty/packaged/jquery.noty.packaged.js"></script> <title>系统即时提醒</title> <script type="text/javascript"> var n; // 消息提示插件对象 $(document).ready(function () { // jQuery noty消息提示插件 n = noty({ text: "", // 默认提示信息为空 type: "alert", // alert样式 layout: 'center', // 提示框居于页面中间 theme: 'defaultTheme' // 使用默认样式 }); }); //*********************WebSocket 调用**********************// // 定义WebSocket客户端对象websocket var websocket = null; //判断当前浏览器是否支持WebSocket if ('WebSocket' in window) { // WebSocket应用名,websocket服务名 websocket = new WebSocket("ws://localhost:8080/WebSocket/websocket"); } else { alert('当前浏览器 Not support websocket') } //连接发生错误的回调方法 websocket.onerror = function () { setMessageAlert("WebSocket连接发生错误"); }; //连接成功建立的回调方法 websocket.onopen = function () { setMessageAlert("WebSocket连接成功"); } //接收到消息的回调方法 websocket.onmessage = function (event) { setMessageAlert(event.data); } //连接关闭的回调方法 websocket.onclose = function () { setMessageAlert("WebSocket连接关闭"); } //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。 window.onbeforeunload = function () { closeWebSocket(); } // 使用noty插件即时展示提醒信息 function setMessageAlert(data) { n.setText(data); } //关闭WebSocket连接 function closeWebSocket() { websocket.close(); } </script> </head> <body> </body> </html>
package com.itheima.ssm.controller; import java.io.IOException;import java.util.concurrent.CopyOnWriteArraySet; import javax.websocket.*;import javax.websocket.server.ServerEndpoint; /** * @ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端, * 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端 */@ServerEndpoint("/websocket")public class WebSocketTest { //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。 private static int onlineCount = 0; //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识 public static CopyOnWriteArraySet<WebSocketTest> webSocketSet = new CopyOnWriteArraySet<WebSocketTest>(); //与某个客户端的连接会话,需要通过它来给客户端发送数据 private Session session; /** * 连接建立成功调用的方法 * @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据 */ @OnOpen public void onOpen(Session session){ this.session = session; webSocketSet.add(this); //加入set中 addOnlineCount(); //在线客户端数加1 System.out.println("有新连接加入!当前在线客户端数为" + getOnlineCount()); } /** * 连接关闭调用的方法 */ @OnClose public void onClose(){ webSocketSet.remove(this); //从set中删除 subOnlineCount(); //在线客户端数减1 System.out.println("有一连接关闭!当前在线客户端数为" + getOnlineCount()); } /** * 收到客户端消息后调用的方法 * @param message 客户端发送过来的消息 * @param session 可选的参数 */ @OnMessage public void onMessage(String message, Session session) { System.out.println("来自客户端的消息:" + message); } /** * 发生错误时调用 * @param session * @param error */ @OnError public void onError(Session session, Throwable error){ System.out.println("发生错误"); error.printStackTrace(); } /** * 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。 * @param message * @throws IOException */ public void sendMessage(String message) throws IOException{ this.session.getBasicRemote().sendText(message); } public static synchronized int getOnlineCount() { return onlineCount; } public static synchronized void addOnlineCount() { WebSocketTest.onlineCount++; } public static synchronized void subOnlineCount() { WebSocketTest.onlineCount--; } }
/** * 触发后模拟服务器主动向前端发送系统即时提醒 */@RequestMApping("sendMsg")public void sendMsg() { SimpleDateFormat sf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss"); // 模拟向已连接的WebSocket客户端发送系统提醒 for(WebSocketTest item: WebSocketTest.webSocketSet){ try { item.sendMessage("系统提醒:当前时间," + sf.format(new Date()) + ",请尽快完成任务!"); } catch (IOException e) { e.printStackTrace(); continue; } } }
注意
通过上面的讲述,WebSocket 的优势已经很明显了,但是作为一个正在演变中的 Web 规范,我们也要看到目前用 Websocket 构建应用程序的一些风险。首先,WebSocket 规范目前还处于草案阶段,也就是它的规范和 API 还是有变动的可能,另外的一个风险就是微软的 IE 作为占市场份额最大的浏览器,和其他的主流浏览器相比,对 HTML5 的支持是比较差的,这是我们在构建企业级的 Web 应用的时候必须要考虑的一个问题。