WebSocket的介绍与使用,带你彻底搞懂WebSocket
背景
在没有WebSocket时,我们如果想不断的拿到服务器的消息,比如要求实时性非常高的场景:股票、在线聊天、通知等。我们需要使用短轮询或长轮询的方式实现。
短轮询
浏览器每隔一小段时间,去请求服务器询问是否有新消息,实现很简单,使用定时器 setTimeout 即可。
这样会导致哪些不足呢?
- 因为需要不断的去请求,定义时间短了就会导致发送了很多非必要的请求,时间长了实时性又不高。
- 频繁的建立关闭连接
长轮询
长轮询是为了解决短轮询频繁请求的问题。他的表现为客户端发送了请求,服务器没有新消息就等待直到有了新消息才发送给客户端。
长轮询也有不足:
- 只有服务器有了新消息才会发给浏览器,这就会导致长时间没有响应给浏览器导致超时,浏览器就会主动断开本次连接。当然,我们可以在超时后重新发送请求,但这样会导致之前超时的请求都是无意义的。
- 在连接的过程中,如果服务器没有发送消息,就会一直占用着资源,但什么也没有做。
为什么使用WebSocket
无论短轮询还是长轮询,都是客户端主动发送请求给服务器实现通信,他们都是建立在HTTP 协议之上,所以不支持服务器主动给浏览器推送消息。而 websocket 具有全双工通信(服务器和客户端可同时发送和接收消息)的能力,与 HTTP 都是基于 TCP 的应用层协议。
以下是 WebSocket 需要注意的点,面试中可能会问到:
- WebSocket 是在 HTML5 新实现的功能,对古老版本的浏览器并不支持
- 对于消息少的,即不会频繁响应消息的场景,不必使用 WebSocket,不然会一直维持着 TCP 的连接,造成资源白白浪费。
- WebSocket 的使用中会经历两个阶段:握手和通信
- 握手阶段
需要注意的是使用 HTTP 协议发送请求而完成的 WebSocket握手。- websocket相关的请求头如下:
Sec-Websocket-Key: DBY94ycLOm0InY0I+OgGYg== // base64编码,要求服务端有对应加密的编码返回
Connection: Upgrade // 通知服务端升级
Upgrade: websocket // 升级为websocket
Sec-Websocket-Version: 13 // websocket的版本 - websocket相关的响应头如下:
Connection: Upgrade
Sec-Websocket-Accept: D8gT50cLdyuUbQzFvG6BWUhJEJw= // 与客户端相同密钥计算后的密文
Upgrade: websocket - 响应行中包含 switching protocals 表示握手成功,可以开始TCP通信了
- websocket相关的请求头如下:
- 通信阶段
握手结束后,不再使用HTTP,双方可任意给对方发送消息,直到双方主动断开或者因为网络等原因被迫断开,可以通过心跳检测机制实现断线重连。
- 握手阶段
- 基于 HTTP 的短轮询或长轮询,都需要先请求后响应,服务器无法主动响应消息给浏览器,而 WebSocket 可以在使用 HTTP 完成握手之后与服务器建立持久连接,实现双方即时通信。
WebSocket 的简单使用
- node 后端部分
const { WebSocketServer } = require('ws');
function createWebSocketServer(port) {
const ws = new WebSocketServer({ port }, () => {
console.log(`WebSocket server is listening on port ${port}`);
});
// 建立连接
ws.on('connection', (socket) => {
// 收到客户端的消息
socket.on('message', (message) => {
socket.send(message.toString()); // 回显消息
})
})
}
createWebSocketServer(3000);
- 前端部分,这里使用 vue 做了一个简单实现
<template>
<div class="button-groups">
<button @click="connect">建立连接</button>
<button @click="closeConnect">关闭连接</button>
</div>
<div class="input-groups">
<input type="text" placeholder="请输入内容" @keydown.enter="send" v-model="content" />
<button @click="send">发送</button>
</div>
<ul class="container">
<li class="item" v-for="item in items" :key="item.id">{{ item.content }}</li>
</ul>
</template>
<script setup lang='ts'>
import { ref } from 'vue';
interface IItems {
id: number,
content: string
}
const items = ref<IItems[]>([]);
const socket = ref<WebSocket | null>(null);
const content = ref('');
const connect = () => {
if (socket.value) {
return;
}
socket.value = new WebSocket('ws://localhost:3000');
socket.value.onopen = () => {
console.log('已建立连接');
}
socket.value.onmessage = (event) => {
items.value.push({
id: items.value.length,
content: e.data // event.data 是服务器发送的数据
});
}
socket.value.onclose = () => {
console.log('连接已关闭')
}
}
const send = () => {
if (!content.value) {
return;
}
const val = content.value.trim();
socket.value?.send(val);
content.value = '';
}
const closeConnect = () => {
if (!socket.value) {
return;
}
socket.value.close();
}
</script>
从上面代码可以看出目前的通信是点对点,只能通知单个客户端,如果想通知所有客户端该如何实现呢?
很简单,我们只需利用广播模式将之前监听消息的代码改为:
socket.on('message', (message) => {
ws.clients.forEach((client) => {
client.send(message.toString()); // 回显消息
})
})
https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket/WebSocket mdn参考文档