从零开始使用Socket.IO加Node仿Discord聊天室【第三篇】

第三章 双端的消息处理

前言

前面一篇介绍了我们定义的数据字段格式,这一章介绍数据的传输处理和实现聊天效果用到的方法。
源代码都在 Github地址


一、流程图

在第一章我介绍过我们的设计流程图,我们就从流程图着手,看看代码的实现。

流程图

二、数据处理

1. 创建订阅流

使用观察者模式传递webSocket数据,代码更好维护

/**
 * 订阅流
 */
export class MessageService {
  private _messages = new Subject<any>();

  // 获取
  get messages(): Observable<any> {
    return this._messages.asObservable();
  }

  // 发射消息
  sendMessage(message: any): void {
    this._messages.next(message);
  }

  // 关闭订阅
  close(): void {
    this._messages.complete();
  }
}

2. 连接处理

根据官网的API,可以在opt参数的extraHeaders内自定义字段,建议放用户的信息
前端连接参数

const opt = {
  extraHeaders: {
    // 角色
    role: SessionUtil.getRoleId(),
    // token
    token: SessionUtil.getToken().tokenValue
  }
};
this.socketIo = io(`wss://url`, opt);

后端获取自定义数据

// token
const token: string | any = socket.handshake.headers.token;
if (token) {
	/***/
} else {
	/***/
}

根据传过来的token获取当前用户
进入房间
连接成功的用户自动进入未满的房间

/**
     * 添加房间
     * @param roomName 房间名
     * @param user 用户信息
     * @param socketId 用户socketId
     */
    joinRoom(roomName: string, user: any, socketId: string): Promise<ChatChannelRoomInterface> {
        return new Promise((resolve) => {
            for (let i = 0; i < this.roomsState.length; i++) {
                // 判断该用户是否已经在房间内
                const findIndex = this.roomsState[i].users.findIndex(item => item.id === user.id);
                if (findIndex >= 0) {
                    // 重新赋值socketId
                    this.roomsState[i].users[findIndex].socketId = socketId;
                    // 返回加入的房间
                    return resolve(this.roomsState[i]);
                } else {
                    // 新加入没满的房间
                    if (this.roomsState[i].users.length < ROOM_MAX_CAPACITY) {
                        this.roomsState[i].users.push({
                            socketId,
                            id: user.id,
                            userName: user.username,
                            avatar: user.avatar, // 头像
                            remarks: user.remarks, // 备注
                            role: user.role,
                            roleName: user.roleName
                        });
                        // 返回加入的房间
                        return resolve(this.roomsState[i]);
                    }
                }
            }
            // 新增房间
            // const roomId = uuidv4();
            const roomId: string = '8808';
            console.log('新增房间', {id: user.id, userName: user.username});
            const room: ChatChannelRoomInterface = {
                roomId,
                roomName: `${roomName}${this.roomsState.length + 1}`,
                users: [{
                    socketId,
                    id: user.id,
                    userName: user.username,
                    avatar: user.avatar, // 头像
                    remarks: user.remarks, // 备注
                    role: user.role,
                    roleName: user.roleName
                }],
            };
            // 加入房间
            this.roomsState.push(room);
            // 返回加入的房间
            return resolve(room);
        });
    }

获取房间roomID,并给该房间发送系统消息
通知当前房间用户某某进入房间
进入房间的字段systemStates定义为join

// 获取房间的信息
const room = joinRoom(...)
// 转发给客户端房间信息
socket.emit(ChatChannelsMessageTypeEnum.systemMessage, {systemStates: SystemMessagesEnum.roomInfo, ...room});
// 添加room
socket.join(room.roomId);
// 系统消息 发送给room房间
socket.to(room.roomId).emit(ChatChannelsMessageTypeEnum.systemMessage, {
    systemStates: SystemMessagesEnum.join,
    id: decode.id,
    socketId: socket.id,
    userName: user.username,
    avatar: user.avatar, // 头像
    remarks: user.remarks, // 备注
    role: user.role,
    roleName: user.roleName,
    timestamp: new Date().toISOString()
});

前端订阅系统消息并发射数据到组件

// 订阅系统消息
this.socketIo.on(ChatChannelsMessageTypeEnum.systemMessage, (msg) => {
  // console.log('系统消息', msg);
  const message: ChatChannelSubscribeInterface = {
    type: ChatChannelsMessageTypeEnum.systemMessage,
    msg
  };
  // 发射
  this.messages.sendMessage(message);
});

// 读取消息
this.messages.messages.subscribe((message: ChatChannelSubscribeInterface) => {
	// 赋值房间信息
	this.roomChannel = message.msg as ChatChannelRoomInterface;
	console.log('房间信息', this.roomChannel);
	// onlineUserList 为该房间的在线用户
	this.onlineUserList = this.roomChannel.users.map((item) => {
	  return item;
	});
	// messagesList为消息列表
	const join: any = {
     // 类型为系统消息
     type: this.chatMessagesType.system,
     systemStates: SystemMessagesEnum.join,
     userName: message.msg.userName,
     id: message.msg.id,
     socketId: message.msg.socketId,
     timestamp: message.msg.timestamp
   };
	this.messagesList.push(join);
})

效果图如下

效果图1
效果图2

3. 发送消息和退出房间

3.1 刷屏验证:

