一、简介
WebSocket是一种网络通信协议,它提供了在单个TCP连接上进行全双工通信的功能。在下面这个聊天应用示例中,WebSocket被用于实现实时的聊天功能,包括用户之间的消息发送、接收,用户状态管理以及其他相关的交互操作,为用户带来流畅的聊天体验。
二、后端实现
(一)模块引入与初始化
后端代码基于Node.js实现。首先,引入必要的模块:
const { WebSocketServer } = require("ws");
const WebSocket = require("ws");
const { getOneUserInfo } = require("../service/user");
const { createChat } = require("../service/chat/index");
// 在线列表
const onlineList = [];
initWebsocket
函数是整个WebSocket后端功能的核心初始化函数:
const initWebsocket = () => {
// 设置WebSocket服务的端口号
const wss = new WebSocketServer({ port: 8889 });
if (wss) {
console.log("websocket Initialized successfully on port: " + 8889);
}
};
在这个函数中,创建了一个监听在8889
端口的WebSocketServer
实例(wss
),并在创建成功后在控制台打印相应信息。
(二)连接事件处理
当有新的连接建立(wss.on("connection",... )
)时:
1.错误处理
为每个新连接设置错误处理(ws.on("error", console.error);
),这样当连接出现错误时,错误信息会在控制台输出,方便调试。
2. 消息处理
当新连接收到消息(ws.on("message",... )
)时,先将接收到的消息数据解析为JSON对象,然后根据消息的type
属性进行不同的处理:
- 初始化消息(init
类型)
case "init":
if (message.user_id) {
// 为当前用户的 ws连接绑定 用户id 用于用户断开链接时 改变用户在线状态
ws.user_id = message.user_id;
const user = await getOneUserInfo({ id: message.user_id });
if (user) {
message.nick_name = user.nick_name;
message.avatar = user.avatar;
// 上线
keepLatestOnlineList("online", message);
}
} else {
sendOnlineToAll();
}
break;
当用户连接成功后发送初始化消息时,如果消息中包含user_id
,则将该用户ID绑定到当前的WebSocket连接对象上。接着通过getOneUserInfo
获取用户信息,如果获取成功,将用户的昵称和头像信息添加到消息对象中,并调用keepLatestOnlineList
函数将用户标记为在线状态,同时向所有在线用户发送在线用户列表。若消息中没有user_id
,则直接调用sendOnlineToAll
函数。
- 普通消息(message
类型)
case "message":
const user = await getOneUserInfo({ id: message.user_id });
if (user) {
message.nick_name = user.nick_name;
message.avatar = user.avatar;
}
const res = await createChat(message);
if (res) {
message.id = res.id;
}
wss.clients.forEach(function each(client) {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(message), { binary: false });
}
});
break;
对于用户发送的普通消息,先使用filterSensitive
函数过滤消息内容中的敏感信息。然后通过getOneUserInfo
获取用户信息并添加到消息对象中。接着调用createChat
创建聊天记录,如果创建成功,将聊天记录的ID添加到消息对象中。最后,遍历所有连接的客户端,如果客户端处于打开状态,则将消息发送给该客户端。
- 撤回消息(revert
类型)
case "revert":
if (message.message_id) {
wss.clients.forEach(function each(client) {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(message), { binary: false });
}
});
}
break;
当收到撤回消息的请求且消息中包含message_id
时,遍历所有连接的客户端,将撤回消息的ID发送给所有在线客户端。
- 用户下线(offline
类型)
case "offline":
if (message.user_id) {
// 下线用户
getOneUserInfo({ id: message.user_id }).then((user) => {
if (user) {
keepLatestOnlineList("close", { user_id: user.id, nick_name: user.nick_name });
}
});
}
break;
当收到用户下线消息且消息中包含user_id
时,通过getOneUserInfo
获取用户信息,然后调用keepLatestOnlineList
函数将用户标记为离线状态。
- 连接关闭处理
当WebSocket连接被动断开(ws.on("close",... )
)时:
ws.on("close", function () {
if (ws.user_id) {
getOneUserInfo({ id: ws.user_id }).then((user) => {
if (user) {
keepLatestOnlineList("close", { user_id: ws.user_id, nick_name: user.nick_name });
}
});
}
});
如果连接的ws
对象有user_id
,则获取用户信息,并调用keepLatestOnlineList
函数将用户标记为离线状态。
(三)在线用户列表维护
keepLatestOnlineList
函数用于维护在线用户列表:
function keepLatestOnlineList(type, message) {
let index = onlineList.findIndex((item) => item.user_id === message.user_id);
switch (type) {
case "online":
if (index!== -1) {
onlineList.splice(index, 1);
}
onlineList.push({
user_id: message.user_id,
nick_name: message.nick_name,
avatar: message.avatar,
createTime: new Date(),
});
console.log(message.nick_name + " 上线了...");
break;
case "close":
if (index!== -1) {
onlineList.splice(index, 1);
if (message.nick_name) {
console.log(message.nick_name + " 断开连接...");
}
}
break;
default:
break;
}
sendOnlineToAll();
}
它首先通过findIndex
在onlineList
中查找用户的索引。当用户上线(type
为"online"
)时,如果用户已在列表中则先删除旧记录,然后将新的用户信息添加到列表中,并在控制台打印上线提示信息。当用户离线(type
为"close"
)时,如果用户在列表中则将其删除,并在有昵称的情况下打印离线提示信息。最后,无论哪种情况,都会调用sendOnlineToAll
函数向所有在线用户发送最新的在线用户列表。
sendOnlineToAll
函数用于向所有在线用户群发在线人数信息:
function sendOnlineToAll() {
// 群发在线人数
let latestList = [];
wss.clients.forEach(function each(client) {
if (client.readyState === WebSocket.OPEN) {
latestList.push(client.user_id);
let message = JSON.stringify({
type: "onlineList",
list: onlineList,
});
client.send(message, { binary: false });
}
});
}
它遍历所有连接的客户端,将处于打开状态的客户端的user_id
添加到临时列表中,然后创建一个包含在线用户列表的消息对象,将其序列化为JSON字符串后发送给所有在线客户端。
三、前端实现
(一)功能函数实现
1.发送消息函数(sendMessage
和wsSend
)
sendMessage
函数是发送消息的入口:
const sendMessage = async () => {
messageType.value = "text";
if (!getUserInfo.value.id) {
ElNotification({
offset: 60,
title: "温馨提示",
duration: 3000,
message: h("div", { style: "color: #e6c081; font-weight: 600;" }, "请先登录"),
});
return;
}
if (!inputChatRef.value.innerHTML) {
ElNotification({
offset: 60,
duration: 3000,
title: "温馨提示",
message: h("div", { style: "color: #e6c081; font-weight: 600;" }, "请输入消息再发送"),
});
return;
}
if (websocket.readyState!== 1) {
// 重连后再发送
reConnect();
} else {
// 在线就 直接发送
wsSend();
}
};
它首先将messageType
设置为"text"
,然后检查用户是否登录和输入框是否有内容。如果WebSocket连接未处于打开状态,则调用reConnect
函数重连后再发送;如果连接打开,则调用wsSend
函数。wsSend
函数根据messageType
的值构建不同类型的消息(文本或图片)并发送:
const wsSend = () => {
let message;
switch (messageType.value) {
case "text":
if (!inputChatRef.value.innerHTML) return;
message = {
type: "message",
user_id: getUserInfo.value.id,
content: inputChatRef.value.innerHTML,
content_type: "text", // 信息是文本
};
websocket.send(JSON.stringify(message));
inputChatRef.value.innerHTML = "";
break;
case "image":
if (!yourImageUrl.value) return;
message = {
type: "message",
user_id: getUserInfo.value.id,
content: yourImageUrl.value,
content_type: "image", // 信息是文本
};
websocket.send(JSON.stringify(message));
yourImageUrl.value = "";
imageUpload.value && imageUpload.value.clearFiles();
break;
default:
break;
}
};
发送完成后进行相应的清理操作,如清空输入框或清除图片上传组件中的文件。
2.WebSocket初始化与重连(initWebsocket
和reConnect
)
initWebsocket
函数用于初始化WebSocket连接,也可用于重连(通过参数isReconnect
判断):
const initWebsocket = async (isReconnect = false) => {
isConnecting.value = true;
// 如果说发现了异常 断开连接了 之前的websocket 还在的话就清空 重连
if (websocket) {
websocket.close();
websocket = null;
}
// websocket = new WebSocket("ws://mrzym.top/ws/");
websocket = new WebSocket("ws://localhost:8889/");
if (websocket) {
websocket.onopen = () => {
isConnecting.value = false;
websocket.send(
JSON.stringify({
type: "init",
user_id: getUserInfo.value.id || "",
})
);
console.log("WebSocket连接成功");
// 连上以后设置心跳检测 如果断开就重新连接 并清空之前的心跳检测 防止内存泄漏
clearInterval(heartBreak);
heartBreak = null;
heartBreak = setInterval(() => {
if (websocket.readyState!== 1) {
reConnect();
}
}, 30000);
};
websocket.onmessage = (event) => {
if (event.data) {
const data = JSON.parse(event.data);
let index;
// tips 表示提示 message 表示用户发送的消息
switch (data.type) {
case "tips":
if (isReconnect) {
// 这里重连就重新发送
wsSend();
console.log("重连成功");
} else {
ElNotification({
offset: 60,
title: "提示",
duration: 3000,
message: h("div", { style: "color: #7ec050; font-weight: 600;" }, data.content),
});
}
break;
case "message":
if (data.content) {
messageList.value.push(data);
if (data.user_id!== getUserInfo.value.id) {
newMessageCount.value++;
}
nextTick(() => {
scrollToBottom();
});
}
break;
case "onlineList":
onlineList.value = data.list;
index = onlineList.value.findIndex((item) => item.user_id === getUserInfo.value.id);
if (index === -1) {
clearWebsocket();
}
break;
case "revert":
index = messageList.value.findIndex((item) => item.id === data.message_id);
if (index!== -1) {
messageList.value.splice(index, 1);
}
break;
default:
break;
}
}
};
websocket.onerror = () => {
console.log("WebSocket连接错误");
};
} else {
console.log("WebSocket连接失败");
ElNotification({
offset: 60,
title: "错误提示",
duration: 3000,
message: h(
"div",
{ style: "color: #f56c6c; font-weight: 600;" },
"聊天室连接失败 正在重新连接"
),
});
if (timer) return;
timer = setInterval(() => {
reConnectionCount.value++;
initWebsocket();
// 连上了就不重连了
if (websocket) {
clearInterval(timer);
}
// 尝试五次 实在是连不上就不连了
if (reConnectionCount.value == 5) {
clearInterval(timer);
}
}, 5000);
}
};
在初始化过程中,先设置isConnecting
为true
,如果之前存在websocket
则关闭它。然后创建一个新的WebSocket连接,连接成功后发送初始化消息,设置连接成功后发送初始化消息,设置心跳检测定时器。当接收到服务器消息时,根据消息类型进行处理,如处理提示信息、新消息、在线用户列表更新、消息撤回等。如果连接出现错误或连接失败,会进行相应的提示和重连操作。reConnect
函数只是简单地调用initWebsocket
并传入true
:
const reConnect = () => {
initWebsocket(true);
};
3.其他功能函数
- getMessageList
函数用于获取聊天消息列表:
const getMessageList = async () => {
loadingMessage.value = true;
const res = await getChatList({
size: 10,
last_id: messageList.value.length > 0? messageList.value[0].id : "",
});
if (res.code == 0) {
const list = res.result.list;
if (messageList.value.length > 0) {
if (Array.isArray(list) && list.length) {
messageList.value = list.concat(messageList.value);
if (list.length == 10) {
canLoadMore.value = true;
} else {
canLoadMore.value = false;
}
} else {
canLoadMore.value = false;
}
} else {
if (Array.isArray(list) && list.length) {
messageList.value = list;
if (list.length == 10) {
canLoadMore.value = true;
} else {
canLoadMore.value = false;
}
} else {
canLoadMore.value = false;
}
}
loadingMessage.value = false;
}
};
根据获取结果更新messageList
和canLoadMore
等响应式数据。
- clearHistory
函数用于清空聊天记录:
const clearHistory = async () => {
ElMessageBox.confirm("确认清空吗", "提示", {
confirmButtonText: "确认",
cancelButtonText: "取消",
}).then(async () => {
const res = await clearChat();
if (res.code == 0) {
ElNotification({
offset: 60,
title: "提示",
duration: 3000,
message: h("div", { style: "color: #7ec050; font-weight: 600;" }, "聊天记录已清空"),
});
messageList.value = [];
canLoadMore.value = false;
}
});
};
通过弹框确认后调用clearChat
API函数,并在成功后更新相关数据和显示提示信息。
- offlineUser
函数用于强制某个用户下线:
const offlineUser = (user_id, nick_name) => {
ElMessageBox.confirm(`确认强制下线${nick_name}吗`, "提示", {
confirmButtonText: "确认",
cancelButtonText: "取消",
}).then(() => {
websocket &&
websocket.send(
JSON.stringify({
type: "offline",
user_id: user_id,
})
);
});
};
通过弹框确认后向服务器发送下线指令。
- scrollToBottom
函数用于将聊天容器滚动到最底部:
const scrollToBottom = () => {
chatContainerRef.value &&
chatContainerRef.value.scrollTo({
top: chatContainerRef.value.scrollHeight,
behavior: "smooth",
});
};
以显示最新的聊天消息。
- selectIcon
函数用于将用户选择的图标(如表情)插入到输入框中,并更新光标的位置索引:
const selectIcon = (val) => {
const text = val;
if (currentIndex.value == inputChatRef.value.innerHTML.length) {
inputChatRef.value.innerHTML += `${text}`;
} else {
// 拼接表情
let input = inputChatRef.value.innerHTML;
let start = input.slice(0, currentIndex.value);
let end = input.slice(currentIndex.value);
inputChatRef.value.innerHTML = start + `${text}` + end;
}
// 每次拼接完就加一下下标 一个表情的长度是两个字节
currentIndex.value += 2;
};
const keepIndex = () => {
currentIndex.value = getCurrentIndex();
};
function getCurrentIndex() {
var range;
if (window.getSelection) {
//ie11 10 9 ff safari
range = window.getSelection();
return range.focusOffset;
} else if (document.selection) {
range = document.selection.createRange();
return range.focusOffset;
}
}
- `handleChange`函数用于处理图片上传操作:
const handleChange = async (uploadFile) => {
imageUploading.value = true;
const img = await imgUpload(uploadFile);
if (img.code == 0) {
const { url } = img.result;
yourImageUrl.value = url;
messageType.value = "image";
wsSend();
imageUploading.value = false;
}
};
上传成功后更新相关数据并发送图片消息。
- revertOneChat
函数用于撤回一条聊天消息:
const revertOneChat = async (id) => {
if (!id) return;
const res = await deleteOneChat(id);
if (res.code == 0) {
let index = messageList.value.findIndex((item) => item.id === id);
if (index!== -1) {
messageList.value.splice(index, 1);
}
// websocket 发送撤回消息的信息 通知其他用户撤回消息
websocket.send(
JSON.stringify({
type: "revert",
message_id: id,
})
);
ElNotification({
offset: 60,
title: "提示",
duration: 3000,
message: h("div", { style: "color: #7ec050; font-weight: 600;" }, "撤回成功"),
});
}
};
先删除本地消息列表中的消息,然后向服务器发送撤回消息的通知。
- clearWebsocket
函数用于关闭WebSocket连接并清理相关的定时器资源:
const clearWebsocket = () => {
websocket && websocket.close();
websocket = null;
clearInterval(heartBreak);
heartBreak = null;
};
(二)数据监听与生命周期钩子
使用watch
监听getUserInfo.value.id
的变化,当用户ID改变时,重新初始化WebSocket连接并设置hasLoaded
为true
:
watch(
() => getUserInfo.value.id,
async () => {
await initWebsocket();
hasLoaded.value = true;
},
{
immediate: true,
}
);
同时监听chatVisible.value
的变化,根据聊天窗口的可见性设置文档的overflowY
样式:
watch(
() => chatVisible.value,
(newV) => {
if (newV) {
document.documentElement.style.overflowY = "hidden";
} else {
document.documentElement.style.overflowY = "visible";
}
},
{
immediate: true,
}
);
在onMounted
生命周期钩子中调用getMessageList
函数获取聊天消息列表:
onMounted(() => {
getMessageList();
});
在onBeforeUnmount
钩子中调用clearWebsocket
函数关闭WebSocket连接:
onBeforeUnmount(() => {
clearWebsocket();
});
标签:websocket,WebSocket,解与,端详,value,user,const,message,id
From: https://blog.csdn.net/qq_64546210/article/details/143875074