最近研究了下WebSocket,总结下目前对WebSocket的认知。本文不是基于WebSocket展开的一个从0到1的详细介绍。如果你从来没有了解过WebScoket,建议可以先搜一些介绍WebSocket的文章,这类文章还是挺多的,我就不再赘述了。
下面的内容是基于你对WebSocket有基本了解后展开的几个小的知识点:
- ping/pong协议;
- 如何使
ERROR_INTERNET_DISCONNECTED
错误信息不显示在控制台;
ping/pong协议
背景:连接WebSocket的时候,发现WebSocket刚连接上没过多久就断开了,为了保持长时间的连接,就想到了ping/pong协议。
问题:
- ping/pong是一种特殊的帧类型吗,还是说只是一种设计思想?
- JS有原生方法支持发送ping/pong消息吗
通过WebSocket协议,发现ping/pong确实是一种特殊的帧类型:
The Ping frame contains an opcode of 0x9.
The Pong frame contains an opcode of 0xA.
那么,上面所说的opcode又是什么东西呢?讲opcode就得说到帧数据格式:
通过上图可以发现,除了最后面的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:
socket: <stream.Duplex> Network socket between the server and client
socket有一个write方法,该方法是可以用来写帧数据的,也就是上面帧格式里面的全部数据,而不仅仅是Payload Data。
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出来。
总结
基于目前了解的知识总结的一篇博客,如有错误,欢迎留言讨论。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。