思路大纲
整体思路
前端使用全局变量来控制播放的音乐,无论是前后切换歌曲还是点播,都只是传递的内个全局音乐id而不是操作本身,调用方只需要去修改,当id变化,将触发websocket的同步逻辑,这应当使用监听实现。
业务模块
分为websocket模块,播放模块还有主体业务模块
- websocket模块主要就是前端进行指令发送,进行操作同步。
- 播放模块主要由前端完成,使用全局变量维持,页面的唯一性,就是打开多个页面,得到的数据都一样,如qq音乐,当操作一个播放页面,另一个页面的歌曲也会发生变化,使用的就是全局变量,相当于单例,前端保存的roomId也是同理。
- 主体业务模块主要是通过redis维护房间(set集合),邀请逻辑,生成邀请链接逻辑,创建房间,销毁房间,连接其余两个模块。
补充
在实现时,还有一些细节但重要的问题:
不能只存储用户,所以不能使用set,因为使用set无法分清谁是房主,虽然权力相同,但是歌单要用房主的。这个其实有些瑕疵,之后再进行修改。再播放器页面应该拥有一个接口供双方选择自己的歌单。
然后切换音乐的时候,使用歌单id和index进行切换,把两个变量全存储到全局变量里,可以解决现有所有问题。因为之前想法是通过musicid传递,但是缺陷太明显了,如果我现在的index为1,点击下一首,双方的音乐id都发生切换,但是index并没有发生切换,客人再点击上一首就切到了index为0的歌曲,这就导致比较糟糕的用户体验了。
websocket逻辑和http不同,ws需要考虑通信双方的需求和功能实现,因为他们往往一个ws功能要分成两个实现,一个发送者的发,一个接受者的接收,并且如果这两者身份不同,就会变成4个实现,两个发和两个收。举个例子,需要完成客人加入房间,主人可以实时显示出客人的登录(由于主人一定先于客人今日房间,所以客人不需要接收主人的加入通知),然后设置一个加入房间的报文,加入房间这个逻辑就不需要前端发送了,因为可以直接在服务端的onOpen里发送给主人的客户端,需要判断当前open的账号是不是主人,如果是则不执行(为什么直接使用onOpen呢,因为onOpen的触发节点就是连接开始,与加入房间触发时间一致),前端拥有一个全局变量是otherUser当这个值不存在时,才可以被赋值(减少无用的重复赋值)。这是ws逻辑,然后对其优化,先在redis里获取另一个用户的数据,如果有就赋值,可以解决很多未知的问题,比如当主人和客人同时刷新,redis里已经有客人加入了,但是主人刷新好的时候,客人已经刷新好了,导致客人客户端引起的服务端onOpen发送的消息并未成功发送,就需要客户端接入,还有客人的主人信息加载也要去redis里拿,因为当客人在房间内时,主人必在房间内。
实现逻辑
主体代码如下:
@OnMessage
public void onMessage(String message, @PathParam("account") String account, @PathParam("roomId") String roomId) {
// 0. 直接向房间号发送指令,然后对房间内所有人都执行相同操作
// 获取房间内的所有用户
SimpleTogetherMessage operationMessage = JSON.parseObject(message, ChangMusicMessage.class);
// 对报文进行解析
Integer type = operationMessage.getType();
ChangMusicOperationData data = (ChangMusicOperationData) operationMessage.getData();
if (type.equals(WebsocketConstants.OPERATION_CHANGE_MUSIC)) {
// 是切换音乐操作,进行音乐同步,redis进行存储,可以改用redis监听降低耦合,这里直接同步写了 todo
if (data.getMusicIndex() != null) redisTemplate.opsForHash().put(TOGETHER_ROOM_KEY + roomId, TOGETHER_HASH_CURRENT_MUSIC_INDEX_KEY, data.getMusicIndex());
if (data.getMusicSheet() != null) redisTemplate.opsForHash().put(TOGETHER_ROOM_KEY + roomId, TOGETHER_HASH_CURRENT_MUSIC_SHEET_KEY, data.getMusicSheet());
}
// 对房间内所有用户的客户端发出报文,报文不变
// 获取对应Session
Session session = getSuitableSession();
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
private Session getSuitableSession() {
if (isOwner) {
String visitorAccount = (String) redisTemplate.opsForHash().get(TOGETHER_ROOM_KEY + roomId, TOGETHER_HASH_VISITOR_KEY);
return onlineUsers.get(visitorAccount);
} else {
String ownerAccount = (String) redisTemplate.opsForHash().get(TOGETHER_ROOM_KEY + roomId, TOGETHER_HASH_OWNER_KEY);
return onlineUsers.get(ownerAccount);
}
}
主要就是向另一个用户发送切换音乐的报文,在客户端:
websocket.onmessage = function (event) {
const message = JSON.parse(event.data)
console.log(message)
// 开始进行处理
if (message.type === 0) {
// 这个是加入房间的报文,可以有个提示之类的
ElMessage.success('你的好友已加入房间!')
// 然后对otherUser进行修改
if (!useStore().otherUser.exist) {
useStore().otherUser = message.data
}
}
else if (message.type === 1) {
ElMessage.success('我接收到了')
useStore().togetherRoomInfo.togetherPlayMusicIndex = message.data.musicIndex
useStore().togetherRoomInfo.togetherPlayMusicSheet = message.data.musicSheet
}
ElMessage.info('收到消息对象:' + message)
console.log('收到消息对象:', message)
}
websocket.onerror = function (event) {
ElMessage.error('WebSocket出现错误:', event)
// 可以在这里考虑重新连接的策略,比如延迟一段时间后重新尝试连接
}
websocket.onclose = function (event) {
ElMessage.error('WebSocket关闭了')
}
// 然后去查看这个房间内有没有别人进来
// 监听 togetherPlayMusic 的变化
watch(() => useStore().togetherRoomInfo.togetherPlayMusicIndex, (newValue, oldValue) => {
if (newValue === oldValue) return
const data = {
musicIndex: useStore().togetherRoomInfo.togetherPlayMusicIndex,
musicSheet: useStore().togetherRoomInfo.togetherPlayMusicSheet
}
const message = {
type: 1,
data: data
}
if (websocket && websocket.readyState === WebSocket.OPEN) {
console.log(JSON.stringify(message))
websocket.send(JSON.stringify(message))
} else {
console.error('WebSocket已关闭')
}
})
客户端分两块大体逻辑,向服务端发送消息和接收来自服务端的消息,发送逻辑:就是当index有变化时,就发送相应的报文信息,所有的歌曲变化都与index关联;接受逻辑:接收到切换音乐报文后,就进行全局变量的赋值(响应式),然后一个再向服务端发一个相同的报文,这个报文可以起一个ack的作用(目前没作用,增加一个扩展点,但是这个功能没必要这样,一致性和安全性会涉及心跳来保证)。最开始的发送端又发现传过来的index和自己现在的index相同,就return掉。
ui如下: