一个简单的WebSocket小练习,适合像我一样的小白。和开发同事用Vscode终端聊天,摸鱼大师。
先来粗略整理一下WebSocket的相关知识点:
WebSocket
WebSocket是HTML5提供的一种协议,与传统的HTTP请求-响应模型不同,WebSocket允许在客户端和服务器之间建立持久连接,双方可进行双向通信,直到连接关闭。
应用场景 由于WebSocket持久连接、双向通信、低延迟的特点,常用的应用场景:实时聊天、实时通知、多人在线游戏、实时协作工具等。
服务端初步搭建
初始化项目
- 创建项目文件夹
- 在文件夹目录下执行
npm init
初始化package.json
文件 - 创建
index.js
,作为服务器 - 安装Node.js的
ws
库:npm install ws
创建WebSocket服务器 创建服务器:new WebSocketServer(options[, callback])
js
// 引入WebSocketServer类
const { WebSocketServer } = require('ws');
// 自定义端口号
const PORT = 8124;
// 创建并启动WebSocket服务器,监听PORT端口号
const wsServer = new WebSocketServer({ port: PORT }, () => {
console.log("WebSocket服务端创建成功,地址为 ws://127.0.0.1:8124");
})
服务器监听连接事件 connection事件,回调函数参数:
websocket
: WebSocket实例(服务器与接入的客户端之间的连接)request
: 客户端发送的http请求信息,本例中未用到
js
// 服务器监听connection事件,有客户端成功连接时触发
wsServer.on("connection", (ws) => {
})
监听消息 对于每个接入的客户端(webSocket实例,即回调函数中的ws
),我们监听其message
、error
、close
事件,以实现消息接收、错误处理、连接关闭。
js
// 服务器监听connection事件,有客户端成功连接时触发
wsServer.on("connection", (ws) => {
// 监听消息事件
ws.on('message', (message) => {
console.log('接收消息:', message);
});
// 监听关闭事件
ws.on('close', () => {
console.log('客户端关闭!');
});
// 监听错误事件
ws.on('error', (error) => {
console.log('WebSocket error:', error);
});
})
至此,简单的服务器搭建好了,我们先来创建客户端,双方能够连接成功再进行下一步。
客户端初步搭建
初始化项目
- 另外创建项目文件夹
- 其他步骤同服务器端
创建WebSocket连接 客户端创建连接:new WebSocket(address,[, protocols][, options])
此处的address(连接地址)为服务端的IP地址及端口号,其他可选参数本例未用到。
js
// 引入WebSocket类
const { WebSocket } = require("ws");
// 创建webSocket
const ws = new WebSocket("ws://127.0.0.1:8124");
监听open事件 连接成功建立时,会触发open事件。
js
// 连接成功建立
ws.on('open', () => {
console.log('连接成功!');
})
监听消息 接收服务端发来的消息,通过监听message
事件,回调函数参数:data
为Buffer类型。
Buffer用于处理二进制数据,通过toLocaleString()将其转换为字符串
js
ws.on("message", function message(data) {
console.log(data.toLocaleString());
});
// 监听错误
ws.on("error", console.error);
测试连接
完成以上步骤后,我们先测试连接是否能成功建立:先运行服务端index.js
,创建服务器,再运行客户端的index.js
,连接到服务器。注意IP地址和端口号保持一致。
如果连接成功,服务端终端:
客户端:
这里成功开启了连接,但双方还不能发送消息,接下来我们实现发送消息的部分。
客户端发送消息
客户端通过命令行来进行输入,首先我们需要先获取用户输入。此处我们使用node内置模块readline
获取用户输入。
创建readline实例
- input 属性指定了从哪里读取输入,这里使用了 process.stdin,表示从标准输入流中读取用户的输入
- output 属性指定了输出到哪里,这里使用了 process.stdout,表示向标准输出流中输出信息,通常是在控制台中显示
js
// 引入内置模块readline
const readline = require('readline');
// 创建readline实例
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
获取用户输入 在连接成功的回调内,监听readline的'line'事件,获取用户输入,并发送给服务器:
js
// 连接成功建立
ws.on('open', () => {
console.log('连接成功!');
// 读取输入
rl.on('line', (input) => {
// 将用户输入发送给服务器
if (input) {
ws.send(input)
}
})
})
创建昵称 这样虽然可以发送消息,但不知道是谁发送的,所以在发送之前,我们先要求每个用户为自己创建昵称。 通过readline向命令行输出问题,并获取用户输入。 之后修改上面的写法,在每次发送消息时,捎带上用户名。
js
let userName = '匿名';
// 连接成功建立
ws.on('open', () => {
console.log('连接成功!');
rl.question("请输入昵称:", (name) => {
// 输入有效,则将用户输入的昵称存入全局变量
name && (userName = name);
// 读取输入
rl.on('line', (input) => {
// 将用户输入发送给服务器,每次捎带上用户名
if (input) {
ws.send(userName + ":" + input);
}
})
})
})
服务器转发消息
转发消息 服务器收到某一客户端的消息后,需要将其转发,以便其他所有客户端能看到这条消息,其中ws
代表当前发来消息的客户端,它不需要再被转发。
js
// 服务器监听connection事件,有客户端成功连接时触发
wsServer.on("connection", (ws) => {
// 监听消息事件
ws.on('message', (message) => {
wsServer.clients.forEach((client) => {
if (client !== ws && client.readyState === 1) {
client.send(message);
}
});
});
})
测试消息收发
重新运行服务端程序,开启多个客户端程序,测试某个客户端发消息,其他客户端是否能收到消息。
简易版完整代码
以上是具备基础功能的命令行聊天工具,开发测试完成后,运行起服务端程序,把client
文件夹甩给局域网内的其他人,就可以一起聊天啦!注意连接地址的IP为服务器IP地址。
以下是这个简易版程序的完整代码。
github项目地址:github.com/LightBoatA/...
服务端
js
// 引入WebSocketServer类
const { WebSocketServer } = require('ws');
// 自定义端口号
const PORT = 8124;
// 创建并启动WebSocket服务器,监听PORT端口号
const wsServer = new WebSocketServer({ port: PORT }, () => {
console.log("WebSocket服务端创建成功,地址为 ws://127.0.0.1:8124");
})
// 服务器监听connection事件,有客户端成功连接时触发
wsServer.on("connection", (ws) => {
// 监听消息事件
ws.on('message', (message) => {
wsServer.clients.forEach((client) => {
if (client !== ws && client.readyState === 1) {
client.send(message);
}
});
});
// 监听关闭事件
ws.on('close', () => {
console.log('客户端关闭!');
});
// 监听错误事件
ws.on('error', (error) => {
console.log('WebSocket error:', error);
});
})
客户端
js
// 引入WebSocket类
const { WebSocket } = require("ws");
// 引入内置模块readline
const readline = require('readline');
// 创建readline实例
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
// 创建webSocket
const ws = new WebSocket("ws://127.0.0.1:8124");
let userName = '匿名';
// 连接成功建立
ws.on('open', () => {
console.log('连接成功!');
rl.question("请输入昵称:", (name) => {
// 输入有效,则将用户输入的昵称存入全局变量
name && (userName = name);
// 读取输入
rl.on('line', (input) => {
// 将用户输入发送给服务器,每次捎带上用户名
if (input) {
ws.send(userName + ":" + input);
}
})
})
})
// 监听消息
ws.on("message", function message(data) {
console.log(data.toLocaleString());
});
// 监听错误
ws.on("error", console.error);
花里胡哨版
在简易版基础上添加了用户上线通知、下线通知、当前用户查看、服务器通知等功能。 github项目地址:github.com/LightBoatA/...
服务端
js
const { WebSocketServer } = require("ws");
const NOTICE_PERFIX = '--- 通知☆ --- ';
const REPLY_PERFIX = '--- 服务器回复☆ --- ';
let onlineUsers = [];
const wss = new WebSocketServer({ port: 8124 }, () => {
console.log("success:ws://127.0.0.1:8124");
});
wss.on("connection", function connection(ws) {
ws.on("error", console.error);
ws.on("message", function message(data) {
try {
const msg = JSON.parse(data);
switch (msg.type) {
case 'add_user':
addUser(msg.data, ws);
break;
case 'user_list':
sendUserList(ws);
break;
case 'message':
broadcastUsers(msg.data, ws);
break;
default:
break;
}
} catch (error) {
broadcastUsers(getServerErrorMsg(error))
}
});
ws.on("close", () => {
removeUser(ws)
});
});
const getAllOnlineUsersMsg = (perfix) => {
return (perfix || NOTICE_PERFIX) + '当前在线用户:' + onlineUsers.map(user => user.username).join('、');
}
const getAddUserMsg = (username) => {
return NOTICE_PERFIX + username + '上线了!' + '\n' + getAllOnlineUsersMsg();
}
const getRemoveUserMsg = (username) => {
return NOTICE_PERFIX + username + '下线了!' + '\n' + getAllOnlineUsersMsg();
}
const getServerErrorMsg = (error) => {
return NOTICE_PERFIX + '服务器故障:' + error;
}
const addUser = (username, ws) => {
onlineUsers.push({ username, socket: ws });
const data = getAddUserMsg(username)
broadcastUsers(data);
console.log('用户加入');
}
const removeUser = (ws) => {
const offlineUser = onlineUsers.find(user => user.socket === ws);
if (offlineUser) {
onlineUsers = onlineUsers.filter(user => user.socket !== ws)
const data = getRemoveUserMsg(offlineUser.username);
broadcastUsers(data);
console.log('用户离开');
}
}
const sendUserList = (ws) => {
const data = getAllOnlineUsersMsg(REPLY_PERFIX);
ws.send(data);
}
const broadcastUsers = (data, ws) => {
wss.clients.forEach((client) => {
if (client !== ws && client.readyState === 1) {
client.send(data);
}
});
}
客户端
js
const { WebSocket } = require("ws");
const ws = new WebSocket("ws://127.0.0.1:8124");
const readline = require("readline").createInterface({
input: process.stdin,
output: process.stdout,
});
let name = "匿名用户";
const onLine = () => {
readline.on("line", (input) => {
if (input === "close") {
ws.close();
readline.close();
} else if (input === "user list") {
ws.send(JSON.stringify({ type: "user_list"}))
}else {
// 否则将输入发送给服务器
ws.send(JSON.stringify({ type: "message", data: name + ":" + input}));
}
});
};
ws.on("error", console.error);
ws.on("open", function open() {
console.log("连接成功");
readline.question("请输入昵称:", (input) => {
input && (name = input);
ws.send(JSON.stringify({ type: "add_user", data: name }))
onLine();
});
});
ws.on("message", function message(data) {
console.log(data.toLocaleString());
});