为什么需要WebSocket?
我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?
答案很简单,因为 HTTP 协议有一个缺陷:通信只能由客户端发起。
举例来说,我们想了解今天的天气,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。
什么是WebSocket
WebSocket 是一种网络传输协议,可在单个TCP连接上进行全双工通信,位于OSI模型的应用层。WebSocket协议在2008年诞生,在2011年由IETF标准化为RFC 6455,后由RFC 7936补充规范。Web IDL中的WebSocket API由W3C标准化。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。
特点
可以由服务端向客户端主动推送消息,也可以由客户端主动向服务器端发送消息
-
建立在 TCP 协议之上,服务器端的实现比较容易。
-
与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
-
数据格式比较轻量,性能开销小,通信高效。
-
可以发送文本,也可以发送二进制数据,多字节的消息作为整体、按照顺序到达。
-
没有同源限制,客户端可以与任意服务器通信。
-
协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
HTML中的WebSocket
不同于http协议的短连接、无状态等特点,WebSocket 是一种长链接、持久性链接。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。(在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。)
使用场景:
可用于主动向客户端推送消息,用于聊天室或消息提醒等功能
优点:
websockets建立一次链接,长久通讯;节省服务器资源和带宽,并且能够更实时地进行通讯,避免轮询所产生的 请求一次都要三次握手。请求完毕就会断开链接,每断开一次都要四次挥手;
WebSocket连接
WebSocket(url[, protocols]) 返回一个 WebSocket 对象
var ws = new WebSocket('ws://localhost:8080');
执行上面语句之后,客户端就会与服务器进行连接。Websocket是应用层第七层上的一个应用层协议,它必须依赖 HTTP 协议进行一次握手 ,握手成功后,数据就直接从 TCP 通道传输,与 HTTP 无关了。即:websocket分为握手和数据传输阶段,即进行了HTTP握手 + TCP连接。
握手阶段
可以看到:
Request URL: ws://localhost:3001/ifc/userinfo
Request Method: GET
Status Code: 101 Switching Protocols
该响应代码101表示本次连接的HTTP协议即将被更改,更改后的协议就是Upgrade: websocket指定的WebSocket协议。
版本号和子协议规定了双方能理解的数据格式,以及是否支持压缩等等。如果仅使用WebSocket的API,就不需要关心这些。
现在,一个WebSocket连接就建立成功,浏览器和服务器就可以随时主动发送消息给对方。消息有两种,一种是文本,一种是二进制数据。通常,我们可以发送JSON格式的文本,这样,在浏览器处理起来就十分容易。
为什么WebSocket连接可以实现全双工通信而HTTP连接不行呢?实际上HTTP协议是建立在TCP协议之上的,TCP协议本身就实现了全双工通信,但是HTTP协议的请求-应答机制限制了全双工通信。WebSocket连接建立以后,其实只是简单规定了一下:接下来,咱们通信就不使用HTTP协议了,直接互相发数据吧。
安全的WebSocket连接机制和HTTPS类似。首先,浏览器用 wss://xxx 创建WebSocket连接时,会先通过HTTPS创建安全的连接,然后,该HTTPS连接升级为WebSocket连接,底层通信走的仍然是安全的SSL/TLS协议。 接下来先看Request Headers:
- Connection: Upgrade 表示要升级协议
- Upgrade: websocket 要升级协议到websocket协议
- Sec-WebSocket-Version 表示websocket的版本。如果服务端不支持该版本,需要返回一个Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。
- Sec-WebSocket-Key 对应服务端响应头的Sec-WebSocket-Accept,由于没有同源限制,websocket客户端可任意连接支持websocket的服务。这个就相当于一个钥匙一把锁,避免多余的,无意义的连接。
以及服务端响应的 Response Headers
下面是 HTTP最后负责的区域,告知客户端,我已成功切换协议
Upgrade: websocket
Connection: Upgrade
固定的,告诉客户端即将升级的是Websocket协议,Sec-WebSocket-Accept 这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key,加密前的为 Request Headers 中的 Sec-WebSocket-Key
Sec-WebSocket-Accept: 1UICHxfa2f/7BPbUtf8Y4160J/k=
实现方式如下:
// 指定拼接字符 该值貌似为固定常量
var ws_key = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
// 生成相应key
function getAccpectKey(rSWKey) {
return crypto.createHash('sha1').update(rSWKey + ws_key).digest('base64')
}
数据传输阶段
在WebSocket协议中,数据是通过帧序列(frame)来传输的。为了数据安全原因,客户端必须掩码(mask)它发送到服务器的所有帧,当它收到一个没有掩码的帧时,服务器必须关闭连接。不过服务器端给客户端发送的所有帧都不是掩码的,如果客户端检测到掩码的帧时,也一样必须关闭连接。当帧被关闭的时候,可能发送状态码1002(协议错误)。
一条消息可以分为几个frame,按照先后顺序传输出去。这样做有几个好处:
- 大数据的传输可以分片传输,不用考虑到数据大小导致的长度标志位不足够的情况。
- 和http的chunk一样,可以边生成数据边传递消息,即提高传输效率。 基本帧协议如下:
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位,用来表明这是一个消息的最后的消息片断,当然第一个消息片断也可能是最后的一个消息片断,否则为0;
-
RSV1, RSV2, RSV3: 分别都是1位,如果双方之间没有约定自定义协议,那么这几位的值都必须为0,否则必须断掉WebSocket连接;
-
Opcode: 4位操作码,定义有效负载数据,表示帧的类型,比如是文本类型还是二进制类型,如果收到了一个未知的操作码,连接也必须断掉,以下是定义的操作码:
* %x0 表示一个延续帧。当 Opcode 为 0 时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片。
* %x1 表示文本消息片断
* %x2 表示二进制消息片断
* %x3-7 为将来的非控制消息片断保留的操作码
* %x8 表示连接关闭
* %x9 表示心跳检查的ping
* %xA 表示心跳检查的pong
* %xB-F 为将来的控制消息片断的保留操作码
-
Mask: 1位,定义传输的数据是否有加掩码,如果设置为1,掩码键必须放在masking-key区域,客户端发送给服务端的所有消息,此位的值都是1;为 0 时,不添加掩码,没有masking-key
-
Payload length: 传输数据的长度,以字节的形式表示:7位、7+16位、或者7+64位。
如果这个值以字节表示是0-125这个范围,那这个值就表示传输数据的长度;如果这个值是126,则随后的两个字节表示的是一个16进制无符号数,用来表示传输数据的长度;如果这个值是127,则随后的是8个字节表示的一个64位无符合数,这个数用来表示传输数据的长度。多字节长度的数量是以网络字节的顺序表示。负载数据的长度为扩展数据及应用数据之和,扩展数据的长度可能为0,因而此时负载数据的长度就为应用数据的长度。
-
Masking-key: 0或4个字节,客户端发送给服务端的数据,都是通过内嵌的一个32位值作为掩码的;掩码键只有在掩码位设置为1的时候存在。
-
Payload data: (x+y)位,负载数据为扩展数据及应用数据长度之和。
-
Extension data: x位,如果客户端与服务端之间没有特殊约定,那么扩展数据的长度始终为0,任何的扩展都必须指定
扩展数据
的长度,或者长度的计算方式,以及在握手时如何确定正确的握手方式。如果存在扩展数据,则扩展数据就会包括在负载数据的长度之内。 -
Application data: y位,任意的应用数据,放在扩展数据之后,应用数据的长度=负载数据的长度-扩展数据的长度。
-
小结
基础数据帧协议通过ABNF(增强型巴科斯范式)进行了正式的定义。需要重点知道的是,这些数据都是二进制的,而不是ASCII字符。例如,长度为1 bit的字段的值为%x0 / %x1代表的是一个值为0/1的单独的bit,而不是一整个字节(8 bit)来代表ASCII编码的字符“0”和“1”。一个长度为4 bit的范围是%x0-F的字段值代表的是4个bit,而不是字节(8 bit)对应的ASCII码的值。不要指定字符编码:“规则解析为一组最终的值,有时候是字符。在ABNF中,字符仅仅是一个非负的数字。在特定的上下文中,会根据特定的值的映射(编码)编码集(例如ASCII)”。在这里,指定的编码类型是将每个字段编码为特定的bits数组的二进制编码的最终数据。
WebSocket中的属性及事件
事件
-
open
当客户和websocket客户端连接成功的时候就会触发,事件需要用addEventListener绑定:比如open事件注册示例
websocket.addEventListener('open',function(e){ console.log(e); }); websocket.onopen= function(event) { // handle open event };
-
message
客户端接受到服务器返回的数据时,会触发message事件,所以我们监听这个事件并注册回调函数就行:
websocket.addEventListener('message',function(e){ console.log(event); console.log(event.data);// event里的data属性,就是服务器返回的数据 }); websocket.onmessage= function(event) { // handle message event };
-
close 连接断开的时候触发的事件,同上绑定方法。
websocket.addEventListener('close',function(e){ console.log(event); console.log(event.data);// event里的data属性,就是服务器返回的数据 }); websocket.onclose= function(event) { // handle close event };
-
error 用于指定连接失败后的回调函数,同上绑定方法。
websocket.addEventListener('error',function(e){ console.log(event); console.log(event.data);// event里的data属性,就是服务器返回的数据 }); websocket.onerror= function(event) { // handle error event };
属性
- WebSocket.protocol 是个只读属性,用于返回服务器端选中的子协议的名字;本质是客户端与服务器协商处理数据的方式,类似于HTTP请求头里带的Accept和Accept-Encoding头字段一样,说明自己你可以接受那些类型文件,或者编码。在建立连接的时候进行设置:
this.socket = new WebSocket("ws://localhost:3001/ifc/userinfo",'jsonrpc');
-
WebSocket.extensions 是只读属性,返回服务器已选择的扩展值。目前,链接可以协定的扩展值只有空字符串或者一个扩展列表。
就像其他的 HTTP 请求头字段一样,这个请求头字段可以被切割成几行或者几行合并成一行。因此,下面这两段是等价的:
Sec-WebSocket-Extensions: foo Sec-WebSocket-Extensions: bar; baz=2
是等价于:
Sec-WebSocket-Extensions: foo, bar; baz=2
-
WebSocket.url 是一个只读属性,返回值为当构造函数创建WebSocket实例对象时URL的绝对路径。
-
WebSocket.readyState 返回当前 WebSocket 的链接状态,只读,共有四种。
CONNECTING:值为0,表示正在连接。 OPEN:值为1,表示连接成功,可以通信了。 CLOSING:值为2,表示连接正在关闭。 CLOSED:值为3,表示连接已经关闭,或者打开连接失败。
方法
- WebSocket.close() 关闭当前连接或连接尝试(如果有的话)。如果连接已经关闭,则此方法不执行任何操作。
-
code (可选)
一个数字状态码,它解释了连接关闭的原因。如果没有传这个参数,默认使用1005,如果客户端主动发起关闭请求,则code应传
1000
。CloseEvent的允许的状态码见状态码列表。无效code则会抛出异常INVALID_ACCESS_ERR
。 -
reason (可选)
一个人类可读的字符串,它解释了连接关闭的原因。这个UTF-8编码的字符串不能超过123个字节,如超过则会抛出异常
SYNTAX_ERR
。
-
- WebSocket.send(data) 方法将需要通过 WebSocket 链接传输至服务器的数据排入队列,并根据所需要传输的data bytes的大小来增加 bufferedAmount的值 。若数据无法传输(例如数据需要缓存而缓冲区已满)时,套接字会自行关闭。
- data 用于传输至服务器的数据。它必须是以下类型之一:
-
USVString
文本字符串。字符串将以 UTF-8 格式添加到缓冲区,并且 bufferedAmount 将加上该字符串以 UTF-8 格式编码时的字节数的值。
-
ArrayBuffer
您可以使用一有类型的数组对象发送底层二进制数据;其二进制数据内存将被缓存于缓冲区,bufferedAmount 将加上所需字节数的值。
-
Blob
Blob 类型将队列 blob 中的原始数据以二进制中传输。 bufferedAmount 将加上原始数据的字节数的值。blob的数据是逐渐上传,参见demo
const blob = new Blob([sen], { type: "text/plain" });; this.socket.send(blob); console.log(`未发送至服务器的字节数:${this.socket.bufferedAmount}`); let timer = setInterval(()=>{ console.log(`未发送至服务器的字节数:${this.socket.bufferedAmount}`); if (this.socket.bufferedAmount === 0) { clearInterval(timer) } },2)
-
ArrayBufferView
您可以以二进制帧的形式发送任何 JavaScript 类数组对象 ;其二进制数据内容将被队列于缓冲区中。值 bufferedAmount 将加上必要字节数的值。
-
- data 用于传输至服务器的数据。它必须是以下类型之一: