HTML5 之 WebSocket

2013 年 6 月 14 日

什么是 WebSocket

WebSocket 是 HTML5 提出的一个协议规范, 参考rfc6455. 不过目前还都是在草案, 没有成为标准, 毕竟 HTML5 还在路上.

WebSocket 约定了一个通信的规范, 通过一个握手的机制, 客户端(浏览器)和服务器(WebServer)之间能建立一个类似 TCP 的连接, 从而方便 CS 之间的通信.

在 WebSocket 出现之前, web 交互一般是基于 HTTP 协议的短连接或者长连接. 短连接的过程大概有下面几个步骤:

  • 建立 TCP 连接
  • 浏览器发出 HTTP 请求
  • WebServer 应答
  • 断开 TCP 连接.

优点是简洁明了, 缺点也很明显, 每一次的交互中, 建立和断开 TCP 连接带来了比较大的开销, 而且 HTTP 协议头比较长, 也会带来带宽的浪费.

通过设置 HTTP 头中的keep-alive域可以实现 HTTP 长连接, 避免了建立和断开连接的开销, 但是 HTTP 协议头的问题仍然无法解决.

除此之外, WebServer 主动向浏览器推送数据的处理会比较麻烦, 要么是浏览器发起轮询(毫无疑问, 这是一个低效的方式), 或者利用comet技术), 比较复杂, 而且这已经不是在协议层上解决问题了.

而 WebSocket 的出现, 解决了上面描述的问题.

WebSocket 握手协议

WebSocket 是一种全新的协议, 不属于 HTTP 无状态协议, 协议名为"ws", 这意味着一个 WebSocket 连接地址会是这样的写法:ws://**.

WebSocket 协议本质上是一个基于 TCP 的协议. 建立连接需要握手, 客户端(浏览器)首先向服务器(WebServer) 发起一条特殊的 HTTP 请求, WebServer 解析后生成应答到浏览器, 这样子一个 WebSocket 连接就建立了, 直到某一方关闭连接.

由于是草案的原因, 前前后后就出现了多个版本的握手协议, 分情况说明一下.

基于 flash 的握手协议

使用场景是IE的多数版本, 因为 IE 的多数版本不都不支持 WebSocket 协议, 以及 FF、Chrome 等浏览器的低版本, 还没有原生的支持 WebSocket. 此处, server 唯一要做的, 就是准备一个 WebSocket-Location 域给 client, 没有加密, 可靠性很差.

#客户端请求:
GET /ls HTTP/1.1
Upgrade: WebSocket
Connection: Upgrade
Host: www.xx.com
Origin: http://www.xx.com

#服务器返回:
HTTP/1.1 101 Web Socket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
WebSocket-Origin: http://www.xx.com
WebSocket-Location: ws://www.xx.com/ls

基于md5加密方式的握手协议

# 客户端请求:
GET /demo HTTP/1.1
Host: example.com
Connection: Upgrade
Sec-WebSocket-Key2: **
Upgrade: WebSocket
Sec-WebSocket-Key1: **
Origin: http://example.com
[8-byte security key]

# 服务端返回:
HTTP/1.1 101 WebSocket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
WebSocket-Origin: http://example.com
WebSocket-Location: ws://example.com/demo
[16-byte hash response]

其中 Sec-WebSocket-Key1, Sec-WebSocket-Key2 和 [8-byte security key] 这几个头信息是web server用来生成应答信息的来源, 依据 draft-hixie-thewebsocketprotocol-76 草案的定义, web server基于以下的算法来产生正确的应答信息:

  • 逐个字符读取 Sec-WebSocket-Key1 头信息中的值, 将数值型字符连接到一起放到一个临时字符串里, 同时统计所有空格的数量.
  • 将在第(1)步里生成的数字字符串转换成一个整型数字, 然后除以第(1)步里统计出来的空格数量, 将得到的浮点数转换成整数型.
  • 将第(2)步里生成的整型值转换为符合网络传输的网络字节数组.
  • 对 Sec-WebSocket-Key2 头信息同样进行第(1)到第(3)步的操作, 得到另外一个网络字节数组.
  • 将 [8-byte security key] 和在第(3)、(4)步里生成的网络字节数组合并成一个16字节的数组.
  • 对第(5)步生成的字节数组使用MD5算法生成一个哈希值, 这个哈希值就作为安全密钥返回给客户端, 以表明服务器端获取了客户端的请求, 同意创建 WebSocket 连接.

基于sha加密方式的握手协议

也是目前见的最多的一种方式, 这里的版本号目前是需要13以上的版本.

# 客户端请求:
GET /ls HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: www.xx.com
Sec-WebSocket-Origin: http://www.xx.com
Sec-WebSocket-Key: 2SCVXUeP9cTjV+0mWB8J6A==
Sec-WebSocket-Version: 13

