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(尚未实现)

WebSocket协议详解及应用(二)-WebSocket握手协议之浏览器请求

目前WebSocket协议版本已更新到13,本系列文章均以WebSocket 13版本为例。

一、通过WebSocket API与服务器进行握手

WebSocket的构造方法中有两个参数,其原型如下:WebSocket(url, [protocols]),url为需要连接的地址。WebSocket的协议头的写法有2个,一个是ws://,另一个是wss://,它们的区别就是后者相当于https,是加密的。如果url没有加端口号,当协议头为ws时默认为80端口,当协议头为wss时默认为443端口。如果需要连接其他端口,则需要向http协议一样,加上端口号,如ws://localhost:12345。url其他部分与标准URL一致,以下URL是合法的:ws://localhost:12345/path/?queryString=queryString。具体URL标准将在下面协议详解中详细说明。
第二个参数protocols为可选参数,代表WebSocket协议的子协议(可以是用户自定义的)。你可能已经注意到了第二个参数名为protocols最后有个s,它可以是一个字符串亦可以是一个数组,如果protocols是一个字符串,则它等价于一个单一值的数组。例如,
WebSocket(‘ws://localhost’,’subprotocol’)
等价于
WebSocket(‘ws://localhost’,[‘subprotocol’])

二、WebSocket URL解析

以下内容部分翻译自W3C规范

WebSocket URL解析组件解析URL步骤如下,这些步骤会返回一个主机名、端口号、资源名和一个安全标识符否则返回失败:

  1. 如果URL不是一个绝对URL,算法失败
  2. 将URL字符串转化为UTF-8格式
  3. 如果URL的模式转为小写(模式是不区分大小写的,即WS、WSS都是可以的)不是ws或wss,则算法失败
  4. 如果解析的URL结果中存在fragment(#后面的),则算法失败
  5. 如果是ws模式的URL,则将安全标识符设为false,如果是wss模式的URL则将安全标示符置为true
  6. 解析url的host名,并转为小写
  7. 如果有端口号则设置端口号,否则使用隐式端口号声明
  8. 如果端口号是隐式声明的,当安全标示符为true时,端口号是443,否则为80
  9. url的path作为资源名,可以是空字符串
  10. 如果资源名是非空的,则将其设置到结果中,否则将/作为资源名
  11. 如果URL请求字符串(?后面的内容)非空,则将请求字符串加入结果中,并使用?拼接(同HTTP中URL的请求字符串写法)
  12. 将主机名、端口号、资源名和安全标示符返回

三、浏览器发送握手包

通过本文第一部分我们可知,浏览器端发送握手请求的API非常简单,只需要new出一个WebSocket的对象,最少情况只需要带一个参数,如var ws = new WebSocket(‘ws://localhost’);
我们在http://dev.w3.org中打开控制台使用var ws = new WebSocket(‘ws://localhost’,’test’);浏览器会发出类似如下请求(注意:这里不会存在跨域问题,不需要在同域下即可,因为WebSocket是在TCP层面上传输数据):

GET ws://localhost/ HTTP/1.1
Host: localhost
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: http://dev.w3.org
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol:test
User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.122 Safari/537.36
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.4
Sec-WebSocket-Key: 32pdAhmqFrFZik/MP7fU8A==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

其实这个就是一个标准的HTTP协议,WebSocket协议中的握手过程是HTTP协议,而一旦握手成功后,就不再是HTTP协议,而是直接通过TCP传输数据。
请求头中大部分内容与HTTP协议一致,这里不再解释,只解释那些不曾出现在普通请求的请求头。
Connection: Upgrade是固定的,表示需要转换为其他协议
Upgrade: websocket是固定的,说明要转换成的协议时WebSocket
Sec-WebSocket-Version: 13代表WebSocket的版本,目前版本是13
Sec-WebSocket-Key: 32pdAhmqFrFZik/MP7fU8A==这个是握手的认证串,服务端需要将此key进行一定处理后返回,再由浏览器验证有效性,必须符合算法结果才可正常建立连接(只是简单的哈希计算)
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits代表客户端支持的扩展类型
Sec-WebSocket-Protocol:test为子协议,是否存在取决于构造方法的第二个参数,如果第二个参数是个数组,则此处的值为数组按一个逗号和一个空格分隔,相当于[].join(‘, ‘);
其中Sec-WebSocket-Key是非常重要的请求头,我们会在服务端处理这个请求头,只有处理正确,浏览器才会正确的与服务器建立连接。