使用Vue、Nodejs以及websocket搭建一个简易聊天室

简易聊天室

  • 说在前面
  • 效果展示
  • websocket
    • websocket的由来
    • websocket的特点
  • vue前端
    • 静态结构
      • 效果
      • 代码
    • 点击切换用户以及该用户高亮
      • 实现思路
      • 效果展示
    • 发送消息功能
      • 效果展示
    • 连接服务端
  • Nodejs服务器端
    • 实现步骤
    • 代码

说在前面

  • 在学习计算机网络的时候,看到了websocket这个网络通信协议,它提供了在单个 TCP 连接上进行全双工通信的能力,也就是说不需要客户端请求就可以自动向客户端发送响应,然后就发现可以用这个做一个聊天功能,于是就有了这篇博客。
  • 下面的聊天室功能还用到了注册登录的相关知识,在我的另一篇博客中有写,链接:vue+nodejs实现登录注册功能
  • 项目的源代码我也发到了github上面,大家可以去获取:github地址,点击跳转

效果展示

在这里插入图片描述

websocket

  • 这里我先给大家介绍一下websocket协议

websocket的由来

  • 在 WebSocket 出现之前,Web 应用主要依赖于 HTTP 协议进行通信。然而,HTTP 是一种无状态的、请求-响应式的协议,每次通信都需要建立新的连接,这导致了较高的延迟和资源消耗。为了实现实时通信,开发者通常会使用一些变通的方法,如轮询(Polling)或长轮询(Long Polling),但这些方法效率较低。
  • 于是websocket应运而生,WebSocket 协议于 2011 年被正式标准化,旨在解决这些问题,提供一种更高效、更实时的通信方式。

websocket的特点

  1. 全双工通信:允许服务器和客户端同时发送和接收消息,而无需等待对方的响应
  2. 低延迟:一旦websocket连接建立,数据就可以在客户端和服务器之间快速传输,无需每次都建立新的连接
  3. 轻量级:websocket的数据帧格式简单,头部开销较小,适合传输小数据包

vue前端

  • 首先我们需要明白搭建一个这样聊天室的步骤(以下是波煮自己写项目时候的顺序,可参考)
  1. 先写出聊天室的静态结构,这里波煮是照着微信的界面画的;
  2. 思考用什么存储消息并进行渲染,波煮的脑子想到的是对象数组,为什么要用对象数组呢?因为我们只用一个数组那么就需要区分这个消息是对方发送的还是自己发送的,从而就行不同的渲染;
  3. 然后就是与服务器的链接了,以及一些用户点击事件的绑定。

静态结构

  • 下面的静态结构代码其实已经不静态了,这是绑定事件后的代码了。不过大体还是一样的,这里用到了Element-plus的container组件,

效果

在这里插入图片描述

代码