# 服务器返回:
HTTP/1.1 101 Switching Protocols
Upgrade: :websocket
Connection: Upgrade
Sec-WebSocket-Accept: mLDKNeBNWz6T9SxU+o0Fy/HgeSw=

它的原理, 是把客户端上报的 key 拼上一段 GUID “258EAFA5-E914-47DA-95CA-C5AB0DC85B11″, 拿这个字符串做 SHA-1 hash 计算, 然后再把得到的结果通过 base64 加密, 最后在返回给客户端.

WebSocket 数据帧

WebScoket 协议中, 数据以帧序列的形式传输, 具体的协议标准可以参考rfc6455

(1) 客户端向服务器传输的数据帧必须进行掩码处理:服务器若接收到未经过掩码处理的数据帧, 则必须主动关闭连接.

(2) 服务器向客户端传输的数据帧一定不能进行掩码处理. 客户端若接收到经过掩码处理的数据帧, 则必须主动关闭连接.

针对上情况, 发现错误的一方可向对方发送 close 帧(状态码是1002, 表示协议错误), 以关闭连接.

FIN:1位

表示这是消息的最后一帧(结束帧), 一个消息由一个或多个数据帧构成. 若消息由一帧构成, 起始帧即结束帧.

RSV1, RSV2, RSV3:各1位

MUST be 0 unless an extension is negotiated that defines meanings for non-zero values. If a nonzero value is received and none of the negotiated extensions defines the meaning of such a nonzero value,  the receiving endpoint MUST Fail the WebSocket Connection.

如果未定义扩展, 各位是0;如果定义了扩展, 即为非0值. 如果接收的帧此处非0, 扩展中却没有该值的定义, 那么关闭连接.

OPCODE:4位

解释 Payload Data, 如果接收到未知的 opcode, 接收端必须关闭连接.
- 0x0 表示附加数据帧
- 0x1 表示文本数据帧
- 0x2 表示二进制数据帧
- 0x3-7 暂时无定义, 为以后的非控制帧保留
- 0x8 表示连接关闭
- 0x9 表示 ping
- 0xA 表示 pong
- 0xB-F 暂时无定义, 为以后的控制帧保留

MASK:1位

用于标识 Payload Data 是否经过掩码处理. 如果是1, Masking-key 域的数据即是掩码密钥, 用于解码 Payload Data. 客户端发出的数据帧需要进行掩码处理, 所以此位是1.

Payload length:7位, 7+16位, 7+64位, Payload Data 的长度(以字节为单位)

- 如果其值在0-125, 则是payload的真实长度.
- 如果值是126, 则后面2个字节形成的16位无符号整型数的值是payload的真实长度. 注意, 网络字节序, 需要转换.
- 如果值是127, 则后面8个字节形成的64位无符号整型数的值是payload的真实长度. 注意, 网络字节序, 需要转换.
- 长度表示遵循一个原则, 用最少的字节表示长度(我理解是尽量减少不必要的传输). 举例说, payload真实长度是124, 在0-125之间, 必须用前7位表示;不允许长度1126127, 然后长度2124, 这样违反原则.

分片

这种情况就比较复杂, 具体可以参考 rfc, 一般在日常中用到的应该会比较少.

客户端API

WebSocket 的客户端 API 可以参考Tutorial

一个简单的代码参考:

<!DOCTYPE HTML>
<html>
<head>
    <script type="text/javascript">
    function WebSocketTest()
    {
        if ("WebSocket" in window)
        {
            alert("WebSocket is supported by your Browser!");
            // Let us open a web socket
            var ws = new WebSocket("ws://localhost:9998/echo");
            ws.onopen = function()
            {
                // Web Socket is connected,  send data using send()
                ws.send("Message to send");
                alert("Message is sent...");
            };
            ws.onmessage = function (evt)
            {
                var received_msg = evt.data;
                alert("Message is received...");
            };
            ws.onclose = function()
            {
                // websocket is closed.
                alert("Connection is closed...");
            };
        }
        else
        {
            // The browser doesn't support WebSocket
            alert("WebSocket NOT supported by your Browser!");
        }
    }
    </script>
</head>
<body>
    <div id="sse">
        <a href="javascript:WebSocketTest()">Run WebSocket</a>
    </div>
</body>
</html>

WebSocket服务器

目前开源的 WebSocket server 的代码还是不少的.

自己也整理过一份C的, 支持上面三种握手情况, 并嵌在 reactor 框架下, 使用chrome测试过: gbase-wsconn

目前主流的浏览器对 WebSocket 的支持:

浏览器 支持性
Chrome Supported in version 4+
Firefox Supported in version 4+
IE Supported in version 10+
Opera Supported in version 10+
Safari Supported in version 5+

参考文章

  1. WIKI
  2. 使用 HTML5 WebSocket 构建实时 Web 应用
  3. WebSocket不同版本的三种握手方式以及一个Netty实现JAVA类
  4. WebSocket协议分析