WebSocket
2013-06-08 16:54:49 阿炯

WebSocket是HTML5开始提供的一种浏览器与服务器间进行全双工通讯的网络技术,WebSocket通信协议于2011年被IETF定为标准 RFC 6455,WebSocket API被W3C定为标准。


在WebSocket API中,浏览器和服务器只需要要做一个握手的动作,然后浏览器和服务器之间就形成了一条快速通道,两者之间就直接可以数据互相传送。当然这不仅需要服务器的支持,浏览器对这种方式的支持是必须的,目前主流的(开源)浏览器对WebSocket都有很好的支持,像Firefox、Chrome等。



在WebSocket出现之前,我们有一些其它的实时通讯方案,比较常用的有轮询、长轮询、流,还有基于Flash的交换数据的方式,接下来我们分析总结一下各种通信方式的特点。

特点
(1)、建立在 TCP 协议之上,服务器端的实现比较容易。
(2)、与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
(3)、数据格式比较轻量,性能开销小,通信高效。
(4)、可以发送文本,也可以发送二进制数据。
(5)、没有同源限制,客户端可以与任意服务器通信。
(6)、协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。


1、轮询
这是最早的一种实现实时web应用的方案;原理比较简单易懂,就是客户端以一定的时间间隔向服务器发送请求,以频繁请求的方式来保持客户端和服务器端的数据同步。但是问题也很明显:当客户端以固定频率向服务器端发送请求时,服务器端的数据可能并没有更新,这样会带来很多无谓的请求,浪费带宽,效率低下。

2、长轮询
长轮询是对定时轮询的改进和提高,目地是为了降低无效的网络传输。当服务器端没有数据更新的时候,连接会保持一段时间周期直到数据或状态改变或者时间过期,通过这种机制来减少无效的客户端和服务器间的交互。当然,如果服务端的数据变更非常频繁的话,这种机制和定时轮询比较起来没有本质上的性能的提高。

3、流


WebSocket通过浏览器提供的API真正实现了具备像C/S架构下的桌面应用的实时通讯能力。其原理是使用JavaScript调用浏览器的API发出一个WebSocket请求至服务器,经过一次握手,和服务器建立了TCP通讯,因为其本质上是一个TCP连接,所以数据传输的稳定性强和数据传输量比较小。

WebSocket 协议基本原理
WebSocket 协议本质上是一个基于 TCP 的协议。为了建立一个 WebSocket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,其中附加头信息”Upgrade: WebSocket”表明这是一个申请协议升级的 HTTP 请求,服务器端解析这些附加的头信息然后产生应答信息返回给客户端,客户端和服务器端的 WebSocket 连接就建立起来了,双方就可以通过这个连接通道自由的传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接。


WebSocket 协议是浏览器和服务器之间建立起连接以后,只要双方愿意,这个连接是一直保持的,双方可以在任意时刻,任意向对方发送信息(以 frame 为基本单位,即 websocket 协议规定的),frame 携带的 payload 可以是文本,二进制数据等(将来可能会继续扩展),大的数据可以被分片(fragment)成多个 frame。然后双方可以友好的断开连接。所以 websocket 使得浏览器和服务器之间保持高效的和自定义可扩展的沟通成为了可能。另外要注意的是websocket是有状态的协议,而http是无状态的。

websocket约定了一个通信的规范,通过一个握手的机制,客户端和服务器之间能建立一个类似tcp的连接,从而方便它们之间的通信。在websocket出现之前,web交互一般是基于http协议的短连接或者长连接。websocket是一种全新的协议,不属于http无状态协议,协议名为"ws",这意味着一个websocket连接地址会是这样的写法:ws://*,websocket协议本质上是一个基于tcp的协议。为什么HTTP不是长连接,原因是早期的HTTP在发起每个请求,响应完成后就会关闭Socket。但是后来加入了多路复用KeepAlive协议后的HTTP协议已经可以实现长连接了,可以处理长连接事务了。至于添加WebSocket特性,是为了更好、更灵活、更轻量的与服务器通讯。因为WebSocket提供了简单的消息规范,可以更快的适应长连接的环境;其实现在HTTP协议自身就可以做,但实现起来还是相对复杂。


