应用 HTML5 的 WebSocket 实现 BiDirection 数据交换

IBMdW 发布于 2012/01/04 20:43
阅读 2K+
收藏 29

【开源中国 APP 全新上线】“动弹” 回归、集成大模型对话、畅读技术报告”

HTML5 是新一代的 Web 标准。虽然 HTML5 标准还没有最终确定但是它已然成为主流,各大厂商都开始提前实现其草案中的功能。HTML5 提供了很多新特征,比如 Canvas、离线存储、多线程、视频标签等等。其中一个很重要的特征是 WebSocket,它提供了一种双向全双工的服务器和客户端通信的功能。WebSocket 现对于以前的技术实现方案来说,有着本质的不同。它是原生的支持双向通信的 B/S 应用协议,有着多种优势。

WebSocket 的优势:

  1. 它可以实现真正的实时数据通信。众所周知,B/S 模式下应用的是 HTTP 协议,是无状态的,所以不能保持持续的链接。数据交换是通过客户端提交一个 Request 到服务器端,然后服务器端返回一个 Response 到客户端来实现的。而 WebSocket 是通过 HTTP 协议的初始握手阶段然后升级到 Web Socket 协议以支持实时数据通信。
  2. WebSocket 可以支持服务器主动向客户端推送数据。一旦服务器和客户端通过 WebSocket 建立起链接,服务器便可以主动的向客户端推送数据,而不像普通的 web 传输方式需要先由客户端发送 Request 才能返回数据,从而增强了服务器的能力。
  3. WebSocket 协议设计了更为轻量级的 Header,除了首次建立链接的时候需要发送头部和普通 web 链接类似的数据之外,建立 WebSocket 链接后,相互沟通的 Header 就会异常的简洁,大大减少了冗余的数据传输。

WebSocket 提供了更为强大的通信能力和更为简洁的数据传输平台,能更为方便的完成 Web 开发中的双向通信功能。

WebSocket 和 Ajax 的技术实现和流量分析

普通 HTTP 请求方式

一般情况下,通过浏览器访问一个网页,需要浏览器发送一个 HTTP Request,服务器接收到浏览器的请求,返回相应的消息。在一些数据更新比较频繁的应用里,页面的数据要想得到最新的结果需要重新刷新页面,但这样会 产生大量的冗余数据在服务器和客户端传输,另外由于页面是同步处理的,所以在页面加载完毕之前是不能继续操作的。这样会阻塞用户的动作,显然不是一个好的 解决方案。

异步传输方式

随着技术发展,后来出现了新的技术方案,即 Ajax 技术。Ajax 全称是 Asynchronous JavaScript and XML,即异步 JavaScript 和 XML。它的核心是 XMLHttpRequest 技术,通过 XMLHttpRequest 对象可以向服务器提交异步请求,服务器可以将数据以异步方式返回给客户端,通过 Javascript 局部的更新页面,不会阻塞当前用户操作,这样的解决方案相对于前面的来说用户体验提高了很多。这样可以通过 JavaScript 使客户端不断地向服务器发送异步请求,并用返回的数据刷新局部页面来达到“实时”的数据更新。

但是这样的方案也有问题,因为有些情况下,服务器端的数据更新间隔我们是不能预知的,通常我们都是在客户端设定一定的时间间隔去服务端请求数 据,即所谓的轮询技术。但是如果服务端数据的更新间隔小于我们设定的频率,那么就会有一些数据取不到。而如果服务端数据的更新间隔大于我们设定的频率,那 么就会有冗余的数据传输。

长轮询技术

鉴于这些缺点,有一些改进的方案应运而生,Comet 就是其中的代表。

Comet 技术被称为(long-polling)长轮询技术,它改变了服务器和客户端的交互方式的。首先由客户端发出请求,服务器接收到请求后并不一定立即返回,而是等到有数据更新时才返回或者直到连接超时。这样就不会出现冗余的数据请求。


图 1. Comet 技术工作方式
图 1. Comet 技术工作方式

