说到 WebSocket,不得不提 HTML5,做为近年来Web技术领域最大的改进与变化,包含CSS三、离线与存储、多媒体、链接性( Connectivity )等一系列领域,而即将介绍的 WebSocket 则是 HTML5 链接性领域( Connectivity )最值得称道的改进。javascript
HTTP是用于文档传输简单同步请求的响应式协议,本质上是无状态的应用层协议,半双工的链接特性。传输层依然是传输控制协议( TCP )。java
在介绍WebSocket以前,首先有必要介绍下在 WebSocket 未出现以前咱们是怎样实现 HTTP 服务器与客户端交互通讯。node
一种定时的同步调用。当频率太低,信息不及时,当频率太高,对服务器负担大,会产生大量的没必要要的链接开销。web
// client var time = 1000 * x; // 轮询频率 / ms setInterval(function(){ //ajax }, time);
客户端向服务端请求,这个请求在有数据时返回,若是没有数据,这个请求会一直被挂起,直到有数据返回或超时。结束完成再次向服务器请求。在HTML/1.1以后,浏览器默认链接改成长链接( keep-alive ),正是基于此,服务器才能长时间和客户端保持链接直到返回或超时。ajax
这种无需第三方插件仅依靠长链接维持客户端与服务器交互的技术广泛称为 comet 或 反向Ajax。api
对于部分浏览器来讲,同时创建过多的长链接会形成阻塞,例如IE,在打开两个长链接以后,第三个HTTP请求会被阻塞。由于HTTP 1.1规范对长链接数有相应规定,不建议创建两个以上的长链接,由于某些浏览器严格执行了这些规范。所以咱们不管用哪一种方式创建长链接的时候,若是要考虑应用程序性能体验,都须要注意避免这种状况——避免IE给咱们形成的麻烦。数组
客户端发送一个请求,服务器发送并维护一个持续更新和保持打开的开放响应,这个请求除非超时或主动关闭,不然会一直源源不断向客户端返回数据。
流化一般有两种技术方案,第一种经过在HTML页面添加一个隐藏的Iframe标签,而后再这个Iframe的src属性设置一个长链接请求,服务器据此不断向客户端发送数据。这个数据形式就是Javascript的函数调用。这种方式早期部分浏览器会一直处于loading状态。第二种利用部分浏览器的multi-part标志,但这种的局限性也很明显,受限于支持multi-part标志的浏览器,由于XMLHttpRequest在不一样浏览器上有着不一样的实现。
即使流的处理方式与长轮询同样具备较低延迟的特色,但仍要注意的是,不少流化的处理方式对于存在防火墙和代理网络并不太友好,因为链接一直打开,代理或防火墙可能会缓存响应,增长信息交付延迟。浏览器
/** 使用 Forever IFrame */ // client <iframe id='hidden_iframe_polling' name='hidden_iframe_polling' style='display:none;' /> <script> var url=''; // 请求地址 var time = 1000 * x; // 轮询频率 / ms setInterval(function(){ document.getElementById('hidden_iframe_polling').src = url + "?t=" + new Date(); //时间戳保证每次都是最新请求 window.frames["hidden_iframe_polling"].location.reload(); }, time); </script>
/** 使用 multi-part */ // client var url = ''; // 请求地址 var type = ''; // 请求方式 post / get var xhr = new XMLHttpRequest(); xhr.multipart = true; // multi-part 标志设置为 true xhr.timeout = 1000 * x; // 超时 / ms xhr.onreadystatechange = state_change_call; xhr.open(type, url, true); xhr.send(null); function state_change_call(){ //回调 } // server // 创建长链接,设置content-type的值为multipart/mixed或multipart/x-mixed-replace // 例:multipart/x-mixed-replace;boundary="string类型数据"
由于服务器端语言不一样,没法一致描述,但本质上解决方式仍是同样的。缓存
其它方式这边再也不讨论,诸如捎带轮询( piggyback polling )、第三方插件( FlashSockets )之类。安全
直至今日,即使WebSocket有不少成熟的案例,但考虑到老版本浏览器兼容问题,以上部分技术( comet )依然在普遍使用,即使是一款成熟的WebSocket组件,大部分都会提供降级服务。所以咱们在学习开发一套基于WebSocket的组件时,若是想用于生产环境,为了应用程序的健壮、稳定、兼容性,最好也为老版本浏览器提供降级服务,在不支持WebSocket的浏览器上使用 comet 技术。固然,完成以后也别忘了必要的性能和可伸缩性测试。
WebSocket是全双工、双向、单套接字链接。WebSocket是一个低层网络协议,能够在它的基础上构建其它标准协议。
HTML5在Web端为咱们带来了革命性的变化,这些变化仿佛注入了一股强心剂,不管视觉仍是用户体验,或甚是开发上简洁简约的体验,相较以前,都有了质的飞跃,在链接领域 WebSocket 也印证了这一点。
说的直观一点,WebSocket就是Http链接的升级版,当你须要进行WebSocket链接时,你就须要把即将要链接的Http请求的升级为WebSocket。
一旦创建起WebSocket请求,不须要客户端发起,客户端也能及时接收到来自服务端的数据。WebSocket使用起来相比传统HTTP通讯更加的简洁、高效、直观,它解决了HTTP通讯的诸多不足之处,并且它真的足够简单,这每每是说服咱们使用它的理由。
当链接前判断浏览器是否支持原生WebSocket,只须要以下一行代码便可:
if(window.WebSocket){ // 支持WebSocket }
WebSocket的链接都基于HTTP请求,那怎么区分此次请求是HTTP仍是WebSocket呢?
只须要在请求头当中包含一个Upgrade的请求头,这是向服务器指定某种传输协议。例如指定WebSocket协议就是:
// // -client // 浏览器发送一个请求到服务器,表示它想把HTTP协议转为WebSocket。客户端经过更新头字段(Upgrade header)实现了这个目的 GET /echo HTTP/1.1 Sec-WebSocket-Key: xx Sec-WebSocket-Verson: xx Connection: Upgrade Upgrade: websocket // // -server // 若是服务器识别WebSocket协议,它经过Upgrade header接受协议转换 Connection:Upgrade Sec-WebSocket-Accept: xx Upgrade: WebSocket
此时HTTP链接会被基于TCP/IP链接的WebSocket链接所取代。WebSocket链接默认使用和HTTP(80)或者HTTPS(443)同样的端口,一样,你能够像部署Web服务同样使用其它端口。
另外,WebSocket为了完成握手,服务器必须响应一个计算出来的键值。这个响应说明服务器理解WebSocket协议。就像暗号同样,只有对上暗号才是本身人。
那么话说回来,到底是如何计算响应键值的呢?很简单,响应函数从客户端的Sec-WebSocket-Key请求头中取得键值,并在Sec-WebSocket-Accept请求头中返回根据客户端经过SHA1计算出键值,经过BASE64返回字符串。
// -- node.js // 计算响应键值函数 var KEY_SUFFIX = ""; //协议规范中包含的一个固定键值后缀,服务器必须得知道这个值 var hashWebSocketKey = function(key){ var sha1_enc= crypto.createHash("sha1").update(key + KEY_SUFFIX, "utf8"); return sha1_enc.digest("base64"); }
在创建WebSocket链接握手成功以后,服务器会一直以帧( frame )的形式往客户端发送数据。
WebSocket传输内容支持文本或二进制数据,这些数据的边界靠帧( frame )来维护。咱们不妨看一下WebSocket的帧特性。
这是官方标准协议提供的结构图,接下来要作的就是解析这张图。第一眼看到这张图的时候,或许有点懵圈,仔细看就会明白。从第二行开始,竟然是十进制的数值依次排开,其实就是第几位( bit )。而后再往下的内容都是追述及说明。
WebSocket帧头第一个字节第一位( bit )表示FIN码,由于WebSocket能够多帧,当你须要多帧的时候,前面的全部帧FIN位设值为0,最后一帧设置为1,用来标识结束帧。第一个字节第二位( bit )到第四位都是RSV码,分别占1bit,若是通讯两端没有设置自定义协议,那么设置为0便可。第一个字节的后四位是操做码( opcode ),这是一个十六位无符号整数。
操做码 | 消息类型 | |
---|---|---|
%x0 | 0 | 附加数据帧 |
%x1 | 1 | 文本 |
%x2 | 2 | 二进制数据 |
%x8 | 8 | 链接关闭 |
%x9 | 9 | ping |
%xA | 10 | pong |
其它 | 一共16位,其他所有保留用做未来扩展 |
这里要提一句,WebSocket以文本传输的时候,都为UTF-8编码,是WebSocket协议容许的惟一编码。
第二个字节高1位( bit )为MASK掩码,俗称“屏蔽”,就是用来标识客户端到服务端的数据是否加密混淆内容(payload)。
第二个字节低7位( bit )用来标识消息内容的长度( payload len )。上图的Extended payload length是用来标识扩展的长度。这样作的好处是使用可变位数来标示编码长度能使消息更加紧凑。数据长度一共有三种状况,全都由低7位的值认定,若是取值在126之内,不包括126,则数据真实长度就是低7位的值。若是取值为126,则须要额外的两个字节来表示数据的真实长度,16位的无符号整数。若是取值127,那么须要额外的8个字节表示数据的真实长度,64位的无符号整数。
以后就是Masking-key,一共4个字节,固然这是上一段说到的MASK掩码设置为1的时候,且在客户端到服务端发送消息时才会存在。那么当存在Masking-key时,服务器接收的每一个数据包在处理以前都须要解除掩码。
var data; //数据 var mask; //掩码 // 解除掩码 var content= new Buffer(data.length); for(var i= 0; i < data.length; i++){ content[i] = buffer[i] ^ mask[i%4]; } // content 就是解除掩码以后的数据
再以后就是Payload数据了。由此也可看出,WebSocket最小的传输大小仅为2KB,譬如关闭( %x8 )帧。
这个图表达更为形象,帧的全部内容几乎都被囊括其中。到这里关于帧也再也不赘述。
以上简单叙述了握手、链接并以帧形式的传输数据包,那么接下来咱们要看一下WebSocket的关闭握手。
WebSocket关闭时,不论客户端仍是服务端都会发送一个终止的数字代码,以及一个表示关闭缘由的字符串。固然,这些就像上面关于帧( frame )内容提到的同样,关闭操做的数据边界依然以帧的形式传输,操做符( opcode )为8,其中包含终止代码和内容文本。
到这里其实要简单叙述下WebSocket数字代码的含义。
代码 | 描述 | 场景 |
---|---|---|
0~999 | 禁止 | 1000如下代码皆无效,不用于任何目的 |
1000~2999 | 保留 | 这些代码都用于WebSocket的扩展和修订版本 |
3000~3999 | 须要注册 | 这些代码用于"应用程序、程序库、框架",可在IANA( 互联网分配机构 )注册 |
4000~4999 | 私有 | 能够用做自定义 |
而咱们所说的关闭代码正是在1000~2999之间,例如1000表示正常关闭,1001表示离开,1002表示协议错误,1007表示无效数据,1011表示意外状况等。
关于WebSocket协议,与TCP同样能够异步发送消息,都是能够用做高级协议的传输层。这么说不是把WebSocket协议等同于TCP,尽管把WebSocket看成传输层使用,它的层次仍然在TCP之上。根据OSI的协议层次,IP在网络层,TCP/UDP在传输层,HTTP位于应用层,与WebSocket协议同样,一样有着帧实现的SPDY( 由Google提出,增量性的提升HTTP的性能 ),位于会话层。
下面是各个协议之间对比:
TCP | HTTP | WebSocket | |
---|---|---|---|
寻址 | IP地址+端口 | URL | URL |
并发传输 | 全双工 | 半双工 | 全双工 |
内容 | 字节 | MIMI消息 | 文本或二进制 |
消息定界 | 否 | 是 | 是 |
消息定向 | 是 | 否 | 是 |
另外,若是咱们想对WebSocket协议扩展,可使用Sec-WebSocket-Extensions请求头,这个请求头包含扩展的名称。
上面内容主要讨论了WebSocket Protocol相关内容,对其做了一个简单介绍。那么下面将介绍如何去使用WebSocket,若是要使用WebSocket,那么将用到WebSocket API。
WebSocket API用来控制WebSocket协议,响应服务器触发的事件。利用这个API,能够用来打开、关闭、发送、接受和监听服务器触发的事件。
1.WebSocket构造函数
var url =""; //URL地址 var protocols = []; //协议数组 var ws = new WebSocket(url, protocols); //构造函数
WebSocket(url, protocols)构造函数接受一个或两个参数。
第一个参数url指定要链接的url。这个url多是ws:或wss:,相似于HTTP请求的http:或者https:。WebSocket也提供了传输层安全性的链接( TLS/SSL )。
第二个是一个协议数组,非必填项。若是它是一个字符串,它至关于一个数组组成的字符串,若是省略,它至关于空数组。其实在protocols参数指定的协议基本有三种类型:1、注册协议,向注册管理实体IANA正式注册的标准协议;2、开放协议,普遍使用的标准化协议,如XMPP或STOMP;3、自定义协议,本身编写并和WebSocket一块儿使用的协议。例如,protocols有多是简单对象访问协议( SOAP )或其它自定义协议。
当建立的WebSocket构造函数被调用时,会先解析URL参数,获取主机、端口、资源名称、安全。若是操做失败,抛出SyntaxError异常并终止操做。若是存在一个安全组件,例如套接字安全协议https,但分析出这个安全是false,例如无效的安全证书,那么抛出一个SecurityError异常。若是protocols协议数组或字符串中Sec-WebSocket-Protocol头字段定义的值超过一次不匹配,则抛出一个SyntaxError异常并停止。此时返回这个WebSocket的对象,可是后台依然会继续这些操做。
2.事件
因为WebSocket应用程序监听WebSocket对象上的事件,用于处理数据和链接状态,WebSocket对象存在4个事件,包括open、message、error、close。
var socket = new WebSocket('ws://game.example.com:12010/updates'); // 打开事件 socket.onopen = function (e) { console.log("websocket 打开"); }; // 消息事件 socket.binaryType = ""; socket.onmessage = function (e) { if (typeof e.data === "string"){ console.log("处理文本格式数据.") ; if (event.data == 'on') { console.log("处理当数据等于on时.") ; } } }; //error 事件 socket.onerror = function(e){ console.log("正在处理错误"); } //关闭事件 socket.onclose = function(e){ console.log("链接关闭"); }
这里额外提一下,close事件有三个属性,可用于处理和恢复:wasClean、code、reason。wasClean属性为布尔类型,表示是否顺利链接,若是接收到一个正常的close帧,则该属性为true,若是由于其它缘由关闭,该属性为false。code和reason分别表明错误代码和关闭缘由,这个在下文介绍close()方法会具体阐述如何使用。
3.方法
WebSocket有两个方法:send()和close()。
// 发送消息 var msg =""; //定义消息 socket.onopen = function(e){ socket.send(msg); //发送 } // OR function sendHandler(e){ if (ws.readState === WebSocket.Open){ socket.send(msg); } else { } } // 关闭方法 var code = ""; //定义代码 var reason = ""; //关闭缘由 socket.close(code,reason);
send()方法在WebSocket链接打开以后使用。
4.对象特性
readState,用于报告链接状态。从下图能够看到WebSocket的只读状态从链接到关闭的取值的整个对象生命周期。
常量 | 值 | 状态 |
---|---|---|
WebSocket.CONNECTION | 0 | 正在握手请求中,还未完成链接 |
WebSocket.OPEN | 1 | 链接已打开 |
WebSocket.CLOSING | 2 | 链接正在关闭 |
WebSocket.CLOSED | 3 | 链接已关闭 |
bufferAmount,用于检查发往服务器的缓冲数据量。调用send()方法能使咱们当即往服务器发送数据,可是数据量较大、网络带宽有限的时候,咱们就想知道网络传输速率。这时候咱们就能够用bufferedAmount检查发送队列中未发送到服务器的字节数。
// -client var info_size= 1024 * 100; //传输数据长度 var _url =""; //WebSocket服务器地址 var ws = new WebSocket(_url); ws.onopen = function(){ setInterval(function(){ if(ws.bufferedAmount < info_size){ //do something } },1000); }
protocol,用于服务器理解客户端在WebSocket上使用的协议。只有在握手完成以后、关闭以前,且服务器选择了客户端提供的协议,这个特性才会存值。不然为空。
附:SSE ( Server-Send Event ) 尽管这个并不属于WebSocket,可是属于HTML5规范一部分,放在这提一下是由于HTML5除了提供WebSocket这种强大特性以外,还提供这种增强了comet的技术。这样的话,你依然能够不经过WebSocket实现某些业务需求。顾名思义,SSE主要功能是向客户端广播或推送消息。若是仅需使用到服务器单方面推送或广播,并不须要双向全双工通讯,那么SSE是一个很不错的选择。在应用发面,好比能够推送新闻、天气等。