优点
1、Header
服务器与客户端之间交换的标头信息很小,大概只有2字节(早期版本)。

2、服务器推送
服务器可以主动传送数据给客户端。

3、握手协议
在实现websocket连线过程中,需要透过浏览器发出websocket连线请求,然后服务器发出回应,这个过程通常称为“握手” (handshaking)。
后期的版本大多属于功能上的扩充,例如使用第7版的握手协议同样也适用于第8版的握手协议。

WebSocket 效率
下图是来自websocket.org的测试结果:


在流量和负载增大的情况下,WebSocket 方案相比传统的 Ajax 轮询方案有极大的性能优势;而在开发方面,也十分简单,只需要实例化WebSocket,创建连接,查看是否连接成功,然后就可以发送和相应消息了。

Web实时推送

谈到Web实时推送,就不得不说WebSocket。在WebSocket出现之前,很多网站为了实现实时推送技术,通常采用的方案是轮询(Polling)和Comet技术,Comet又可细分为两种实现方式,一种是长轮询机制,一种称为流技术,这两种方式实际上是对轮询技术的改进,这些方案带来很明显的缺点,需要由浏览器对服务器发出HTTP request,大量消耗服务器带宽和资源。面对这种状况,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽并实现真正意义上的实时推送。

很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔,由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。

HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。当你获取 Web Socket 连接后,你可以通过 send() 方法来向服务器发送数据,并通过 onmessage 事件来接收服务器返回的数据。



WebSocket协议本质上是一个基于TCP的协议,它由通信协议和编程API组成,WebSocket能够在浏览器和服务器之间建立双向连接,以基于事件的方式,赋予浏览器实时通信能力。既然是双向通信,就意味着服务器端和客户端可以同时发送并响应请求,而不再像HTTP的请求和响应。

为了建立一个WebSocket连接,客户端浏览器首先要向服务器发起一个HTTP请求,这个请求和通常的HTTP请求不同,包含了一些附加头信息,其中附加头信息”Upgrade: WebSocket”表明这是一个申请协议升级的HTTP请求,服务器端解析这些附加的头信息然后产生应答信息返回给客户端,客户端和服务器端的WebSocket连接就建立起来了,双方就可以通过这个连接通道自由的传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接。

WebSocket的一些与HTTP相关的技术分析

1.WebSocket和HTTP的关系
WebSocket只有在建立握手连接的时候借用了HTTP协议的头,连接成功后的通信部分都是基于TCP的连接。总体来说,WebSocket协议是HTTP协议的升级版。

2.研究WebSocket的思路
服务器端自己实现WebSocket非常复杂。我们虽然不求能够完全自己实现,但是还是应该了解一下后端实现WebSocket的整体思路。WebSocket的实现主要分为两个部分:建立连接(握手)和数据传输,下面将对这两个过程分别进行分析。

3.建立连接
(1)客户端发送请求
WebSocket协议的实现首先需要客户端和服务器进行握手连接。首先客户端向服务器发送请求,请求报文中的重点内容如下:
GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

从GET处可以看出,这是一个基于HTTP的请求。接下来的Upgrade字段和Connection字段完成了对HTTP协议的升级(HTTP Upgrade Request)。Upgrade字段通知服务器,现在要使用一个升级版协议——websocket。接下来是Sec-WebSocket-Key字段,这个字段是一串生成的BASE64加密的密钥,它被一同发送到服务器端。

此外还有诸如Sec-WebSocket-Version、Sec-WebSocket-Protocol等字段,由于是可选字符,对WebSocket协议的实质影响不大,同时为了文章更简洁更容易理解,先不进行介绍。