<template><div class="common-layout"><el-container><el-aside class="aside" width="200px"><el-scrollbar :always="false"><!-- 侧边栏用户栏 --><divv-for="(item, index) in UserData":key="item.id"class="userItem":class="isActive === index ? 'ItemActive' : 'ItemUsual'"@click="onChangeUserItem(index)"><el-avatarstyle="margin-left: 10px; margin-right: 10px"shape="square"size="8px":src="item.avator"/><span>{{ item.username }}</span></div></el-scrollbar></el-aside><el-container><el-header class="header">{{ UserData[isActive].username }}</el-header><el-main class="main"><divv-for="(item, index) in Allmessages":class="item.type === 'other' ? 'message_other ' : 'message_my'":key="item.id"><div v-if="item.type === 'other'" class="main_message"><el-avatar:class="item.type === 'other' ? 'other_avatar ' : 'my_avatar'"shape="square"size="10px":src="item.type === 'other' ? UserData[isActive].avator : CurrentUserName.avator"/><span :class="item.type === 'other' ? 'other_span ' : 'my_span'">{{ item.message }}</span></div><div v-else class="main_message"><span :class="item.type === 'other' ? 'other_span ' : 'my_span'">{{ item.message }}</span><el-avatar:class="item.type === 'other' ? 'other_avatar ' : 'my_avatar'"shape="square"size="10px":src="item.type === 'other' ? UserData[isActive].avator : CurrentUserName.avator"/></div></div></el-main><el-footer class="footer"><el-inputv-model="textarea":rows="7"type="textarea"placeholder="请输入内容"class="InputArea"@keydown.enter.exact.prevent="onSubmit"/><el-button @click="onSubmit" type="primary">发送</el-button></el-footer></el-container></el-container></div>
</template><style scoped>
.common-layout {width: 700px;height: 700px;background-color: aquamarine;margin: 56px auto;display: flex;
}
.aside {background-color: white;border-left: 1px solid rgb(218, 213, 213);border-top: 1px solid rgb(218, 213, 213);
}
.header {flex: 0.15;background-color: rgb(246, 248, 248);text-align: center;align-content: center;border-bottom: 1px solid rgb(218, 213, 213);border-left: 1px solid rgb(218, 213, 213);
}
.main {background-color: rgb(246, 248, 248);flex: 0.75;border-bottom: 1px solid rgb(218, 213, 213);border-left: 1px solid rgb(218, 213, 213);display: flex;flex-direction: column;
}
.main_message{display: flex;flex-direction: row
}
.footer {display: flex;justify-content: space-between; /* 子元素水平分布 */align-items: flex-end; /* 子元素垂直对齐到容器的底部 */padding: 10px; /* 添加一些内边距 */background-color: rgb(246, 248, 248);border-left: 1px solid rgb(218, 213, 213);flex: 0.3;
}.footer div {flex: 1; /* 让文本内容占据剩余空间 */
}.footer .el-button {margin-left: 10px; /* 添加一些左边距,使按钮与文本内容分开 */
}
.message_other {background-color: rgb(246, 248, 248);width: 200px;display: flex;justify-content: start;align-items: center;height: auto;margin-bottom: 15px;
}
.message_my {background-color: rgb(246, 248, 248);width: 200px;display: flex;justify-content: end;align-items: center;margin-bottom: 15px;margin-left: 250px;
}
.userItem {height: 70px;background-color: white;display: flex;align-items: center;
}
.ItemActive {background-color: rgb(231, 233, 233);
}
.my_span{padding: 5px;background-color: greenyellow;line-height: 30px;
}
.my_avatar{margin-right: 10px; margin-left: 10px
}
.other_span{padding: 5px;background-color: white; line-height: 30px
}
.other_avatar{margin-left: 10px; margin-right: 10px
}
</style>

点击切换用户以及该用户高亮

实现思路

  1. 定义一个响应式数据来记录当前在和哪个用户聊天
//当前用户
const isActive = ref(0);
  1. 通过isActive来进行动态绑定类名,从而实现不同的css样式,这个是vue的方便之处,可以直接使用:class来实现动态类名的绑定,这里高亮区分我是通过设置不同的背景颜色来区分的
:class="isActive === index ? 'ItemActive' : 'ItemUsual'"
.ItemActive {background-color: rgb(231, 233, 233);
}
  1. 绑定点击事件,获取当前用户的index,或修改isActive响应式数据为当前用户的index
//切换用户回调函数
const onChangeUserItem = (index) => {isActive.value = index;
};

效果展示

在这里插入图片描述

发送消息功能

  • 我们先来理清整体思路:首先点击发送时需要获取文本框的内容,并把这个内容添加到消息数组中,接着把输入框中的内容清空。
  1. 点击发送时获取文本框内容,先定义一个响应式数据与输入框进行双向绑定:
//文本输入区域
const textarea = ref("");//通过v-model双向绑定
<el-inputv-model="textarea":rows="7"type="textarea"placeholder="请输入内容"class="InputArea"@keydown.enter.exact.prevent="onSubmit"
/>
  1. 为发送按钮添加点击事件,并将信息以对象的形式添加到消息数组中
//点击发送的回调函数
const onSubmit = () => {//获取当前用户用户名console.log(UserData.value[isActive.value].username);const toname = UserData.value[isActive.value].username;//将消息发送给服务端再由服务端推送给指定用户const json = {type: "message",toName: toname,message: textarea.value,};Allmessages.value.push({type:"my",id:id.value,message:textarea.value})id.value++//向服务端发送该消息socket.send(JSON.stringify(json));textarea.value = "";
};

