Vue+Koa2+websocket实现简单的消息中心

前一段时间在写管理系统的时候研究了一下websocket,期间基于Koa2和websocket实现了一个简易的消息中心的功能,这篇文章将介绍如何基于koa2来搭建websocket服务后端,以及前端消息中心的界面开发。

WebSocket服务搭建

Websocket 是一种自然的全双工、双向、单套接字连接,是建立在 TCP 协议上的。相比于 HTTP 协议,websocket 连接一旦建立,即可进行双向的实时通信。

首先,koa搭建websocket服务,nodejs中,使用最广泛的websocket模块就是ws,因此首先在koa2工程中安装ws模块👇:

npm install ws --save

Websocket与Http是两套服务,虽然都集成在Koa框架里面,但它们实际上各自独立,想要Websocket与Http共用一个端口,这样启动和销毁/重启这些操作都只需要控制一处,那么就需要对app.js进行改造。

首先在utils目录下新建websocket.js文件👇:

const WebSocketApi = (wss, app) => {
  wss.on('connection', (ws, req) => {
    console.log('连接成功')
  }
}

module.exports = WebSocketApi

然后将websocket.js文件引入到app.js,并做出相应的改造,使websocket与http共用一个端口👇:

const Koa = require('koa');
const http = require('http');
const WebSocket = require('ws');
const WebSocketApi = require('../utils/websokect');

const port = normalizePort(process.env.PORT || '3000');
const app = new Koa();

const server = http.createServer(app.callback());
const wss = new WebSocket.Server({ server });

WebSocketApi(wss, app);

server.listen(port);

此时运行koa2工程后,在浏览器控制台输入👇:

var ws = new WebSocket('ws://localhost:3000')

可以看到已经创建了一个websocket连接,并且其中readyState=1表示连接成功。

连接对象维护

前面我们已经搭建好了websocket服务,客户端能正常的创建一个websocket连接,但是我们知道消息中心是存在多个客户端之间的数据传递的,那么我们搭建的websocket服务器如何作为信令服务器,在大量客户端连接到服务器时正确找到需要通信的两方并传递数据呢?

首先我们需要将所有的客户端websocket连接进行分类,所有的连接分为发起方和接收方两种,另外,维护发起方与接收方之间的联系需要Koa2应用创建两个全局变量,cusSender和cusReader两个数组,分别保存所有的发起方连接和接收方连接。

app.context下添加cusSender和cusReader数组👇

var server = http.createServer(app.callback());
app.context.cusSender = [];
app.context.cusReader = [];
const wss = new WebSocket.Server({ server });

cusSender和cusReader数组保存所有的ws实例,后续所有的连接查找和状态维护都是在这两个数组下面操作。

我们将ws连接进行分类后,还需要对每个类别进行标识,这样每一个ws连接就具有了唯一标识,根据这个唯一ID标识,就可以进行一对一或一对多的通信。

发起方连接我们传入两个参数,分别表示角色role以及唯一标识roomId,接收方连接我们传入两个参数,分别表示角色role和唯一标识userId。

发起方与接收方连接如下👇

socketA = new WebSocket('ws://localhost:3000/sender/1');
socketB = new WebSocket('ws://localhost:3000/reader/2');

针对客户端传入的连接参数,服务端接收消息后根据角色将连接实例分别保存到全局变量cusSender和cusReader数组中👇

const WebSocketApi = (wss, app) => {
  wss.on('connection', (ws, req) => {
    let { url } = req // url 的值是 /$role/$uniId
    let { cusSender, cusReader } = app.context;
    if (!url.includes('sender') || !url.includes('reader')) {
      return ws.clode() // 关闭 url 前缀不是 /sender或/reader 的连接
    }
    let [ role, uniId] = url.slice(1).split('/')
    if(!uniId) {
      console.log('缺少参数')
      return ws.clode()
    }
    console.log('已连接客户端数量:', wss.clients.size)
    // 判断如果是发起端连接
    if (role == 'sender') {
      // 此时 uniId 就是 roomid
      ws.roomid = uniId
      let index = (cusSender = cusSender || []).findIndex(
        row => row.roomid == ws.roomid
      )
      // 判断是否已有该发送端,如果有则更新,没有则添加
      if (index >= 0) {
        cusSender[index] = ws
      } else {
        cusSender.push(ws)
      }
      app.context.cusSender = [...cusSender]
    }
    if (role == 'reader') {
      // 接收端连接
      ws.userid = uniId
      let index = (cusReader = cusReader || []).findIndex(
        row => row.userid == ws.userid
      )
      if (index >= 0) {
        cusReader[index] = ws
      } else {
        cusReader.push(ws)
      }
      app.context.cusReader = [...cusReader]
    }
    ws.on('close', () => {
      if (role == 'sender') {
        // 清除发起端
        let index = app.context.cusSender.findIndex(row => row == ws)
        app.context.cusSender.splice(index, 1)
        // 解绑接收端
        if (app.context.cusReader && app.context.cusReader.length > 0) {
          app.context.cusReader
            .filter(row => row.roomid == ws.roomid)
            .forEach((row, ind) => {
              app.context.cusReader[ind].roomid = null
              row.send('leaveline')
            })
        }
      }
      if (role == 'reader') {
        // 接收端关闭逻辑
        let index = app.context.cusReader.findIndex(row => row == ws)
        if (index >= 0) {
          app.context.cusReader.splice(index, 1)
        }
      }
    });
 })
}
module.exports = WebSocketApi

至此,客户端ws连接对象的维护已经完成,接下来就是发起方与接收方的消息通信的实现了。

消息发送与接收

上一步我们在koa2后端创建了两个数组,分别保存了发起方与接收方的所有ws连接实例,那么我们客户端的消息发送方在发送数据时,指定消息接收方唯一标识,这样就可以保证发送与接收双方的消息传递。

发送方消息发送时添加接收方的userId参数👇

const message = 'hello!'
socketA.send(`message|2|${message}`); // 发送信息

服务端添加处理逻辑,进行消息转发👇

const WebSocketApi = (wss, app) => {
  wss.on('connection', (ws, req) => {
    ...
   ws.on('message', msg => {
      if (typeof msg != 'string') {
        msg = msg.toString()
        // return console.log('类型异常:', typeof msg)
      }
      let { cusSender, cusReader } = app.context;
      eventHandel(msg, ws, role, cusSender, cusReader);
    })
 })
}

const eventHandel = (message, ws, role, cusSender, cusReader) => {
  if (role == 'reader') {
    let arrval = message.split('|')
    let [type, roomid, val] = arrval;
    console.log(type, roomid, val);
    if (type == 'message') {
      let seader = cusSender.find(row => row.roomid == roomid)
      if (seader) {
        console.log(message, type, val);
        seader.send(`${type}|${val}`)
      }
    }
  }
  if (role == 'sender') {
    let arrval = message.split('|')
    let [type, userid, val] = arrval
    // 注意:这里的 type, userid, val 都是通用值,不管传啥,都会原样传给 reader
    if (type == 'message') {
      console.log(cusReader,userid);
      let reader = cusReader.find(row => row.userid == userid)
      console.log(reader);
      if (reader) {
        console.log(message, type, val)
        reader.send(`${type}|${val}`)
      }
    }
  }
}

module.exports = WebSocketApi

接收方订阅消息👇

socketB.onmessage = evt => {
  let string = evt.data
  let value = string.split('|')
  if (value[0] == 'message') {
    let message = value[1];
    console.log(message);
  }
}

发送方与接收方的发送消息与订阅消息的逻辑一致,至此客户端之间消息通信已全部完成。

消息中心组件

消息中心组件MessageCenter.vue主要包括消息同步显示功能和消息发送功能,消息显示组件为MessageItem.vue,组件保存的消息列表包括发送的消息与接收到的消息,类似微信的聊天,发送的消息展示在左侧,接收的消息展示在右侧,MessageItem.vue组件代码如下👇

<template>
  <div class="message-wrapper">
    <div class="sender" v-if="props.role === 'sender'">
      <span class="avatar">
        <img :src="url"/>
      </span>

      <div class="content">
        <div class="triangle"></div>
        <p>{{ props.content }}</p>
      </div>

    </div>
    <div class="reciever" v-if="props.role === 'reciever'">
      <div class="content">
        <div class="triangle"></div>
        <p>{{ props.content }}</p>
      </div>
      <span class="avatar">
        <img :src="url"/>
      </span>
    </div>
  </div>
</template>

<script lang="ts" setup>
import { ref, defineProps} from 'vue';
const props = defineProps<{
  userid: 1,
  role: '',
  content: ''
}>();

// 暂时没有获取头像的功能,静态图片显示
const url = props.userid === 1 ? 'assets/images/avatar.jpg' : 'assets/images/bg.jpg';
</script>

<style lang="less" scoped>
.message-wrapper {
  display: flow-root;
  margin-top: 10px;
  width: 100%;
  .sender {
    // float: left;
    display: flex;
    .avatar {
      width: 40px;
      height: 40px;
      margin-left: 10px;
      img {
        height: 40px;
        width: 40px;
        border-radius: 4px;
      }
    }
    .content {
      margin-left: 20px;
      padding-right: 70px;
      position: relative;
      .triangle {
        width: 0;
        height: 0;
        border: 8px solid;
        border-left-color: transparent;
        border-top-color: transparent;
        border-right-color: white;
        border-bottom-color: transparent;
        position: absolute;
        top: 5px;
        left: -16px;
      }
      p {
        background: white;
        font-size: 18px;
        padding: 5px 10px;
        border-radius: 5px;
        word-wrap: break-word;
        word-break: break-all;
      }
    }
  }
  .reciever {
    float: right;
    display: flex;
    .avatar {
      margin-left: 20px;
      margin-right: 10px;
      width: 40px;
      height: 40px;
      img {
        height: 40px;
        width: 40px;
        border-radius: 4px;
      }
    }
    .content {
      position: relative;
      padding-left: 70px;
      .triangle {
        width: 0;
        height: 0;
        border: 8px solid;
        border-left-color: yellowgreen;
        border-top-color: transparent;
        border-right-color: transparent;
        border-bottom-color: transparent;
        position: absolute;
        top: 5px;
        right: -16px;
      }
      p {
        background: yellowgreen;
        font-size: 18px;
        padding: 5px 10px;
        border-radius: 5px;
        word-wrap: break-word;
        word-break: break-all;
      }
    }
  }
}
</style>

MessageCenter.vue组件的消息发送与功能👇

let value1 = ref('');
let value2 = ref('');
const messageList1 = ref(new Array<MessageItem>());
const messageList2 = ref(new Array<MessageItem>());

const socketA = new WebSocket('ws://localhost:3000/sender/1');
const socketB = new WebSocket('ws://localhost:3000/reader/2');

// 订阅消息
const onMessage = () => {
  socketA.onmessage = evt => {
    let string = evt.data
    let value = string.split('|')
    if (value[0] == 'message') {
      let message = value[1];
      console.log(message, 'value1');
      messageList1.value.push({
        userid: 2,
        role: 'reciever',
        constent: message
      });
    }
  };
  socketB.onmessage = evt => {
    let string = evt.data
    let value = string.split('|')
    if (value[0] == 'message') {
      let message = value[1];
      console.log(message, 'value2');
      messageList2.value.push({
        userid: 1,
        role: 'reciever',
        constent: message
      });
    }
  }
};

const onMessageSend1 = () => {
  messageList1.value.push({
    userid: 1,
    role: 'sender',
    constent: value1.value
  });
  socketA.send(`message|2|${value1.value}`); // 发送信息
  value1.value = ''; // 发送后清空输入框
}

const onMessageSend2 = () => {
  messageList2.value.push({
    userid: 2,
    role: 'sender',
    constent: value2.value
  });
  socketB.send(`message|1|${value2.value}`); // 发送信息
  value2.value = ''; // 发送后清空输入框
}
...

消息中心的界面实现如下👇

图片

如上便是基于websocket的消息中心功能,支持一对多的长连接消息通信,后续的功能进行完善后也可以应用到很多其他需要文字聊天的场景中。

作者:jayray 来源:公众号—— 玩心大胆子小

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(0)

相关推荐

发表回复

登录后才能评论