在浏览器和 Node.js 使用 WebSocket
WebSocket 是一种网络通信协议。WebSocket 只需要建立一次连接,客户端和服务端之间就可以很方便的发送数据,最主要是服务端也可以主动发送数据,不需要每次都由客户端先发起请求。
HTTP 是一种单向通信方式,客户端发送请求,服务端响应回复,然后断开通信。如果你要使用 HTTP 获取一些实时数据,比如聊天消息、多人游戏数据之类的,你可能需要使用 AJAX 每隔几秒就向服务器请求一次数据,资源占用和延迟都会比较高。
HTTP 和 WebSocket 的使用
WebSocket 需要和 HTTP 配合使用,打开网页时,需要先通过 HTTP 请求到页面,发起 WebSocket 握手时,走的也是 HTTP 协议。
WebSocket 虽然好用,但也不是每个网站都适合。WebSocket 在成功连接后,客户端和服务端都会占用一部分资源,只要不断开连接,资源就会一直占用着。对于客户端来说问题不是太大,但是服务器需要同时服务很多客户端,如果资源不能及时释放,访问量大的时候可能会耗尽服务器资源。
一般如果不是需要实时获取数据的网站,都没有必要用 WebSocket。
浏览器端
下面简单写一下浏览器端使用 WebSocket:
// 创建 WebSocket 连接
const socket = new WebSocket('ws://localhost:7771');
// WebSocket 成功建立连接时触发
socket.addEventListener('open', () => {
console.log('连接建立');
// 给服务端发送消息
socket.send('Hello');
});
// 收到服务端消息时触发
socket.addEventListener('message', ev => {
// 输出接收到的数据
console.log(ev.data);
});
// WebSocket 连接断开时触发
socket.addEventListener('close', () => {
console.log('已断开');
});
// WebSocket 连接发生错误时触发
socket.addEventListener('error', error => {
console.log(error);
});
new WebSocket
需要传入一个连接地址,WebSocket 的连接地址也是 ws://
开头,后面的地址和端口号和 HTTP 地址是一样的。
在 WebSocket 成功连接后,可以随时调用 WebSocket 的 send
来向服务器发送消息,不需要向 HTTP 一样要设置请求头之类的。
Node.js 服务端
浏览器端的 WebSocket 也需要在 HTTP 服务器环境下才能使用,直接打开 HTML 文件也是不能用的。
Node.js 创建 HTTP 服务我这里就不写了,要快速创建 HTTP 服务可以用 Express,关于 Express 的使用可以看 Node.js Web 框架 Express 的基本使用 。
Node.js 处理 WebSocket 需要使用 ws 模块,下面使用 npm 安装 ws:
npm install ws --save
下面直接使用 Node.js 创建 WebSocket 服务:
const ws = require('ws');
// 创建 WebSocket 服务
const socketServer = new ws.WebSocketServer({port: 7771});
// 有客户端连入时触发
socketServer.on('connection', (wsc) => {
console.log('有客户端连入');
// 向客户端发送消息
wsc.send('Hello');
// 收到客户端消息时触发
wsc.on('message', message => {
// 在控制台输出收到的数据
console.log(`收到:${message}`);
// 向客户端发送数据
wsc.send(`你是不是说:${message}`);
});
// 连接断开时触发
wsc.on('close', () => {
console.log('和客户端断开连接');
});
});
有客户端连入时,回调函数可以接收一个 wsc
客户端连接,给客户端连接绑定 message
事件可以接收消息,在客户端断开连接时,可以触发 close
事件。
处理多个 WebSocket 连接
上面的 Node.js 在触发 connection
事件后没有单独保存 wsc
客户端连接,在有多个客户端连入的情况下,我只能主动给最后一个连入的客户端发消息,不能主动给其他客户端发消息。
下面修改一下 Node.js,可以给所有客户端发消息,也可以给指定客户端发消息:
const ws = require('ws');
// 创建 WebSocket 服务
const socketServer = new ws.WebSocketServer({port: 7771});
const clientList = new Map(); // 用来存储客户端连接和ID
// 有客户端连入时触发
socketServer.on('connection', (wsc) => {
// 随机生成一串字符串作为客户端ID
const id = Math.random().toString(36).substring(2, 10);
// 把客户端ID和连接存入 clientList
clientList.set(id, wsc);
// 给所有客户端发消息
clientList.forEach(client => {
client.send(`新增 ${id},当前连接数 ${clientList.size}`);
});
// 收到客户端消息时触发
wsc.on('message', (message) => {
// 在控制台输出收到的数据和客户端ID
console.log(`收到 ${id} 的:${message}`);
});
// 连接断开时触发
wsc.on('close', () => {
// 删除 clientList 的客户端连接
clientList.delete(id);
console.log(`${id} 断开连接`);
});
});
上面的 clientList
使用 Map
存储了客户端连接和客户端 ID,我如果要给指定客户端发消息可以使用 clientList.get(id).send('message')
,只需要在 Map 的 get
传入 ID 就能找到指定客户端。
实现一个简易的多人聊天室
下面使用 WebSocket 实现一个简易聊天室,只要有新的客户端连入或离开,所有客户端的成员列表都会更新,有客户端发送消息,所有客户端也都能看到。
Node.js 服务端:
const ws = require('ws');
// 创建 WebSocket 服务
const socketServer = new ws.WebSocketServer({port: 7771});
const clientList = new Map(); // 用来存储客户端连接和ID
// 有客户端连入时触发
socketServer.on('connection', (wsc) => {
// 随机生成一串字符串作为客户端ID
const id = Math.random().toString(36).substring(2, 10);
// 把客户端ID和连接加入 clientList
clientList.set(id, wsc);
// 给所有客户端发送成员列表
sendMessage({type: 'list', list: Array.from(clientList.keys())});
// 收到客户端消息时触发
wsc.on('message', (message) => {
// 把收到的消息、ID、时间发给所有连接的客户端
sendMessage({
type: 'message',
message: message.toString(),
id: id,
time: Math.round(new Date().getTime() / 1000),
});
});
// 连接断开时触发
wsc.on('close', () => {
// 删除 clientList 的客户端连接
clientList.delete(id);
// 给所有客户端发送成员列表
sendMessage({type: 'list', list: Array.from(clientList.keys())});
});
});
// 给所有已连接的客户端发送消息
function sendMessage(message) {
// 把传入的消息转换为 JSON 字符串
message = JSON.stringify(message);
clientList.forEach(client => {
client.send(message);
});
}
我的 Node.js 给客户端发送的是转换为 JSON 字符串的对象,每个对象都包含 type
属性,type
为 list
就是发送成员列表,type
为 message
就是普通消息。
浏览器端 HTML:
<!--消息输入-->
<input type="text" id="message-input">
<!--提交-->
<button type="button" id="submit-btn" disabled>发送</button>
<!--消息列表-->
<div>消息:</div>
<ul id="message-list"></ul>
<!--成员列表-->
<div>成员列表</div>
<ul id="user-list"></ul>
浏览器端 JavaScript:
const messageInput = document.querySelector('#message-input'); // 消息输入
const submitBtn = document.querySelector('#submit-btn'); // 消息提交
const messageList = document.querySelector('#message-list'); // 消息列表
const userList = document.querySelector('#user-list'); // 成员列表
// 创建 WebSocket 连接
const socket = new WebSocket('ws://localhost:7771');
// WebSocket 成功建立连接时触发
socket.addEventListener('open', () => {
// 取消发送按钮的禁用状态
submitBtn.disabled = false;
});
// 收到服务端消息时触发
socket.addEventListener('message', ev => {
// 获取消息
const message = JSON.parse(ev.data);
// 根据消息类型判断是普通聊天消息还是成员数量变更
if (message.type === 'message') {
// 创建 li 列表项
const li = document.createElement('li');
// 设置时间、ID、聊天消息
li.innerHTML = `${message.time} ${message.id}:${message.message}`;
// 把创建的列表插入到消息列表
messageList.appendChild(li);
}else {
// 清空成员列表
userList.innerHTML = '';
message.list.forEach(item => {
userList.innerHTML += `<li>${item}</li>`;
});
}
});
// WebSocket 连接断开时触发
socket.addEventListener('close', () => {
// 禁用发送按钮
submitBtn.disabled = true;
alert('与服务器断开连接');
});
// WebSocket 连接发生错误时触发
socket.addEventListener('error', error => {
// 禁用发送按钮
submitBtn.disabled = true;
alert('无法连接到服务器');
});
// 发送按钮点击
submitBtn.addEventListener('click', () => {
// 消息输入框为空就不再往下执行
if (messageInput.value === '') return false;
// 发送消息
socket.send(messageInput.value);
// 清空消息输入
messageInput.value = '';
});
最终实现的简易聊天室如下:
上面的给客户端分配 ID 只是一种比较简单的处理多连接的方式,更好的方式应该是需要客户端在发消息的时候也一起带上用户信息。
版权声明:本文为原创文章,版权归 Mr. Ma's Blog 所有,转载请联系博主获得授权。
本文地址:https://www.misterma.com/archives/938/
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。