WebSocket 详解

更多文章,请在 Github blog查看

WebSocket 出现前

构建网络应用的过程当中,咱们常常须要与服务器进行持续的通信以保持双方信息的同步。一般这种持久通信在不刷新页面的状况下进行,消耗必定的内存资源常驻后台,而且对于用户不可见。在 WebSocket 出现以前,咱们有如下解决方案:javascript

传统轮询(Traditional Polling)

当前Web应用中较常见的一种持续通讯方式,一般采起 setInterval 或者 setTimeout 实现。例如若是咱们想要定时获取并刷新页面上的数据,能够结合Ajax写出以下实现:php

setInterval(function() {
    $.get("/path/to/server", function(data, status) {
        console.log(data);
    });
}, 10000);

上面的程序会每隔10秒向服务器请求一次数据,并在数据到达后存储。这个实现方法一般能够知足简单的需求,然而同时也存在着很大的缺陷:在网络状况不稳定的状况下,服务器从接收请求、发送请求到客户端接收请求的总时间有可能超过10秒,而请求是以10秒间隔发送的,这样会致使接收的数据到达前后顺序与发送顺序不一致。因而出现了采用 setTimeout 的轮询方式:html

function poll() {
    setTimeout(function() {
        $.get("/path/to/server", function(data, status) {
            console.log(data);
            // 发起下一次请求
            poll();
        });
    }, 10000);
}

程序首先设置10秒后发起请求,当数据返回后再隔10秒发起第二次请求,以此类推。这样的话虽然没法保证两次请求之间的时间间隔为固定值,可是能够保证到达数据的顺序。java

长轮询(Long Polling)

上面两种传统的轮询方式都存在一个严重缺陷:程序在每次请求时都会新建一个HTTP请求,然而并非每次都能返回所需的新数据。当同时发起的请求达到必定数目时,会对服务器形成较大负担。这时咱们能够采用长轮询方式解决这个问题。git

长轮询与如下将要提到的服务器发送事件和WebSocket不能仅仅依靠客户端JavaScript实现,咱们同时须要服务器支持并实现相应的技术。