(2)服务器端进行处理
服务器收到客户端请求后要进行响应。首先服务器需要处理客户端传递过来的Sec-WebSocket-Key。服务器端有一个全局唯一标识符GUID,这个是固定的。服务器端将客户端传来的字符串和服务器端的GUID拼接到一起后进行SHA1处理,再进行一次BASE64加密,准备将其返回客户端。

(3)服务器端返回响应
服务器端处理完成后,将给客户端返回响应报文,重要部分如下:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

第一行再次表明此次连接的建立是以HTTP协议为基础的,同时返回了101状态码。101状态码是switching protocols,表示服务器已经理解了客户端的请求,并将通过Upgrade子段通知客户端采用WebSocket协议来完成这个请求。那么协议是什么时候升级到WebSocket的呢?当发送完这个响应最后的空行后,服务器就会切换到Upgrade消息头定义的WebSocket协议。至此完成了从HTTP协议升级的WebSocket协议的过程。同时需要注意,如果返回的状态码不是101,就表示握手升级的过程失败了。

中间两行就不用说了,最后一行Sec-WebSocket-Accept返回的就是服务器端处理后的字符串。只有返回了这个字符串才表明握手成功了,返回其它的字符串都表示握手失败。


对上面的分析展开可做如下的分析

WebSocket协议请求头中请求头中重要的字段:
Connection:Upgrade
Upgrade:websocket
Sec-WebSocket-Extensions:permessage-deflate; client_max_linux_bits
Sec-WebSocket-Key:mg8LvEqrB2vLccCNnCJV3Q==
Sec-WebSocket-Version:13


1. Connection和Upgrade字段告诉服务器,客户端发起的是WebSocket协议请求
2. Sec-WebSocket-Extensions表示客户端想要表达的协议级的扩展
3. Sec-WebSocket-Key是一个Base64编码值,由浏览器随机生成
4. Sec-WebSocket-Version表明客户端所使用的协议版本

而得到的响应头中重要的字段:
Connection:Upgrade
Upgrade:websocket
Sec-WebSocket-Accept:AYtabcampsFjE0lu3kFQrmOCzLQ=


1. Connection和Upgrade字段与请求头中的作用相同
2. Sec-WebSocket-Accept表明服务器接受了客户端的请求

Status Code:101 Switching Protocols
表示http请求完成后响应的状态码为101,指示切换了协议,说明WebSocket协议要通过http协议来建立运输层的TCP连接,之后便与http协议无关了。


4.数据传输
WebSocket的数据帧格式如下:



      0                   1                   2                   3
      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     +-+-+-+-+-------+-+-------------+-------------------------------+
     |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
     |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
     |N|V|V|V|       |S|             |   (if payload len==126/127)   |
     | |1|2|3|       |K|             |                               |
     +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
     |     Extended payload length continued, if payload len == 127  |
     + - - - - - - - - - - - - - - - +-------------------------------+
     |                               |Masking-key, if MASK set to 1  |
     +-------------------------------+-------------------------------+
     | Masking-key (continued)       |          Payload Data         |
     +-------------------------------- - - - - - - - - - - - - - - - +
     :                     Payload Data continued ...                :
     + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
     |                     Payload Data continued ...                |
     +---------------------------------------------------------------+


这里需要对数据帧进行一个解析,需要返回一个由键值对组成对象,这里先不对解析数据帧的方法进行解析。这个对象就是解析后的数据帧,里面有各个字段以及对应的值。对每个字段的含义如下:
FIN      表示信息的最后一帧,flag,也就是标记符
RSV 1-3  以后备用的,默认都为 0
Opcode   帧类型
Mask     掩码,表示是否加密数据,默认必须置为1
Payload  数据的长度
Masking-key   掩码
Payload data  数据
Extension data   扩展数据
Application data 程序数据

将数据帧解析之后,生成的就是上面各个字段加其对应的值。这里面的重点部分是Opcode和Payload data字段。Opcode表示帧类型,每次用户代理接收到数据包时,都要先对Opcode进行判断。Opcode的状态值及其对应含义列表如下:
0    Continuation Frame
1    Text Frame
2    Binary Frame
8    Connection Close Frame
9    Ping Frame
10    Pong Frame