效果展示

在这里插入图片描述

连接服务端

  • 因为我们用的是websocket协议,所以我们需要创建一个websocket连接,再监听连接以及websocket的错误
  1. 创建websocket连接:
//创建websocket连接
const socket = new WebSocket("http://localhost:3000");
  1. 监听连接,在这里面需要在刚开始连接上服务器的时候向服务器发送该用户的信息,以便服务器向别的用户广播该用户的上线状态。
  2. 监听服务器发来的消息,这里也就是别人向你发送过来的消息,别人先将要对你发送的消息发送给服务器,然后再由服务器根据一些条件发送给你。 服务器就像一个中介一样,链接这你们两个
  3. 拿到服务器发送给你的消息并插入到消息列表中进行渲染
//创建websocket连接
const socket = new WebSocket("http://localhost:3000");//监听websocket链接
socket.onopen = async () => {//获取用户信息const token = localStorage.getItem("token");const res = await axios.get("http://0.0.0.0:8080/my/info", {headers: {Authorization: token,},});// console.log(res,"userinfo");CurrentUserName.value.username = res.data.data.username;CurrentUserName.value.isOnline = true;CurrentUserName.value.avator = res.data.data.user_pic;// console.log("连接到服务器");socket.send(JSON.stringify({ type: "userInfo", data: CurrentUserName.value }));
};
//监听服务器消息
socket.onmessage = (event) => {// const message = event.data;const message = JSON.parse(event.data);console.log(message, "message");// CurrentUserName.value = message.userif (message.type === "message") {// console.log(message,"jijiy");Allmessages.value.push({type:"other",id:id.value,message:message.message});id.value++;// console.log(message_other.value, "value");}if (message.type === "1") {UserData.value = message.users.filter((item) => {return item.username !== CurrentUserName.value.username;});//表示进入聊天室ElMessage({message: message.message,type: "success",});//将该用户加入}if (message.type === 0) {//表示退出聊天室ElMessage(message.message);}
};
//监听websocket错误
socket.onerror = (err) => {console.log("Websocket错误:", err);
};

Nodejs服务器端

实现步骤

  • 下面是服务器端的websocket实现步骤:
  1. 创建 WebSocket 服务器
    使用 WebSocket.Server 创建了一个 WebSocket 服务器,绑定到一个 HTTP 服务器(HttpServer)上。
//创建Websocket服务器
const wss = new WebSocket.Server({ server: HttpServer });
  1. 存储连接的客户端
    使用一个 Map 对象(clients)来存储当前连接的客户端及其用户信息。
    Map 的键是WebSocket连接对象(ws),值是用户信息(userInfo)。
//存储连接的客户端
const clients = new Map();
  1. 判断用户是否已存在
    通过函数isSaveInClients,用于检查某个用户名是否已经存在于 clients 中。通过将 clients 的值转换为数组并检查是否包含指定的用户名来实现。
//判断用户是否存在于clients中
const isSaveInClients = (username)=>{const clientsArray = [...clients.values()];if(clientsArray.includes(`username:${username}`)){return true}return false
}
  1. 客户端连接处理
    当客户端通过 WebSocket 连接时,触发 connection 事件。
    每个连接的客户端都会绑定以下事件监听器:
    • 接收消息
      监听 message 事件,处理客户端发送的消息。
      消息以 JSON 格式传输,包含 type 和 data 字段。
      用户信息(type: “userInfo”):
      如果客户端发送的是用户信息,服务器会检查该用户是否已经存在于 clients 中。
      如果用户不存在,则将用户信息存储到 clients 中,并广播一条消息给所有客户端,通知新用户已进入聊天室。

    • 聊天信息(type: "message")
      如果客户端发送的是聊天信息,服务器会查找目标用户(data.toName)。
      如果找到目标用户且其 WebSocket 连接处于打开状态,则将聊天信息发送给目标用户。