长轮询的基本思想是在每次客户端发出请求后,服务器检查上次返回的数据与这次请求时的数据之间是否有更新,若是有更新则返回新数据并结束这次链接,不然服务器 hold 住这次链接,直到有新数据时再返回相应。而这种长时间的保持链接能够经过设置一个较大的 HTTP timeout` 实现。下面是一个简单的长链接示例:github

服务器(PHP):web

<?php
    // 示例数据为data.txt
    $filename= dirname(__FILE__)."/data.txt";
    // 从请求参数中获取上次请求到的数据的时间戳
    $lastmodif = isset( $_GET["timestamp"])? $_GET["timestamp"]: 0 ;
    // 将文件的最后一次修改时间做为当前数据的时间戳
    $currentmodif = filemtime($filename);

    // 当上次请求到的数据的时间戳*不旧于*当前文件的时间戳,使用循环"hold"住当前链接,并不断获取文件的修改时间
    while ($currentmodif <= $lastmodif) {
        // 每次刷新文件信息的时间间隔为10秒
        usleep(10000);
        // 清除文件信息缓存,保证每次获取的修改时间都是最新的修改时间
        clearstatcache();
        $currentmodif = filemtime($filename);
    }

    // 返回数据和最新的时间戳,结束这次链接
    $response = array();
    $response["msg"] =Date("h:i:s")." ".file_get_contents($filename);
    $response["timestamp"]= $currentmodif;
    echo json_encode($response);
?>

客户端:ajax

function longPoll (timestamp) {
    var _timestamp;
    $.get("/path/to/server?timestamp=" + timestamp)
    .done(function(res) {
        try {
            var data = JSON.parse(res);
            console.log(data.msg);
            _timestamp = data.timestamp;
        } catch (e) {}
    })
    .always(function() {
        setTimeout(function() {
            longPoll(_timestamp || Date.now()/1000);
        }, 10000);
    });
}

长轮询能够有效地解决传统轮询带来的带宽浪费,可是每次链接的保持是以消耗服务器资源为代价的。尤为对于Apache+PHP 服务器,因为有默认的 worker threads 数目的限制,当长链接较多时,服务器便没法对新请求进行相应。算法

服务器发送事件(Server-Sent Event)

服务器发送事件(如下简称SSE)是HTML 5规范的一个组成部分,能够实现服务器到客户端的单向数据通讯。经过 SSE ,客户端能够自动获取数据更新,而不用重复发送HTTP请求。一旦链接创建,“事件”便会自动被推送到客户端。服务器端SSE经过 事件流(Event Stream) 的格式产生并推送事件。事件流对应的 MIME类型 为 text/event-stream ,包含四个字段:event、data、id和retry。event表示事件类型,data表示消息内容,id用于设置客户端 EventSource 对象的 last event ID string 内部属性,retry指定了从新链接的时间。json

服务器(PHP):

<?php
    header("Content-Type: text/event-stream");
    header("Cache-Control: no-cache");
    // 每隔1秒发送一次服务器的当前时间
    while (1) {
        $time = date("r");
        echo "event: ping\n";
        echo "data: The server time is: {$time}\n\n";
        ob_flush();
        flush();
        sleep(1);
    }
?>

客户端中,SSE借由 EventSource 对象实现。EventSource 包含五个外部属性:onerror, onmessage, onopen, readyState、url,以及两个内部属性:reconnection timelast event ID string。在onerror属性中咱们能够对错误捕获和处理,而 onmessage 则对应着服务器事件的接收和处理。另外也可使用 addEventListener 方法来监听服务器发送事件,根据event字段区分处理。

客户端:

var eventSource = new EventSource("/path/to/server");
eventSource.onmessage = function (e) {
    console.log(e.event, e.data);
}
// 或者
eventSource.addEventListener("ping", function(e) {
    console.log(e.event, e.data);
}, false);

SSE相较于轮询具备较好的实时性,使用方法也很是简便。然而SSE只支持服务器到客户端单向的事件推送,并且全部版本的IE(包括到目前为止的Microsoft Edge)都不支持SSE。若是须要强行支持IE和部分移动端浏览器,能够尝试 EventSource Polyfill(本质上仍然是轮询)。SSE的浏览器支持状况以下图所示:

image

对比

>>>>>>>>>>>> 传统轮询 长轮询 服务器发送事件 WebSocket
浏览器支持 几乎全部现代浏览器 几乎全部现代浏览器 Firefox 6+ Chrome 6+ Safari 5+ Opera 10.1+ IE 10+ Edge Firefox 4+ Chrome 4+ Safari 5+ Opera 11.5+
服务器负载 较少的CPU资源,较多的内存资源和带宽资源 与传统轮询类似,可是占用带宽较少 与长轮询类似,除非每次发送请求后服务器不须要断开链接 无需循环等待(长轮询),CPU和内存资源不以客户端数量衡量,而是以客户端事件数衡量。四种方式里性能最佳。
客户端负载 占用较多的内存资源与请求数。 与传统轮询类似。 浏览器中原生实现,占用资源很小。 同Server-Sent Event。
延迟 非实时,延迟取决于请求间隔。 同传统轮询。 非实时,默认3秒延迟,延迟可自定义。 实时。
实现复杂度 很是简单。 须要服务器配合,客户端实现很是简单。 须要服务器配合,而客户端实现甚至比前两种更简单。 须要Socket程序实现和额外端口,客户端实现简单。

WebSocket 是什么

WebSocket 协议在2008年诞生,2011年成为国际标准。全部浏览器都已经支持了。

WebSocket一样是HTML 5规范的组成部分之一,现标准版本为 RFC 6455。WebSocket 相较于上述几种链接方式,实现原理较为复杂,用一句话归纳就是:客户端向 WebSocket 服务器通知(notify)一个带有全部 接收者ID(recipients IDs) 的事件(event),服务器接收后当即通知全部活跃的(active)客户端,只有ID在接收者ID序列中的客户端才会处理这个事件。因为 WebSocket 自己是基于TCP协议的,因此在服务器端咱们能够采用构建 TCP Socket 服务器的方式来构建 WebSocket 服务器。

这个 WebSocket 是一种全新的协议。它将 TCP 的 Socket(套接字)应用在了web page上,从而使通讯双方创建起一个保持在活动状态链接通道,而且属于全双工(双方同时进行双向通讯)。

实际上是这样的,WebSocket 协议是借用 HTTP协议 的 101 switch protocol 来达到协议转换的,从HTTP协议切换成WebSocket通讯协议。

它的最大特色就是,服务器能够主动向客户端推送信息,客户端也能够主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。其余特色包括:

  • 创建在 TCP 协议之上,服务器端的实现比较容易。
  • 与 HTTP 协议有着良好的兼容性。默认端口也是 80 和 443 ,而且握手阶段采用 HTTP 协议,所以握手时不容易屏蔽,能经过各类 HTTP 代理服务器。
  • 数据格式比较轻量,性能开销小,通讯高效。
  • 能够发送文本,也能够发送二进制数据。
  • 没有同源限制,客户端能够与任意服务器通讯。
  • 协议标识符是ws(若是加密,则为wss),服务器网址就是 URL。

协议

WebSocket协议被设计来取代现有的使用HTTP做为传输层的双向通讯技术,并受益于现有的基础设施(代理、过滤、身份验证)。

概述

本协议有两部分:握手和数据传输。

来自客户端的握手看起来像以下形式:

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

来自客户端的首行遵守 Request-Line 格式。 来自服务器的首行遵守 Status-Line 格式。

Request-Line 和 Status-Line 制品定义在 RFC2616

一旦客户端和服务器都发送了它们的握手,且若是握手成功,接着开始数据传输部分。 这是一个每一端均可以的双向通讯信道,彼此独立,随意发生数据。

一个成功握手以后,客户端和服务器来回地传输数据,在本规范中提到的概念单位为“消息”。 在线路上,一个消息是由一个或多个帧的组成。 WebSocket 的消息并不必定对应于一个特定的网络层帧,能够做为一个能够被一个中间件合并或分解的片断消息。

一个帧有一个相应的类型。 属于相同消息的每一帧包含相同类型的数据。 从广义上讲,有文本数据类型(它被解释为 UTF-8 RFC3629文本)、二进制数据类型(它的解释是留给应用)、和控制帧类型(它是不许备包含用于应用的数据,而是协议级的信号,例如应关闭链接的信号)。这个版本的协议定义了六个帧类型并保留10以备未来使用。

握手

客户端:申请协议升级

首先,客户端发起协议升级请求。能够看到,采用的是标准的 HTTP 报文格式,且只支持GET方法。

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==

重点请求首部意义以下:

  • Connection: Upgrade:表示要升级协议
  • Upgrade: websocket:表示要升级到 websocket 协议。
  • Sec-WebSocket-Version: 13:表示 websocket 的版本。若是服务端不支持该版本,须要返回一个 Sec-WebSocket-Versionheader ,里面包含服务端支持的版本号。
  • Sec-WebSocket-Key:与后面服务端响应首部的 Sec-WebSocket-Accept 是配套的,提供基本的防御,好比恶意的链接,或者无心的链接。

服务端:响应协议升级

服务端返回内容以下,状态代码101表示协议切换。到此完成协议升级,后续的数据交互都按照新的协议来。

HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=

Sec-WebSocket-Accept

Sec-WebSocket-Accept 根据客户端请求首部的 Sec-WebSocket-Key 计算出来。

计算公式为:

Sec-WebSocket-Key258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接。

经过 SHA1 计算出摘要,并转成 base64 字符串。

伪代码以下:

>toBase64( sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 )  )

数据帧

WebSocket 客户端、服务端通讯的最小单位是 帧(frame),由 1 个或多个帧组成一条完整的 消息(message)

  • 发送端:将消息切割成多个帧,并发送给服务端;
  • 接收端:接收消息帧,并将关联的帧从新组装成完整的消息;

数据帧格式概览

用于数据传输部分的报文格式是经过本节中详细描述的 ABNF 来描述。

下面给出了 WebSocket 数据帧的统一格式。熟悉 TCP/IP 协议的同窗对这样的图应该不陌生。

从左到右,单位是比特。好比 FINRSV1各占据 1 比特,opcode占据 4 比特。

内容包括了标识、操做代码、掩码、数据、数据长度等。

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 ...                |
 +---------------------------------------------------------------+

数据帧格式详解

针对前面的格式概览图,这里逐个字段进行讲解,若有不清楚之处,可参考协议规范,或留言交流。

FIN:1 个比特。

若是是 1,表示这是 消息(message)的最后一个分片(fragment),若是是 0,表示不是是 消息(message)的最后一个 分片(fragment)

RSV1, RSV2, RSV3:各占 1 个比特。

通常状况下全为 0。当客户端、服务端协商采用 WebSocket 扩展时,这三个标志位能够非 0,且值的含义由扩展进行定义。若是出现非零的值,且并无采用 WebSocket 扩展,链接出错。

Opcode: 4 个比特。

操做代码,Opcode 的值决定了应该如何解析后续的 数据载荷(data payload)。若是操做代码是不认识的,那么接收端应该 断开链接(fail the connection)。可选的操做代码以下:

  • %x0:表示一个延续帧。当 Opcode 为 0 时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片。
  • %x1:表示这是一个文本帧(frame)
  • %x2:表示这是一个二进制帧(frame)
  • %x3-7:保留的操做代码,用于后续定义的非控制帧。
  • %x8:表示链接断开。
  • %x8:表示这是一个 ping 操做。
  • %xA:表示这是一个 pong 操做。
  • %xB-F:保留的操做代码,用于后续定义的控制帧。
Mask: 1 个比特。

表示是否要对数据载荷进行掩码操做。从客户端向服务端发送数据时,须要对数据进行掩码操做;从服务端向客户端发送数据时,不须要对数据进行掩码操做

若是服务端接收到的数据没有进行过掩码操做,服务端须要断开链接。

若是 Mask 是 1,那么在 Masking-key 中会定义一个 掩码键(masking key),并用这个掩码键来对数据载荷进行反掩码。全部客户端发送到服务端的数据帧,Mask 都是 1。

Payload length:数据载荷的长度

单位是字节。为 7 位,或 7+16 位,或 1+64 位。

假设数 Payload length === x,若是

  • x 为 0~126:数据的长度为 x 字节。
  • x 为 126:后续 2 个字节表明一个 16 位的无符号整数,该无符号整数的值为数据的长度。
  • x 为 127:后续 8 个字节表明一个 64 位的无符号整数(最高位为 0),该无符号整数的值为数据的长度。

此外,若是 payload length 占用了多个字节的话,payload length 的二进制表达采用 网络序(big endian,重要的位在前)

Masking-key:0 或 4 字节(32 位)

全部从客户端传送到服务端的数据帧,数据载荷都进行了掩码操做,Mask 为 1,且携带了 4 字节的 Masking-key。若是 Mask 为 0,则没有 Masking-key

备注:载荷数据的长度,不包括 mask key 的长度。

Payload data:(x+y) 字节

载荷数据:包括了扩展数据、应用数据。其中,扩展数据 x 字节,应用数据 y 字节。

扩展数据:若是没有协商使用扩展的话,扩展数据数据为 0 字节。全部的扩展都必须声明扩展数据的长度,或者能够如何计算出扩展数据的长度。此外,扩展如何使用必须在握手阶段就协商好。若是扩展数据存在,那么载荷数据长度必须将扩展数据的长度包含在内。

应用数据:任意的应用数据,在扩展数据以后(若是存在扩展数据),占据了数据帧剩余的位置。载荷数据长度 减去 扩展数据长度,就获得应用数据的长度。

掩码算法

掩码键(Masking-key)是由客户端挑选出来的 32 位的随机数。掩码操做不会影响数据载荷的长度。掩码、反掩码操做都采用以下算法:

首先,假设:

  • 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 客户端、服务端创建链接后,后续的操做都是基于数据帧的传递。

WebSocket 根据 opcode 来区分操做的类型。好比0x8表示断开链接,0x0-0x2 表示数据交互。

数据分片

WebSocket 的每条消息可能被切分红多个数据帧。当 WebSocket 的接收方收到一个数据帧时,会根据FIN的值来判断,是否已经收到消息的最后一个数据帧。

FIN=1 表示当前数据帧为消息的最后一个数据帧,此时接收方已经收到完整的消息,能够对消息进行处理。FIN=0,则接收方还须要继续监听接收其他的数据帧。

此外,opcode 在数据交换的场景下,表示的是数据的类型。0x01表示文本,0x02表示二进制。而0x00比较特殊,表示延续帧(continuation frame),顾名思义,就是完整消息对应的数据帧还没接收完。

链接保持 + 心跳

WebSocket 为了保持客户端、服务端的实时双向通讯,须要确保客户端、服务端之间的 TCP 通道保持链接没有断开。然而,对于长时间没有数据往来的链接,若是依旧长时间保持着,可能会浪费包括的链接资源。

但不排除有些场景,客户端、服务端虽然长时间没有数据往来,但仍须要保持链接。这个时候,能够采用心跳来实现。

  • 发送方 ->接收方:ping
  • 接收方 ->发送方:pong

ping、pong 的操做,对应的是 WebSocket 的两个控制帧,opcode分别是 0x九、0xA

关闭链接

一旦发送或接收到一个Close控制帧,这就是说,_WebSocket 关闭阶段握手已启动,且 WebSocket 链接处于 CLOSING 状态。

当底层TCP链接已关闭,这就是说 WebSocket链接已关闭 且 WebSocket 链接处于 CLOSED 状态。 若是 TCP 链接在 WebSocket 关闭阶段已经完成后被关闭,WebSocket链接被说成已经 彻底地 关闭了。

若是WebSocket链接不能被创建,这就是说,WebSocket链接关闭,但不是 彻底的 。

状态码

当关闭一个已经创建的链接(例如,当在打开阶段握手已经完成后发送一个关闭帧),端点能够代表关闭的缘由。 由端点解释这个缘由,而且端点应该给这个缘由采起动做,本规范是没有定义的。 本规范定义了一组预约义的状态码,并指定哪些范围能够被扩展、框架和最终应用使用。 状态码和任何相关的文本消息是关闭帧的可选的组件。

当发送关闭帧时端点可使用以下预约义的状态码。

状态码 名称 描述
0–999 保留段, 未使用.
1000 CLOSE_NORMAL 正常关闭; 不管为什么目的而建立, 该连接都已成功完成任务.
1001 CLOSE_GOING_AWAY 终端离开, 可能由于服务端错误, 也可能由于浏览器正从打开链接的页面跳转离开.
1002 CLOSE_PROTOCOL_ERROR 因为协议错误而中断链接.
1003 CLOSE_UNSUPPORTED 因为接收到不容许的数据类型而断开链接 (如仅接收文本数据的终端接收到了二进制数据).
1004 保留. 其意义可能会在将来定义.
1005 CLOSE_NO_STATUS 保留.  表示没有收到预期的状态码.
1006 CLOSE_ABNORMAL 保留. 用于指望收到状态码时链接非正常关闭 (也就是说, 没有发送关闭帧).
1007 Unsupported Data 因为收到了格式不符的数据而断开链接 (如文本消息中包含了非 UTF-8 数据).
1008 Policy Violation 因为收到不符合约定的数据而断开链接. 这是一个通用状态码, 用于不适合使用 1003 和 1009 状态码的场景.
1009 CLOSE_TOO_LARGE 因为收到过大的数据帧而断开链接.
1010 Missing Extension 客户端指望服务器商定一个或多个拓展, 但服务器没有处理, 所以客户端断开链接.
1011 Internal Error 客户端因为遇到没有预料的状况阻止其完成请求, 所以服务端断开链接.
1012 Service Restart 服务器因为重启而断开链接.
1013 Try Again Later 服务器因为临时缘由断开链接, 如服务器过载所以断开一部分客户端链接.
1014 由 WebSocket 标准保留以便将来使用.
1015 TLS Handshake 保留. 表示链接因为没法完成 TLS 握手而关闭 (例如没法验证服务器证书).
1016–1999 由 WebSocket 标准保留以便将来使用.
2000–2999 由 WebSocket 拓展保留使用.
3000–3999 能够由库或框架使用.不该由应用使用. 能够在 IANA 注册, 先到先得.
4000–4999 能够由应用使用.

客户端的 API

WebSocket 构造函数

WebSocket 对象提供了用于建立和管理 WebSocket 链接,以及能够经过该链接发送和接收数据的 API。

WebSocket 构造器方法接受一个必须的参数和一个可选的参数:

WebSocket WebSocket(in DOMString url, in optional DOMString protocols);
WebSocket WebSocket(in DOMString url,in optional DOMString[] protocols);

参数

  • url

表示要链接的URL。这个URL应该为响应WebSocket的地址。

  • protocols 可选

能够是一个单个的协议名字字符串或者包含多个协议名字字符串的数组。这些字符串用来表示子协议,这样作可让一个服务器实现多种 WebSocket子协议(例如你可能但愿经过制定不一样的协议来处理不一样类型的交互)。若是没有制定这个参数,它会默认设为一个空字符串。

构造器方法可能抛出如下异常:SECURITY_ERR 试图链接的端口被屏蔽。

var ws = new WebSocket('ws://localhost:8080');

执行上面语句以后,客户端就会与服务器进行链接。

属性

属性名 类型 描述
binaryType DOMString 一个字符串表示被传输二进制的内容的类型。取值应当是"blob"或者"arraybuffer"。"blob"表示使用DOM Blob 对象,而"arraybuffer"表示使用 ArrayBuffer 对象。
bufferedAmount unsigned long 调用 send()) 方法将多字节数据加入到队列中等待传输,可是还未发出。该值会在全部队列数据被发送后重置为 0。而当链接关闭时不会设为0。若是持续调用send(),这个值会持续增加。只读。
extensions DOMString 服务器选定的扩展。目前这个属性只是一个空字符串,或者是一个包含全部扩展的列表。
onclose EventListener 用于监听链接关闭事件监听器。当 WebSocket 对象的readyState 状态变为 CLOSED 时会触发该事件。这个监听器会接收一个叫close的 CloseEvent 对象。
onerror EventListener 当错误发生时用于监听error事件的事件监听器。会接受一个名为“error”的event对象。
onmessage EventListener 一个用于消息事件的事件监听器,这一事件当有消息到达的时候该事件会触发。这个Listener会被传入一个名为"message"的 MessageEvent 对象。
onopen EventListener 一个用于链接打开事件的事件监听器。当readyState的值变为 OPEN 的时候会触发该事件。该事件代表这个链接已经准备好接受和发送数据。这个监听器会接受一个名为"open"的事件对象。
protocol DOMString 一个代表服务器选定的子协议名字的字符串。这个属性的取值会被取值为构造器传入的protocols参数。
readyState unsigned short 链接的当前状态。取值是 Ready state constants 之一。 只读。
url DOMString 传入构造器的URL。它必须是一个绝对地址的URL。只读。

webSocket.onopen

实例对象的 onopen 属性,用于指定链接成功后的回调函数。

ws.onopen = function () {
  ws.send('Hello Server!');
}

若是要指定多个回调函数,可使用addEventListener方法。

ws.addEventListener('open', function (event) {
  ws.send('Hello Server!');
});

webSocket.onclose

实例对象的 onclose 属性,用于指定链接关闭后的回调函数。

ws.onclose = function(event) {
  var code = event.code;
  var reason = event.reason;
  var wasClean = event.wasClean;
  // handle close event
};

ws.addEventListener("close", function(event) {
  var code = event.code;
  var reason = event.reason;
  var wasClean = event.wasClean;
  // handle close event
});

webSocket.onmessage

实例对象的 onmessage 属性,用于指定收到服务器数据后的回调函数。

ws.onmessage = function(event) {
  var data = event.data;
  // 处理数据
};

ws.addEventListener("message", function(event) {
  var data = event.data;
  // 处理数据
});

注意,服务器数据多是文本,也多是 二进制数据(blob对象或Arraybuffer对象)。

ws.onmessage = function(event){
  if(typeof event.data === String) {
    console.log("Received data string");
  }

  if(event.data instanceof ArrayBuffer){
    var buffer = event.data;
    console.log("Received arraybuffer");
  }
}

除了动态判断收到的数据类型,也可使用 binaryType 属性,显式指定收到的二进制数据类型。

// 收到的是 blob 数据
ws.binaryType = "blob";
ws.onmessage = function(e) {
  console.log(e.data.size);
};

// 收到的是 ArrayBuffer 数据
ws.binaryType = "arraybuffer";
ws.onmessage = function(e) {
  console.log(e.data.byteLength);
};

常量

Ready state 常量

这些常量是 readyState 属性的取值,能够用来描述 WebSocket 链接的状态。

常量 描述
CONNECTING 0 链接还没开启。
OPEN 1 链接已开启并准备好进行通讯。
CLOSING 2 链接正在关闭的过程当中。
CLOSED 3 链接已经关闭,或者链接没法创建。

方法

close()

关闭 WebSocket 链接或中止正在进行的链接请求。若是链接的状态已是 closed,这个方法不会有任何效果

void close(in optional unsigned short code, in optional DOMString reason);

code 可选

一个数字值表示关闭链接的状态号,表示链接被关闭的缘由。若是这个参数没有被指定,默认的取值是1000 (表示正常链接关闭)。 请看 CloseEvent 页面的 list of status codes来看默认的取值。

reason 可选

一个可读的字符串,表示链接被关闭的缘由。这个字符串必须是不长于123字节的UTF-8 文本(不是字符)。

可能抛出的异常

  • INVALID_ACCESS_ERR:选定了无效的code。
  • SYNTAX_ERR:reason 字符串太长或者含有 unpaired surrogates

send()

经过 WebSocket 链接向服务器发送数据。

void send(in DOMString data);
void send(in ArrayBuffer data);
void send(in Blob data);

data:要发送到服务器的数据。

可能抛出的异常:

  • INVALID_STATE_ERR:当前链接的状态不是OPEN。
  • SYNTAX_ERR:数据是一个包含 unpaired surrogates 的字符串。

发送文本的例子。

ws.send('your message');

发送 Blob 对象的例子。

var file = document
  .querySelector('input[type="file"]')
  .files[0];
ws.send(file);

发送 ArrayBuffer 对象的例子。

// Sending canvas ImageData as ArrayBuffer
var img = canvas_context.getImageData(0, 0, 400, 320);
var binary = new Uint8Array(img.data.length);
for (var i = 0; i < img.data.length; i++) {
  binary[i] = img.data[i];
}
ws.send(binary.buffer);

服务端的实现

WebSocket 服务器的实现,能够查看维基百科的列表

经常使用的 Node 实现有如下三种。

问答

和TCP、HTTP协议的关系

WebSocket 是基于 TCP 的独立的协议。它与 HTTP 惟一的关系是它的握手是由 HTTP 服务器解释为一个 Upgrade 请求。

WebSocket协议试图在现有的 HTTP 基础设施上下文中解决现有的双向HTTP技术目标;一样,它被设计工做在HTTP端口80和443,也支持HTTP代理和中间件,

HTTP服务器须要发送一个“Upgrade”请求,即101 Switching Protocol到HTTP服务器,而后由服务器进行协议转换。

Sec-WebSocket-Key/Accept 的做用

前面提到了,Sec-WebSocket-Key/Sec-WebSocket-Accept 在主要做用在于提供基础的防御,减小恶意链接、意外链接。

做用大体概括以下:

避免服务端收到非法的 websocket 链接(好比 http 客户端不当心请求链接 websocket 服务,此时服务端能够直接拒绝链接)

确保服务端理解 websocket 链接。由于 ws 握手阶段采用的是 http 协议,所以可能 ws 链接是被一个 http 服务器处理并返回的,此时客户端能够经过 Sec-WebSocket-Key 来确保服务端认识 ws 协议。(并不是百分百保险,好比老是存在那么些无聊的 http 服务器,光处理 Sec-WebSocket-Key,但并无实现 ws 协议。。。)

用浏览器里发起 ajax 请求,设置 header 时,Sec-WebSocket-Key 以及其余相关的 header 是被禁止的。这样能够避免客户端发送 ajax 请求时,意外请求协议升级(websocket upgrade)

能够防止反向代理(不理解 ws 协议)返回错误的数据。好比反向代理先后收到两次 ws 链接的升级请求,反向代理把第一次请求的返回给 cache 住,而后第二次请求到来时直接把 cache 住的请求给返回(无心义的返回)。

Sec-WebSocket-Key 主要目的并非确保数据的安全性,由于 Sec-WebSocket-KeySec-WebSocket-Accept 的转换计算公式是公开的,并且很是简单,最主要的做用是预防一些常见的意外状况(非故意的)。

数据掩码的做用

WebSocket 协议中,数据掩码的做用是加强协议的安全性。但数据掩码并非为了保护数据自己,由于算法自己是公开的,运算也不复杂。除了加密通道自己,彷佛没有太多有效的保护通讯安全的办法。

那么为何还要引入掩码计算呢,除了增长计算机器的运算量外彷佛并无太多的收益(这也是很多同窗疑惑的点)。

答案仍是两个字:安全。但并非为了防止数据泄密,而是为了防止早期版本的协议中存在的代理缓存污染攻击(proxy cache poisoning attacks)等问题。

参考

相关文章
相关标签/搜索