WebSocket(三)——WebSocket协议

连接握手

每个WebSocket连接都始于一个HTTP请求,该请求包含特殊的首标UpgradeSec-WebSocket-Key。服务端响应101代码、ConnectionUpgradeSec-WebSocket-Accept

图片
图片

其中Sec-WebSocket-Accept响应首标由Sec-WebSocket-Key请求首标计算而来,包含特殊的响应键值。 这两组键值实际上是为了保护非WebSocket服务器,避免跨协议攻击。握手时要求服务端返回客户端期望收到的键值,如果客户端没有收到或与期望的键值不一致,服务端会返回“Error during WebSocket handshake: Sec-WebSocket-Accept mismatch”,然后客户端会关闭连接。那么Sec-WebSocket-Accept怎么计算得来呢?

  • 在Sec-WebSocket-Key键值后添加258EAFA5-E914-47DA-95CA-C5AB0DC85B11(GUID,全局唯一标识符)
  • SHA1转换
  • Base64编码

这样一来得到Sec-WebSocket-Accept并返回给客户端,客户端检查和期望的一致后,连接建立。 php中采用如下的方式获取

1
2
3
function calcKey()
return base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
}

值得注意的是sha1()函数的第二个参数是可选的,默认为false,此时返回值是一个 40 字符长度的十六进制数字。在计算Sec-WebSocket-Accept的时候需要传入true,以 20 字符长度的原始格式返回。

数据帧

图片

  • FIN:1 bit
    为1表明这个是消息的最后片段。

  • RSV1, RSV2, RSV3:各1 bit
    双方协定自定义协议,否则这三位必须是 0。

  • Opcode:4 bits
    4位指定消息载荷类型的操作码。

    • %x0 代表一个继续帧
    • %x1 代表一个文本帧
    • %x2 代表一个二进制帧
    • %x8 代表连接关闭
    • %x9 代表 ping
    • %xA 代表 pong
    • %x3-7和%xB-F 保留用于未来的控制帧

ping和pong能够保持连接打开,为数据流动做好准备。一个 ping 即可以充当一个 keepalive,也可以作为验证远程端点仍可响应的手段。ping和pong可以从连接的任意一端发起,但大部分的ping和pong是由服务器端发起的。当收到一个ping时,接收端必须响应一个pong;一个pong也可以未经请求发送,用于单向的心跳。

  • Mask:1 bit
    为1表明“负载数据”是掩码的,掩码键出现在 masking-key,用于解掩码 “负载数据”。从客户端发送到服务端的所有帧有这个位设置为 1。

  • Payload length:7 bits, 7+16 bits, 或7+64 bits
    “负载数据”的长度,以字节为单位:如果 0-125,这是负载长度。如果 126,之后的两字节解释为一个 16 位的无符号整数是负载长度。如果 127,之后的 8字节解释为一个 64 位的无符号整数(最高有效位必须是 0)是负载长度负载长度是“扩展数据”长度+“应用数据”长度。“扩展数据”长度可能是零,在这种情况下,负载长度是“应用数据”长度。

  • Masking-key:0 或 4 bytes
    客户端发送到服务端的所有帧通过一个包含在帧中的 32 位值来掩码。掩码键是由客户端随机选择的 32 位值。当准备一个掩码的帧时,客户端必须从允许的 32 位值集合中选择一个新的掩码键。

服务端收到掩码处理后的数据后,解码采用如下的算法。将Payload原始数据的每个字符的顺序下标模4,然后将此原始数据字符与掩码的模4相应位置的字符进行异或操作。这个算法对于加密和解密的操作都是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function decode($buffer) {
$len = $masks = $data = $decoded = null;
$len = ord($buffer[1]) & 127;
if ($len === 126) {
$masks = substr($buffer, 4, 4);
$data = substr($buffer, 8);
}
else if ($len === 127) {
$masks = substr($buffer, 10, 4);
$data = substr($buffer, 14);
}
else {
$masks = substr($buffer, 2, 4);
$data = substr($buffer, 6);
}
for ($index = 0; $index < strlen($data); $index++) {
$decoded .= $data[$index] ^ $masks[$index % 4];
}
return $decoded;
}
  • Payload data:(x+y) bytes
    “负载数据”定义为“扩展数据”和“应用数据”。如果没有定义扩展,则没有扩展数据,仅含有应用数据。

关闭握手

为了关闭WebSocket连接,一端必须发送一个关闭的控制帧,此时WebSocket 关闭阶段握手已启动, WebSocket 连接处于CLOSING状态。当两端都发送了关闭数据帧时,双方都要关闭所有的连接资源,当关闭之后,双方处于CLOSED状态。控制帧为一个“状态码”和一个“原因说明”,正常关闭的状态码为1000;如果close控制帧不包含状态码,close状态码被认为是1005;如果WebSocket连接已经关闭且端点没有接收到close状态码(例如可能发生在底层传输连接丢失时),close状态码被认为是1006。


参考文章

Just a beginner.<br /><a href='https://github.com/yaoshanliang/about' target='_blank'>profile</a>