# WebSocket

网道（WangDoc.com），互联网文档计划

WebSocket 是一种网络通信协议，很多高级功能都需要它。

初次接触 WebSocket 的人，都会问同样的问题：我们已经有了 HTTP 协议，为什么还需要另一个协议？它能带来什么好处？

答案很简单，因为 HTTP 协议有一个缺陷：通信只能由客户端发起。举例来说，我们想了解今天的天气，只能是客户端向服务器发出请求，服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。HTTP 协议的这种单向请求的特点，注定了如果服务器有连续的状态变化，客户端要获知就非常麻烦。我们只能使用“轮询”：每隔一段时候，就发出一个询问，了解服务器有没有新的信息。最典型的场景就是聊天室。

轮询的效率低，非常浪费资源（因为必须不停连接，或者 HTTP 连接始终打开）。因此，工程师们一直在思考，有没有更好的方法。WebSocket 就是这样发明的。

## 简介 #

WebSocket 协议在2008年诞生，2011年成为国际标准。所有浏览器都已经支持了。

它的最大特点就是，服务器可以主动向客户端推送信息，客户端也可以主动向服务器发送信息，是真正的双向平等对话，属于服务器推送技术的一种。WebSocket 允许服务器端与客户端进行全双工（full-duplex）的通信。举例来说，HTTP 协议有点像发电子邮件，发出后必须等待对方回信；WebSocket 则是像打电话，服务器端和客户端可以同时向对方发送数据，它们之间存着一条持续打开的数据通道。

其他特点包括：

（1）建立在 TCP 协议之上，服务器端的实现比较容易。

（2）与 HTTP 协议有着良好的兼容性。默认端口也是80和443，并且握手阶段采用 HTTP 协议，因此握手时不容易屏蔽，能通过各种 HTTP 代理服务器。

（3）数据格式比较轻量，性能开销小，通信高效。

（4）可以发送文本，也可以发送二进制数据。

（5）没有同源限制，客户端可以与任意服务器通信，完全可以取代 Ajax。

（6）协议标识符是`ws`（如果加密，则为`wss`，对应 HTTPS 协议），服务器网址就是 URL。

    ws://example.com:80/some/path

## WebSocket 握手 #

浏览器发出的 WebSocket 握手请求类似于下面的样子：

    GET / HTTP/1.1
    Connection: Upgrade
    Upgrade: websocket
    Host: example.com
    Origin: null
    Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
    Sec-WebSocket-Version: 13

上面的头信息之中，有一个 HTTP 头是`Upgrade`。HTTP1.1 协议规定，`Upgrade`字段表示将通信协议从`HTTP/1.1`转向该字段指定的协议。`Connection`字段表示浏览器通知服务器，如果可以的话，就升级到 WebSocket 协议。`Origin`字段用于提供请求发出的域名，供服务器验证是否许可的范围内（服务器也可以不验证）。`Sec-WebSocket-Key`则是用于握手协议的密钥，是 Base64 编码的16字节随机字符串。

服务器的 WebSocket 回应如下。

    HTTP/1.1 101 Switching Protocols
    Connection: Upgrade
    Upgrade: websocket
    Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
    Sec-WebSocket-Origin: null
    Sec-WebSocket-Location: ws://example.com/

上面代码中，服务器同样用`Connection`字段通知浏览器，需要改变协议。`Sec-WebSocket-Accept`字段是服务器在浏览器提供的`Sec-WebSocket-Key`字符串后面，添加 RFC6456 标准规定的“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”字符串，然后再取 SHA-1 的哈希值。浏览器将对这个值进行验证，以证明确实是目标服务器回应了 WebSocket 请求。`Sec-WebSocket-Location`字段表示进行通信的 WebSocket 网址。

完成握手以后，WebSocket 协议就在 TCP 协议之上，开始传送数据。

## 客户端的简单示例 #

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.');
    };

## 客户端 API #

浏览器对 WebSocket 协议的处理，无非就是三件事。

- 建立连接和断开连接
- 发送数据和接收数据
- 处理错误

### 构造函数 WebSocket #

`WebSocket`对象作为一个构造函数，用于新建`WebSocket`实例。

    var ws = new WebSocket('ws://localhost:8080');

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

### 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;
    }

### webSocket.onopen #

实例对象的`onopen`属性，用于指定连接成功后的回调函数。

    ws.onopen = function () {
      ws.send('Hello Server!');
    }

如果要指定多个回调函数，可以使用`addEventListener`方法。

    ws.addEventListener('open', function (event) {
      ws.send('Hello Server!');
    });

### 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
    });

### 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);
    };

### 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);

### webSocket.bufferedAmount #

实例对象的`bufferedAmount`属性，表示还有多少字节的二进制数据没有发送出去。它可以用来判断发送是否结束。

    var data = new ArrayBuffer(10000000);
    socket.send(data);

    if (socket.bufferedAmount === 0) {
      // 发送完毕
    } else {
      // 发送还没结束
    }

### webSocket.onerror #

实例对象的`onerror`属性，用于指定报错时的回调函数。

    socket.onerror = function(event) {
      // handle error event
    };

    socket.addEventListener("error", function(event) {
      // handle error event
    });

## WebSocket 服务器 #

WebSocket 协议需要服务器支持。各种服务器的实现，可以查看维基百科的列表。

常用的 Node 实现有以下三种。

- µWebSockets
- Socket.IO
- WebSocket-Node

具体的用法请查看它们的文档，本教程不详细介绍了。

## 参考链接 #

- Ryan Stewart, Real-time data exchange in HTML5 with WebSockets
- Malte Ubl & Eiji Kitamura，Introducing WebSockets: Bringing Sockets to the Web
- Jack Lawson, WebSockets: A Guide
- Michael W., Starting with Node and Web Sockets
- Jesse Cravens, Introduction to WebSockets
- Matt West, An Introduction to WebSockets
- Maciej Sopyło, Node.js: Better Performance With Socket.IO and doT
- Jos Dirksen, Capture Canvas and WebGL output as video using websockets
- Fionn Kellehe, Understanding Socket.IO
- How to Use WebSockets
- WebSockets - Send & Receive Messages

本教程采用知识共享 署名-相同方式共享 3.0协议。

分享本文