判断了Opcode后,根据Opcode的具体状态,决定如何对PayLoad data的数据进行解析,从而进行数据传输。


As of the WebSocket version specified by the RFC, there's only a header in front of each packet. It's quite a complex header, however. Here are its building blocks explained:

fin (1 bit): indicates if this frame is the final frame that makes up the message. Most of the time the message fits into a single frame and this bit will always be set. Experiments show that Firefox makes a second frame after 32K.

rsv1, rsv2, rsv3 (1 bit each): must be 0 unless an extension is negotiated that defines meanings for non-zero values. If a nonzero value is received and none of the negotiated extensions defines the meaning of such a nonzero value, the receiving endpoint must fail the connection.

opcode (4 bits): says what the frame represents. The following values are currently in use:
    0x00: this frame continues the payload from the previous.
    0x01: this frame includes text data.
    0x02: this frame includes binary data.
    0x08: this frame terminates the connection.
    0x09: this frame is a ping.
    0x0a: this frame is a pong.
    (As you can see, there are enough values unused; they've been reserved for future use).

mask (1 bit): indicates if the connection is masked. As it stands right now, every message from a client to a server must be masked and the spec would want to terminate the connection if it's unmasked.

payload_len (7 bits): the length of the payload. WebSocket frames come in the following length brackets:
    0–125 indicates the length of the payload. 126 means that the following two bytes indicate the length, 127 means the next 8 bytes indicate the length. So the length of the payload comes in ~7bit, 16bit, and 64bit brackets.

masking-key (32 bits): all frames sent from the client to the server are masked by a 32-bit value that is contained within the frame.

payload: the actual data which most likely is masked. Its length is the length of the payload_len.


WebSocket 服务端的实现

目前还是有不少的实现,具体可以查看维基百科所提供的列表。Node 实现的 Socket.IO 还是不错的:Socket.IO是一个开源的WebSocket库,它通过Node.js实现WebSocket服务端,同时也提供客户端JS库。它支持以事件为基础的实时双向通讯,它可以工作在任何平台、浏览器或移动设备。支持4种协议:WebSocket、htmlfile、xhr-polling、jsonp-polling,它会自动根据浏览器选择适合的通讯方式,从而让开发者可以把精力放到功能的实现而不是平台的兼容性上,同时具有不错的稳定性和性能。


WebSocket 客户端的实现

WebSocket 的用法相当简单,下面是一个网页脚本的例子。
var ws = new WebSocket("wss://echo.websocket.org");
ws.onopen = function(evt) {
 console.log("Connection open ...");
 ws.send("Hello WebSockets!");
};

ws.onmessage = function(evt) {
 console.log( "Received Message: " + evt.data);
 ws.close();
};

ws.onclose = function(evt) {
 console.log("Connection closed.");
};      


WebSocket 客户端的 API 如下:

1、WebSocket 构造函数
WebSocket 对象作为一个构造函数,用于新建 WebSocket 实例。
var ws = new WebSocket('ws://localhost:8080');

执行上面语句之后,客户端就会与服务器进行连接。

实例对象的所有属性和方法清单参见此处

2、webSocket.readyState
readyState属性返回实例对象的当前状态,共有四种:
CONNECTING:值为0,表示正在连接。
OPEN:值为1,表示连接成功,可以通信了。
CLOSING:值为2,表示连接正在关闭。
CLOSED:值为3,表示连接已经关闭,或者打开连接失败。

下面是一个示例
switch (ws.readyState) {
  case WebSocket.CONNECTING:
// do something
break;
  case WebSocket.OPEN:
// do something
break;
  case WebSocket.CLOSING:
// do something
break;
  case WebSocket.CLOSED:
// do something
break;
  default:
// this never happens
break;
}

3、webSocket.onopen
实例对象的onopen属性,用于指定连接成功后的回调函数。
ws.onopen = function(){
  ws.send('Hello Server!');
}

如果要指定多个回调函数,可以使用addEventListener方法。
ws.addEventListener('open', function (event){
  ws.send('Hello Server!');
});

4、webSocket.onclose
实例对象的onclose属性,用于指定连接关闭后的回调函数。
ws.onclose = function(event){
  var code = event.code;
  var reason = event.reason;
  var wasClean = event.wasClean;
  // handle close event
};
ws.addEventListener("close", function(event) {
  var code = event.code;
  var reason = event.reason;
  var wasClean = event.wasClean;
  // handle close event
});

5、webSocket.onmessage
实例对象的onmessage属性,用于指定收到服务器数据后的回调函数。
ws.onmessage = function(event){
  var data = event.data;
  // 处理数据
};
ws.addEventListener("message", function(event){
  var data = event.data;
  // 处理数据
});

注意,服务器数据可能是文本,也可能是二进制数据(blob对象或Arraybuffer对象)。

ws.onmessage = function(event){
  if(typeof event.data === String){
    console.log("Received data string");
}
  if(event.data instanceof ArrayBuffer){
    var buffer = event.data;
    console.log("Received arraybuffer");
  }
}

除了动态判断收到的数据类型,也可以使用binaryType属性,显式指定收到的二进制数据类型。
// 收到的是 blob 数据
ws.binaryType = "blob";
ws.onmessage = function(e){
  console.log(e.data.size);
};
// 收到的是 ArrayBuffer 数据
ws.binaryType = "arraybuffer";
ws.onmessage = function(e) {
  console.log(e.data.byteLength);
};

6、webSocket.send()
实例对象的send()方法用于向服务器发送数据,下面为发送文本的例子。
ws.send('your message');

发送 Blob 对象的例子。
var file = document.querySelector('input[type="file"]').files[0];
ws.send(file);

发送 ArrayBuffer 对象的例子。
// Sending canvas ImageData as ArrayBuffer
var img = canvas_context.getImageData(0, 0, 400, 320);
var binary = new Uint8Array(img.data.length);
for (var i = 0; i < img.data.length; i++){
  binary[i] = img.data[i];
}
ws.send(binary.buffer);

7、webSocket.bufferedAmount
实例对象的bufferedAmount属性,表示还有多少字节的二进制数据没有发送出去,可以用来判断发送是否结束。
var data = new ArrayBuffer(10000000);
socket.send(data);
if (socket.bufferedAmount === 0) {
  // 发送完毕
} else {
  // 发送还没结束
}

8、webSocket.onerror
实例对象的onerror属性,用于指定报错时的回调函数。
socket.onerror = function(event) {
  // handle error event
};
socket.addEventListener("error", function(event) {
  // handle error event
});

WebSocket 属性

以下是 WebSocket 对象的属性。假定我们使用了以上代码创建了 Socket 对象:

属性描述
Socket.readyState

只读属性 readyState 表示连接状态,可以是以下值:

  • 0 - 表示连接尚未建立。

  • 1 - 表示连接已建立,可以进行通信。

  • 2 - 表示连接正在进行关闭。

  • 3 - 表示连接已经关闭或者连接不能打开。

Socket.bufferedAmount

只读属性 bufferedAmount 已被 send() 放入正在队列中等待传输,但是还没有发出的 UTF-8 文本字节数。

WebSocket 事件

以下是 WebSocket 对象的相关事件。假定我们使用了以上代码创建了 Socket 对象:

事件事件处理程序描述
openSocket.onopen连接建立时触发
messageSocket.onmessage客户端接收服务端数据时触发
errorSocket.onerror通信发生错误时触发
closeSocket.onclose连接关闭时触发

WebSocket 方法

以下是 WebSocket 对象的相关方法。假定我们使用了以上代码创建了 Socket 对象:

方法描述
Socket.send()

使用连接发送数据

Socket.close()

关闭连接