websocket

133 阅读5分钟

原因

解决 http 协议客户端发送请求到服务器,轮询浪费资源

简介

服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息

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

应用层协议基于 TCP 传输协议复用 HTTP 的握手通道

如何建立连接

客户端通过 HTTP 请求与 WebSocket 服务端协商升级协议。协议升级完成后,后续的数据交换则遵照 WebSocket 的协议

客户端:申请协议升级

标准的 HTTP 报文格式,且只支持 GET 方法

在握手阶段,可以通过相关请求首部进行 安全限制、权限校验等

  • Connection: Upgrade:表示要升级协议
  • Upgrade: websocket:表示要升级到 websocket 协议。
  • Sec-WebSocket-Version: 13:表示 websocket 的版本。如果服务端不支持该版本,需要返回一个 Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。
  • Sec-WebSocket-Key:与后面服务端响应首部的 Sec-WebSocket-Accept是配套的,提供基本的防护,比如恶意的连接,或者无意的连接
服务端:响应协议升级

每个 header 都以 \r\n 结尾,并且最后一行加上一个额外的空行 \r\n。此外,服务端回应的 HTTP 状态码只能在握手阶段使用。过了握手阶段后,就只能采用特定的错误码

  • 101 Switching Protocols

  • Connection:Upgrade

  • Upgrade: websocket

  • Sec-WebSocket-Accept

    • 将 Sec-WebSocket-Key 跟 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接。
    • 通过 SHA1 计算出摘要,并转成 base64 字符串

数据传递

基于数据帧的传递

  • 每条消息可能被切分成多个数据帧。当 WebSocket 的接收方收到一个数据帧时,会根据FIN的值来判断,是否已经收到消息的最后一个数据帧

  • 根据 opcode 来区分操作的类型

  • 客户端、服务端通信的最小单位是帧(frame),由1个或多个帧组成一条完整的消息

    • 发送端:将消息切割成多个帧,并发送给服务端;
    • 接收端:接收消息帧,并将关联的帧重新组装成完整的消息;

连接保持心跳

Sec-WebSocket-Key/Accept的作用

  • 避免服务端收到非法的 websocket 连接(http 客户端不小心请求连接websocket 服务)
  • 确保服务端理解 websocket 连接。因为 ws 握手阶段采用的是 http 协议,因此可能 ws 连接是被一个 http 服务器处理并返回的,此时客户端可以通过 Sec-WebSocket-Key 来确保服务端认识 ws 协议
  • 用浏览器里发起 ajax 请求,设置 header 时,Sec-WebSocket-Key 以及其他相关的 header 是被禁止的。这样可以避免客户端发送 ajax 请求时,意外请求协议升级(websocket upgrade)
  • 可以防止反向代理返回错误的数据。比如反向代理前后收到两次 ws 连接的升级请求,反向代理把第一次请求的返回给 cache 住,然后第二次请求到来时直接把 cache 住的请求给返回(无意义的返回) Sec-WebSocket-Key 主要目的并不是确保数据的安全性

使用

WebSocket 构造函数

new WebSocket

客户端就会与服务器进行连接

webSocket.readyState

返回实例对象的当前状态

  • CONNECTING:值为0,表示正在连接。
  • OPEN:值为1,表示连接成功,可以通信了。
  • CLOSING:值为2,表示连接正在关闭。
  • CLOSED:值为3,表示连接已经关闭,或者打开连接失败。

webSocket.onopen

连接成功后的回调函数

webSocket.onclose

连接关闭后的回调函数

webSocket.onmessage

收到服务器数据后的回调函数

webSocket.send()

向服务器发送数据

webSocket.bufferedAmount

还有多少字节的二进制数据没有发送出去。它可以用来判断发送是否结束

webSocket.onerror

报错时的回调函数

var ws = new WebSocket("wss://echo.websocket.org");
//   显式指定类型
// 收到的是 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);
//   };

ws.onopen = function (evt) {
  console.log("Connection open ...");
  ws.send("Hello WebSockets!");
  // 发送 Blob 对象
  // var file = document.querySelector('input[type="file"]').files[0];
  // ws.send(file);
  // 发送 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);
  // if (ws.bufferedAmount === 0) {
  //   发送完毕
  // } else {
  //   发送还没结束
  // }
};
//   ws.addEventListener("open", function (evt) {
//     ws.send("Hello Server!");
//   });
ws.onmessage = function (evt) {
  // 二进制数据(blob对象或Arraybuffer对象)
  if (typeof evt.data === "string") {
    console.log("Received data string");
  }
  if (evt.data instanceof ArrayBuffer) {
    var buffer = evt.data;
    console.log("Received arraybuffer");
  }
  ws.close();
};
// ws.addEventListener("message", function(evt) {
//   var data = evt.data;
// });
ws.onclose = function (evt) {
  console.log("Connection closed.");
};
//   ws.addEventListener("close", function (evt) {
//     var code = evt.code;
//     var reason = evt.reason;
//     var wasClean = evt.wasClean;
//   });
ws.onerror = function (event) {
  // handle error event
};

ws.addEventListener("error", function (event) {
  // handle error event
});
switch (ws.readyState) {
  case WebSocket.CONNECTING:
    console.log("正在连接");
    break;
  case WebSocket.OPEN:
    console.log("连接成功");
    break;
  case WebSocket.CLOSING:
    console.log("正在关闭");
    break;
  case WebSocket.CLOSED:
    console.log("已经关闭");
    break;
  default:
    break;
}

兼容

  • Adobe Flash Socket
  • ActiveX HTMLFile (IE)
  • 基于 multipart 编码发送 XHR
  • 基于长轮询的 XHR

服务

  • µWebSockets
  • Socket.IO
  • WebSocket-Node
  • WebSocketd