wss.on("connection", (ws) => {var userInfo = {};//监听websocket连接ws.on("message", (message) => {const data = JSON.parse(message);//如果传入的是用户信息if (data.type == "userInfo") {//判断该用户是否已经在clients中if(isSaveInClients(data.data.username)){return}// console.log("进来了");//如果客户端发送来的是用户信息userInfo = data.data;clients.set(ws, userInfo);// console.log(clients.values(), "clients");//将map转为arrayconst clientsArray = [...clients.values()];//广播消息给所有客户端for (const [clientWs, clientUserInfo] of clients) {//给其他用户广播有新用户登录if (clientWs.readyState === WebSocket.OPEN) {// console.log("进来了广播");clientWs.send(JSON.stringify({type: "1",user: {username: userInfo.username,isOnline: true,},message: `${userInfo.username}进入聊天室`,users: clientsArray,}));}}}//如果传入的是聊天信息if(data.type === "message"){// console.log("传入的是聊天信息"); console.log(data,"聊天信息");for(const [clientWs,clientUserInfo] of clients){if(clientUserInfo.username === data.toName && clientWs.readyState === WebSocket.OPEN){clientWs.send(JSON.stringify({type:"message",message:data.message}))}}}//  console.log("收到消息", data);});
  1. 客户端断开连接
    监听 close 事件,处理客户端断开连接的情况。
    当客户端断开连接时,服务器会广播一条消息给所有客户端,通知该用户已退出聊天室。
    然后从 clients 中删除该客户端的记录。
  //监听客户端断开连接ws.on("close", () => {// console.log(username +"退出聊天室");//退出聊天室广播for (const [clientWs,clientUserInfo] of clients) {if (clientWs.readyState === WebSocket.OPEN) {clientWs.send(JSON.stringify({type: "0",user: {username: userInfo.username,isOnline: true,},message: `${userInfo.username}退出聊天室`,}));}}clients.delete(ws);});
});
  1. 启动服务器
    定义了一个端口号 PORT(3000),并启动 HTTP 服务器。
    服务器运行后,会监听指定端口,并在控制台输出服务器的地址。
//启动服务器
const PORT = 3000;
HttpServer.listen(PORT, () => {console.log(`服务器运行在 http://localhost:${PORT}`);
});

代码

//创建Websocket服务器
const wss = new WebSocket.Server({ server: HttpServer });
//存储连接的客户端
const clients = new Map();
//判断用户是否存在于clients中
const isSaveInClients = (username)=>{const clientsArray = [...clients.values()];if(clientsArray.includes(`username:${username}`)){return true}return false
}//客户端连接
wss.on("connection", (ws) => {var userInfo = {};//监听websocket连接ws.on("message", (message) => {const data = JSON.parse(message);//如果传入的是用户信息if (data.type == "userInfo") {//判断该用户是否已经在clients中if(isSaveInClients(data.data.username)){return}// console.log("进来了");//如果客户端发送来的是用户信息userInfo = data.data;clients.set(ws, userInfo);// console.log(clients.values(), "clients");//将map转为arrayconst clientsArray = [...clients.values()];//广播消息给所有客户端for (const [clientWs, clientUserInfo] of clients) {//给其他用户广播有新用户登录if (clientWs.readyState === WebSocket.OPEN) {// console.log("进来了广播");clientWs.send(JSON.stringify({type: "1",user: {username: userInfo.username,isOnline: true,},message: `${userInfo.username}进入聊天室`,users: clientsArray,}));}}}//如果传入的是聊天信息if(data.type === "message"){// console.log("传入的是聊天信息"); console.log(data,"聊天信息");for(const [clientWs,clientUserInfo] of clients){if(clientUserInfo.username === data.toName && clientWs.readyState === WebSocket.OPEN){clientWs.send(JSON.stringify({type:"message",message:data.message}))}}}//  console.log("收到消息", data);});//监听客户端断开连接ws.on("close", () => {// console.log(username +"退出聊天室");//退出聊天室广播for (const [clientWs,clientUserInfo] of clients) {if (clientWs.readyState === WebSocket.OPEN) {clientWs.send(JSON.stringify({type: "0",user: {username: userInfo.username,isOnline: true,},message: `${userInfo.username}退出聊天室`,}));}}clients.delete(ws);});
});//启动服务器
const PORT = 3000;
HttpServer.listen(PORT, () => {console.log(`服务器运行在 http://localhost:${PORT}`);
});

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/74903.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【免费】2005-2019年各地级市绿色专利申请量数据