// 刷屏监听参数 3秒内连续发言超过5次则算刷屏
  continuousChat: { count: number, time: number, timer: any } = {
    count: 0,
    time: 3,
    timer: null
  };
 // 每次发消息 数量累加,3秒内达到上限则停止发送
   this.continuousChat.count += 1;
   // 如果没有定时器则开始定时
   if (!this.continuousChat.timer) {
     this.continuousChat.timer = setInterval(() => {
       // 判断3秒时间到,则重置消息次数
       if (this.continuousChat.time <= 0) {
         // 清除定时器
         clearInterval(this.continuousChat.timer);
         // 重置消息次数
         this.continuousChat.count = 0;
       }
       // 每秒钟-1
       this.continuousChat.time -= 1;
     }, 1000);
   }
   // 判断是否刷屏
   if (this.continuousChat.count > 5 && this.continuousChat.time > 0) {
     this.$message.info(`您的消息太频繁,请稍后${this.continuousChat.time}`);
     return;
   }

3.2 无意义的多次换行验证:


    // 判断空字符
    if (this.textValue === '') {
      return;
    }
    // 判断无意义的多段换行
    const split = this.textValue.split(`\n`);
    let count: number = 0;
    for (let i = 0; i < split.length; i++) {
      if (split[i] === '') {
        count += 1;
      }
    }
    /**
     * 如果只有多段换行   并且超过3条只显示3条
     * 注:换行符split之后的length会默认+2 所以判断要大于5
     */
    if (count === split.length && count > 5) {
      this.textValue = `\n\n\n`;
    }
    const message: ChatMessagesInterface = {
      // 附件
      attachments: [],
      // 消息发送者
      author: {
        // 头像
        avatar: this.userInfo.avatar,
        // 头像描述
        avatar_decoration: null,
        // 鉴别器
        discriminator: null,
        // 全局名称
        global_name: null,
        // id
        id: this.userInfo.id,
        // 公共标签
        public_flags: 0,
        // 用户名
        username: this.userInfo.userName,
      },
      // 频道id
      channel_id: CHANNEL_ID,
      // 组件
      components: [],
      // 消息内容
      content: this.textValue,
      // 编辑消息的时间
      edited_timestamp: null,
      // 嵌入
      embeds: [],
      // 标志
      flags: 0,
      // id
      id: this.userInfo.id,
      // 提及的人
      mention_everyone: this.message.mention_everyone || false,
      // 提及的角色
      mention_roles: this.message.mention_roles || [],
      // 提及的人名称信息
      mentions: this.message.mentions || null,
      // 留言参考
      message_reference: [],
      // 参考消息
      referenced_message: [],
      // 固定
      pinned: false,
      // 时间
      timestamp: new Date().toISOString(),
      tts: false,
      // 消息类型 用于前端展示判断
      type: ChatMessagesTypeEnum.general
    };

// 使用emit发送
this.socket.emit(ChatChannelsMessageTypeEnum.publicMessage, message, (response) => {
  if (response.status === ChatChannelsCallbackEnum.ok) {
    console.log('消息发送成功');
    message.states = ChatChannelsMessageStatesEnum.success;
    this.messagesList.push(this.isContinuous(message));
  } else {
    // todo 重发
    console.log('消息发送失败');
    message.states = ChatChannelsMessageStatesEnum.error;
    this.messagesList.push(this.isContinuous(message));
  }
});

消息发送之后,让服务端验证是否发送成功,如果成功则回调成功函数callback
前端判断callback返回的结果来定义消息是否发送成功,是否需要重发(目前功能还未做)

/**
* 接收房间消息
* roomMessage 为规定好的事件名称
*/
socket.on(ChatChannelsMessageTypeEnum.roomMessage, (parseMessage: ChatMessagesInterface, callback) => {
   try {
       console.log('房间消息', parseMessage);
       // 发送给room房间
       socket.to(parseMessage.channel_id).emit(ChatChannelsMessageTypeEnum.roomMessage, parseMessage);
       chatHistoryInformation.push(parseMessage);
       // 接收消息成功回调
       callback({
           status: ChatChannelsCallbackEnum.ok
       });
   } catch (e) {
       // 接收消息失败回调
       callback({
           status: ChatChannelsCallbackEnum.error
       });
   }
});

效果图如下
效果图3
效果图4
效果图5

3.3 退出房间

用户的断开连接需要后端监听并告知所在的房间
通知当前房间用户某某离开房间
离开房间的字段systemStates定义为left

/**
 * 连接断开
 */
socket.on('disconnect', () => {
    console.log('连接断开', socket.id);
    // 删除断开的房间用户
    const {userName, id} = new ChatChannelRoom(roomsList).leaveRoom(CHANNEL_ID, socket.id);
    // console.log('roomsList', roomsList[0]);
    const parseMessage = {
        systemStates: SystemMessagesEnum.left,
        userName,
        id,
        socketId: socket.id,
        timestamp: new Date().toISOString()
    };
    chatHistoryInformation.push(parseMessage);
    // 消息发送至房间ID
    socket.to(CHANNEL_ID + '').emit(ChatChannelsMessageTypeEnum.systemMessage, parseMessage);
});

在这里插入图片描述
在这里插入图片描述

总结

花费了三篇文章讲解了制作一个简单模仿discord界面的聊天社区,使用了Socket.IO,也讲解了实现流程的代码,核心点如下:

  1. 前端连接时带上参数信息以便后端校验和进入房间
  2. 连接成功和断开连接都需要监听并给房间发送通知
  3. 用户的连续消息可以只展示第一次的头像信息
  4. 用户发送消息之后后端需要回调结果

当然我们还有很多没有完善的功能BUG,例如

  1. 用户在房间断开后重连导致socketID重新生成的问题
  2. 如果使用人数多了,负载能力的提升,可考虑使用mqtt等工具

最后,所有的代码都在 Github地址 ,如果觉得写的还可以的话可以点个标星哦,其他功能目前还在持续开发中。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值