WebSocket初探
众所周知,socket是编写网络通信应用的基本技术,网络数据交换大多直接或间接通过socket进行。对于直接使用socket的客户端与服务端,一旦连接被建立则均可主动向对方传送数据,而对于使用更上层的HTTP/HTTPS协议的应用,由于它们是非连接协议,所以通常只能由客户端主动向服务端发送请求才能获得服务端的响应并取得相关的数据。而当前越来越多的应用希望能够及时获取服务端提供的数据,甚至希望能够达到接近实时的数据交换(例如很多网站提供的在线客户系统)。为达到此目的,通常采用的技术主要有轮询、长轮询、流等,而伴随着HTML5的出现,相对更优异的WebSocket方案也应运而生。
一、非WebSocket方案简介
1.轮询
轮询是由客户端定时向服务端发起查询数据的请求的一种实现方式。早期的轮询是通过不断自动刷新页面而实现的(在那个基本是IE统治浏览器的时代,那不断刷新页面产生的噪声就难以让人忍受),后来随着技术的发展,特别是Ajax技术的出现,实现了无刷新更新数据。但本质上这些方式均是客户端定时轮询服务端,这种方式的最显著的缺点是如果客户端数量庞大并且定时轮询间隔较短服务端将承受响应这些客户端海量请求的巨大的压力。
2.长轮询
在数据更新不够频繁的情况下,使用轮询方法获取数据时客户端经常会得到没有数据的响应,显然这样的轮询是一个浪费网络资源的无效的轮询。长轮询则是针对普通轮询的这种缺陷的一种改进方案,其具体实现方式是如果当前请求没有数据可以返回,则继续保持当前请求的网络连接状态,直到服务端有数据可以返回或者连接超时。长轮询通过这种方式减少了客户端与服务端交互的次数,避免了一些无谓的网络连接。但是如果数据变更较为频繁,则长轮询方式与普通轮询在性能上并无显著差异。同时,增加连接的等待时间,往往意味着并发性能的下降。
3.流
所谓流是指客户端在页面之下向服务端发起一个长连接请求,服务端收到这个请求后响应它并不断更新连接状态,以确保这个连接在客户端与服务端之间一直有效。服务端可以通过这个连接将数据主动推送到客户端。显然,这种方案实现起来相对比较麻烦,而且可能被防火墙阻断。
二、WebSocket简介
1.WebSocket协议简介
WebSocket是为解决客户端与服务端实时通信而产生的技术。其本质是先通过HTTP/HTTPS协议进行
握手后创建一个用于交换数据的TCP连接,此后服务端与客户端通过此TCP连接进行实时通信。
WebSocket规范当前还没有正式版本,草案变化也较为迅速。Tomcat7(本文中的例程来自7.0.42)当前支持RFC 6455(/html/rfc6455)定义的WebSocket,而RFC 6455目前还未冻结,将来可能会修复一些Bug,甚至协议本身也可能会产生一些变化。
RFC6455定义的WebSocket协议由握手和数据传输两个部分组成。
来自客户端的握手信息类似如下:
GET /chat HTTP/1.1
Host:
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
服务端的握手信息类似如下:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
一旦客户端和服务端都发送了握手信息并且成功握手,则数据传输部分将开始。数据传输对客户端和服务端而言都是一个双工通信通道,客户端和服务端来回传递的数据称之为“消息”。
客户端通过WebSocket URI发起WebSocket连接,WebSocket URIs模式定义如下:
ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]
wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]
ws是普通的WebSocket通信协议,而wss是安全的WebSocket通信协议(就像HTTP与HTTPS之间的差异一样)。在缺省情况下,ws的端口是80而wss的端口是443。
关于WebSocke协议规范的完整详尽说明,请参考RFC 6455。
2.Tomcat7提供的WebSocket包简介
Tomcat7提供的与WebSocket相关的类均位于包org.apache.catalina.websocket之中(包org.apache.catalina.websocket的实现包含于文件catalina.jar之中),它包含有类Constants、MessageInbound、StreamInbound、WebSocketServlet、WsFrame、WsHttpServletRequestWrapper、WsInputStream、WsOutbound。这些类的关系如图1所示。
图 1 包org.apache.catalina.websocket
包org.apache.catalina.websocket中的这些类为WebSocket开发服务端提供了支持,这些
类的主要功能简述如下:
Constants:包org.apache.catalina.websocket中用到的常数定义在这个类中,它只包含静态常数定义,无任何逻辑实现。
MessageInbound:基于消息的WebSocket实现类(带内消息),应用程序应当扩展这个类并实现其抽象方法onBinaryMessage和onTextMessage。
StreamInbound:基于流的WebSocket实现类(带内流),应用程序应当扩展这个类并实现其抽象方法onBinaryData和onTextData。
WebSocketServlet:提供遵循RFC6455的WebSocket连接的Servlet基本实现。客户端使用WebSocket连接服务端时,需要将WebSocketServlet的子类作为连接入口。同时,该子类应当实现WebSocketServlet的抽象方法createWebSocketInbound,以便创建一个inbound实例(MessageInbound或StreamInbound)。
WsFrame:代表完整的WebSocket框架。
WsHttpServletRequestWrapper:包装过的HttpServletRequest对象。
WsInputStream:基于WebSocket框架底层的socket的输入流。
WsOutbound:提供发送消息到客户端的功能。它提供的所有向客户端的写方法都是同步的,可以防止多线程同时向客户端写入数据。
三、基于Tomcat7的WebSocket例程
利用当前HTML5和Tomcat7为WebSocket提供的支持,基本只需要编写简单的代码对不同的事件做相应的逻辑处理就可以实现利用WebSocket进行实时通信了。
Tomcat7为WebSocket提供了3个例程(echo、chat及snake),以下就其中的echo和chat 分别做一简要解析。
echo例程主要演示以下功能:客户端连接服务端、客户端向服务端发送消息、服务端收到客户端发送的消息后将其原样返回给客户端、客户端收到消息后将其显示在网页之上。
在客户端页面选择streams或messages作为“Connect using”,然后点击“Connect”按钮,可以在右侧窗口看到WebSocket连接打开的消息。随后点击“Echo message”按钮,客户端将向服务端发送一条消息,在右侧窗口,可以看到,消息发出的后的瞬间,客户端已经收到了服务端原样返回的消息。
客户端页面及运行效果截图如图2所示。
图 2 echo例程客户端页面
客户端实现上述功能的核心脚本如下,其关键点通过注释的形式加以说明:
<script type="text/javascript">
var ws = null;
// 界面元素可用性控制
function tConnected(connected) {
}
function connect() {
// 取得WebSocket连接入口(WebSocket URI)
var target = ElementById('target').value;
if (target == '') {
alert('Plea lect rver side connection implementation.');
return;
}
// 创建WebSocket
if ('WebSocket' in window) {
ws = new WebSocket(target);
} el if ('MozWebSocket' in window) {
ws = new MozWebSocket(target);
} el {
alert('WebSocket is not supported by this browr.');
return;
}
// 定义Open事件处理函数
tConnected(true);
log('Info: WebSocket connection opened.');
};
// 定义Message事件处理函数(收取服务端消息并处理)
log('Received: ' + event.data);
};
// 定义Clo事件处理函数
tConnected(fal);
log('Info: WebSocket connection clod.');
};
}
// 关闭WebSocket连接
function disconnect() {
if (ws != null) {
ws.clo();
ws = null;
}
tConnected(fal);
}
function echo() {
if (ws != null) {
var message = ElementById('message').value;
log('Sent: ' + message);
// 向服务端发送消息
ws.nd(message);
} el {
alert('WebSocket connection not established, plea connect.');
}
}
// 生成WebSocket URI
function updateTarget(target) {
if (window.location.protocol == 'http:') {
'ws://' + window.location.host + target;
} el {
'wss://' + window.location.host + target;
}
}
// 在界面显示log及消息
function log(message) {
var console = ElementById('console');
var p = ateElement('p');
p.style.wordWrap = 'break-word';
p.ateTextNode(message));
console.appendChild(p);
while (console.childNodes.length > 25) {
}
console.scrollTop = console.scrollHeight;
}
</script>
注:完整代码参见apache-tomcat-7.0.42\webapps\examples\websocket\echo.html
以上客户端可以根据“Connect using”的不同选择连接不同的服务端WebSocket。例如messages选项对应的服务端代码如下,其核心逻辑是在收到客户端发来的消息后立即将其发回客户端。
public class EchoMessage extends WebSocketServlet {
private static final long rialVersionUID = 1L;
private volatile int byteBufSize;
private volatile int charBufSize;
@Override
public void init() throws ServletException {
super.init();