马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
说在前面
- 在学习盘算机网络的时间,看到了websocket这个网络通讯协议,它提供了在单个 TCP 毗连上举行全双工通讯的本领,也就是说不必要客户端哀求就可以主动向客户端发送相应,然后就发现可以用这个做一个谈天功能,于是就有了这篇博客。
- 下面的谈天室功能还用到了注册登录的干系知识,在我的另一篇博客中有写,链接:vue+nodejs实现登录注册功能
- 项目的源代码我也发到了github上面,各人可以去获取:github地点,点击跳转
效果展示
websocket
websocket的由来
- 在 WebSocket 出现之前,Web 应用重要依赖于 HTTP 协议举行通讯。然而,HTTP 是一种无状态的、哀求-相应式的协议,每次通讯都必要创建新的毗连,这导致了较高的延长和资源斲丧。为了实实际时通讯,开辟者通常会使用一些变通的方法,如轮询(Polling)或长轮询(Long Polling),但这些方法服从较低。
- 于是websocket应运而生,WebSocket 协议于 2011 年被正式标准化,旨在办理这些题目,提供一种更高效、更及时的通讯方式。
websocket的特点
- 全双工通讯:允许服务器和客户端同时发送和吸取消息,而无需等候对方的相应
- 低延长:一旦websocket毗连创建,数据就可以在客户端和服务器之间快速传输,无需每次都创建新的毗连
- 轻量级:websocket的数据帧格式简朴,头部开销较小,得当传输小数据包
vue前端
- 起首我们必要明确搭建一个如许谈天室的步调(以下是波煮本身写项目时间的序次,可参考)
- 先写出谈天室的静态结构,这里波煮是照着微信的界面画的;
- 思考用什么存储消息并举行渲染,波煮的脑筋想到的是对象数组,为什么要用对象数组呢?由于我们只用一个数组那么就必要区分这个消息是对方发送的还是本身发送的,从而就行差异的渲染;
- 然后就是与服务器的链接了,以及一些用户点击事故的绑定。
静态结构
- 下面的静态结构代码着实已经不静态了,这是绑定事故后的代码了。不外大要还是一样的,这里用到了Element-plus的container组件,
效果
代码
- <template>
- <div class="common-layout">
- <el-container>
- <el-aside class="aside" width="200px">
- <el-scrollbar :always="false">
- <!-- 侧边栏用户栏 -->
- <div
- v-for="(item, index) in UserData"
- :key="item.id"
- class="userItem"
- :class="isActive === index ? 'ItemActive' : 'ItemUsual'"
- @click="onChangeUserItem(index)"
- >
- <el-avatar
- style="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">
- <div
- v-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-input
- v-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>
复制代码 点击切换用户以及该用户高亮
实现思绪
- //当前用户
- const isActive = ref(0);
复制代码
- 通过isActive来进举措态绑定类名,从而实现差异的css样式,这个是vue的方便之处,可以直接使用:class来实现动态类名的绑定,这里高亮区分我是通过设置差异的配景颜色来区分的
- :class="isActive === index ? 'ItemActive' : 'ItemUsual'"
复制代码- .ItemActive {
- background-color: rgb(231, 233, 233);
- }
复制代码
- 绑定点击事故,获取当前用户的index,或修改isActive相应式数据为当前用户的index
- //切换用户回调函数
- const onChangeUserItem = (index) => {
- isActive.value = index;
- };
复制代码 效果展示
发送消息功能
- 我们先来理清团体思绪:起首点击发送时必要获取文本框的内容,并把这个内容添加到消息数组中,接着把输入框中的内容清空。
- 点击发送时获取文本框内容,先界说一个相应式数据与输入框举行双向绑定:
- //文本输入区域
- const textarea = ref("");
- //通过v-model双向绑定
- <el-input
- v-model="textarea"
- :rows="7"
- type="textarea"
- placeholder="请输入内容"
- class="InputArea"
- @keydown.enter.exact.prevent="onSubmit"
- />
复制代码
- 为发送按钮添加点击事故,并将信息以对象的情势添加到消息数组中
- //点击发送的回调函数
- 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的错误
- //创建websocket连接
- const socket = new WebSocket("http://localhost:3000");
复制代码
- 监听毗连,在这内里必要在刚开始毗连上服务器的时间向服务器发送该用户的信息,以便服务器向别的用户广播该用户的上线状态。
- 监听服务器发来的消息,这里也就是别人向你发送过来的消息,别人先将要对你发送的消息发送给服务器,然后再由服务器根据一些条件发送给你。 服务器就像一个中介一样,链接这你们两个
- 拿到服务器发送给你的消息并插入到消息列表中举行渲染
- //创建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.user if (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 服务器
使用 WebSocket.Server 创建了一个 WebSocket 服务器,绑定到一个 HTTP 服务器(HttpServer)上。
- //创建Websocket服务器
- const wss = new WebSocket.Server({ server: HttpServer });
复制代码
- 存储毗连的客户端
使用一个 Map 对象(clients)来存储当前毗连的客户端及其用户信息。
Map 的键是WebSocket毗连对象(ws),值是用户信息(userInfo)。
- //存储连接的客户端
- const clients = new Map();
复制代码
- 判定用户是否已存在
通过函数isSaveInClients,用于查抄某个用户名是否已经存在于 clients 中。通过将 clients 的值转换为数组并查抄是否包罗指定的用户名来实现。
- //判断用户是否存在于clients中
- const isSaveInClients = (username)=>{
- const clientsArray = [...clients.values()];
- if(clientsArray.includes(`username:${username}`)){
- return true
- }
- return false
- }
复制代码
- 客户端毗连处理惩罚
当客户端通过 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转为array
- const 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);
- });
复制代码
- 客户端断开毗连
监听 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);
- });
- });
复制代码
- 启动服务器
界说了一个端标语 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转为array
- const 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}`);
- });
复制代码 |