WebSocket协议详解及应用(四)-WebSocket握手协议之通信建立与错误处理

本篇主要介绍浏览器在服务器返回的握手协议有问题时如何进行处理。注:本文介绍的内容已在浏览器内部实现,不需要做任何编码工作,以下内容只介绍原理和协议本身。

一、连接异常的情况

以下情况,客户端必须停止握手并断开连接:

  1. 服务器域名不能解析
  2. 数据包不能成功的传递到服务器
  3. 服务器指定端口禁止连接
  4. 服务器TLS传输失败,例如服务器证书无法验证
  5. 服务器无法完成握手协议,例如目标服务器不是WebSocket服务器(返回非101状态码)
  6. 服务器握手成功,但一些选项引起客户端放弃连接(如服务器提供了客户端无法识别子协议)
  7. 服务器在完成握手协议后意外关闭了连接

以上所有的情况都会使WebSocket以1006(连接意外断开)的退出码断开连接。

二、握手成功后浏览器的工作

当整个握手阶段无任何异常时,服务器与浏览器已经建立连接,此时浏览器会完成一些初始化工作。
首先将readyState属性设置为OPEN(WebSocket的常量,值为1),readyState是WebSocket的一个属性,代表连接的状态,包含CONNECTING、OPEN、CLOSING、CLOSED等4个状态。其中OPEN即连接已建立,可以进行数据通信了。
如果extensions非空,则设置此属性的值。
如果protocol的值非空,则设置此属性的值。
如果返回了cookie设置的响应头,则需要将指定的cookie设置到WebSocket构造函数的第二个参数URL下。
最后触发WebSocket的onopen事件处理函数。

三、WebSocket对象的属性及方法介绍

调用WebSocket构造方法后,会返回一个WebSocket对象,类似如下:

URL: "ws://localhost/"
binaryType: "blob"
bufferedAmount: 0
extensions: ""
onclose: null
onerror: null
onmessage: null
onopen: null
protocol: ""
readyState: 3
url: "ws://localhost/"
CLOSED: 3
CLOSING: 2
CONNECTING: 0
OPEN: 1
close: function close() { [native code] }
send: function send() { [native code] }

binaryType为二进制使用哪种类型的数据结构,取值可以是blob或arraybuffer.
bufferedAmount为缓存中剩余的字节数,有时数据并不是准备好才传输到网络上,而是一边生成一边传递,这时需要将数据缓存到一定大小再发送,并且需要定时将缓存中剩余的内容发送出去。bufferedAmount就记录了send函数中的应用数据(UTF-8的字符串或二进制的数据)队列中还未被发送到网络的字节数。如果连接关闭,该属性值会在send方法被调用时增长。
onclose在连接关闭时触发
onerror在发生错误时触发
onmessage在浏览器接到服务器发来的数据时触发
onopen在连接建立时触发
readyState连接状态
CLOSED连接已经关闭
CLOSING连接关闭中,此时处于半关闭状态,浏览器已经发送了关闭指令,但此时服务器还未响应关闭指令。浏览器不能再收发数据了。
CONNECTING正在连接中,浏览器已经发送了握手请求,等待服务器返回握手响应。
OPEN已建立连接,可以传输数据。
close方法用于浏览器主动关闭连接,它会发送操作码8到服务器,它接受2个可选参数,关闭码和关闭原因。
send方法用于浏览器向服务器发送数据,它支持多种数据类型的传输。

WebSocket协议详解及应用(三)-WebSocket握手协议之服务器响应

本文主要介绍服务端应用在接收到WebSocket握手后,如何构造正确的响应包。本文部分翻译自rfc6455

一、响应包Sec-WebSocket-Accept字段值的计算

浏览器发送的最重要的一个请求头是Sec-WebSocket-Key,服务端程序需要根据RFC6455中的算法计算Sec-WebSocket-Accept的值。我们以浏览器发送了Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==为例,介绍如何计算响应值。
首先服务端程序要将Sec-WebSocket-Key的值与一个魔法字符串“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”拼接到一起,得到“dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11”。
第二步,将上一步中合并的字符串使用sha1计算sha1值,这里如果使用PHP的sha1函数进行计算,要注意sha1的第二个参数必须显式的给出true值,否则sha1的结果是一个16进制的字符串,而不是二进制数值,其他语言如果有类似的情况也要注意,求出的结果是二进制,而不是转换后的16进制值。
第三步,将第二步中的二进制值使用base64进行编码,使用PHP计算方法如下:
base64_encode(sha1($secWebSocketKey."258EAFA5-E914-47DA-95CA-C5AB0DC85B11",true));
最后得出的结果为”s3pPLMBiTxaQ9kYGzzhZRbK+xOo=”,将这个结果作为Sec-WebSocket-Accept的值,放入响应头中。

二、响应头的构成

1.HTTP状态码
握手响应的HTTP状态码为101 Switching Protocols,代表协议转换,如HTTP/1.1 101 Switching Protocols
2.Upgrade字段
Upgrade: websocket固定,代表转换为WebSocket协议,同浏览器请求头
3.Connection字段
Connection: Upgrade固定,同浏览器请求头
4.Sec-WebSocket-Accept
通过上面的方法计算出的值
5.Sec-WebSocket-Protocol
可选返回头,根据浏览器发送的子协议返回。如果浏览器发送了多个子协议,这里可以选择一个或多个进行返回,此处是子协议协商的过程

三、服务端响应步骤

  1. 判断Origin是否可信,WebSocket不存在跨域问题,在任何域中都可以与其他域建立WebSocket连接。但出于某些原因,我们不希望一些其他的网站连接服务器,这时可以验证Origin是否在访问源白名单中
  2. 通过Cookie验证身份,由于WebSocket的握手协议仍是通过HTTP协议进行的,它会向服务端发送目标域下(当前运行的WebSocket服务器)的cookie。服务端程序可以通过cookie将该socket与用户身份绑定到一起,就不需要在以后传输身份信息
  3. 读取Sec-WebSocket-Version的值判断协议版本,如果不是13,则需要用其他方法解析(本系列只介绍13版本,其他版本的握手及通信不在介绍范围内)
  4. 返回通过Sec-WebSocket-Key计算Sec-WebSocket-Accept的值
  5. 读取Sec-WebSocket-Protocol,根据服务器实现情况返回支持的一个或多个子协议名,使用半角逗号分隔
  6. 将第二节中的响应头发送给浏览器

四、关于Sec-WebSocket-Extensions

在WebSocket的请求和响应头中,还有一个可选的字段Sec-WebSocket-Extensions,目前很少使用,现将RFC6455内容翻译如下:
“Sec-WebSocket-Extensions”字段仅用在WebSocket连接握手阶段。它先由客户端(浏览器)发送到服务器,然后再由服务器传回客户端,以便协商连接过程中的协议层扩展集。
“Sec-WebSocket-Extensions”字段可能在HTTP请求中出现多次(等价于出现一次,但有多个值),但是最多只能在HTTP响应中出现一次。

五、握手阶段的错误处理

WebSocket协议中并未明确规定当服务器遇到错误(伪造)的握手请求该如何处理,你可以直接断开客户端的连接,但这样并不是比较好的方法,推荐使用下面的做法(浏览器会忽略出101之外的状态码,返回非101状态码浏览器会自动切断连接):
1.当客户端请求中缺少host、Upgrade、Connection、Sec-WebSocket-Key、Sec-WebSocket-Version时返回状态码400(语法格式错误)
2.当Origin不在可信源中时,返回403
3.当身份验证(cookie)出错时,返回401或403
4.当version或protocol不支持时,返回501(尚未实现)