原因
解决 http 协议客户端发送请求到服务器,轮询浪费资源
简介
服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息
- 建立在
TCP协议之上,服务器端的实现比较容易 - 与
HTTP协议有着良好的兼容性。默认端口也是 80 和 443,并且握手阶段采用HTTP协议,因此握手时不容易屏蔽,能通过各种HTTP代理服务器 - 数据格式比较轻量,性能开销小,通信高效
- 可以发送文本,也可以发送二进制数据
- 没有同源限制,客户端可以与任意服务器通信
- 协议标识符是
ws(如果加密,则为wss),服务器网址就是URL ws协议定义了扩展,用户可以扩展协议,或者实现自定义的子协议。(比如支持自定义压缩算法等)
应用层协议基于 TCP 传输协议复用 HTTP 的握手通道
如何建立连接
客户端通过 HTTP 请求与 WebSocket 服务端协商升级协议。协议升级完成后,后续的数据交换则遵照 WebSocket 的协议
客户端:申请协议升级
标准的 HTTP 报文格式,且只支持 GET 方法
在握手阶段,可以通过相关请求首部进行 安全限制、权限校验等
- Connection: Upgrade:表示要升级协议
- Upgrade: websocket:表示要升级到
websocket协议。 - Sec-WebSocket-Version: 13:表示
websocket的版本。如果服务端不支持该版本,需要返回一个 Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。 - Sec-WebSocket-Key:与后面服务端响应首部的 Sec-WebSocket-Accept是配套的,提供基本的防护,比如恶意的连接,或者无意的连接
服务端:响应协议升级
每个 header 都以 \r\n 结尾,并且最后一行加上一个额外的空行 \r\n。此外,服务端回应的 HTTP 状态码只能在握手阶段使用。过了握手阶段后,就只能采用特定的错误码
-
101 Switching Protocols
-
Connection:Upgrade
-
Upgrade: websocket
-
Sec-WebSocket-Accept
- 将
Sec-WebSocket-Key跟258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接。 - 通过
SHA1计算出摘要,并转成base64字符串
- 将
数据传递
基于数据帧的传递
-
每条消息可能被切分成多个数据帧。当
WebSocket的接收方收到一个数据帧时,会根据FIN的值来判断,是否已经收到消息的最后一个数据帧 -
根据
opcode来区分操作的类型 -
客户端、服务端通信的最小单位是帧(
frame),由1个或多个帧组成一条完整的消息- 发送端:将消息切割成多个帧,并发送给服务端;
- 接收端:接收消息帧,并将关联的帧重新组装成完整的消息;
连接保持心跳
Sec-WebSocket-Key/Accept的作用
- 避免服务端收到非法的
websocket连接(http客户端不小心请求连接websocket服务) - 确保服务端理解
websocket连接。因为ws握手阶段采用的是http协议,因此可能ws连接是被一个http服务器处理并返回的,此时客户端可以通过Sec-WebSocket-Key来确保服务端认识ws协议 - 用浏览器里发起
ajax请求,设置header时,Sec-WebSocket-Key以及其他相关的header是被禁止的。这样可以避免客户端发送ajax请求时,意外请求协议升级(websocket upgrade) - 可以防止反向代理返回错误的数据。比如反向代理前后收到两次
ws连接的升级请求,反向代理把第一次请求的返回给cache住,然后第二次请求到来时直接把cache住的请求给返回(无意义的返回)Sec-WebSocket-Key主要目的并不是确保数据的安全性
使用
WebSocket 构造函数
new WebSocket
客户端就会与服务器进行连接
webSocket.readyState
返回实例对象的当前状态
- CONNECTING:值为0,表示正在连接。
- OPEN:值为1,表示连接成功,可以通信了。
- CLOSING:值为2,表示连接正在关闭。
- CLOSED:值为3,表示连接已经关闭,或者打开连接失败。
webSocket.onopen
连接成功后的回调函数
webSocket.onclose
连接关闭后的回调函数
webSocket.onmessage
收到服务器数据后的回调函数
webSocket.send()
向服务器发送数据
webSocket.bufferedAmount
还有多少字节的二进制数据没有发送出去。它可以用来判断发送是否结束
webSocket.onerror
报错时的回调函数
var ws = new WebSocket("wss://echo.websocket.org");
// 显式指定类型
// 收到的是 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);
// };
ws.onopen = function (evt) {
console.log("Connection open ...");
ws.send("Hello WebSockets!");
// 发送 Blob 对象
// var file = document.querySelector('input[type="file"]').files[0];
// ws.send(file);
// 发送 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);
// if (ws.bufferedAmount === 0) {
// 发送完毕
// } else {
// 发送还没结束
// }
};
// ws.addEventListener("open", function (evt) {
// ws.send("Hello Server!");
// });
ws.onmessage = function (evt) {
// 二进制数据(blob对象或Arraybuffer对象)
if (typeof evt.data === "string") {
console.log("Received data string");
}
if (evt.data instanceof ArrayBuffer) {
var buffer = evt.data;
console.log("Received arraybuffer");
}
ws.close();
};
// ws.addEventListener("message", function(evt) {
// var data = evt.data;
// });
ws.onclose = function (evt) {
console.log("Connection closed.");
};
// ws.addEventListener("close", function (evt) {
// var code = evt.code;
// var reason = evt.reason;
// var wasClean = evt.wasClean;
// });
ws.onerror = function (event) {
// handle error event
};
ws.addEventListener("error", function (event) {
// handle error event
});
switch (ws.readyState) {
case WebSocket.CONNECTING:
console.log("正在连接");
break;
case WebSocket.OPEN:
console.log("连接成功");
break;
case WebSocket.CLOSING:
console.log("正在关闭");
break;
case WebSocket.CLOSED:
console.log("已经关闭");
break;
default:
break;
}
兼容
- Adobe Flash Socket
- ActiveX HTMLFile (IE)
- 基于 multipart 编码发送 XHR
- 基于长轮询的 XHR
服务
- µWebSockets
- Socket.IO
- WebSocket-Node
- WebSocketd