在上一篇文章中,我们了解到
socket.io是 基于engine.io进行封装的库。 所以对engine.io不清楚的童鞋可以点击进行了解: engine.io 详解
1.概述
socket.io 是基于 Websocket 的Client-Server 实时通信库。
socket.io 底层使用engine.io 封装了一层协议。
两者的依赖关系可参考: package.json
2. WebSocket 简介
Websocket 定义 参考规范 rfc6455
规范解释
Websocket 是一种提供客户端(提供不可靠秘钥)与服务端(校验通过该秘钥)进行双向通信的协议。
在没有websocket协议之前,要提供客户端与服务端实时双向推送消息,就会使用polling技术,客户端通过xhr或jsonp 发送消息给服务端,并通过事件回调来接收服务端消息。
这种技术虽然也能保证双向通信,但是有一个不可避免的问题,就是性能问题。客户端不断向服务端发送请求,如果客户端并发数过大,无疑导致服务端压力剧增。因此,websocket就是解决这一痛点而诞生的。
这里再延伸一些名词:
- 长轮询
客户端向服务端发送
xhr请求,服务端接收并hold该请求,直到有新消息push到客户端,才会主动断开该连接。然后,客户端处理该response后再向服务端发起新的请求。以此类推。
HTTP1.1默认使用长连接,使用长连接的HTTP协议,会在响应头中加入下面这行信息:Connection:keep-alive
- 短轮询:
客户端不管是否收到服务端的response数据,都会定时想服务端发送请求,查询是否有数据更新。
- 长连接
指在一个
TCP连接上可以发送多个数据包,在TCP连接保持期间,如果没有数据包发送,则双方就需要发送心跳包来维持此连接。
连接过程: 建立连接——数据传输——...——(发送心跳包,维持连接)——...——数据传输——关闭连接
- 短连接
指通信双方有数据交互时,建立一个
TCP连接,数据发送完成之后,则断开此连接。
连接过程: 建立连接——数据传输——断开连接——...——建立连接——数据传输——断开连接
Tips
这里有一个误解,长连接和短连接的概念本质上指的是传输层的
TCP连接,因为HTTP1.1协议以后,连接默认都是长连接。没有短连接说法(HTTP1.0默认使用短连接),网上大多数指的长短连接实质上说的就是TCP连接。http使用长连接的好处: 当我们请求一个网页资源的时候,会带有很多js、css等资源文件,如果使用的时短连接的话,就会打开很多tcp连接,如果客户端请求数过大,导致tcp连接数量过多,对服务端造成压力也就可想而知了。
- 单工 数据传输的方向唯一,只能由发送方向接收方的单一固定方向传输数据。
- 半双工 即通信双方既是接收方也是发送方,不过,在某一时刻只能允许向一个方向传输数据。
- 全双工: 即通信双方既是接收方也是发送方,两端设备可以同时发送和接收数据。
Tips
单工、半双工和全双工 这三者都是建立在
TCP协议(传输层上)的概念,不要与应用层进行混淆。
3. 什么是Websocket
Websocket 协议也是基于TCP协议的,是一种双全工通信技术、复用HTTP握手通道。
Websocket默认使用请求协议为:ws://,默认端口:80。对TLS加密请求协议为:wss://,端口:443。
3.1 特点
- 支持浏览器/Nodejs环境
- 支持双向通信
- API简单易用
- 支持二进制传输
- 减少传输数据量
3.2 建立连接过程
Websocket复用了HTTP的握手通道。指的是,客户端发送HTTP请求,并在请求头中带上Connection: Upgrade 、Upgrade: websocket,服务端识别该header之后,进行协议升级,使用Websocket协议进行数据通信。
参数说明
Request URL请求服务端地址Request Method请求方式 (支持get/post/option)Status Code101 Switching Protocols
规范解释: 当收到101请求状态码时,表明服务端理解并同意客户端请求,更改
Upgradeheader字段。服务端也必须在response中,生成对应的Upgrade值。
-
Connection设置upgradeheader,通知服务端,该request类型需要进行升级为websocket。 upgrade_mechanism 规范 -
Host服务端 hostname -
Origin客户端 hostname:port -
Sec-WebSocket-Extensions客户端向服务端发起请求扩展列表(list),供服务端选择并在响应中返回 -
Sec-WebSocket-Key秘钥的值是通过规范中定义的算法进行计算得出,因此是不安全的,但是可以阻止一些误操作的websocket请求。 -
Sec-WebSocket-Accept计算公式: 1. 获取客户端请求header的值:Sec-WebSocket-Key2. 使用魔数magic = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' 3. 通过SHA1进行加密计算, sha1(Sec-WebSocket-Key + magic) 4. 将值转换为base64 -
Sec-WebSocket-Protocol指定有限使用的Websocket协议,可以是一个协议列表(list)。服务端在response中返回列表中支持的第一个值。 -
Sec-WebSocket-Version指定通信时使用的Websocket协议版本。最新版本:13,历史版本 -
Upgrade通知服务端,指定升级协议类型为websocket
3.3 数据帧格式
数据格式定义参考:规范 RFC6455
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
FIN: 1 bit 如果该位值为1,表示这是message的最终片段(fragment),如果为0,表示这是一个message的第一个片段。RSV1, RSV2, RSV3: 各占1 bit 一般默认值是0,除非协商扩展,为非零值进行定义,否则收到非零值,并且没有进行协商扩展定义,则websocket连接失败。Opcode: 4 bits 根据操作码(Opcode),解析有效载荷数据(Payload data).如果接受到未定义操作码,则应该断开websocket连接。Mask: 1 bit 定义是否需要的载荷数据(``Payload data),进行掩码操作。如果设置值为1,那么在Masking-key中会定义一个掩码key,并用这个key对载荷数据进行反掩码(unmask)操作。所有从客户端发送到服务端的数据帧(frame),mask都被设置为1.Payload length: 7 bits, 7+16 bits, or 7+64 bits 载荷数据的长度。Masking-key: 0 or 4 bytes 所有从客户端传送到服务端的数据帧,数据载荷都进行了掩码操作,Mask为1,且携带了4字节的Masking-key。如果Mask为0,则没有Masking-key。Payload data: (x+y) bytes
3.4 心跳检测
为了确保客户端与服务端的长连接正常,有时即使客户端连接中断,但是服务端未触发onclose事件,这就有可能导致无效连接占用。所以需要一种机制,确保两端的连接处于正常状态,心跳检测就是这种机制。客户端每隔一段时间,会向服务端发送心跳(数据包),服务端也会返回response进行反馈连接正常。
4. socket.io 简介
socket.io与engine.io的一大区别在于,socket.io并不直接提供连接功能,而是在engine.io层提供。
socket.io提供了一个房间(Namespace)概念。当客户端创建一个新的长连接时,就会分配一个新的Namespace进行区分。
根据流程图,可以看出:
- 创建长连接的方式有三种:
websocket、xhr、jsonp。其中,后两种使用长轮询的方式进行模拟。 - 所谓的长轮询是指,客户端发送一次
request,当服务端有消息推送时会push一条response给客户端。客户端收到response后,会再次发送request,重复上述过程,直到其中一端主动断开连接为止。
// lookup 源码
var parsed = url(uri)
var source = parsed.source
var id = parsed.id
var path = parsed.path
// 查找相同房间
var sameNamespace = cache[id] && path in cache[id].nsps
// 如果房间号已存在,创建新连接
var newConnection = sameNamespace
// ...
socket.io也提供支持多路复用(built-in multiplexing )方式,这表明每一个数据包(Packet)都始终属于给定的namespace,并有path进行标识(例如: /xxxx)
socket.io可以在 open 之前,emit 消息,并且该消息会在 open 之后发出。而engine.io必须等到open 之后,才能 send消息。
socket.io也支持断网重连(reconnection)功能。
5. socket.io 工作流程
当使用
socket.io创建一个长连接时,到底发生了什么呢?下面我们就来进入本文的正体:
const socket = io('http://localhost', {
path: '/myownpath'
});
首先,socket.io通过一个http请求,并且该请求头中带有升级协议(Connection:Upgrade、Upgrade:websocket)等信息,告诉服务端准备建立连接,此时,后端返回的response数据。
数据格式如下:
0{"sid":"ab4507c4-d947-4deb-92e4-8a9e34a9f0b2","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":60000}
- 0: 代表
open标识 - sid: session id
- upgrades: 升级协议类型
- pingInterval:
ping的间隔时长 - pingTimeout: 判断连接超时时长
当客户端收到响应之后,scoket.io会根据当前客户端环境是否支持Websocket。如果支持,则建立一个websocket连接,否则使用polling(xhr、jsonp)长轮询进行双向数据通信。
6. socket.io 协议解析
socket.io协议中定义的数据格式称之为Pakcet ,每一个Packet都含有nsp的对象值。
Packet
编码包可以是UTF8或二进制数据,编码格式如下:
<包类型id>[<data>]
例如:
2probe
包类型id(packet type id)是一个整型,具体含义如下:
- 0 open 当打开一个新传输时,服务端检测并发送
- 1 close 请求关闭传输,但不是主动断开连接
- 2 ping
客户端发出,服务端应该返回包含相同数据的
pongpacket进行应答 - 3 pong
服务端发出,用以响应客户端的
pingpacket - 4 message
真实数据,客户端和服务端应该调用回调中的
data
// 服务端发送
send('4HelloWorld')
// 客户端接收数据并调用回调
socket.on('message', function (data) { console.log(data); });
// 客户端发送
send('4HelloWorld')
// 服务端接收数据并调用回调
socket.on('message', function (data) { console.log(data); })
- 5 upgrade
在
engine.io切换传输之前,它会测试服务器和客户端是否可以通过此传输进行通信。如果此测试成功,客户端将发送升级数据包,请求服务器刷新旧传输上的缓存并切换到新传输。 - 6 noop
nooppacket。主要用于在收到传入的websocket连接时强制轮询周期。- 客户端通过新的传输连接
- 客户端发送
2send - 服务端接收并发送
3probe - 客户端结束并发送
5 - 服务端刷新并关闭旧的传输连接并切换到新传输连接
7. socket.io 全家桶
区别