上一篇文章《浅析一次HTTP请求》咱们分析了简单的一次 HTTP 请求具体是怎么样完成的,分析了 HTTP 协议的数据结构,如何链接,如何断开,又是如何多路复用的,那么今天咱们来聊聊另一个协议,WebSocket。因为 WebSocket 的协议的内容很是多,本文只会取其冰山一角进行简单阐述,不会铺开详细说。html
在 WebSocket 协议出现之前,建立一个和服务端进双通道通讯的 web 应用,须要依赖HTTP协议,进行不停的轮询,这会致使一些问题:git
服务端被迫维持来自每一个客户端的大量不一样的链接github
大量的轮询请求会形成高开销,好比会带上多余的header,形成了无用的数据传输。web
因此,为了解决这些问题,WebSocket 协议应运而生。算法
WebSocket 是一种在单个TCP链接上进行全双工通讯的协议。 WebSocket 使得客户端和服务器之间的数据交换变得更加简单,容许服务端主动向客户端推送数据。浏览器
在 WebSocket API 中,浏览器和服务器只须要完成一次握手,二者之间就直接能够建立持久性的链接, 并进行双向数据传输。(维基百科)缓存
下图是我参考 RFC6455 5.2章节画的websocket 基础帧的数据结构图,接下里咱们重点解析下数据结构图。安全
FIN:占用1 bit,表示这是消息的最后一个片断。第一个片断也有多是最后一个片断。bash
RSV1,RSV2,RSV3: 每一个1 bit服务器
必须设置为0,除非扩展了非0值含义的扩展。若是收到了一个非0值可是没有扩展任何非0值的含义,接收终端必须断开WebSocket链接。
Opcode: 4 bit,操做码,若是收到一个未知的操做码,接收终端必须断开WebSocket链接。
%x0 表示一个持续帧
%x1 表示一个文本帧
%x2 表示一个二进制帧
%x3-7 预留给之后的非控制帧
%x8 表示一个链接关闭包
%x9 表示一个ping包
%xA 表示一个pong包
%xB-F 预留给之后的控制帧
Mask: 1 bit,mask标志位,定义“有效负载数据”是否添加掩码。若是设置为1,那么掩码的键值存在于Masking-Key中。
Payload length: 7 bits, 7+16 bits, or 7+64 bits,以字节为单位的“有效负载数据”长度。
Masking-Key: 0 or 4 bytes,
全部从客户端发往服务端的数据帧都已经与一个包含在这一帧中的32 bit的掩码进行过了运算。若是mask标志位(1 bit)为1,那么这个字段存在,若是标志位为0,那么这个字段不存在。 备注:载荷数据的长度,不包括mask key的长度。。
Payload data: 有效负载数据
为了安全,但并非为了防止数据泄密,而是为了防止早期版本的协议中存在的代理缓存污染攻击(proxy cache poisoning attacks)等问题。
我写了一个DMEMO用来抓包分析 websocket,源代码会放在文章末尾的连接。DEMO效果以下:
页面提供链接与断开功能,输入本身的名字发送,服务端返回Hello,名字!功能很简单,咱们先看看页面的请求和响应。
请求:
响应:
这里的请求与响应就是反应了 WebSocket 的一次握手,咱们根据上图能够简单抽象一下 WebSocket 的请求和响应格式: 客户端握手请求格式:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
复制代码
服务端握手响应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
复制代码
咱们重点说明下结果请求字段:
Upgrade:表示HTTP协议升级为webSocket
connection:Upgrade 请求升级。
Sec-WebSocket-Key: 用于服务端进行标识认证,生成全局惟一id,GUID。
Sec-WebSocket-Version: 版本
Sec-WebSocket-Protocol: 请求服务端使用指定的子协议。若是指定了这个字段,服务器须要包含相同的字段,而且从子协议的之中选择一个值做为创建链接的响应。
Sec-WebSocket-Extensions: WebSocket的扩展。
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= 生成的全局惟一id,GUID。
GUID的生成算法
算法思想:经过 Sec-WebSocket-Key 传入的 值,dGhlIHNhbXBsZSBub25jZQ==,链接服务端生成的字符串,拼接格式以下
dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-
C5AB0DC85B11
复制代码
, 而后采用SHA-1哈希算法,而后用base64编码生成最终的 Sec-WebSocket-Accept的值,生成的值就是
s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
复制代码
(注意,这里SHA1哈希算法生成的结果必须是二进制的哈希结果,好比
Python代码中的
h = hashlib.sha1("dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
.digest()
复制代码
,若是用在线处理工具生成,生成的Hash是16进制的哈希,用 Base64就会生成错误结果)。
我在DEMO中的操做流程以下:
用 Wireshark 抓包以下:
A请求 GET /gs-guide-websocket/info?t=1551252237372 HTTP/1.1
B请求 GET /gs-guide-websocket/690/pdsz5x1q/websocket HTTP/1.1
复制代码
根据 RFC6455 协议规定 WebSocket 只须要一次握手就能够完成,因此咱们只须要分析第二次的http 握手请求,A请求应该是使用的框架层面本身实现。
咱们根据截图能够知道,B请求对应的响应是序号 192 的数据,返回码是101,根据 HTTP 返回码咱们能够知道,服务器已经理解了客户端的请求,并将经过Upgrade 消息头通知客户端采用不一样的协议来完成这个请求。在发送完这个响应最后的空行后,服务器将会切换到在 Upgrade 消息头中定义的那些协议,也就是升级为 WebSocket 协议。因此接着193的包已经变成了 WebSocket 协议了。到这里,WebSocket 的握手链接就已经完成了。
接下来咱们分析下发送消息的流程,这里你们确定会疑惑,就发送了一条消息,为啥会有这么多 WebSocket 的包呢?其实这里多余的包是框架层面进行发送的,好比要进行订阅与发布的注册等等操做。因此真正使咱们操做的包就只有断开链接的相关包和发送“LUOZHOU”的包
根据上图咱们发现 序号229的包是一个文本类型的包,opcode:1
,而后采用了掩码处理,同时是最后一个处理包。咱们仔细发现全部客户端发送服务端的包都会有[MASKED]标记,服务端返回的没有,这就说明了从客户端向服务端发送数据时,须要对数据进行掩码操做;从服务端向客户端发送数据时,不须要对数据进行掩码操做。
WebSocket 是为了在 web 应用上进行双通道通讯而产生的协议,相比于轮询HTTP请求的方式,WebSocket 有节省服务器资源,效率高等优势。
WebSocket 中的掩码是为了防止早期版本中存在中间缓存污染攻击等问题而设置的,客户端向服务端发送数据须要掩码,服务端向客户端发送数据不须要掩码。
WebSocket 中 Sec-WebSocket-Key 的生成算法是拼接服务端和客户端生成的字符串,进行SHA1哈希算法,再用base64编码。
WebSocket 协议握手是依靠 HTTP 协议的,依靠于 HTTP 响应101进行协议升级转换。
[1]RFC6455