首页 > 其他分享 >WebSocket的应用:前后端详解与使用

WebSocket的应用:前后端详解与使用

时间:2024-11-19 15:44:40浏览次数:3  
标签:websocket WebSocket 解与 端详 value user const message id

一、简介

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函数将用户标记为离线状态。

  1. 连接关闭处理
    当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();
}

它首先通过findIndexonlineList中查找用户的索引。当用户上线(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.发送消息函数(sendMessagewsSend
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初始化与重连(initWebsocketreConnect
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);
  }
};

在初始化过程中,先设置isConnectingtrue,如果之前存在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;
  }
};

根据获取结果更新messageListcanLoadMore等响应式数据。
- 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连接并设置hasLoadedtrue

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

相关文章

  • aiortc && WebSocket and django-channels
    aiortchttps://github.com/aiortc/aiortc/tree/mainWebRTCandORTCimplementationforPythonusingasyncioWhatisaiortc?aiortcisalibraryforWebReal-TimeCommunication(WebRTC)andObjectReal-TimeCommunication(ORTC)inPython.Itisbuilton......
  • @Transactional事务注解与函数内多线程并发编程出现的问题
    @Transactional当@Transactional注解写在函数上之后,就表示这个函数开启了事务。事务是基于数据库连接的connect。parallelStream这是针对List进行多线程Stream的操作。//对list集合开启多线程操作list.parallelStream().forEach(item->{//业务代码})@Transactional和pa......
  • ECharts饼图-饼图33,附视频讲解与代码下载
    引言: 在数据可视化的世界里,ECharts凭借其丰富的图表类型和强大的配置能力,成为了众多开发者的首选。今天,我将带大家一起实现一个饼图图表,通过该图表我们可以直观地展示和分析数据。此外,我还将提供详细的视频讲解和代码下载链接,帮助大家快速上手。一、图表效果预览二、视频......
  • 缓存与数据库不一致的解决方案:深入理解与实践
    目录前言缓存与数据库不一致的原因缓存与数据库交互的基本策略常见的缓存与数据库不一致解决方案方案一:读写穿透模式方案二:Cache-Aside模式方案三:先删除缓存,再更新数据库方案四:先更新数据库,再删除缓存方案五:异步更新缓存数据不一致的经典场景与应对策略总结前言在分......
  • 深入浅出:Java 中的经典排序算法详解与实现
    文章目录1.冒泡排序(BubbleSort)基本思路详细步骤Java实现2.插入排序(InsertionSort)基本思路详细步骤Java实现3.选择排序(SelectionSort)基本思路详细步骤Java实现4.快速排序(QuickSort)基本思路详细步骤Java实现5.归并排序(MergeSort)基本思路......
  • CSS复合选择器详解与应用指南
    CSS复合选择器详解与应用指南CSS复合选择器是建立在基础选择器之上,对基本选择器进行组合形成的,可以更准确、更高效地选择目标元素(标签)。以下是CSS复合选择器所有重要的基础知识点:一、复合选择器的类型1.后代选择器:又称为包含选择器,可以选择父元素里面的子元素。其写法......
  • nginx代理minio的websocket问题,求解决!!!
    1.问题描述nginx代理minio集群,web控制台浏览buckets下的列表一直在loading...,不使用nginx代理地址,通过minio集群直接访问是没有问题的从报错来看是websocket连接失败问题,按照官方文档以及其他博主的方式都配置了,仍未解决!!!!!  2.minio镜像版本:minio/minio:RELEASE.2024-11......
  • SQL 中的 WHERE 与 HAVING 子句:深入理解与最佳实践
    SQL中的WHERE与HAVING子句:深入理解与最佳实践在SQL查询中,WHERE和HAVING子句是用于过滤查询结果的两个关键子句。尽管它们看起来相似,但它们的使用场景和作用有所不同。本文将深入探讨WHERE和HAVING子句的区别,并通过具体示例说明其应用场景和最佳实践。1.表结构与......
  • 使用Websocket构建小型的IM通讯程序
    目录目录使用PWA构建小型的IM通讯程序简介项目地址界面预览技术栈服务端支持的协议前端总结使用PWA构建小型的IM通讯程序简介使用Websocket、MQTT协议构建小型的IM通信程序,可用于物联网或及时通信等,通过PWA技术,安装到用户手机桌面,提高用户整体交互感。项目地......
  • SQL NULL 值处理:深入理解与最佳实践
    SQLNULL值处理:深入理解与最佳实践在SQL数据库中,NULL是一个特殊的标记,用于表示“未知”或“不存在”的值。它与空字符串('')和零(0)有本质区别,具有独特的运算规则和处理机制。本文将深入探讨NULL值的处理规则,并通过具体示例说明其影响,最后提供最佳实践建议。1.NULL值基础概......