深刻浅出Websocket(一)Websocket协议

前言

最近在构建两个系统的实时通讯部分,总结一下所学。html

这是一个系列文章,暂时主要构思四个部分nginx

正文

本文主要介绍Websocket是什么以及其协议内容。git

WebSocket 协议实如今受控环境中运行不受信任代码的一个客户端到一个从该代码已经选择加入通讯的远程主机之间的全双工通讯。该协议包括一个打开阶段握手规定以及通讯时基本消息帧的定义。其基于TCP之上。此技术的目标是为基于浏览器的应用程序提供一种机制,这些应用程序须要与服务器进行双向通讯,而不依赖于打开多个HTTP链接例如,使用XMLHttpRequest或<iframe>和长轮询)。github

Websocket能作什么

过去,建立须要在客户端和服务之间双向通讯(例如,即时消息和游戏应用)的web应用,须要经过HTTP来轮询服务器来获取更新而后若是是推送消息则发送另外一个请求(如今不少应用也依旧采用这种方式)。这样作会存在一些问题。web

  • 服务器端被迫提供两类接口,一类提供给客户端轮询新消息,一类提供给客户端推送消息给服务器端。
  • HTTP协议有较多的额外费用(overhead),每次发送消息都会有一个HTTP header信息,并且若是不用Keep-Alive每次还都要握手。
  • 客户端的脚本好比JS可能还须要跟踪整个过程,也就是说我发送一个消息后,我可能须要跟踪这个消息的返回。

一个简单的办法是使用单个TCP链接双向传输。这是为何提供WebSocket 协议。与WebSocket API结合[WSAPI],它提供了一个HTTP轮询的替代来进行从web 页面到远程服务器的双向通讯。npm

协议内容

Websocket协议主要包括两个部分,一个是握手的规则,另外一个是数据传输的方式及载体格式。这里给个网上找的例子(点这里),能够开发者工具看看Network里面的内容。跨域

一旦客户端和服务器握手成功后,数据传输部分就开始了,这是一个全双工的通讯。客户端与服务器之间互相传输数据的的基本单位根据规格说明书里咱们称为“Messages”。在实际网络中,这些Message由一个或多个Frames组成,Websocket的Message里的frame和计算机网络里说的的frame并非对应关系,后面会详细介绍Frame的结构。浏览器

握手

打开阶段握手目的是兼容基于HTTP的服务器软件和中间件,以便单个端口能够用于与服务器交流的HTTP客户端和与服务器交流的WebSocket客户端。因此WebSocket客户端的握手是一个HTTP Upgrade请求(Http status code 101): 安全

Charles中请求信息请求头(上)和响应头(下)

这里关于字段就讲几个字段以及它们的考量bash

Origin(请求头)

Origin用来指明请求的来源,Origin头部主要用于保护Websocket服务器免受非受权的跨域脚本调用Websocket API的请求。也就是不想没被受权的跨域访问与服务器创建链接,服务器能够经过这个字段来判断来源的域并有选择的拒绝。

Sec-WebSocket-Key(请求头)以及Sec-WebSocket-Accept(响应头)

另外一方面,Websocket协议须要保证客户端发起的Websocket链接请求只会被能理解Websocket协议的服务器所识别。

Really, as you are mentioned, if you are aware of websockets (that is what to be checked), you could pretend to be a websocket server by sending correct response. But then, if you will not act correctly (e.g. form frames correctly), it will be considered as a protocol violation. Actually, you can write a websocket server that is incorrect, but there will be not much use in it.

And another purpose is to prevent clients accidentally requesting websockets upgrade not expecting it (say, by adding corresponding headers manually and then expecting smth else). Sec-WebSocket-Key and other related headers are prohibited to be set using setRequestHeader method in browsers.

stackoverflow参考资料

数据传输

下面介绍下Frame的结构

以前也说过,客户端与服务器之间互相传输数据的的基本单位根据规格说明书里咱们称为“Messages”。在实际网络中,这些Message由一个或多个Frames组成。

  1. FIN,指明Frame是不是一个Message里最后Frame(以前说过一个Message可能又多个Frame组成)
  2. RSV1-3必须是0,除非有扩展定义了非零值的意义。
  3. Opcode这个比较重要,有以下取值是被协议定义的
    • %x0 denotes a continuation frame

    • %x1 表示一个text frame

    • %x2 表示一个binary frame

    • %x3-7 are reserved for further non-control frames

    • %x8 表示链接关闭

    • %x9 表示 ping (心跳检测相关,后面会讲)

    • %xA 表示 pong (心跳检测相关,后面会讲)

    • %xB-F are reserved for further control frames

  4. Mask,这个是指明“payload data”是否被计算掩码。这个和后面的Masking-key有关
  5. Payload len,数据的长度,不赘述了。
  6. Masking-key,这里不赘述了,给一个Websocket中掩码的意义
  7. Payload data,帧真正要发送的数据,能够是任意长度,但尽管理论上帧的大小没有限制,但发送的数据不能太大,不然会致使没法高效利用网络带宽,正如上面所说Websocket提供分片。

