第三章 双端的消息处理
前言
此文章来源于我的CSDN。
前面一篇介绍了我们定义的数据字段格式,这一章介绍数据的传输处理和实现聊天效果用到的方法。
源代码都在 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);
})
效果图如下
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.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,也讲解了实现流程的代码,核心点如下:
- 前端连接时带上参数信息以便后端校验和进入房间
- 连接成功和断开连接都需要监听并给房间发送通知
- 用户的连续消息可以只展示第一次的头像信息
- 用户发送消息之后后端需要回调结果
当然我们还有很多没有完善的功能BUG,例如
- 用户在房间断开后重连导致socketID重新生成的问题
- 如果使用人数多了,负载能力的提升,可考虑使用
mqtt
等工具
最后,所有的代码都在 Github地址 ,如果觉得写的还可以的话可以点个标星哦,其他功能目前还在持续开发中。
© 版权声明
文章版权归作者所有,未经允许请勿转载,侵权请联系 admin@trc20.tw 删除。
THE END