前端WebSocket知识点总结

最近研究了下WebSocket,总结下目前对WebSocket的认知。本文不是基于WebSocket展开的一个从0到1的详细介绍。若是你历来没有了解过WebScoket,建议能够先搜一些介绍WebSocket的文章,这类文章仍是挺多的,我就再也不赘述了。html

下面的内容是基于你对WebSocket有基本了解后展开的几个小的知识点:node

  1. ping/pong协议;
  2. 如何使ERROR_INTERNET_DISCONNECTED错误信息不显示在控制台;

ping/pong协议

背景:链接WebSocket的时候,发现WebSocket刚链接上没过多久就断开了,为了保持长时间的链接,就想到了ping/pong协议。git

问题:github

  1. ping/pong是一种特殊的帧类型吗,仍是说只是一种设计思想?
  2. JS有原生方法支持发送ping/pong消息吗

经过WebSocket协议,发现ping/pong确实是一种特殊的帧类型:web

The Ping frame contains an opcode of 0x9.
The Pong frame contains an opcode of 0xA.

那么,上面所说的opcode又是什么东西呢?讲opcode就得说到帧数据格式
image.png
经过上图能够发现,除了最后面的Payload Data,也就是咱们要发送的数据以外,还会有一些其余信息。我以为能够类比http请求的请求头部分。上图中第5-8位表示的就是opcode的内容。其他字段的含义能够参考上述WebSocket规范,或者搜WebSocket协议数据帧格式,这类博客仍是挺多的。设计模式

拿nodeJS举个例子:
在浏览器端发起WebSocket的时候,会发送一个http请求,注意请求头里面的Upgrade字段,意思就是我要升级到websocket链接:浏览器

GET /chat HTTP/1.1
Host: example.com:8000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

此时,nodeJS就能够监听upgrade事件,去作拒绝或者升级操做,注意下这个事件里面有个参数socket:websocket

socket: <stream.Duplex> Network socket between the server and client

socket有一个write方法,该方法是能够用来写帧数据的,也就是上面帧格式里面的所有数据,而不只仅是Payload Data。socket

ws仓库就是使用了socket的write方法发送了根据WebSocket协议定义的ping/pong,部分关键代码以下:函数

doPing(data, mask, readOnly, cb) {
  this.sendFrame(
    Sender.frame(data, {
      fin: true,
      rsv1: false,
      opcode: 0x09, // ping opcode
      mask,
      readOnly
    }),
    cb
  );
}
doPong(data, mask, readOnly, cb) {
  this.sendFrame(
    Sender.frame(data, {
      fin: true,
      rsv1: false,
      opcode: 0x0a, // pong opcode
      mask,
      readOnly
    }),
    cb
  );
}
sendFrame(list, cb) {
  if (list.length === 2) {
    this._socket.cork();
    this._socket.write(list[0]);
    this._socket.write(list[1], cb);
    this._socket.uncork();
  } else {
    this._socket.write(list[0], cb);
  }
}

因此,nodeJS是能够实现WebSocket协议定义的ping/pong帧的。缘由是咱们能够拿到socket对象,而且该对象提供了能够发送完整帧数据的方法。那么浏览器端呢?

浏览器提供了原生的WebSocket构造函数用来建立一个WebSocket实例,该实例只提供了一个send方法,而且该send方法只能用来发送上述协议中Payload Data的内容,浏览器会根据send的参数自动生成一个完整的帧数据。因此,在浏览器端是无法控制除了Payload Data以外的帧内容的,也就是没法自定义opcode。因此,也就实现不了WebSocket规范定义的ping/pong协议。

此时,咱们就能够把ping/pong当成一种用来解决特定问题的设计模式。既然咱们只能自定义Payload Data的内容,那么咱们能够简单的在Payload Data里面添加一个字段用于区分是ping/pong帧,仍是普通的数据帧,好比type。当type字段是ping/pong的时候代表是ping/pong帧,若是是其余字段才是普通的数据帧。

如何使ERROR_INTERNET_DISCONNECTED错误信息不显示在控制台

当断网的时候,链接WebSocket会发现浏览器控制台会log一个错误信息:

WebSocket connection to 'ws://...' failed: Error in connection establishment: net::ERR_INTERNET_DISCONNECTED

原先的开发经验是,控制台若是有报错的话,确定是代码某个地方有错误,而且没有被咱们的代码捕获到,因此就会在控制台抛出,若是使用了try catch 或者全局的window.onerror捕获到了错误信息,就不会在控制台打印了。因此,我就尝试了上述方法,发现捕捉不到,仍是会在控制台log。

另外,WebSocket提供了两个事件,onerror和onclose。当发生上述错误信息的时候,onerror和onclose是会被调用的。可是,此时控制台仍是会有上述报错信息。

通过一番查找,发现没法阻止上述错误信息显示在控制台。

那么,为何浏览器会设计这样的行为呢?猜想缘由以下:
上面说到经过onerror和onclose事件是能够捕捉到WebSocket建立失败的,可是,查看这两个事件的参数,咱们只能从中找到一个code是1006的属性,输出在控制台的错误信息ERR_INTERNET_DISCONNECTED在参数里面找不到。接着,看一下code1006相关的东西:

User agents must not convey any failure information to scripts in a way that would allow a script to distinguish the following situations:

*   A server whose host name could not be resolved.
*   A server to which packets could not successfully be routed.
*   A server that refused the connection on the specified port.
*   A server that failed to correctly perform a TLS handshake (e.g., the server certificate can't be verified).
*   A server that did not complete the opening handshake (e.g. because it was not a WebSocket server).
*   A WebSocket server that sent a correct opening handshake, but that specified options that caused the client to drop the connection (e.g. the server specified a subprotocol that the client did not offer).
*   A WebSocket server that abruptly closed the connection after successfully completing the opening handshake.

In all of these cases, the the WebSocket connection close code would be 1006, as required by WebSocket Protocol. 

Allowing a script to distinguish these cases would allow a script to probe the user's local network in preparation for an attack.

从上述规范能够看到,规范是禁止浏览器向脚本传递下述形成WebSocket链接失败的具体缘由的,只容许向脚本传递一个1006的code码,不然,用户就能够探测到局部网的信息,进而发起攻击。举个例子,上面那种断网的状况,脚本中只能获得1006的状态码,好比下面这种报错

Error in connection establishment: net::ERR_CONNECTION_REFUSED

也只能从onerror中得到一个1006的code码。

因此,做为开发人员,浏览器要怎么在告诉咱们具体的错误信息的同时又阻止有可能发生的攻击呢?答案就是在控制台把具体的错误信息log出来。

总结

基于目前了解的知识总结的一篇博客,若有错误,欢迎留言讨论。

相关文章
相关标签/搜索