2005-2019年各地级市绿色专利申请量数据 1、时间2005-2019年 2、来源&#xff1a;国家知识产权局 3、指标&#xff1a;省份、城市、年份、绿色发明专利申请量、绿色实用新型专利申请量 4、范围&#xff1a;360地级市 5、指标解释&#xff1a;绿色专利是指涉及环保、新能源…

架构师面试(二十六):系统拆分

问题 今天我们聊电商系统实际业务场景的问题&#xff0c;考查对业务系统问题的分析能力、解决问题的能力和对系统长期发展的整体规划能力。 一电商平台在早期阶段业务发展迅速&#xff0c;DAU在 10W&#xff1b;整个电商系统按水平分层架构进行设计&#xff0c;包括【入口网关…

2. Qt界面文件原理

本节主要介绍ui文件如何与窗口关联&#xff0c;并通过隐式连接方式显示对话框 本文部分ppt、视频截图原链接&#xff1a;[萌马工作室的个人空间-萌马工作室个人主页-哔哩哔哩视频] 1 UI文件如何与窗口关联 1.1 mainwindow.cpp的头文件ui_mainwindow.h 根据编译原理的基本规…

雅思大作文写作——词伙、简单句、并列句的使用

词伙是一些可以表达我们常用观点的单词组合,这个组合可能不只是2-3个单词,也可能是很多单词组成的一个短句。 一、词伙使用 1. 不要中译英 2. 重视词伙,而非单词 如何替换表达 1. 如果要替换的是一个名词,如students,则有下面的一些方法: A. 使用替换词或者词组:y…

⭐算法OJ⭐滑动窗口最大值【双端队列(deque)】Sliding Window Maximum

文章目录 双端队列(deque)详解基本特性常用操作1. 构造和初始化2. 元素访问3. 修改操作4. 容量操作 性能特点时间复杂度&#xff1a;空间复杂度&#xff1a; 滑动窗口最大值题目描述方法思路解决代码 双端队列(deque)详解 双端队列(deque&#xff0c;全称double-ended queue)是…

电机的了解到调试全方面讲解

一、什么是电机 电机是一种将电能转换为机械能的装置,通常由定子、转子和电磁场组成。 当电流通过电机的绕组时,产生的磁场会与电机中的磁场相互作用,从而使电机产生旋转运动。电机广泛应用于各种机械设备和工业生产中,是现代社会不可或缺的重要设备之一。 常见的电机种…

分布式微服务系统架构第97集:JVM底层原理

加群联系作者vx&#xff1a;xiaoda0423 仓库地址&#xff1a;https://webvueblog.github.io/JavaPlusDoc/ https://1024bat.cn/ JVM 内存结构 Java 虚拟机的内存空间分为 5 个部分&#xff1a; 程序计数器 Java 虚拟机栈 本地方法栈 堆 方法区 JDK 1.8 同 JDK 1.7 比&…

制定大运维管理体系的标准、流程、机制、规范

规划并制定大运维管理体系的标准、流程、机制、规范&#xff0c;对于确保平台的可用性和稳定性至关重要。这一过程涉及从顶层设计到具体执行的全面考量&#xff0c;需要综合考虑业务需求、技术架构、团队能力等多方面因素。以下是一个基本框架&#xff0c;用于指导如何构建有效…

TruPlasma RF 3006 软件TRUMPF HUETTINGER TRUPLASMA RF 3006 调试监控软件

TruPlasma RF 3006 软件TRUMPF HUETTINGER TRUPLASMA RF 3006 调试监控软件

第16届蓝桥杯单片机模拟试题Ⅱ

试题 代码 sys.h #ifndef __SYS_H__ #define __SYS_H__#include <STC15F2K60S2.H> //ds1302.c extern unsigned char time[3]; void w_ds1302(); void r_ds1302(); //iic.c float v_adc(unsigned char addr); //sys.c extern float light_v; extern float rb2_v; exte…

清华《数据挖掘算法与应用》FP-Growth算法