动手算一下 下面是charles里面截取的一段内容

// 十六进制
81 84 3a a6 ac e4 51 c3 c7 81

// 二进制
10000001 10000100 00111010 10100110 10101101 11100100 01010001 11010011 11010111 10000001
复制代码

opcode为0001,0x1表示一个Text frame

payload len为0000100,0x4表示长度为4字节

掩码是 00111010 10100110 10101101 11100100

payload是 01010001 11010011 11010111 10000001

具体的处理能够参考Node.js ws的源码 其中的buffer-utils

Websocket的使用及API

讲完Websocket协议部分,如今说说如何相关的Web API。

// 客户端
var ws = new WebSocket('wss://example.com/socket'); ➊
ws.onerror = function (error) { ... } ➋ 
ws.onclose = function () { ... } ➌
ws.onopen = function () { ➍
ws.send("Connection established. Hello server!"); ➎
}
ws.onmessage = function(msg) { ➏ 
    if(msg.data instanceof Blob) { ➐
        processBlob(msg.data);
    } else {
        processText(msg.data);
    }
}
复制代码
  1. 打开新的安全 WebSocket 链接(wss)
  2. 可选的回调,在链接出错时调用
  3. 可选的回调,在链接终止时调用
  4. 可选的回调,在 WebSocket 链接创建时调用
  5. 客户端先向服务器发送一条消息
  6. 回调函数,服务器每发回一条消息就调用一次
  7. 根据接收到的消息,决定调用二进制仍是文本处理逻辑

心跳检测

在使用websocket的过程当中,有时候会遇到客户端网络关闭的状况,而这时候在服务端并无触发onclose事件。这样会:

  • 多余的链接
  • 服务端会继续给客户端发数据,这些数据会丢失

因此就须要一种机制来检测客户端和服务端是否处于正常链接的状态。心跳检测就是这样的一种机制,通常来讲客户端每过必定时间

ws模块对心跳的处理

ws模块如何经过心跳检测去检测和关闭坏掉的链接

const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

function noop() {}

function heartbeat() {
  this.isAlive = true;
}

wss.on('connection', function connection(ws) {
  ws.isAlive = true;
  ws.on('pong', heartbeat);
});

const interval = setInterval(function ping() {
  wss.clients.forEach(function each(ws) {
    if (ws.isAlive === false) return ws.terminate();

    ws.isAlive = false;
    ws.ping(noop);
  });
}, 30000);
复制代码

根据规范,当接收到Ping消息后Pong响应消息会自动发送。

解决ws与wss共存

下面是个人nginx配置,顺带加了负载均衡。测了可用,就是证书因为是自签名的因此有点问题。

Websocket怎么作身份认证

大致上Websocket的身份认证都是发生在握手阶段,经过请求中的内容来认证。一个常见的例子是在url中附带参数。

new WebSocket("ws://localhost:3000?token=xxxxxxxxxxxxxxxxxxxx");
复制代码

淘宝的直播弹幕也是用这种方式作的身份认证

以npm的ws模块实现为例,其建立Websocket服务器时提供了verifyClient方法。

const wss = new WebSocket.Server({
  host: SystemConfig.WEBSOCKET_server_host,
  port: SystemConfig.WEBSOCKET_server_port,
  // 验证token识别身份
  verifyClient: (info) => {
    const token = url.parse(info.req.url, true).query.token
    let user
    console.log('[verifyClient] start validate')
    // 若是token过时会爆TokenExpiredError
    if (token) {
      try {
        user = jwt.verify(token, publicKey)
        console.log(`[verifyClient] user ${user.name} logined`)
      } catch (e) {
        console.log('[verifyClient] token expired')
        return false
      }
    }
    // verify token and parse user object
    if (user) {
      info.req.user = user
      return true
    } else {
      info.req.user = {
        name: `游客${parseInt(Math.random() * 1000000)}`,
        mail: ''
      }
      return true
    }
  }
})
复制代码

相关的ws源码位于ws/websocket-server

// ...
  if (this.options.verifyClient) {
    const info = {
      origin: req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`],
      secure: !!(req.connection.authorized || req.connection.encrypted),
      req
    };

    if (this.options.verifyClient.length === 2) {
      this.options.verifyClient(info, (verified, code, message) => {
        if (!verified) return abortHandshake(socket, code || 401, message);
        this.completeUpgrade(extensions, req, socket, head, cb);
      });
      return;
    }

    if (!this.options.verifyClient(info)) return abortHandshake(socket, 401);
  }
  this.completeUpgrade(extensions, req, socket, head, cb);
}
复制代码

后记

参考资料:

rfc6455》 The WebSocket Protocol

《High Performance Browser Networking》- 【加】Ilya Grigorik

学习WebSocket协议—从顶层到底层的实现原理(修订版)

相关文章
相关标签/搜索