在 websocket 协议出现之前,前端想实现即时通讯,只能通过下面2种方式:
客户端每隔一小段时间,就向服务器请求一次,询问有没有新消息。
实现起来很简单,只需要开启一个计时器不断发送请求即可。但缺点比较明显:
为了解决短轮询的问题,出现了长轮询。原理如下图:
虽然长轮询让每次请求和响应都变的有意义,但依然存在一些问题:
客户端长时间收不到响应会导致超时,从而主动断开和服务器的连接。
可以在 ajax 请求因为超时而结束时,立即重新发送请求到服务器。虽然会让之前的请求无意义,但比短轮询好多了。
因为客户端可能【过早的】请求了服务器,所以服务器不得不挂起这个请求,直到新消息出现。
这会让服务器长时间占用资源却没有做任何事情。
websocket 协议 HTML5 带来的新协议,相对于 http,它是一个持久连接的协议,它利用 http 协议完成握手,然后通过 TCP 连接通道发送消息,使用 websocket 协议可以实现服务器主动推送消息的能力。
从上图可以看出:
websocket 协议内容比较复杂,这里只介绍下握手协议。(下面会有例子说明)
当客户端需要和服务器使用 websocket 进行通信时,首先会使用HTTP协议完成一次特殊的请求-响应,这一次的请求-响应就是websocket握手。
在握手阶段,首先由客户端向服务器发送一个请求,请求地址格式如下:
# 使用 HTTP
ws://mysite.com/path
# 使用 HTTPS
wss://mysite.com/path
请求头:
Connection: Upgrade /* 协议需要升级,不使用 HTTP了 */
Upgrade: websocket /* 协议升级为 websocket */
Sec-WebSocket-Version: 13 /* websocket协议版本为 13 */
Sec-WebSocket-Key: YWJzZmFkZmFzZmRhYw== /* 连接的 key */
服务器如果同意,响应如下消息:
HTTP/1.1 101 Switching Protocols /* 切换协议,101表示切换协议 */
Connection: Upgrade /* 协议升级 */
Upgrade: websocket /* 升级到 websocket */
Sec-WebSocket-Accept: ZzIzMzQ1Z2V3NDUyMzIzNGVy /* 重新编码后的 key */
Sec-WebSocket-Accept
是将 Sec-WebSocket-Key
使用特殊的算法重新编码生成的。浏览器使用它来确保响应与请求相对应。
握手完成后,后续的消息收发不再使用 HTTP,任何一方都可以主动发消息给对方。
客户端:
<button>发送数据到服务器</button>
<script>
// 创建一个websocket,同时,发送连接到服务器
const ws = new WebSocket("ws://localhost:3002");
ws.onopen = function () {
// http 握手完成
console.log("连接已建立");
};
ws.onclose = function () {
console.log("通道关闭");
};
document.querySelector("button").onclick = function () {
ws.send("客户端数据123");
};
// ws.close(); //客户端主动断开连接
</script>
服务器:
const net = require("net");
const server = net.createServer((socket) => {
console.log("收到客户端的连接");
socket.once("data", (chunk) => {
// 解析请求报文,
const httpContent = chunk.toString("utf-8");
let parts = httpContent.split("\r\n");
parts.shift();
parts = parts
.filter((s) => s)
.map((m) => {
const i = m.indexOf(":");
return [m.slice(0, i), m.slice(i + 1).trim()];
});
// 变成对象的形式,为了取出请求头 Sec-WebSocket-Key
const headers = Object.fromEntries(parts);
const crypto = require("crypto"); // 加密模块
const hash = crypto.createHash("sha1");
// 创建 Sec-WebSocket-Accept,后面是一个随机的 guid。
const key = hash.update(headers["Sec-WebSocket-Key"] + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest("base64");
// 响应,注意格式。
socket.write(`HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: ${key}
`);
// 接收客户端的消息
socket.on("data", (chunk) => {
console.log(chunk.toString("utf-8"));
});
});
});
server.listen(3002);
注意数据格式为 Buffer 需要转码,因为 websocket 的消息需要特定的格式,数据量较大时会切片传输。但每个切片到达的顺序可能不一样,所以为了保证将接收到的数据,能按照顺序拼接,所以数据格式为 Buffer 二进制的形式。
一般使用 websocket 大多都会使用它 socket.io
测试使用版本 v4.7.2,消息格式都是字符串而不是 Buffer,所以不用转码了。
浏览器:访问地址 http://localhost:5500/index.html
<button>发送数据到服务器</button>
<script src="https://cdn.bootcdn.net/ajax/libs/socket.io/4.7.2/socket.io.js"></script>
<script>
const socket = io("http://localhost:3002");
document.querySelector("button").onclick = function () {
socket.emit("to-server", "来自浏览器的消息");
};
// 监听服务器的消息,约定事件名 to-client
socket.on("to-client", (chunk) => {
console.log(chunk);
});
// 服务器断开连接时触发
socket.on("disconnect", () => {
console.log("closed");
});
</script>
服务器:写法参考
启动后的服务器地址:http://localhost:3002
,所以会发生跨域。解决
const Koa = require("koa");
const { createServer } = require("http");
const { Server } = require("socket.io");
const app = new Koa();
const httpServer = createServer(app.callback());
const io = new Server(httpServer, {
cors: {
origin: "http://localhost:5500",
},
});
io.on("connection", (socket) => {
// 当有一个新的客户端连接到服务器成功之后,触发的事件
console.log("新的客户端连接进来了");
// 监听客户端发送的消息,约定事件为 to-server
socket.on("to-server", (chunk) => {
// 监听客户端的msg消息
console.log(chunk);
});
let count = 0;
const timer = setInterval(function () {
// 每隔两秒钟,发送一个消息给客户端,约定事件为 to-client
socket.emit("to-client", `来自服务器的第${count++}次消息`);
}, 2000);
socket.on("disconnect", () => {
clearInterval(timer);
console.log("closed");
});
});
// 监听端口
httpServer.listen(3002, () => {
console.log("server listening on 3002");
});
效果展示:
当页面中需要观察实时数据的变化(比如聊天、k 线图)时,过去我们往往使用两种方式完成(短轮询,长轮询)
无论是哪一种方式,都暴露了 http 协议的弱点,即响应必须在请求之后发生,服务器是被动的,无法主动推送消息。而让客户端不断的发起请求又会占用了资源。
websocket 的出现就是为了解决短轮询,长轮询的缺点,它利用 http 协议完成握手之后,就可以与服务器建立持久的连接,服务器可以在需要的时候主动推送消息给客户端,这样占用的资源最少,同时实时性也最高。
以上。