【例 8.7】实现FP 树算法,并对模拟数据集 simpDat挖掘频繁项集,最小支持度为2,绘制 FP树并输出频繁项集。 运行结果&#xff1a; 声明&#xff1a;著作权归作者所有。商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处。 # -*- coding: utf-8 -*- ""&q…

npm 项目命名规则

以下是 npm 项目命名规则的详细说明&#xff1a; 一、核心命名规则 必须使用小写字母 名称中不能包含大写字母。原因&#xff1a; 跨平台兼容性&#xff08;如 Linux 区分大小写&#xff0c;而 Windows 不区分&#xff09;。避免命令行和 URL 中的大小写冲突&#xff08;例如包…

Ubertool 的详细介绍、安装指南及使用说明

Ubertool&#xff1a;多协议网络分析与调试平台 一、Ubertool 简介 Ubertool 是一款开源的 多协议网络分析工具&#xff0c;专为物联网&#xff08;IoT&#xff09;、嵌入式系统和工业自动化领域设计。它支持蓝牙、Wi-Fi、LoRa、CAN总线等多种通信协议的实时监控、数据包捕获…

AI重构农业:从“面朝黄土“到“数字原野“的产业跃迁—读中共中央 国务院印发《加快建设农业强国规划(2024-2035年)》

在东北黑土地的万亩良田上&#xff0c;无人机编队正在执行精准施肥作业&#xff1b;在山东寿光的智慧大棚里&#xff0c;传感器网络实时调控着番茄生长的微环境&#xff1b;在云南的咖啡种植园中&#xff0c;区块链溯源系统记录着每粒咖啡豆的旅程。这场静默的农业革命&#xf…

FogFL: Fog-Assisted Federated Learning for Resource-Constrained IoT Devices

摘要 提示&#xff1a;这里可以添加系列文章的所有文章的目录&#xff0c;目录需要自己手动添加 -在本文中&#xff0c;我们提出了一个支持雾的联邦学习框架–FogFL–来促进资源受限的物联网环境中延迟敏感应用的分布式学习。联邦学习&#xff08;FL&#xff09;是一种流行的分…

linux下编译Websocketpp,适用x86和armv8

编译boost库 下载源文件&#xff1a;Version 1.79.0 编译&#xff1a; sudo ./bootstrap.sh sudo ./b2 install 安装websocketpp git clone https://github.com/zaphoyd/websocketpp.git cd websocketpp #进入目录 mkdir build cd build cmake .. make sudo make ins…

Linux学习笔记——零基础详解:什么是Bootloader?U-Boot启动流程全解析!

零基础详解&#xff1a;什么是Bootloader&#xff1f;U-Boot启动流程全解析&#xff01; 一、什么是Bootloader&#xff1f;&#x1f4cc; 举个例子&#xff1a; 二、U-Boot 是什么&#xff1f;三、U-Boot启动过程&#xff1a;分为两个阶段&#x1f539; 第一阶段&#xff08;汇…

Word 页眉设置(不同章节不同页眉)

需求分析 要给文档设置页眉&#xff0c;但是要不同的页眉不同的页眉 问题点&#xff1a;一旦设置页眉 每个页眉都是一样的 现在要设置不一样的 设置了页眉但是整个文章的页眉都一样 问题解决 取消链接 前一节&#xff08;不和前面的页眉同步更新&#xff09; 小结 不同的…

Debezium日常分享系列之:Debezium3.1版本之增量快照

Debezium日常分享系列之&#xff1a;Debezium3.1版本之增量快照 按需快照触发一次临时增量快照触发临时阻塞快照增量快照增量快照过程如何 Debezium 解决具有相同主键的记录之间的冲突快照窗口触发增量快照使用附加条件运行临时增量快照使用 Kafka 信号通道触发增量快照临时增量…

音视频开发从入门到精通:编解码、流媒体协议与FFmpeg实战指南

音视频开发从入门到精通&#xff1a;编解码、流媒体协议与FFmpeg实战指南 音视频技术作为数字媒体领域的核心&#xff0c;正在成为互联网和移动应用的重要组成部分。本文将全面介绍音视频开发的学习路径&#xff0c;从基础概念到高级应用&#xff0c;从编解码原理到实战案例&a…