基本知识
WebSocket 是一种应用层协议,基于TCP协议;
WebSocket protocol 是HTML5一种新的协议。它是实现了浏览器与服务器全双工通信(full-duplex)。
WebSocket通信过程
客户端发起握手请求
客户端发送握手请求内容
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: [http://example.com](http://example.com/)
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
字段说明
- Sec-WebSocket-Protocol:字段表示客户端可以接受的子协议类型,也就是在Websocket协议上的应用层协议类型。上面可以看到客户端支持chat和superchat两个应用层协议,当服务器接受到这个字段后要从中选出一个协议返回给客户端;
- Upgrade:告诉服务器这个HTTP连接是升级的Websocket连接;
- Connection:告知服务器当前请求连接是升级的;
- Origin:该字段是用来防止客户端浏览器使用脚本进行未授权的跨源攻击,这个字段在WebSocket协议中非常重要。服务器要根据这个字段判断是否接受客户端的Socket连接。可以返回一个HTTP错误状态码来拒绝连接;
- Sec-WebSocket-Key:为了表示服务器同意和客户端进行Socket连接, 服务器端需要使用客户端发送的这个Key进行校验 ,然后返回一个校验过的字符串给客户端,客户端验证通过后才能正式建立Socket连接。服务器验证方法是: 首先进行 Key + 全局唯一标示符(GUID)“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”连接起来,然后将连接起来的字符串使用SHA-1哈希加密,再进行base64加密,将得到的字符串返回给客户端作为握手依据。其中GUID是一个对于不识别WebSocket的网络端点不可能使用的字符串 。
客户端发送握手请求的要求
- 请求的WebSocket URI必须要是定义的有效的URI;
- 如果客户端已经有一个WebSocket连接到远程服务器端,不论是否是同一个服务器,客户端必须要等待上一个连接关闭后才能发送新的连接请求,也就是同一客户端一次只能存在一个WebSocket连接。如果想同一个服务器有多个连接,客户端必须要串行化进行。如果客户端检测到多个到不同服务器的连接,应该限制一个最大连接数,在web浏览器中应该设定最多可以打开的标签页的数目。这样可以防止到远程服务器的DDOS攻击,但这是对到多个服务器的连接,如果是到同一个服务器连接,并没有数目限制;
- 如果使用了代理服务器,那么客户端建立连接的时候需要告知代理服务器向目标服务器打开TCP连接;
- 如果连接没有打开,一定是某一方出现错误,此时客户端必须要关闭再次连接的尝试;
- 连接建立后,握手必须要是一个有效的HTTP请求;
- 请求的方式必须是GET,HTTP协议的版本至少是1.1;
- Upgrade字段必须包含而且必须是"websocket",Connection字段必须内容必须是“Upgrade”;
- Sec-Websocket-Version必须,而且必须是13。
服务端回应握手请求
握手服务端返回内容
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
字段说明
- 首行返回的是HTTP/1.1协议版本和状态码101,表示变换协议(Switching Protocol);
- Upgrade 和 Connection:这两个字段是服务器返回的告知客户端同意使用升级并使用websocket协议,用来完善HTTP升级响应;
- Sec-WebSocket-Accept:服务器端将加密处理后的握手Key通过这个字段返回给客户端表示服务器同意握手建立连接;
- Sec-Websocket-Procotol:服务器选择的一个应用层协议;
在请求中的“Sec-WebSocket-Key”是随机的,服务器端会用这些数据来构造出一个SHA-1的信息摘要。把“Sec-WebSocket-Key”加上一个魔幻字符串“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”。使用SHA-1加密,之后进行BASE-64编码,将结果做为“Sec-WebSocket-Accept”头的值,返回给客户端。
服务器端响应步骤
解析握手请求头:获取握手依据Key并进行处理,检测HTTP的GET请求和版本是否准确,Host字段是否有权限,Upgrade字段中websocket是一个与大小写无关的ASCII字符串,Connection字段是一个大小写无关的"Upgrade"ASCII字符串,Websocket协议版本必须为13,其他的关于Origin、Protocol和Extensions可选。
发送握手响应头:检测是否是wss协议连接,如果是就是用TLS握手连接,否则就是普通连接。服务器可以添加额外的验证信息到客户端进行验证。当进行一系列验证之后,服务器必须返回一个有效的HTTP响应头。响应头中每一行一个字段,结束必须为“\r\n”,使用的ABNF语法。
WebSocket关闭握手
当WebSocket关闭时,终止连接的端点可以发送一个数字代码,以及一个表示选择关闭套接字原因的字符串。代码和原因编码为具有关闭操作码(8)的一个帧的载荷。数字代码用一个16位无符号整数表示,原因则是一个UTF-8编码的短字符串。RFC 6455定义了多种特殊的关闭代码。代码1000~1015规定用于WebSocket连接层。这些代码表示网络中或者协议中的某些故障。
WebSocket关闭代码的定义
代码 | 描述 | 何时使用 |
---|---|---|
1000 | 正常关闭 | 当你的会话成功完成时发送这个代码 |
1001 | 离开 | 因应用程序离开且不期望后续的连接尝试而关闭连接时,发送这一代码。服务器可能关闭,或者客户端应用程序可能关闭 |
1002 | 协议错误 | 当因协议错误而关闭连接时发送这一代码 |
1003 | 不可接受的数据类型 | 当应用程序接收到一条无法处理的意外类型消息时发送这一代码 |
1004 | 保留 | 不要发送这一代码。根据RFC 6455,这个状态码保留,可能在未来定义 |
1005 | 保留 | 不要发送这一代码。WebSocket API用这个代码表示没有接收到任何代码 |
1006 | 保留 | 不要发送这一代码。WebSocket API用这个代码表示连接异常关闭 |
1007 | 无效数据 | 在接收一个格式与消息类型不匹配的消息之后发送这一代码。如果文本消息包含错误格式的UTF-8数据,连接应该用这个代码关闭 |
1008 | 消息违反政策 | 党应用程序由于其他代码所包含的原因终止连接,或者不希望泄露消息无法处理的原因时,发送这一代码 |
1009 | 消息过大 | 当接受的消息太大,应用程序无法处理时,发送这一代码(记住,帧的载荷长度最多为64字节。即使你有一个大服务器,有些消息也仍然太大) |
1010 | 需要扩展 | 当应用程序需要一个或者多个服务器无法协商特殊扩展时,从客户端发送这一代码 |
1011 | 意外情况 | 当应用程序由于不可预见的原因,无法继续处理连接时,发送这一代码 |
1015 | TLS失败(保留) | 不要发送这个代码。WebSocket API用这个代码表示TLS在WebSocket握手之前失败 |
WebSocket URI
定义的两个协议框架ws和wss与http类似,而且各自部分的要求也是在HTTP协议中使用的一样,各自的URI如下:
ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]
wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]
其中port是可选项,query前接“?”
WebSocket API
// 创建一个Socket实例
var socket = new WebSocket('ws://localhost:8080', [protocal]);
// 连接建立,即握手成功触发的事件;
socket.onopen = function(event) {
// 发送一个初始化消息
socket.send('I am the client and I\'m listening!');
// 收到服务器消息时触发的事件
socket.onmessage = function(event) {
console.log('Client received a message',event);
};
// 异常触发的事件
socket.onerror=function(event){
console.log(,"error: " event);
}
// 关闭连接触发的事件
socket.onclose = function(event) {
console.log('Client notified socket has closed',event);
};
// 关闭连接
socket.close();
Socket.readyState只读属性readyState表示连接的状态。有以下取值:
CONNECTING(0) 表示连接尚未建立;
OPEN(1) 表示连接已建立,可以进行通信;
CLOSING(2) 表示连接正在进行关闭握手;
CLOSED(3) 表示连接已经关闭或者连接不能打开。
WebSocket包结构
FIN: 最高位用于描述消息是否结束,如果为1则该消息为消息尾部,如果为零则还有后续数据包;
RSV: 后面3位是用于扩展定义的,如果没有扩展约定的情况则必须为0。
OPCODE: 最低4位用于描述消息类型,消息类型暂定有15种,其中有几种是预留设置,操作码定义如下:
操作码 | 消息载荷类型 | 描述 |
---|---|---|
0 | 连续消息标识 | 表示连续消息片段 |
1 | 文本 | 消息的数据类型为文本 |
2 | 二进制 | 消息的数据类型为二进制 |
3~7 | 保留 | 为将来的非控制消息片断保留操作码 |
8 | 关闭 | 客户端或者服务器向对方发送关闭握手 |
9 | ping | 客户端或者服务器向对方发送ping |
A | pong | 客户端或者服务器向对方发送pong |
B~F | 保留 | 为将来的控制消息片断的保留操作码 |
Mask:最高位用0或1来描述是否有掩码处理,也就是所说的屏蔽。从浏览器向服务器发送的WebSocket帧内容进行了“屏蔽”,以混淆其内容。屏蔽的目的不是阻止窃听,而是为了不常见的安全原因,以及改进和现有HTTP代理的兼容性。
Payload length: 传输数据的长度,以字节的形式表示:7位、7+16位、或者7+64位。如果这个值以字节表示是0-125这个范围,那这个值就表示传 输数据的长度;如果这个值是126,则随后的两个字节表示的是一个16进制无符号数,用来表示传输数据的长度;如果这个值是127,则随后的是8个字节表示的一个64位无符号数,这个数用来表示传输数据的长度。多字节长度的数量是以网络字节的顺序表示。负载数据的长度为扩展数据及应用数据之和,扩展数据的长度可能为0,因而此时负载数据的长度就为应用数据的长度。
Masking-key:0或4个字节,客户端发送给服务端的数据,都是通过内嵌的一个32位值作为掩码的;掩码键只有在掩码位设置为1的时候存在。
Payload data: (x+y)位,负载数据为扩展数据及应用数据长度之和。
Extension data:x位,如果客户端与服务端之间没有特殊约定,那么扩展数据的长度始终为0,任何的扩展都必须指定扩展数据的长度,或者长度的计算方式,以及在握手时如何确定正确的握手方式。如果存在扩展数据,则扩展数据就会包括在负载数据的长度之内。
Application data:y位,任意的应用数据,放在扩展数据之后,应用数据的长度=负载数据的长度-扩展数据的长度。
WSS
wss是安全的WebSocket通信协议
注意问题
- 握手过程中,HTTP request method 必须是GET,协议应不小于1.1
- 所有数据传输都是UTF-8编码的数据,当一端接收到的字节流数据不是一个有效的UTF-8数据流,那个么接收到的这一方必须要马上关闭连接。这个规则在开始握手一直到所有的数据交换过程都要进行验证。
浏览器支持
下面是主流浏览器对 HTML5 WebSocket 的支持情况:
http://caniuse.com/#search=websocket
浏览器 | 支持情况 |
---|---|
Chrome | Supported in version 4+ |
Firefox | Supported in version 4+ |
Internet Explorer | Supported in version 10+ |
Opera | Supported in version 10+ |
Safari | Supported in version 5+ |
参考
WebSocket 教程