websocket 协议解析 [RFC6455]

近期工做忙碌,为了赶SegmentFault for Android 4.0版,到了发疯的程度。
我来汇报一个进度,已经实现基于websocket的私信系统了,多亏了70大大不懈的努力,在不久的未来,咱们使用web给手机发送私信的愿望很快就能够达成了。html

那么在这之余,由于我的对各个通讯协议都有很有兴趣,便顺便去看了下ietf写的关于websocket的文章,原文连接在这:java

https://tools.ietf.org/html/r...nginx

固然若是你对实现websocket协议并没什么兴趣的话,本文能够直接略过,由于你大概知道怎么用就能够了= =web

概念与背景

websocket 目前仍未有标准化的文档,因此目前个人理解是基于RFC6455草案。ajax

websocket的诞生场景是由于咱们在浏览器上缺乏一种与服务器保持长链接的标准化的技术,由于HTTP 1.1(如下简称为HTTP)协议只是一个标准的无状态协议,并不存在除了requestresponse生命周期以外的通讯场景。若是咱们须要实如今线聊天的功能的话,那么基于HTTP,咱们只能使用丑陋的long pollingajax 轮询等非正常的技术实现咱们须要的功能。这些方案虽然解决了咱们的使用场景,但并非最好的方案,咱们的HTTP服务器软件并非设计来保持长链接的。跨域

基于以上场景,websocket诞生了。浏览器

  1. 它是一种真正的长链接,能让服务端和客户端进行持久的通讯,轻松的实现服务器推送技术。安全

  2. 它和HTTP并无特别多的关系,可是它的握手 (handshake) 是利用HTTP来完成。服务器

  3. 它的本质是基于TCP的链接,能够说和HTTP属于平级的应用层协议。websocket

草案阐述了websocket的设计目标,除了创建起基于TCP的一层链接之外,还有如下几个特色

  • 为浏览器增长一个基于域名的安全模型(也就是跨域安全模型)

  • 增长一个基于域名和地址的机制,用来支持在一个IP地址上的一端口多域名的多服务模型(相似nginx在同一个ip上使用不一样的域名来路由到不一样的服务上)

  • 在流式的TCP创建一个帧数据包模型,而且没有长度限制

  • 增长了一个额外的关闭链接握手,给代理和其余中间件使用。

链接时握手

websocket的握手实际上就是给服务器发送一个GET请求,里面带上指定的header便可。

request例子以下

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

其中比较特殊的是UpgradeConnection,和Sec开头的几个字段,那么若是请求握手的话,

Upgrade: websocket
Connection: Upgrade

是固定的要填写的两个键值对。
Sec-WebSocket-Key是一个16位的随机值,通过base64编码后生成,给服务器进行UUID链接再编码后由客户端检查用。
Sec-WebSocket-Version是使用的版本号。
Sec-WebSocket-Protocol是选用的子协议,此字段为可选字段,由服务器选择一个子协议与客户端通讯,子协议是由websocket承载的协议。

response例子以下

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

咱们能够看到这是一个状态码为101的响应,响应的头内容基本和request能够对应,Sec-WebSocket-Accept是服务端利用KeyUUID拼接后再进行base64编码产生的一个值,由客户端进行验证。
这样,咱们的链接时握手就完成了。

数据帧

基础帧协议

由于一些安全的缘由,从客户端发送到服务端的帧所有要与掩码进行异或运算过才有效,而服务端发送到客户端的帧不须要进行异或运算。

咱们来看下官方的一幅帧结构定义图

clipboard.png

接下来逐一解释。

名称 长度 注释
FIN 1bit 标明这一帧是不是整个消息体的最后一帧
RSV1 RSV2 RSV3 1bit 保留位,必须为0,若是不为0,则标记为链接失败
opcode 4bit 操做位,定义这一帧的类型
Mask 1bit 标明承载的内容是否须要用掩码进行异或
Masking-key 0 or 4bytes 掩码异或运算用的key
Payload length 7bit or 7 +16bit or 7 + 64bit 承载体的长度(后续会解释为何会有3种长度)

若是从结构角度讲,那么websocket帧结构就这么简单。

操做码 (opcode)

在websocket中,咱们定义了几种操做类型,也就是代表了数据包的行为,数据包大致可分为两种,一种是字符数据包 (string),一种是字节数据包 (byte) 不一样的数据包使用不一样的opcode来传输,opcode定义以下:
首先Opcode占用4bit的长度

定义
%x0 标明这一个数据包是上一个数据包的延续,它是一个延长帧 (continuation frame)
%x1 标明这个数据包是一个字符帧 (text frame)
%x2 标明这个数据包是一个字节帧 (binary frame)
%x3-7 保留值,供将来的非控制帧使用
%x8 标明这个数据包是用来告诉对方,我方须要关闭链接
%x9 标明这个数据包是一个心跳请求 (ping)
%xA 标明这个数据包是一个心跳响应 (pong)
%xB-F 保留至,供将来的控制帧使用

关于掩码 (Mask)

若是是客户端发送到服务端的数据包,咱们须要使用掩码对payload的每个字节进行异或运算,生成masked payload 才能被服务器读取。
具体的运算其实很简单。

假设payload长度为pLenmask-key长度为mLeni做为payload的游标,j做为mask-key的游标,伪代码以下:

for (i = 0; i < pLen; i++){
    int j = i % mLen;
    maskedPayload[i] = payload[i] ^ maskKey[j];
}

Payload长度

Payload Length位占用了可选的7bit或者7 + 16bit 或者 7 + 64bit,这里是什么意思呢? MDN上有文章也是对websocket协议进行了很好的阐述,先贴原文:

编写websocket服务器

引用其中关于Payload length定义的一段文字:

读解负载数据长度

读取负载数据,须要知道读到那里为止。所以获知负载数据长度很重要。这个过程稍微有点复杂,要如下这些步骤:

  1. 读取9-15位 (包括9和15位自己),并转换为无符号整数。若是值小于或等于125,这个值就是长度;若是是 126,请转到步骤 2。若是它是 127,请转到步骤 3。

  2. 读取接下来的 16 位并转换为无符号整数,并做为长度。

  3. 读取接下来的 64 位并转换为无符号整数,并做为长度。

固然咱们这边所使用的都是网络字节序

关闭链接时的握手

关闭链接的时候,只用发送一个opcode为0x08的帧,payload中前2个字节写入定义的code,后续写入关闭链接的reason,那么一个关闭流程就握手就开始,此处再也不赘述。

总结

websocket协议总体看下来其实仍是很简单,也为咱们的工做节省了不少没必要要的麻烦。也许咱们并不须要了解它实现的细节(除非你正在开发非浏览器的客户端或者服务端)

总而言之,想要学习一个技术,看原始规范果真会让人畅快淋漓啊~

相关文章
相关标签/搜索