基本概念
概述
WebSocket是一种在单个TCP连接上进行全双工通信的协议。
旨在解决Web应用程序 客户端 与 服务端 之间若要进行双向通信不得不使用轮询这种开销极大的方式的问题。
特点
- 全双工 服务端可以主动向客户端推送信息,客户端也可以主动向服务端发送信息
- 与HTTP共享端口,基于HTTP完成握手
- 数据传输基于帧,支持发送文本类型数据和二进制数据
- 没有同源限制,客户端可以与任意服务端通信
- 支持协议标识(ws或wss)与寻址,通过URL指定服务
握手
WebSocket握手基于HTTP,只需一次握手即可完成连接,具体握手流程如下:
客户端发起握手
通过HTTP请求告知服务端需要更换通信协议
GET ws://localhost:3000/hello HTTP/1.1
Host: 192.168.0.110:8080
Connection: Upgrade
Upgrade: websocket
Origin: http://localhost:8080
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: ddWE76tEFVgUfKjHIWDWvw==
- 请求必须是有效的HTTP请求
- 请求的方法必须是GET,HTTP版本至少是1.1。
- 请求的URL必须满足以下格式要求 ws:// {host} [ : {port} ] {path} [ ? {query} ]或wss:// {host} [ : {port} ] {path} [ ? {query} ]
- 请求首部必须包含Origin字段
- 请求首部必须包含Upgrade字段,且值为websocket,由于Upgrade字段只在客户端和邻接服务端产生作用,所以也必须包含Connection字段,且值为Upgrade,代理会在转发前删除Upgrade字段和Connection字段,然后再进行转发。
- 请求首部必须包含Sec-WebSocket-Version字段,值为13,用于指定协议版本
- 请求首部必须包含Sec-WebSocket-Key字段,值必须为一个16位的base64编码的字符串
服务端接收握手
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 2EoQihfR9RZchLRSwVgDycMsrOE=
Origin: http://localhost:8080
对请求头中的信息进行验证,若验证通过则返回状态码为101的响应,在此只对Sec-WebSocket-Key字段的验证过程进行简单描述,如下:
- 服务端将请求头中Sec-WebSocket-Key字段的值拿出来与GUID进行拼接
- 将拼接后的值使用SHA-1进行计算出摘要
- 使用base64对摘要进行编码
- 将编码值设置为响应头字段Sec-WebSocket-Accept的值,返回至客户端
- 客户端使用同样的流程,并将最终的结果与服务端返回的值进行比对,若相等,则验证通过
算法的简单Node.js实现如下:
安装依赖
yarn add js-sha1
const requestSecWebSocketKey = "ddWE76tEFVgUfKjHIWDWvw==";
const guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
const sha1 = require("js-sha1");
var hash = sha1.create();
hash.update(requestSecWebSocketKey);
hash.update(guid);
const result = Buffer.from(hash.digest(), "utf-8").toString("base64");
console.log(result);// 2EoQihfR9RZchLRSwVgDycMsrOE=
具体的验证规则请查阅RFC6455
应用
使用Node.js搭建简单的WebSocket服务端
安装依赖
yarn add websocket
var WebSocketServer = require("websocket").server;
var http = require("http");
// 创建http服务
var server = http.createServer(function (request, response) {
response.writeHead(404);
response.end();
});
server.listen(3000, function () {
console.log(new Date() + " Server is listening on port 3000");
});
// 基于http服务创建websocket服务
wsServer = new WebSocketServer({
httpServer: server,
autoAcceptConnections: false,
});
// websocket Handlers
const handlerMap = new Map();
// 初始化 websocket Handlers
handlerMap.set("/hello", function (msg) {
if (msg.type === "utf8") {
const obj = JSON.parse(msg.utf8Data);
this.send(
JSON.stringify({
name: "SERVER",
message: `你好 ${obj.name}!`,
})
);
}
});
// 对请求做处理
wsServer.on("request", function (request) {
if (handlerMap.has(request.resource)) {
const connection = request.accept(null, request.origin);
connection.on("message", (...arg) => {
handlerMap.get(request.resource).apply(connection, arg);
});
} else {
request.reject();
return;
}
});
基于W3C标准的WebSocket API客户端
以下代码基于HTML5 WebSocket,相关API 参照 MDN WebSocket
// 建立连接
const websocket = new WebSocket("ws://localhost:3000/hello");
// 建立连接成功时调用 即readyState为 WebSocket.OPEN 时
websocket.onopen = () => {
websocket.send(
JSON.stringify({
name: "坠落清风",
message: "你好 服务端!",
})
);
};
// 收到服务端消息时调用
websocket.onmessage = (event) => {
const obj = JSON.parse(event.data);
console.log(obj.message);
};
// 连接关闭后调用 即readyState为 WebSocket.CLOSED 时
websocket.onclose = function (event) {
console.log("WebSocket is closed now. reason:" + event.reason);
if (event.code) {
// 根据关闭后的状态码,做出对应的处理
}
};
// 发生错误时执行调用
websocket.onerror = function (event) {
console.error("WebSocket error observed:", event);
};
除了HTML5 WebSocket之外,我们还可以使用一些开源的基于W3C标准的客户端实现,如:
SockJS-client
socket.io-client
子协议
子协议可以看作是客户端和服务端之间如何交换数据的约定。客户端可以通过以下方式请求服务器使用特定的子协议
const websocket = new WebSocket("ws://localhost:3000/hello",['jsonrpc','wamp']);
这些值将会包含在握手时HTTP请求的头部字段Sec-WebSocket-Protocol中。如果该头部被指定,服务端需要从中选择一个所支持的协议。
心跳检测
一种检测客户端与服务端之间是否还处于正常连接的机制(即验证连接有效性),顾名思义,类似于心跳,客户端每隔一段时间就会向服务端发送数据帧,如果连接正常,服务端会返回一个pong帧。
用于解决连接异常关闭,且无法被直接检测到的问题。
以下代码仅表达基本概念,若需要完备的心跳检测机制,可以去使用一些成熟的WebSocket库。
let count = 0;
const intervalId = setInterval(() => {
// 如果大于或等于5次都未收到服务端的pong帧,则判定连接出现异常
if (count >= 5) {
clearInterval(intervalId);
// 进行重连
reconnect();
}
else {
websocket.send("ping");
count++;
}
}, 30000);
websocket.onmessage = (event) => {
if (event.data === "pong") {
count = 0;
}
};