主要功能
- socket建立连接成功提醒及反馈。
- 房间内有人加入时提醒。
- 房间内有人离开时提醒。
- 房间内聊天消息的转发。
- 房间内转盘游戏的状态同步。
概况:用户可选择 【创建房间】、【加入房间】两个选项,每个房间最多容纳8人同时游戏或聊天。
具体实现思路:客户端发起socket连接时路径会带上参数:gameRoomNum_OpenId_type,解释:(gameRoomNum:房间号;OpenId:用户openId,type:用户类型【0房主用户,1房客用户】),此参数会在后台作为session的唯一标识,后台的每个socket对应的session会放在一个Map中,其key为gameRoomNum_OpenId_type,value为socket的session。客户端与服务端定义好会话的code码,根据code码区分会话的类别。服务端主动给客户端发送消息时,遍历map中所有key,与指定客户端的gameRoomNum_OpenId_type一致时,获取该key对应的value,调用其session对应的sendText方法即可向客户端主动发送消息。
截图
下图简述:轮盘为房间内用户的头像及其名称(图片展示的是默认头像及默认提示字符),聊天框内会提示用户进入(不会提示用户离开。可以实现,但个人觉得不必要。实现了用户离开时轮盘会将该用户的头像与提示字符改为默认头像及默认提示字符)、用户在房间内的聊天信息等
代码
后台核心代码:
@Slf4j
@Component
@ServerEndpoint(value = "/wsdemo/{gameRoomNum_OpenId_type}")
public class MyWebSocket {
private static int onlineCount = 0;
private static Map<String, MyWebSocket> webSocketClientMap = new ConcurrentHashMap<>();
private Session session;
private String gameRoomNum_OpenId_type = "";
@OnOpen
public void onOpen(@PathParam(value = "gameRoomNum_OpenId_type") String param, Session session) {
System.err.println("参数:" + param);
gameRoomNum_OpenId_type = param;
this.session = session;
webSocketClientMap.put(param, this);
addOnlineCount();
System.out.println("有新连接加入!当前在线人数为" + getOnlineCount());
try {
sendMessage(session, "socket建立成功");
} catch (Exception e) {
System.out.println("IO异常");
}
}
@OnClose
public void onClose(Session session) {
GameRoomService gameRoomService = SpringAware.getContext().getBean(GameRoomService.class);
GameRoomFlowService gameRoomFlowService = SpringAware.getContext().getBean(GameRoomFlowService.class);
for (String gameRoomNum_OpenId_type : webSocketClientMap.keySet()) {
if (session.equals(webSocketClientMap.get(gameRoomNum_OpenId_type).getSession())) {
System.err.println(gameRoomNum_OpenId_type + "的session移除");
webSocketClientMap.remove(gameRoomNum_OpenId_type);
subOnlineCount();
System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());
}
}
/**
* WQ
* * 转盘游戏相关:
* * 10 创建房间 {code:10,joinGameRoomNum:joinGameRoomNum,openId:roomOwnerOpenId,msg:'创建房间'}
* * 11 加入房间 {code:11,joinGameRoomNum:joinGameRoomNum,openId:roomGuestOpenId,msg:'加入房间'}
* * 12 房主离开房间 {code:12,joinGameRoomNum:joinGameRoomNum,openId:roomOwnerOpenId,msg:'房主离开房间'}
* * 13 游客离开房间 {code:12,joinGameRoomNum:joinGameRoomNum,openId:roomGuestOpenId,msg:'游客离开房间'}
* * 聊天相关:
* * 20 向房间的消息框中发送消息 {code:20,joinGameRoomNum:joinGameRoomNum,openId:sendMsgOpenId,content:'消息内容',msg:'用户向房间内的其他用户发送消息'}
* * 转盘相关:
* * 30 用户点击了轮盘开始按钮 {code:30,joinGameRoomNum:joinGameRoomNum,openId:openId,flag:flag,msg:'房间内的玩家点击了转盘开始按钮'}
*/
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("有客户单发送消息:" + message);
}
@OnError
public void onError(Session session, Throwable error) {
System.out.println("发生错误");
error.printStackTrace();
}
/**
* WQ
* 2022-06-19 8:34
* 消息发送方法。
* session:给该客户端发送消息
* message:发送给客户端的消息内容
*/
public void sendMessage(Session session, String message) {
try {
session.getBasicRemote().sendText(message);
} catch (Exception e) {
System.err.println("向客户端发送消息出错。" + e.getMessage());
}
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
MyWebSocket.onlineCount++;
}
public static synchronized void subOnlineCount() {
MyWebSocket.onlineCount--;
}
public Session getSession() {
return session;
}
public void setSession(Session session) {
this.session = session;
}
public static Map<String, MyWebSocket> getWebSocketClientMap() {
return webSocketClientMap;
}
public static void setWebSocketClientMap(Map<String, MyWebSocket> webSocketClientMap) {
MyWebSocket.webSocketClientMap = webSocketClientMap;
}
}
小程序端核心代码
<template>
<view style="overflow: hidden;">
<!-- 房间创建 -->
<view class='cu-load load-modal' v-if="loadModalCreateGameRoom">
<view class='gray-text'>房间创建中...</view>
</view>
<!-- 房主离开模态框,只允许点击确定,后返回上一界面 -->
<view :class="roomOwnerLeaveModal">
<view class="cu-dialog">
<view class="padding-xl" style="font-size: 32rpx;background-color: #fff;">
<text class="text-bold text-black">房主已离开房间~</text>
</view>
<view class="cu-bar bg-white">
<view class="action margin-0 flex-sub text-grey " @click="submitBackLastPage">
确定
</view>
</view>
</view>
</view>
<view class="title bg-white text-center text-bold text-lg" style="width: 100%;">房间号: {{joinGameRoomNum}}</view>
<!-- ======================================转盘 start============================================ -->
<view style="position: relative; overflow: hidden;width: 100%;" class="bg-white">
<view class="box bg-gradual-pink" :class="isStart?'start':''" :style="'transform: rotate('+deg+'deg)'">
<view class="box_item" v-for="item in data" :key='item.id'>
<view class="cu-avatar radius margin-left" :style="'background-image:url('+item.avatarImg+');'">
<text style="margin-top: 120rpx;" class="text-sm">{{item.name}}</text>
</view>
</view>
</view>
<view class="active bg-gradual-red" @click="active">
开始
</view>
</view>
<!-- ======================================转盘 end============================================ -->
<view>
<view class="heng"></view>
</view>
<!-- ======================================聊天相关 start============================================ -->
<scroll-view class=" cu-chat bg-white shadow" scroll-y='true' style="border-radius: 20rpx;height: 400rpx;"
:scroll-top="scrollTop">
<view id="chat-box" class="cu-chat">
<view v-if="contentList.length === 0" style="text-align: center;">
<text style="" class="text-grey">-----暂无消息-----</text>
</view>
<view v-for="(item,index) in contentList" :key='index' v-else>
<view class="cu-item" v-if="item.type === 1">
<view class="cu-avatar radius" :style="'background-image:url('+item.avatarImg+');'">
</view>
<view class="main">
<view class="content shadow">
<text>{{item.content}}</text>
</view>
</view>
<view class="date ">{{item.sendTime}}</view>
</view>
<view style="text-align: center;" v-if="item.type === 2">
<view class="cu-info">
<text>{{item.content}}({{item.sendTime}})</text>
</view>
</view>
<view class="cu-item self" v-if="item.type === 0">
<view class="main">
<view class="content shadow">
{{item.content}}
</view>
</view>
<view class="cu-avatar radius" :style="'background-image:url('+item.avatarImg+');'"></view>
<view class="date">{{item.sendTime}}</view>
</view>
</view>
</view>
</scroll-view>
<view class="cu-bar input margin-sm" style="border-radius: 20rpx;">
<input class="solid-bottom" maxlength="50" cursor-spacing="10" v-model="content"></input>
<button class="cu-btn bg-green shadow" @click="syncRoomContent">发送</button>
</view>
<!-- ======================================聊天相关 end============================================ -->
</view>
</template>
<script>
import {
request,
websocketUrl
} from "../../util/index";
import {
getRandomNum_AB,
GL
} from '../../util/rotaryTable'
export default {
data() {
return {
//=======================聊天 start============================
content: '',
contentList: [], //聊天框的内容集合
scrollTop: 0, //滚动条距离顶部的距离
//=======================聊天 end============================
roomOwnerLeaveModal: 'cu-modal',
loadModalCreateGameRoom: false,
//=======================转盘相关 start============================
deg: 22.5,
isStart: false,
flag: 0,
roomOwnerUserInfo: {}, //当前房间的房主对象信息
roomGuestUserInfoList: [], //当前房间的游客对象集合信息
//=======================转盘相关 end============================
//=======================webSocket相关 start============================
userOpenId: uni.getStorageSync('userOpenId'),
isOpenSocket: false, //已经打开socket
socketTask: null, //socket连接
type: -1, //类型。创建0 or加入1
joinGameRoomNum: '', //房间号
roomOwnerOpenId: '', //房主openId
roomGuestOpenId: '', //房客们openId
//=======================webSocket相关 end============================
/*
* 发送给服务器的消息格式定义:{code:10,joinGameRoomNum:joinGameRoomNum,openId:userOpenId,msg:''}
* 标识:
* 转盘游戏相关:
* 10 创建房间 {code:10,joinGameRoomNum:joinGameRoomNum,openId:roomOwnerOpenId,msg:'创建房间'} 小程序主动发送给服务器
* 11 加入房间 {code:11,joinGameRoomNum:joinGameRoomNum,openId:roomGuestOpenId,msg:'加入房间'} 小程序主动发送给服务器
* 12 房主离开房间 {code:12,joinGameRoomNum:joinGameRoomNum,openId:roomOwnerOpenId,msg:'房主离开房间'} 服务器主动发送给小程序
* 13 游客离开房间 {code:12,joinGameRoomNum:joinGameRoomNum,openId:roomGuestOpenId,msg:'游客离开房间'} 服务器主动发送给小程序
* 聊天相关:
* 20 向房间的消息框中发送消息 {code:20,joinGameRoomNum:joinGameRoomNum,openId:sendMsgOpenId,content:'',msg:'用户向房间内的其他用户发送消息'} 小程序主动发送给服务器
* 转盘相关:
* 30 用户点击了轮盘开始按钮 {code:30,joinGameRoomNum:joinGameRoomNum,openId:openId,flag:flag,msg:'房间内的玩家点击了转盘开始按钮'} 小程序主动发送给服务器
*/
//建立连接时所带的参数。例:U594EW2V_oQqIy4z11-KEwSftcaz8GImDWFnQ_1:房间号_openId_房主0/游客1
pathParam: '',
//长连接的请求参数封装
websocketReqParam: {
code: 0,
joinGameRoomNum: '',
openId: '',
msg: ''
},
//响应参数封装
websocketRespParam: {}
}
},
computed: {
data() {
//即将返回的结果集
var itemList = []
if (this.roomOwnerUserInfo.avatarImg !== undefined) {
itemList.push({
id: 1,
name: this.roomOwnerUserInfo.nickName,
avatarImg: this.roomOwnerUserInfo.avatarImg
})
}
if (this.roomGuestUserInfoList.length > 0) {
var itemListLength = itemList.length
for (var i = 0; i < this.roomGuestUserInfoList.length; i++) {
itemList.push({
id: ++itemListLength,
name: this.roomGuestUserInfoList[i].nickName,
avatarImg: this.roomGuestUserInfoList[i].avatarImg
})
}
}
var itemListLength = itemList.length
while (itemList.length < 8) {
itemList.push({
id: ++itemListLength,
name: '谢谢惠顾',
avatarImg: '../../static/svg/icon-no-guest.svg'
})
}
return itemList
}
},
onLoad(options) {
var type = options.type
if (type == 0) { //创建房间
this.loadModalCreateGameRoom = true
this.type = 0
this.roomOwnerOpenId = uni.getStorageSync('userOpenId')
//获取房间号
var vm = this
//此处用户授权小游戏时会给用户创建一个游戏房间,该用户就为该游戏房间的房主
request('/getRoomNumByRoomOwnerOpenId', {
roomOwnerOpenId: vm.roomOwnerOpenId
}).then((data) => {
vm.loadModalCreateGameRoom = false
vm.joinGameRoomNum = data.gameRoomEntity.gameRoomNum
//请求参数赋值
vm.websocketReqParam = {
code: 10,
joinGameRoomNum: vm.joinGameRoomNum,
openId: vm.roomOwnerOpenId,
msg: '创建房间'
}
//建立连接时的路径参数赋值
vm.pathParam = vm.joinGameRoomNum + '_' + uni.getStorageSync('userOpenId') + '_0'
//建立socket连接
vm.connectSocketInit()
})
} else if (type == 1) { //加入房间
var vm = this
vm.type = 1
vm.joinGameRoomNum = options.joinGameRoomNum
vm.roomGuestOpenId = uni.getStorageSync('userOpenId')
//请求参数赋值
this.websocketReqParam = {
code: 11,
joinGameRoomNum: vm.joinGameRoomNum,
openId: vm.roomGuestOpenId,
msg: '加入房间'
}
//建立连接时的路径参数赋值
vm.pathParam = vm.joinGameRoomNum + '_' + uni.getStorageSync('userOpenId') + '_1'
//建立socket连接
this.connectSocketInit()
}
},
onUnload() {
this.closeSocket()
},
methods: {
//房主离开了此房间,点击确定按钮,返回上一界面(相当于点击左上角返回箭头)
submitBackLastPage() {
uni.navigateBack({});
},
//===========================webSocket相关 start==================================
// 进入这个页面的时候创建websocket连接【整个页面随时使用】
connectSocketInit() {
var vm = this
// 创建一个this.socketTask对象【发送、接收、关闭socket都由这个对象操作】
this.socketTask = uni.connectSocket({
// 【非常重要】必须确保你的服务器是成功的,如果是手机测试千万别使用ws://127.0.0.1:9099【特别容易犯的错误】
url: `${websocketUrl}/${vm.pathParam}`,
success(data) {
console.log("websocket连接成功", data);
},
});
// 消息的发送和接收必须在正常连接打开中,才能发送或接收【否则会失败】
this.socketTask.onOpen((res) => {
console.log("WebSocket连接正常打开中...!");
vm.isOpenSocket = true;
// 注:只有连接正常打开中 ,才能正常成功发送消息
this.socketTask.send({
data: JSON.stringify(vm.websocketReqParam),
async success() {
console.log("消息发送成功", vm.type);
},
});
// 注:只有连接正常打开中 ,才能正常收到消息
this.socketTask.onMessage((res) => {
console.log("收到服务器内容:", res.data)
vm.handleServerMsg(res.data)
});
})
// 这里仅是事件监听【如果socket关闭了会执行】
this.socketTask.onClose(() => {
console.log("已经被关闭了")
})
},
// 关闭websocket【离开这个页面的时候执行关闭】
closeSocket() {
var vm = this
if (vm.socketTask) {
vm.socketTask.close({
success(res) {
vm.isOpenSocket = false;
console.log("关闭成功", res)
},
fail(err) {
console.log("关闭失败", err)
}
})
} else {
console.log('vm.socketTask为null,无需关闭');
}
},
//服务器端消息处理
handleServerMsg(serverMsg) {
//转成json对象
var serverMsgJson = JSON.parse(serverMsg)
if (serverMsgJson.code === 0) {
var dataObj = serverMsgJson.data
switch (dataObj.type) {
case '010': //房主创建房间的响应
// console.log('房主创建房间的响应', dataObj);
uni.showToast({
title: '创建房间成功',
})
this.roomOwnerUserInfo = dataObj.roomOwnerUserInfo
break;
case '011': //游客加入房间的响应 聊天框中给出提示信息
this.roomOwnerUserInfo = dataObj.roomOwnerUserInfo
this.roomGuestUserInfoList = dataObj.roomGuestUserInfo
var newGuestNickName = dataObj.newGuestUserInfo.nickName
//聊天框中给出有新人加入的提示信息
this.contentList.push({
type: 2,
content: `欢迎 ${newGuestNickName} 加入房间~`,
sendTime: this.getHms()
})
this.autoToBottom()
break;
case '012': //房主离开房间的响应
//弹出模态框,只允许点击离开按钮
this.roomOwnerLeaveModal = 'cu-modal show'
break;
case '013': //游客离开房间的响应
//将游客集合中的该游客移除
for (var i = 0; i < this.roomGuestUserInfoList.length; i++) {
if (this.roomGuestUserInfoList[i].openId === dataObj.openId) {
this.roomGuestUserInfoList.splice(i, 1);
}
}
break;
case '020': //同步聊天内容 "content":"111222测试333","openId":"oQqIy4z11-KEwSftcaz8GImDWFnQ"
//同步聊天内容
var type = 1 // 用户类型。0 自身发送(在发送按钮触发的方法中涉及),1 他人发送(此处为接收到他人的消息,故都是1)
var avatarImg = '' //用户头像
for (var i = 0; i < this.roomGuestUserInfoList.length; i++) {
if (this.roomGuestUserInfoList[i].openId === dataObj.openId) {
avatarImg = this.roomGuestUserInfoList[i].avatarImg
}
}
if (avatarImg === '') {
avatarImg = this.roomOwnerUserInfo.avatarImg
}
this.contentList.push({
type: type,
content: dataObj.content,
avatarImg: avatarImg,
sendTime: dataObj.sendTime
})
this.autoToBottom()
break;
case '030': //同步转盘状态
//同步玩家转盘状态
this.flag = dataObj.flag
this.activeAuto(this.flag)
var gamingNickName = ''
for (var i = 0; i < this.roomGuestUserInfoList.length; i++) {
if (this.roomGuestUserInfoList[i].openId === dataObj.openId) {
gamingNickName = this.roomGuestUserInfoList[i].nickName
}
}
uni.showToast({
title: `玩家 ${gamingNickName} 已启动转盘~`,
icon: 'none'
})
break;
}
} else {
console.log('服务器向客户端发送的消息出错。', serverMsg);
}
},
//告诉房间内的其他玩家,房间内有玩家点击了 开始 按钮
tellOtherGuestAndOwnerActive() {
var msg = {
code: 30,
joinGameRoomNum: this.joinGameRoomNum,
openId: uni.getStorageSync('userOpenId'),
flag: this.flag,
msg: '房间内的玩家点击了转盘开始按钮'
}
if (this.isOpenSocket) {
// websocket的服务器的原理是:发送一次消息,同时返回一组数据【否则服务器会进去死循环崩溃】
this.socketTask.send({
data: JSON.stringify(msg),
async success() {
console.log("消息发送成功");
},
});
}
},
//聊天框消息发送
syncRoomContent() {
if (this.content === '') {
uni.showToast({
title: '内容不可为空',
icon: 'none'
})
return
}
var msg = {
code: 20,
joinGameRoomNum: this.joinGameRoomNum,
openId: uni.getStorageSync('userOpenId'),
content: this.content,
msg: '房间内的用户发送消息'
}
var avatarImg = '' //用户头像
for (var i = 0; i < this.roomGuestUserInfoList.length; i++) {
if (this.roomGuestUserInfoList[i].openId === msg.openId) {
avatarImg = this.roomGuestUserInfoList[i].avatarImg
}
}
if (avatarImg === '') {
avatarImg = this.roomOwnerUserInfo.avatarImg
}
var vm = this
if (this.isOpenSocket) {
// websocket的服务器的原理是:发送一次消息,同时返回一组数据【否则服务器会进去死循环崩溃】
this.socketTask.send({
data: JSON.stringify(msg),
async success() {
//写入消息框内
vm.contentList.push({
type: 0,
content: vm.content,
avatarImg: avatarImg,
sendTime: vm.getHms()
})
},
complete() {
console.log('消息发送,complete()');
//输入框内容重置
vm.content = ''
}
});
} else {
uni.showToast({
title: '与服务器断连',
icon: 'none'
})
}
this.autoToBottom()
},
//自动滚到最底部
autoToBottom() {
var vm = this
//设置滚动条在最底部
wx.createSelectorQuery().select('#chat-box').boundingClientRect(function(rect) {
//滚动的高度=scroll-view内view的高度
vm.scrollTop = rect.height
}).exec()
},
//获取当前的时分秒
getHms() {
var nowDate = new Date();
var hours = nowDate.getHours(); //获取小时
var minutes = nowDate.getMinutes(); //获取分
return hours + ":" + minutes
},
//===========================webSocket相关 end==================================
//===========================轮盘相关 start==================================
//点击开始按钮触发的方法
active() {
//获取概率 (每个人概率相同)
const map = new Map()
var probability = 1 / (this.roomGuestUserInfoList.length + 1)
map.set(1, probability)
for (var i = 1; i <= this.roomGuestUserInfoList.length; i++) {
map.set(i + 1, probability)
}
//若轮盘正在转,则不可点击,直接返回
if (this.isStart) {
return
}
this.isStart = true
// this.flag=getRandomNum_AB(1,8)
// 随机生成 1 ~ 9 之间的数(不包含9)
let gl = new GL({
min: 1,
max: 9,
fenpei: map
});
this.flag = gl.random()
// 指定奖项
this.start(this.flag)
//通知其他玩家
this.tellOtherGuestAndOwnerActive()
},
//其他玩家点击了,这边需要同步的方法
activeAuto() {
this.isStart = true
// 指定奖项
this.start(this.flag)
},
start(n) {
// 获取旋转角度
let deg = 1080 - (45 * n) + 22.5
this.deg += deg
setTimeout(() => {
this.isStart = false
let title = this.data[this.flag - 1].name
uni.showModal({
title: '提示',
content: title,
success: (res) => {
this.deg = 22.5
// if (res.confirm) {
// console.log('用户点击确定');
// this.deg= 21
// } else if (res.cancel) {
// console.log('用户点击取消');
// }
}
});
}, 3200)
},
//===========================轮盘相关 end==================================
},
}
</script>
<style lang="less" scoped>
.active {
position: absolute;
top: 50%;
left: 50%;
width: 150rpx;
height: 150rpx;
// background-color: #DD524D;
transform: translate(-50%, -50%);
line-height: 150rpx;
text-align: center;
border-radius: 50%;
color: #fff;
&::before {
content: '';
width: 0;
height: 0;
border-left: 30rpx solid transparent;
border-right: 30rpx solid transparent;
border-bottom: 60rpx solid #DD524D;
position: absolute;
top: -40rpx;
left: 50%;
transform: translateX(-50%);
}
}
.start {
transition: all 3.5s cubic-bezier(0.09, 0.99, 0.72, 1);
}
.box {
position: relative;
background-color: #007AFF;
width: 700rpx;
height: 700rpx;
border-radius: 50%;
margin: 50rpx auto;
overflow: hidden;
border: 1px solid #FFFFFF;
.box_item {
&::after {
position: absolute;
content: '';
height: 100%;
width: 2rpx;
right: 0;
// transform: translateX(50%);
background-color: #FFFFFF;
transform-origin: 50% 50%;
transform: rotate(22.5deg) translateX(5%);
}
position: absolute;
top: 0;
left: 275rpx;
width: 150rpx;
height: 350rpx;
transform-origin: 50% 100%;
text-align: center;
line-height: 140rpx;
&:nth-child(2) {
transform: rotate(45deg);
}
&:nth-child(3) {
transform: rotate(90deg);
}
&:nth-child(4) {
transform: rotate(135deg);
}
&:nth-child(5) {
transform: rotate(180deg);
}
&:nth-child(6) {
transform: rotate(225deg);
}
&:nth-child(7) {
transform: rotate(270deg);
}
&:nth-child(8) {
transform: rotate(315deg);
}
}
}
.heng {
background: #E0E3DA;
width: 100%;
height: 5rpx;
}
</style>
转载请注明,源码请联系