WebSocket 是一个应用层协议,有点类似 HTTP。但和 HTTP 不一样的是,它支持真正的全双工,即不仅客户端可以主动发消息给服务端,服务端也可以主动发消息给客户端。
大家好,我是前端西瓜哥,今天我们用 WebSocket 来实现一个简单的聊天室。
WebSocket 是一个应用层协议,有点类似 HTTP。但和 HTTP 不一样的是,它支持真正的全双工,即不仅客户端可以主动发消息给服务端,服务端也可以主动发消息给客户端。
尤其是后者,让我们不用再基于 HTTP 长轮询或短轮询的低效方式来实现服务端通知。相比 HTTP,WebSocket 的服务端推送更轻量,并能减少服务端的压力。
服务端
nodejs 并没有提供原生的 WebSocket 模块。如果要实现,需要基于 net 模块,根据 WebSocket 标准去做实现。
因为实现很复杂,所以西瓜哥我选择直接用第三库 ws。
类似 nodejs 原生的 http 等模块,ws 库支持 WebSocket 的服务端或客户端, 提供偏底层的 API。
我们先实现服务端代码:
import { WebSocketServer } from "ws";
// 创建一个 ws 服务
const wsSever = new WebSocketServer({
port: 6060,
});
// 每当一个客户端进行了 ws 连接,就会创建一个 ws 对象
wsSever.on("connection", (ws) => {
// 新客户端连接时,广播
wsSever.clients.forEach((client) => {
client.send(`有人进入聊天室,当前聊天室人数:${wsSever.clients.size}`);
});
// 广播任何客户端发送的消息
ws.on("message", (data) => {
const msg = data.toString();
wsSever.clients.forEach((client) => {
client.send(msg);
});
});
// 当有客户端退出时,广播
ws.on("close", () => {
wsSever.clients.forEach((client) => {
client.send(`有人退出了聊天室,当前聊天室人数:${wsSever.clients.size}`);
});
});
});
每当一个客户端进行了 websocket 连接,都会触发 wsServer 的 connection 事件,然后拿到一个 ws 对象。
这个 ws 对象代表了某个客户端和服务端的连接,我们可以通过它来接收对应客户端的消息,并让服务端对指定客户端进行主动消息推送。
新创建的 ws 对象会在建立连接时保存到 wsServer.clients 集合下,并在关闭连接后移除。所以我们可以利用这个 wsServer.clients 来进行广播,实现聊天室功能。
客户端
客户端使用原生的 WebSocket 对象,来和服务端进行 WebSocket 连接。
const ws = new WebSocket('ws://localhost:6060');
ws.addEventListener('message', (event) => {
const div = document.createElement('div');
div.innerText = event.data;
document.body.append(div);
})
// 点击发送按钮,将输入框中的内容发送给服务器
const input = document.querySelector('input');
const btn = document.querySelector('button');
btn.onclick = () => {
ws.send(input.value);
input.value = '';
}
效果
简易聊天室
改为使用 Socket.IO
ws 库是偏底层的实现,比较简单。
另一个库 Socket.IO 的底层使用了 ws,并做功能上的增强,提供更多的能力。
相比 ws,Socket.IO 能够做到:
- 如果浏览器不支持 WebSocket,回退为 HTTP 长轮询方案来模拟 WebSocket( WebSocket 于 2011 年完成 RFC,已经很久了,目前来说主流浏览器都已经支持 WebSocket 了,还不支持 WebSocket 的浏览器是屑)。
- 使用心跳包机制实现了自动重连。
- 包缓存。断连时发送数据,会将数据保存下来,等重新连接后再发送。
- 自定义事件支持。
- 广播。
相比自己去一个个实现,使用流行的轮子可能是更好的选择。
我们将前面的功能用 Socket.IO 实现一下。
服务端:
import { Server } from "socket.io";
// socket.io v3.x 开始默认不允许跨域,需要在配置显式设置为允许跨域
const io = new Server(6060, { cors: { origin: "*" } });
io.on("connection", (socket) => {
// 新客户端连接时,广播
io.emit("chat", `有人进入聊天室,当前聊天室人数:${io.engine.clientsCount}`);
// 广播任何客户端发送的消息
socket.on("chat", (data) => {
io.emit("chat", data);
});
// 当有客户端退出时,广播
socket.on("disconnect", () => {
io.emit("chat", `有人退出了聊天室,当前聊天室人数:${io.engine.clientsCount}`);
});
});
需要特别注意的是,Socket.IO 的 v3.x 版本开始,默认不允许跨域,需要在配置显式设置为允许跨域。
客户端:
const socket = io('ws://localhost:6060');
socket.on('chat', (data) => {
const div = document.createElement('div');
div.innerText = data;
document.body.append(div);
})
// 点击发送按钮,将输入框中的内容发送给服务器
const input = document.querySelector('input');
const btn = document.querySelector('button');
btn.onclick = () => {
console.log('发送');
socket.emit('chat', input.value);
input.value = '';
}
Socket.IO 优点是实现了生产环境需要的底层非业务能力,让我们能更心无旁骛地去编写业务代码。
缺点是丢失了灵活性。因为做了定制化,所以需要配套使用 Socket.IO 的客户端和服务端库的包,某种意义脱离了网络协议标准。在出现跨语言(比如前端是 JS,后端是 Java)的场景时,需要提供对应的语言的 Socket.IO 实现。
demo
demo 已经放到 github 上了,使用方法在 README.md 中有说明。
https://github.com/F-star/websocket-chat-demo
结尾
本文演示了 WebSocket 简易的聊天室功能是如何实现的,希望对你有所帮助。