本系列第一篇《[WebSocket 基础与应用系列(一)—— 抓个 WebSocket 的包] 》,没看过的同学可以看看,看过的同学也可以回顾一把。
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
Socket.IO 在 Socket.IO server (Node.js) 和 Socket.IO client ( browser, Node.js, or another programming language ) 之间,基于 WebSocket ( 不支持 WebSocket 的情况下,退化成 HTTP long-polling ) 建立一条全双工实时通信通道.
Engine.IO 是一个 Socket.IO 的抽象实现,作为 Socket.IO 的服务器和浏览器之间交换的数据的传输层。它不会取代 Socket.IO,它只是抽象出固有的复杂性,支持多种浏览器,设备和网络的实时数据交换。Engine.IO 使用了 Websocket 和 HTTP long-polling 方式封装了一套 socket 协议。为了兼容不支持 Websocket 的低版本浏览器,使用长轮询 ( polling ) 替代 WebSocket。
Engine.IO 负责在服务器和客户端之间建立底层连接。包括以下功能:
现在主要有 2 种传输通道实现
HTTP long-polling transport (也简称 "polling") 由连续的 HTTP requests 组成:
基于 HTTP long-polling transport 的特性,连续的 emits 可能合并在一个 HTTP Request 中发送。
The WebSocket 传输通道 包含一条 WebSocket 连接,WebSocket 提供了服务端和客户端之间双向通信及低时延的通信通道。
基于传输通道特性,每个 emit 会以一个 WebSocket 数据帧发送,有时候会分为 2 个不同的数据帧发送。
Engine.IO 连接建立的时候, Server 端会发送一些消息到客户端:
{
"sid": "FSDjX-WRwSA4zTZMALqx",
"upgrades": ["websocket"],
"pingInterval": 25000,
"pingTimeout": 20000
}
默认的情况下,客户端先建立 HTTP long-polling 通信通道。
为什么呢?
WebSocket 无疑是最好的双向通道,但是由于公司的代理、个人的防火墙、杀毒软件等,它并不是在什么情况下都能成功建立。
从用户的角度来看,如果 WebSocket 连接建立失败,那么用户至少要等 10S 才能开始真正的数据传输,这无疑伤害了用户的体验。
总的来说,Engine.IO 首先关注可靠性和用户体验,其次才是服务器性能。
升级的时候,客户端会做如下动作:
可以在浏览器抓包看到如下网络连接:
当以下情况出现时,Engine.IO 的连接会判断为关闭。
服务端会以 pingInterval 的间隔发送 PING 数据包,客户端收到后在 pingTimeout 时间之内需要发送 PONG 数据包给服务端,如果服务端在 pingTimeout 时间内没有收到,那么就认为这条连接关闭了。相反,客户端如果在 pingInterval + pingTimeout 时间内没有收到 PING 数据包,客户端也判断连接关闭。
服务端触发断连事件的原因有:
Reason | Description |
---|---|
server namespace disconnect | The socket was forcefully disconnected with socket.disconnect |
client namespace disconnect | The client has manually disconnected the socket using socket.disconnect() |
server shutting down | The server is, well, shutting down |
ping timeout | The client did not send a PONG packet in the pingTimeout delay |
transport close | The connection was closed (example: the user has lost connection, or the network was changed from WiFi to 4G) |
transport error | The connection has encountered an error |
客户端触发断连事件的原因有:
Reason | Description |
---|---|
io server disconnect | The server has forcefully disconnected the socket with socket.disconnect() |
io client disconnect | The socket was manually disconnected using socket.disconnect() |
ping timeout | The server did not send a PING within the pingInterval + pingTimeout range |
transport close | The connection was closed (example: the user has lost connection, or the network was changed from WiFi to 4G) |
transport error | The connection has encountered an error (example: the server was killed during a HTTP long-polling cycle) |
传输通道通过 Engine.IO URL 进行连接建立
连接建立之后,服务端会发一个 JSON 格式的握手数据
sid:会话 id (string)
upgrades: 允许升级的传输通道 (Array of String)
pingTimeout: 服务端配置的 ping 超时时间,发送给客户端,客户端用来检测服务端是否还正常响应 (Number)
pingInterval: 服务端配置的心跳间隔,客户端用来检测服务端是否还正常响应 (Number)
客户端收到服务端定时的 ping packet 之后,需要回复客户端 pong packet
客户端和服务端之间可以传输 message packets
Polling transports 可以发送 close packet 来关闭 socket
GET /engine.io/?EIO=4&transport=polling&t=N8hyd6w
< HTTP/1.1 200 OK
< Content-Type: text/plain; charset=UTF-8
0{"sid":"N-YWtQT1K9uQsb15AAAD","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":5000}
Details:
0 => "open" packet type
{"sid":... => the handshake data
Note: query 参数中的 t 是用来防止浏览器缓存请求.
服务端执行 socket.send ('hey') :
GET /engine.io/?EIO=4&transport=polling&t=N8hyd7H&sid=lv_VI97HAXpY6yYWAAAC
< HTTP/1.1 200 OK
< Content-Type: text/plain; charset=UTF-8
4hey
Details:
4 => "message" packet type
hey => the actual message
Note: query 中的 sid 是握手协议中 sid.
客户端执行:socket.send ('hello'); socket.send ('world');
POST /engine.io/?EIO=4&transport=polling&t=N8hzxke&sid=lv_VI97HAXpY6yYWAAAC
> Content-Type: text/plain; charset=UTF-8
4hello\x1e4world
< HTTP/1.1 200 OK
< Content-Type: text/plain; charset=UTF-8
ok
Details:
4 => "message" packet type
hello => the 1st message
\x1e => separator
4 => "message" message type
world => the 2nd message
GET /engine.io/?EIO=4&transport=websocket&sid=lv_VI97HAXpY6yYWAAAC
< HTTP/1.1 101 Switching Protocols
WebSocket frames:
< 2probe => probe request
> 3probe => probe response
< 5 => "upgrade" packet type
> 4hello => message (not concatenated)
> 4world
> 2 => "ping" packet type
< 3 => "pong" packet type
> 1 => "close" packet type
在这个例子中,客户端只开启了 WebSocket 传输通道 (without HTTP polling).
GET /engine.io/?EIO=4&transport=websocket
< HTTP/1.1 101 Switching Protocols
WebSocket frames:
< 0{"sid":"lv_VI97HAXpY6yYWAAAC","pingInterval":25000,"pingTimeout":5000} => handshake
< 4hey
> 4hello => message (not concatenated)
> 4world
< 2 => "ping" packet type
> 3 => "pong" packet type
> 1 => "close" packet type
Engine.IO url 包含了以下内容
/engine.io/[?<query string>]
有两种不同类型的编码
一个编码的数据包可以是 UTF-8 字符串或者二进制数据。字符串的数据包编码格式如下:
<packet type id>[<data>]
example:
4hello
对于二进制数据,不包括数据包类型(packet type),因为只有 “message” 数据包类型可以包括二进制数据。
新传输通道建立的时候,从服务端发送 Sent from the server when a new transport is opened (recheck)
请求关闭此传输,但不关闭连接本身。
由服务器发送。客户应该用 pong 数据包应答。
example
server sends: 2
client sends: 3
3 pong
由客户端发送以响应 ping 数据包。
4 message
实际传输的消息
example 1
server sends: 4HelloWorld
client receives and calls callback socket.on('message', function (data) { console.log(data); });
example 2
client sends: 4HelloWorld
server receives and calls callback socket.on('message', function (data) { console.log(data); });
5 upgrade
在 engine.io 切换传输通道之前,它测试服务器和客户端是否可以通过该传输进行通信。如果此测试成功,客户端将发送一个升级包,请求服务器刷新旧传输上的缓存,并切换到新传输通道。
6 noop
一个 noop 包。主要用于建立 websocket 连接之后关闭长轮询。
example
client connects through new transport
client sends 2probe
server receives and sends 3probe
client receives and sends 5
server flushes and closes old transport and switches to new.
Payload 是捆绑在一起的一系列 encoded packets。Payload 编码格式如下:
<packet1>\x1e<packet2>\x1e<packet3>
数据包分割符使用 record separator ('\x1e'). 更多可参考: https://en.wikipedia.org/wiki/C0_and_C1_control_codes#Field_separators 当有效负载中包含二进制数据时,它将作为 base64 编码字符串发送。为了解码的目的,将标识符 b 置于包含二进制数据的分组编码之前。可以发送任意数量的字符串和 base64 编码字符串的组合。下面是 base 64 编码消息的示例:
<packet1>\x1eb<packet2 data in b64>[...]
Payload 用于不支持帧的传输通道,例如轮询协议。
不包含二进制的例子:
[
{
"type": "message",
"data": "hello"
},
{
"type": "message",
"data": "€"
}
]
编码后:
4hello\x1e4€
包含二进制的例子:
[
{
"type": "message",
"data": "€"
},
{
"type": "message",
"data": buffer <01 02 03 04>
}
]
编码后:
4€\x1ebAQIDBA==
分解:
4 => "message" packet type
€
\x1e => record separator
b => indicates a base64 packet
AQIDBA== => buffer content encoded in base64
engine.io server 必须支持三种传输通道:
轮询传输包括客户端向服务器发送周期性 GET 请求以获取数据,以及将带有有效负载的请求从客户端发送到服务器以发送数据。
服务器必须支持 CORS 响应。
服务器实现必须使用有效的 JavaScript 进行响应。在响应中需要使用 URL 中 query 中的 j 参数。j 是一个整数。
JSONP 数据包的格式。
`___eio[` <j> `]("` <encoded payload> `");`
为了确保 payload 得到正确处理,需要对 payload 进行转义,使得响应体是一个合法的 JavaScript。
服务器返回的 JSONP 数据帧的例子
___eio[4]("packet data");
Posting data
客户端通过隐藏的 iframe 发送数据。数据以 URI 编码格式发送给服务器,如下所示
d=<escaped packet payload>
除了常规的 qs 转义之外,为了防止浏览器处理的不一致,\n 在被 POSTd 之前将被转义为 \n。
客户端使用 EventSource 对象接收数据,使用 XMLHttpRequest 对象发送数据。
上面的对 payloads 的编码方式并不用于 WebSocket 通道,WebSocket 通道本身已有轻量级的数据帧机制。
发送消息的时候,对数据包进行单独编码,然后依次调用 send () 进行发送。
连接总是以轮询(XHR 或 JSONP)开始。WebSocket 通过发送探针在侧面进行测试 (2probe)。如果探测由服务器响应 (3probe),则客户端会发送一个升级包 (5)。
为了确保没有消息丢失,只有在刷新现有传输的所有缓冲区并认为传输已暂停后,才会发送升级数据包。
当服务器收到升级包时,它必须假定这是新的传输通道,并将所有现有缓冲区(如果有的话)发送给它。
客户端发送的探测器是一个 ping+probe 作为数据发送。(2probe) 服务端发送的探测器是一个 pong+probe 作为数据发送。(3probe)
客户端必须使用握手中发送的 pingTimeout 和 pingInterval 来确定服务器是否无响应。
服务器发送一个 ping 数据包。如果在 pingTimeout 内未收到任何数据包类型,服务器将认为套接字已断开连接。如果收到了 pong 数据包,服务器将在等待 pingInterval 之后再次发送 ping 数据包。
由于这两个值在服务器和客户端之间共享,当客户端在 pingTimeout+pingInterval 内没有接收到任何数据时,客户端也能探测到服务器是否变得无响应。
const engine = require('engine.io');
const server = engine.listen(3000,{
cors: {
origin: "*"
}
});
server.on('listen', () => {
console.log('listening on 3000')
})
server.on('connection', socket => {
console.log('new connection')
socket.send('utf 8 string');
socket.send(Buffer.from('hello world')); // binary data
});
const { Socket } = require('engine.io-client');
const socket = new Socket('ws://localhost:3000');
socket.on('open', () => {
socket.emit('message from client')
socket.on('message', (data) => {
console.log('receive message: ' + data);
socket.send('ack from client.');
});
socket.on('close', (e) => {
console.log('socket close',e)
});
});
1、Polling 传输通道握手
Request:
Response:
2、发起长轮询请求服务端数据
Request:
Response:
3、POST 方式发送数据到服务端
Request:
Request payload:
Response:
4、服务端告诉客户端传输通道已升级,回复一个 6
Request:
Response:
5、WebSocket 通道建立之后,切换为 WebSocket 传输数据
Connect:
Message:
const socket = new Socket('ws://localhost:3000',{ transports: ['websocket'] } );
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8