通常,为了模拟基于半双工 HTTP 上的全双工通信,目前的许多解决方案都使用了两个连接:一个下行连接,一个上行连接。一方面,维护和协调这两个连 接需要大量的系统开销,并增加了复杂性。另一方面,还给网络负载带来了很大压力。

HTTP 请求数据

下面是一次 Ajax 请求的传输数据:


清单 1. HTTP 请求
				
 var worker = new Worker(dedicated.js'); 
 Host:www.demo.com 
 User-AgentMozilla/5.0 (Windows NT 5.1; rv:5.0) Gecko/20100101 Firefox/5.0 
 Accept:*/* 
 Accept-Language:en-us,en;q=0.5 
 Accept-Encoding:gzip, deflate 
 Accept-Charset:ISO-8859-1,utf-8;q=0.7,*;q=0.7 
 Connection:keep-alive 
 Referer:http://www.demo.com/news/12365-demo-Chrome 
 Cookie:_application_cookie_id_=1302165217141933; 
 __utma=185941238.840487749.1311923297.1315056086.1315210256.36; 
 __utmz=185941238.1315210256.36.42.utmcsr=search| 
 utmccn=(organic)|utmcmd=organic|utmctr=websocket%20%C0%FD%D7%D3; 
 __utmc=432143214.765498335.268654276.1565587542.4468744325.36; 
 lzstat_uv=37479629961521195708|2366120@796778; 
 ldsetat_ac=54325299615432195708|235432320@74321778; 
 _application3_session_=BAh7BjoPc2Vzc2lvbl9pZCIlMmM2ZTIyYjhmMmQ3 
 ZTUyNDI2NTRlNTc1YzZjOGYwOWY%3D-- 
 fb0c80e3bb59c54f4a5080652a6e1f0addccf4e0; __utmc=185941238 

WebSocket 请求数据

而 WebSocket 与上述方法不同,它首先通过客户端和服务器在初始握手阶段从 HTTP 协议升级到 WebSocket 协议。握手分为两个阶段,首先是客户端请求:


清单 2. WebSocket 请求
				
 worker.onmessage = function (event) { ... }; 
 GET /demo HTTP/1.1 
 Host: example.com 
 Connection: Upgrade 
 Sec-WebSocket-Key2: 12998 5 Y3 1  .P00 
 Sec-WebSocket-Protocol: sample 
 Upgrade: WebSocket 
 Sec-WebSocket-Key1: 4 @1  46546xW%0l 1 5 
 Origin: http://example.com 
 ^n:ds[4U 

然后是服务器响应:


清单 3. 服务器响应
				
 HTTP/1.1 101 WebSocket Protocol Handshake 
 Upgrade: WebSocket 
 Connection: Upgrade 
 Sec-WebSocket-Origin: http://example.com 
 Sec-WebSocket-Location: ws://example.com/demo 
 Sec-WebSocket-Protocol: sample 
 8jKS'y:G*Co,Wxa- 

在这个请求串中,“Sec-WebSocket-Key1”, “Sec-WebSocket-Key2”和最后的“^n:ds[4U”都是随机的,服务器端会用这些数据来构造出一个 16 字节大小的应答结果。

把第一个 Key 中的数字除以第一个 Key 的空白字符的数量,而第二个 Key 也是如此。然后把这两个结果与请求最后的 8 字节字符串连接起来成为一个字符串,服务器应答正文(“8jKS ’ y:G*Co,Wxa-”)即这个字符串的 MD5 sum。

在建立起 WebSocket 连接之后,服务器和客户端的通信消息头就变的非常简洁了,整个消息头只有仅仅两个字节,以“0x00 ″开头以” 0xFF”结尾,中间传输的数据采用 UTF-8 格式。

数据传输量对比

假设以下场景,如果我们以每秒一次的频率去服务器拉取数据的话,无论每次传输的数据有多少,图 2 中的 HTTP 协议头都必须要传送,这在用户量不太大的情况下还不是太糟糕,但是在多用户的场景下,便会给网络带来的巨大的负载。不同的应用传输的头信息不尽相同,以上 面提到的传输数据为例,假设一次数据请求传输过程中数据头大小为 800Byte,我们来看一下在不同的用户量的条件下,冗余数据的增长情况。


图 2. 数据传输量对比
图 2. 数据传输量对比

由图 2 看以看出,在用户规模大的情况下,情况会变得的愈加恶劣,即使我们把轮询技术改成 comet 技术,也只能减少小部分重复数据的轮询数据,而且 comet 技术还会给服务器带来额外的消耗。所以,使用 WebSocket 技术可以让我们的在大规模的应用场景下减少大量的冗余数据带宽消耗,而且用户规模越大,它所带来的优势就越明显。

WebSocket 的操作简介

1. WebSocket API 的定义和接口介绍

WebSocket 为浏览器端提供了简洁的操作接口,用户可以方便实现与服务器的信息沟通。首先我们先看一下 WebSocket 接口的定义:


清单 4. WebSocket API
				
 [Constructor(DOMString url, optional DOMString protocols), 
 Constructor(DOMString url, optional DOMString[] protocols)] 
 interface WebSocket : EventTarget { 
  readonly attribute DOMString url; 

  // ready state 
  const unsigned short CONNECTING = 0; 
  const unsigned short OPEN = 1; 
  const unsigned short CLOSING = 2; 
  const unsigned short CLOSED = 3; 
  readonly attribute unsigned short readyState; 
  readonly attribute unsigned long bufferedAmount; 

  // networking 
   attribute Function onopen; 
   attribute Function onerror; 
   attribute Function onclose; 
  readonly attribute DOMString extensions; 
  readonly attribute DOMString protocol; 
  void close([Clamp] optional unsigned short code, optional DOMString reason); 

  // messaging 
   attribute Function onmessage; 
   attribute DOMString binaryType; 
  void send(DOMString data); 
  void send(ArrayBuffer data); 
  void send(Blob data); 
 }; 

可以看到,WebSocket API 接口非常简洁。

构造函数 WebSocket(url, protocols) 有两个参数,参数 url 指定了要连接的 URL 地址。参数 protocols 是可选参数,可以为字符串或字符串数组,指定了连接子协议。连接过程的状态保存在 readyState 属性中:CONNECTING、OPEN、CLOSING 和 CLOSED,分别代表了连接过程中的正在连接状态、已连接状态、正在关闭状态和连接已关闭状态。通过 send 方法可以通过建立起来的连接传输数据。其参数可以为 String、Blob 对象或 ArrayBuffer 对象。close 方法会把 readyState 状态设为设为 CLOSING 从而触发连接关闭事件。另外接口还定义了相应的事件处理器:onopen、onerror、onmessage 和 onclose 来响应服务器的事件。

2. 操作 WebSocket API

下面简单介绍一下介绍如何利用 WebSocket 提供的 API 向 Server 发送信息和接收 Server 的消息。

我们可以通过下面一个简单的语句来创建一个 Socket 实例:


清单 5. 创建 WebSocket 实例
				
 var socket = new WebSocket('ws://localhost:8080'); 

需要注意的是,这个里的 url 不是以 http 开始的,而是 ws 开始的,因为我们使用的是 WebSocket 协议而不是普通的 HTTP 协议。

然后,我们需要定义 socket 对象的相应事件来对其作出处理


清单 6. 定义 Socket 打开时的回调方法
				
 socket.onopen = function(event) { 
  // 向服务器发送消息
  socket.send('Hello Server!'); 
 } 


清单 7. 定义消息接收回调方法
				
 socket.onmessage = function(event) { 
 alert(“Got a message from server!”); 
 }; 

onmessage 事件提供了一个 data 属性,它可以包含消息的 Body 部分,通过 event.data 可以取到。消息的 Body 部分必须是一个字符串,可以进行序列化 / 反序列化操作,以便传递更多的数据。


清单 8. Socket 关闭回调方法
				
 socket.onclose = function(event) { 
 console.log('Client notified socket has closed',event); 
 }; 


清单 9. 关闭 Socket 方法
				
 socket.close() ; 

WebSocket API 的设计比较精炼,只需要简单地定义几个回调方法就可以轻易操纵 WebSocket 数据传输,这给我们带来的一定的便利性。

3 .浏览器支持能力检测

由于现在的 HTML5 的标准还没有完成,目前只有一部分浏览器针对 HTML5 进行了支持,而且都没有完全实现 HTML5 的新功能的支持。为了应用 HTML5 的 WebSocket 功能,我们需要检测浏览器是否对其进行支持。


清单 10. 检测浏览器支持状态
				
 if (!window.WebSocket) { 
 alert("WebSocket not supported by this browser!"); 
 } 

WebSocket 的应用示例

WebSocket 适用于需要实时通信的场景,例如实时信息监控,即时消息传递,网页游戏,股票信息推送等等,下面我们来看一个基于 WebSocket 的在线聊天室的例子。

Server 端程序设计


清单 11. Server 端程序设计
				
public class WSChatRoom extends WebSocketServlet{
  private final List clients;
  //构造存放所有已连接的客户端
  public WSChatRoomServlet()
  {
	this.clients = new ArrayList();
  }
   public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol)
  {
	return new WebSocketChat();
  }
  //嵌套类
  class ChatSocket
	implements WebSocket.OnTextMessage
  {
	WebSocket.Connection connection;
	//当连接建立的时候将此ChatSocket对象添加进当前连接客户端列表
	public void onOpen(WebSocket.Connection con)
	{
	  this.connection = con;
	  WSChatRoom.this.clients.add(this);
	}

	//处理客户端发送的消息
	public void onMessage(String data){
	//循环当前连接的所有客户端
		for (ChatWebSocket member : WSChatRoomServlet.this.clients)
		{
		  try
		  {
		//向所有客户端发送信息
			clients.connection.sendMessage(data);
		  }catch (IOException e)
		  {
			Logger.error(e);
		  }
		}
	}
	//关闭连接的时候从集合中删除该客户客户端
	public void onClose(int code, String message)
	{
	  WebSocketChatServlet.this._members.remove(this);
	}
  }
}

这段程序采用 Java 语言,实现了服务器端的处理逻辑。测试服务器采用的 Jetty 7.4.5,程序中 WebSocket 相关的类引用自 jetty-websocket-7.4.5.v20110725.jar,这是 Jetty 服务器自带的包。

程序比较简单,首先通过 doWebSocketConnect 方法返回一个 ChatWebSocket 对象,然后通过实现 onMessage 方法并用循环逐一将收到的消息逐一发送到各个客户端。最后通过 onOpen 和 onClose 来处理与每一客户端建立连接和关闭连接的事件。

由于 HTML5 的标准还没有最终确定,所以现在各个厂商对服务器端的实现都不尽相同,而且只有一部分厂商对 HTML5 中的 WebSocket 进行了支持,例如 Jetty 7.0 以上的版本,resin 4.0.2 以上版本以及 pywebsocket 等等。不过随着时间的推移,会有越来越多的服务器支持 WebSocket 协议。

客户端程序设计


清单 12. 客户端
				
 <script type="text/javascript">
	if (!window.WebSocket) {
		alert("此浏览器不支持WebSocket!");
	}
	var username = '';
	var socket = null;
	//初始化用户界面
	function open() {
		getObject('status').value = '开启';
		getObject('joinDiv').className = 'hidden';
		getObject('joined').className = '';
		getObject('words').focus();
		send(username, '进入聊天室!');
	}
	//建立WebSocket连接并绑定相应的事件处理方法
	function join(name) {
		username = name;
		socket = new WebSocket('ws://localhost:8080/ws/');
		socket.onmessage = getMsg;
		socket.onopen = open;
		socket.onclose = close;
	}
	//处理服务器返回的数据
	function getMsg(event) {
		if (event.data) {
			var c = event.data.indexOf(':');
			var from = event.data.substring(0, c).replace('<', '<').replace(
				'>', '>');
			var text = event.data.substring(c + 1).replace('<', '<')
					.replace('>', '>');
			var chat = getObject('chat');
			var spanFrom = document.createElement('span');
			spanFrom.className = 'from';
			spanFrom.innerHTML = from ;
			var spanText = document.createElement('span');
			spanText.className = 'text';
			spanText.innerHTML = text;
			var lineBreak = document.createElement('br');
			chat.appendChild(spanFrom);
			chat.appendChild(spanText);
			chat.appendChild(lineBreak);
			chat.scrollTop = chat.scrollHeight - chat.clientHeight;
		}
	}
	//连接关闭后的处理
	function close(event) {
		socket = null;
		getObject('joinDiv').className = '';
		getObject('joined').className = 'hidden';
		getObject('username').focus();
		getObject('chat').innerHTML = '';
		getObject('status').value = '关闭';
	}
	//向服务器发送数据
	function send(user, message) {
		user = user.replace(':', '_');
		if (socket) {
			socket.send(user + ':' + message);
		}
	}
	function chat(text) {
		if (text != null  && text.length > 0) {
			send(username, text);
		}
	}
	function getObject() {
		return document.getElementById(arguments[0]);
	}
	function getValue() {
		return document.getElementById(arguments[0]).value;
	}
</script>
</head>
	<div id="chat"></div>
	<div id="input">
	<div id="joinDiv">
			昵称: <input id="username" type="text"> <input
            id="joinButton" class="button" type="submit" name="joinButton"
				value="进入聊天室">
		</div>
		<div id="joined" class="hidden">
			聊天: <input id="words" type="text"> <input
            id="sendButton" class="button" type="submit" name="sendButton"
				value="发送">
		</div>
		<div id="statusDiv">
			WebSocket状态:<input id="status" type="text" readonly="true"
				value="未开启">
		</div>
	</div>
	<script type="text/javascript">
		getObject('joinButton').onclick = function(event) {
			join(getValue('username'));
			return false;
		};
		getObject('sendButton').onclick = function(event) {
			chat(getValue('words'));
			getObject('words').value = '';
			return false;
		};
	</script>

操作 WebSocket 的客户端代码也很简单。核心代码只有以下几点:

首先,我们需要在客户端创建一个 WebSocket 对象 socket = new WebSocket('ws://localhost:8080/ws/');。这样就会请求与服务器建立 WebSocket 连接。

当打开连接时调用的方法:function open(),并将其绑定到 onopen 事件。当连接建立起来后这个方法会被调用,我们在此方法中完成界面的初始化操作。

关闭连接时调用的方法:function close(event) 并将其绑定到 onclose 事件。这是连接关闭时的回调方法,在此方法中可以放置一些连接关闭后的处理工作。

通过 send 方法向服务器发送消息:function send(user, message)。这是最重要的接口方法之一,用来完成向服务器发送数据的功能。

接收服务器发送来的消息:function getMsg(event),并将其绑定到 onmessage 事件。当服务器返回数据时便可以做相应的处理。

我们可以看到,应用 WebSocket 比以前的 Ajax 方案更加方便简洁,只需要简单的处理相应的回调方法即可完成相应的通信功能。

Dojo WebSocket 应用

由于目前不是所有的浏览器都支持 WebSocket,所以对于不支持 WebSocket 的浏览器我们需要提供相应的替代方案。Dojo 1.6 为我们完成了这一工作,它提供了简洁的调用接口,并能自动的检测浏览器对 WebSocket 的支持能力来调用不同的实现技术。

在 Dojo 1.6 中提供了一个基于 WebSocket API 开发的可以进行实时通信的 Dojo socket API, 利用 HTML5 中 WebSocket 提供的一种支持全双工通信的对象,可以非常方便地实时地将消息从服务端直接发送到客户端。

与直接的 WebSocket 调用类似,您可以通过如下语句与服务器建立 WebSocket 连接:


清单 13. 建立 Dojo WebSocket 连接
				
 var socket = dojox.socket( "ws://localhost:8080/ws" ) ; 

或者


清单 14. 建立 Dojo WebSocket 连接
				
 var socket = dojox.socket ( {   
 url:" ws://localhost:8080/ws " ,   
 headers: {   
"Accept" : "application/json" ,   
"Content-Type" : "application/json"  
 } } ) ; 

Dojo WebSocket 可以自动判断浏览器是否支持 WebSocket,以选择是应用 WebSocket 模式还是转换成 HTTP/long-polling 模式。Dojo WebSocket 模块屏蔽了各种不同的 long-polling 不同的实现方式,为其提供了统一的接口。

可以通过 socket.connect() 或者 socket.on() 方法注册处理函数。这两个方法可以通用,socket.on() 方法其实是 socket.connect() 方法的别名。


清单 15. 连接建立回调方法
				
 socket.connect( "open" , function ( event) { 
  alert(“Socket connected”); 
 } ) ; 

当然,我们也可以像以前一样直接定义回调方法:


清单 16. 回调方法定义
				
 socket.onopen=function{ 
 alert(“Socket connected”); 
 } 


清单 17. 接收服务器消息处理方法
				
 socket.connect ( "message" , function ( event) {   
  var data = event.data ;   
  alert(data); 
 } ) ; 


清单 18. 连接关闭
				
 socket.connect ( "close" , function ( event) {   
  alert(“Socket closed”);
 } ) ; 

我们同样可以利用 socket 的 send() 方法发送消息给服务器,调用 socket 的 close() 方法关闭连接。

另外,Dojo 框架还提供了一个 Reconnect 模块,它给 dojox.socket 进行了增强,使其可以实现自动重连的功能。当 WebSocket 连接关闭时,它会自动重新连接服务器,可以容易的通过下面的语句将一个普通的 socket 增强为带有自动连接功能的 socket。


清单 19. Reconnect
				
 socket = dojox.socket.Reconnect(socket); 

我们可以把上面的例子改成用 Dojo WebSocket 的 Reconnect 方式,将


清单 20. WebSocket
				
 socket =new WebSocket('ws://localhost:8080/ws/'); 

改成


清单 21. Reconnect WebSocket
				
 socket = dojox.socket('ws://localhost:8080/ws'); 
 socket = dojox.socket.Reconnect(socket); 

另外把 function close(event) 方法去掉,因为程序已经可以自动重新建立连接。这时聊天室程序便实现了自动重连功能,经过测试,此程序可以保持一直在线而不是像原先的程序一样过一段时间无操作就会断开连接。

总结

HTML5 的 WebSocket 机制为新一代的 Web 开发提供了良好的数据通信基础,利用它可以实现全双工的信息交换,而且服务器可以主动的向 Server 推送数据。利用 WebSocket 提供的方便的接口可以方便的实现服务器和客户端的实时数据传输。它相对于用 Ajax 轮询技术或 Comet“服务器推”技术实现的“实时”通信方式来说,它利用了精简的协议设计和头定义提供了原生的 biDirection 数据通信和更为节省的数据传送量,减轻了网络负载。

文章出处: IBM developerWorks

加载中
0
我土鳖

websocket本身的协议还在完善中。这对推广它的使用造成了一定障碍。

RFC出来真是太棒了!现在推广websocket的使用正合适

0
LongRaindy
LongRaindy
好文就是要MARK。
0
Endiya
Endiya
Mark之,以留备用。
0
悟我
悟我
Mark之!
0
恐龙让梨
恐龙让梨
好文,Mark~
0
TongAlan
TongAlan
Flash的前途正如applet一样
wangxi得
wangxi得
不行的
繁华似水
繁华似水
flash如果在3D上寻找到突破口,未必是条死路。applet就不表示说明了
OSCHINA
登录后可查看更多优质内容
返回顶部
顶部