WebSocket 协议详解
一、WebSocket 协议背景
早期,在网站上推送消息给用户,只能通过轮询的方式或 Comet 技术。轮询就是浏览器每隔几秒钟向服务端发送 HTTP 请求,然后服务端返回消息给客户端。
轮询技术一般在浏览器上就是使用 setInerval 或 setTimeout
这种方式的缺点:
需要不断的向服务端发送 HTTP 请求,这种就比较浪费带宽资源。而且发送 HTTP 请求只能由客户端发起,这也是早期 HTTP1.0/1.1 协议的一个缺点。它做不到由服务端向客户端发起请求。
为了能实现客户端和服务端的双向通信,经过多年发展于是 WebSocket 协议在 2008 年就诞生了。
它最初是在 HTML5 中引入的。经过多年发展后,该协议慢慢被多个浏览器支持,RFC 在 2011 年就把该协议作为一个国际标准,叫 rfc6455。
二、协议简介
WebSocket 是一种支持双向通信的网络协议。
- 双向通信:客户端(比如浏览器)可以向服务端发送消息,服务端也可以主动向客户端发送消息。
这样就实现了客户端和服务端的双向通信,那么上面所说的消息推送就比较容易实现了。
原先的 HTTP1.0/1.1 只能是客户端向服务端发送消息。
协议特点:
- 建立在 TCP 协议之上。
- WebSocket 协议是从 HTTP 协议升级而来。
- 与 HTTP 协议良好兼容新。默认端口是 80 和 443,握手阶段采用 HTTP 协议。
- 数据格式比较轻量,通信效率高,性能开销小。
- 可以发送文本,也可以发送二进制数据。
- 没有同源限制,客户端可以与任意服务端通信。
- 协议标识符是 ws(如果加密,则为 wss),服务器网址就是 URL。
- 可以支持扩展,定了扩展协议。
- 保持连接状态,websocket 是一种有状态的协议,通信就可以省略部分状态信息。
- 实时性更强,因为是双向通信协议,所以服务端可以随时向客户端发送数据。
三、HTTP 升级到 WebSocket 过程
WebSocket 协议建立复用了 HTTP 的握手请求过程。
客户端通过 HTTP 请求与 WebSocket 服务端协商升级协议。协议完成后,后续的数据交互则遵循 WebSocket 的协议。
- 客户端发起协议升级请求
GET / HTTP/1.1
Host: localhost:8080
Origin: http://127.0.0.1:3000
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==
说明:上面请求信息忽略了 HTTP 的一些非必要头部请求信息,剔除多余的干扰。
- Origin: http://127.0.0.1:3000 : 原始的协议和URL
- Connection: Upgrade:表示要升级协议了
- Upgrade: websocket:表示要升级到 WebSocket 协议;
- Sec-WebSocket-Version: 13:表示 WebSocket 的版本。如果服务端不支持该版本,需要返回一个
Sec-WebSocket-Versionheader
,里面包含服务端支持的版本号 - Sec-WebSocket-Key:与后面服务端响应首部的 Sec-WebSocket-Accept 是配套的,提供基本的防护,比如恶意的连接,或者无意的连接
- 服务端响应协议升级
HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
HTTP/1.1 101 Switching Protocols: 状态码 101 表示协议切换
Sec-WebSocket-Accept:根据客户端请求首部的 Sec-WebSocket-Key 计算出来
将 Sec-WebSocket-Key 跟 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接。
通过 SHA1 计算出摘要,并转成 base64 字符串。计算公式如下:
Base64(sha1(Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11))
Connection:Upgrade:表示协议升级
Upgrade: websocket:升级到 websocket 协议
四、WebSocket 数据交换
数据帧格式
在 WebSocket 协议中,客户端与服务端数据交换的最小信息单位叫做帧(frame),由 1 个或多个帧按照次序组成一条完整的消息(message)。
数据传输的格式是由 ABNF 来描述的。
WebSocket 数据帧的统一格式如下图:
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 ... |
+---------------------------------------------------------------+
(https://www.rfc-editor.org/rfc/rfc6455.html#section-5.2 Base Framing Protocol)
上面图中名词解释:
名词 | 说明 | 大小 |
---|---|---|
FIN | 如果是 1,表示这是消息(message)的最后一个分片(fragment);如果是 0,表示不是是消息(message)的最后一个分片(fragment) | 1 个比特 |
RSV1, RSV2, RSV3 | 一般情况下全为 0。当客户端、服务端协商采用 WebSocket 扩展时,这三个标志位可以非 0,且值的含义由扩展进行定义。如果出现非零的值,且并没有采用 WebSocket 扩展,连接出错 | 各占 1 个比特 |
opcode | 操作代码,Opcode 的值决定了应该如何解析后续的数据载荷(data payload)。如果操作代码是不认识的,那么接收端应该断开连接(fail the connection) | 4 个比特 |
mask | 表示是否要对数据载荷进行掩码操作。从客户端向服务端发送数据时,需要对数据进行掩码操作;从服务端向客户端发送数据时,不需要对数据进行掩码操作。 如果服务端接收到的数据没有进行过掩码操作,服务端需要断开连接。 如果 Mask 是 1,那么在 Masking-key 中会定义一个掩码键(masking key),并用这个掩码键来对数据载荷进行反掩码。所有客户端发送到服务端的数据帧,Mask 都是 1。 |
1 个比特 |
Payload length | 数据载荷的长度,单位是字节。假设数 Payload length === x,如果: x 为 0~126:数据的长度为 x 字节。 x 为 126:后续 2 个字节代表一个 16 位的无符号整数,该无符号整数的值为数据的长度。 x 为 127:后续 8 个字节代表一个 64 位的无符号整数(最高位为 0),该无符号整数的值为数据的长度。 此外,如果 payload length 占用了多个字节的话,payload length 的二进制表达采用网络序(big endian,重要的位在前)。 |
为 7 位,或 7+16 位,或 1+64 位。 |
Masking-key | 所有从客户端传送到服务端的数据帧,数据载荷都进行了掩码操作,Mask 为 1,且携带了 4 字节的 Masking-key。如果 Mask 为 0,则没有 Masking-key。 备注:载荷数据的长度,不包括 mask key 的长度。 |
0 或 4 字节(32 位 |
Payload data | 载荷数据:包括了扩展数据、应用数据。其中,扩展数据 x 字节,应用数据 y 字节。The "Payload data" is defined as "Extension data" concatenated with "Application data". 扩展数据:如果没有协商使用扩展的话,扩展数据数据为 0 字节。所有的扩展都必须声明扩展数据的长度,或者可以如何计算出扩展数据的长度。此外,扩展如何使用必须在握手阶段就协商好。如果扩展数据存在,那么载荷数据长度必须将扩展数据的长度包含在内。 应用数据:任意的应用数据,在扩展数据之后(如果存在扩展数据),占据了数据帧剩余的位置。载荷数据长度 减去 扩展数据长度,就得到应用数据的长度。 |
(x+y) 字节 |
表中 opcode 操作码:
- %x0:表示一个延续帧(continuation frame)。当 Opcode 为 0 时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片。
- %x1:表示这是一个文本帧(frame),text frame
- %x2:表示这是一个二进制帧(frame),binary frame
- %x3-7:保留的操作代码,用于后续定义的非控制帧。
- %x8:表示连接断开。connection close
- %x9:表示这是一个 ping 操作。a ping
- %xA:表示这是一个 pong 操作。a pong
- %xB-F:保留的操作代码,用于后续定义的控制帧。
数据帧另外一种表达方式
ws-frame = frame-fin ; 1 bit in length
frame-rsv1 ; 1 bit in length
frame-rsv2 ; 1 bit in length
frame-rsv3 ; 1 bit in length
frame-opcode ; 4 bits in length
frame-masked ; 1 bit in length
frame-payload-length ; either 7, 7+16,
; or 7+64 bits in
; length
[ frame-masking-key ] ; 32 bits in length
frame-payload-data ; n*8 bits in
; length, where
; n >= 0
frame-fin = %x0 ; more frames of this message follow
/ %x1 ; final frame of this message
; 1 bit in length
frame-rsv1 = %x0 / %x1
; 1 bit in length, MUST be 0 unless
; negotiated otherwise
frame-rsv2 = %x0 / %x1
; 1 bit in length, MUST be 0 unless
; negotiated otherwise
frame-rsv3 = %x0 / %x1
; 1 bit in length, MUST be 0 unless
; negotiated otherwise
frame-opcode = frame-opcode-non-control /
frame-opcode-control /
frame-opcode-cont
frame-opcode-cont = %x0 ; frame continuation
frame-opcode-non-control= %x1 ; text frame
/ %x2 ; binary frame
/ %x3-7
; 4 bits in length,
; reserved for further non-control frames
frame-opcode-control = %x8 ; connection close
/ %x9 ; ping
/ %xA ; pong
/ %xB-F ; reserved for further control
; frames
; 4 bits in length
frame-masked = %x0
; frame is not masked, no frame-masking-key
/ %x1
; frame is masked, frame-masking-key present
; 1 bit in length
frame-payload-length = ( %x00-7D )
/ ( %x7E frame-payload-length-16 )
/ ( %x7F frame-payload-length-63 )
; 7, 7+16, or 7+64 bits in length,
; respectively
frame-payload-length-16 = %x0000-FFFF ; 16 bits in length
frame-payload-length-63 = %x0000000000000000-7FFFFFFFFFFFFFFF
; 64 bits in length
frame-masking-key = 4( %x00-FF )
; present only if frame-masked is 1
; 32 bits in length
frame-payload-data = (frame-masked-extension-data
frame-masked-application-data)
; when frame-masked is 1
/ (frame-unmasked-extension-data
frame-unmasked-application-data)
; when frame-masked is 0
frame-masked-extension-data = *( %x00-FF )
; reserved for future extensibility
; n*8 bits in length, where n >= 0
frame-masked-application-data = *( %x00-FF )
; n*8 bits in length, where n >= 0
frame-unmasked-extension-data = *( %x00-FF )
; reserved for future extensibility
; n*8 bits in length, where n >= 0
frame-unmasked-application-data = *( %x00-FF )
; n*8 bits in length, where n >= 0
客户端到服务端的掩码算法
https://www.rfc-editor.org/rfc/rfc6455.html#section-5.3 Client-to-Server Masking
掩码键(Masking-key)是由客户端挑选出来的 32 位的随机数。掩码操作不会影响数据载荷的长度。掩码、反掩码操作都采用如下算法:
举例说明:
Octet i of the transformed data ("transformed-octet-i") is the XOR of
octet i of the original data ("original-octet-i") with octet at index
i modulo 4 of the masking key ("masking-key-octet-j"): j = i MOD 4
transformed-octet-i = original-octet-i XOR masking-key-octet-j
- original-octet-i:为原始数据的第 i 字节。
- transformed-octet-i:为转换后的数据的第 i 字节。
- j:为i mod 4的结果。
- masking-key-octet-j:为 mask key 第 j 字节。
算法描述为: original-octet-i 与 masking-key-octet-j 异或后,得到 transformed-octet-i。
j = i MOD 4
transformed-octet-i = original-octet-i XOR masking-key-octet-j
数据分片
分片的目的:
- 有了消息分片,发送一个消息的时候,就可以发送未知大小的信息。如果消息不能被分片,那么就不得不缓冲整个消息,以便计算长度。而有了分片就可以选择合适大小缓冲区来缓冲分片。
- 第二个目的是可以使用多路复用。
WebSocket 的每条消息(message)可能被切分为多个数据帧。
当 WebSocket 的接收方接收到一个数据帧时,会根据 FIN 值来判断是否收到消息的最后一个数据帧。
从上图可以看出,FIN = 1 时,表示为消息的最后一个数据帧;FIN = 0 时,则不是消息的最后一个数据帧,接收方还要继续监听接收剩余数据帧。
opcode 表示数据传输的类型,0x01 表示文本类型的数据;0x02 表示二进制类型的数据;0x00 比较特殊,表示延续帧(continuation frame),意思就是完整数据对应的数据帧还没有接收完。
更多分片内容请看这里:https://www.rfc-editor.org/rfc/rfc6455.html#section-5.4
消息分片example:
Client: FIN=1, opcode=0x1, msg="hello"
Server: (process complete message immediately) Hi.
Client: FIN=0, opcode=0x1, msg="and a"
Server: (listening, new message containing text started)
Client: FIN=0, opcode=0x0, msg="happy new"
Server: (listening, payload concatenated to previous message)
Client: FIN=1, opcode=0x0, msg="year!"
Server: (process complete message) Happy new year to you too!
(具体例子见:https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers)
五:怎么保持连接
在第二小结中我们介绍了 websocket 的特点,其中有一个是保持连接状态。
websocket 是建立在 tcp 之上,那也就是客户端与服务端的 tcp 通道要保持连接不断开。
怎么保持呢?可以用心跳来实现。
其实 websocket 协议早就想到了,它的帧数据格式中有一个字段 opcode,定义了 2 种类型操作, ping 和 pong,opcode 分别是 0x9、0xA
。
说明:对于长时间没有数据往来的连接,如果依旧长时间保持连接的状态,那么就会浪费连接资源。
[完]
六、参考
- https://www.rfc-editor.org/rfc/rfc6455.html WebScoket RFC6455
- https://www.rfc-editor.org/rfc/rfc5234 ABNF 格式
- https://www.ruanyifeng.com/blog/2017/05/websocket.html websocket 教程,阮一峰
- https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers
WebSocket 协议详解的更多相关文章
- WebSocket协议详解及应用
WebSocket协议详解及应用(七)-WebSocket协议关闭帧 本篇介绍WebSocket协议的关闭帧,包括客户端及服务器如何发送并处理关闭帧.关闭帧错误码及错误处理方法.本篇内容主要翻译自RF ...
- WebSocket协议详解与c++&c#实现
摘要: 随着手机游戏.H5游戏以及微信小游戏的普及,越来越多的客户端-服务器端的通讯采用websocket协议.Websocket协议是全双工的.基于数据帧的.建立在tcp之上的长连接协议.Webso ...
- WebSocket协议详解
转自 http://www.cnblogs.com/lizhenghn/p/5155933.html 1. websocket 是什么 websocket 是html5提出的一个协议规范,参考rfc6 ...
- websocket协议详解;
websocket是基于http协议,借用http协议来完成连接阶段的握手: 当连接建立后,浏览器和服务器之间的通信就和http协议没有关系了,b.s之间只用websocket协议来完成基本通信. = ...
- HTTP协议详解(转)
转自:http://blog.csdn.net/gueter/archive/2007/03/08/1524447.aspx Author :Jeffrey 引言 HTTP是一个属于应用层的面向对象的 ...
- HTTP协议详解
Author :Jeffrey 引言 HTTP 是一个属于应用层的面向对象的协议,由于其简捷.快速的方式,适用于分布式超媒体信息系统.它于1990年提出,经过几年的使用与发展,得到不断地完善和 扩展. ...
- 动态选路、RIP协议&&OSPF协议详解
动态选路.RIP协议&&OSPF协议详解 概念 当相邻路由器之间进行通信,以告知对方每个路由器当前所连接的网络,这时就出现了动态选路.路由器之间必须采用选路协议进行通信,这样的选路协议 ...
- ASP.NET知识总结(3.HTTP协议详解)
引言 HTTP是一个属于应用层的面向对象的协议,由于其简捷.快速的方式,适用于分布式超媒体信息系统.它于1990年提出,经过几年的使用与发展,得到不断地完善和扩展.目前在WWW中使用的是HTTP/1. ...
- 接口测试之HTTP协议详解
引言 HTTP是一个属于应用层的面向对象的协议,由于其简捷.快速的方式,适用于分布式超媒体信息系统.它于1990年提出,经过几年的使用与发展,得到不断地完善和扩展.目前在WWW中使用的是HTTP/1. ...
随机推荐
- 最新出炉的Java面试题(2022亲身经历)
面试题清单 个人近来面试了不少的公司的,该挂的挂,该应付通过的应付通过,目前对面试题部分做一个系统的总结.最起码要保证被问过的问题第二次被问到的时候是可以回答并且理解的.算是一个被动输入学习的过程. ...
- P2P图书馆实践:让知识更好的传播
人才是每个公司最重要的资产,而人的成长自然就成了最重要的事.苏轼曾经说过:"腹有诗书气自华,代码万行零缺陷",阅读对人成长的影响是巨大的.相信不同的团队都有着自己打造学习氛围.技术 ...
- java中遗留的小问题
一.类型转换 short s = 1; s = s + 1; //false,因为1是int类型,会损失精度 short s = 1; s += 1; //true,因为+=有自带强转 二.逻辑运算符 ...
- Shell 脚本是什么?
一个 Shell 脚本是一个文本文件,包含一个或多个命令.作为系统管理员,我们经常需要使用多个命令来完成一项任务,我们可以添加这些所有命令在一个文本文件(Shell 脚本)来完成这些日常工作任务.- ...
- Oracle数据库 如何根据某个字段名的值去查询存在的表列表
declare v_sql varchar2(1000); data_count number; begin -- OWNER是模式名 tablespace_name是表空间 for cur_tabl ...
- FutureTask 是什么 ?
这个其实前面有提到过,FutureTask 表示一个异步运算的任务.FutureTask 里面 可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等 待获取.判断是否已经 ...
- 【C语言】预处理、宏定义、内联函数 _
一.由源码到可执行程序的过程 1. 预处理: 源码经过预处理器的预处理变成预处理过的.i中间文件 1 gcc -E test.c -o test.i 2. 编译: 中间文件经过编译器编译形成.s的 ...
- 在TypeScript项目中进行BDD测试
在TypeScript项目中进行BDD测试 什么是BDD? BDD(Behavior-Driven Design)是软件团队的一种工作方式,通过以下方式缩小业务人员和技术人员之间的差距: 鼓励跨角色协 ...
- CCF201812-2小明放学
题目背景 汉东省政法大学附属中学所在的光明区最近实施了名为"智慧光明"的智慧城市项目.具体到交通领域,通过"智慧光明"终端,可以看到光明区所有红绿灯此时此刻的状 ...
- IDEA中 mybatis-config、applicationContext.xml、log4j.properties、SpringMVC等文件没有图标标识符号,不是一个xml文件
1. 举例说明 mybatis-config.xml文件不显示图标,识别不出该xml文件 2. 解决办法 1)先点击 File -> Settings-,然后贴入下面代码 (2) 具体操作如下图 ...