目录

前言

1. 逻辑分析

2. websocket

3. 小程序端代码实现

4.服务端代码实现

后记


前言

有兴趣的同学先扫码体验一下小程序

继我的个人小程序(“你划我猜出题器”)上线第二版本(自建词库)后,又有新的想法涌现出来,做一个“谁是卧底”在线随机发牌吧(有时间再写一下第一个版本跟第二个版本的博文)。既然如此,就要思考一下“谁是卧底”的技术实现点。

词的来源,可利用现有词库管理系统。接下来的难点就是如何实现在线随机发牌。很明显,http请求无法实现“谁是卧底”的发牌,应该是服务端主动推送词语给各个客户端。

websocket能够很好的实现这个功能。

小程序websocket传送门:https://developers.weixin.qq.com/miniprogram/dev/api/network/websocket/wx.sendSocketMessage.html

1. 逻辑分析

开发惯例,整理基本逻辑。“谁是卧底”的发牌逻辑相对简单。参考微信的“面对面建群”概念,我化用成“面对面建房”。

(1)房的概念用来隔离发牌的环境,每一个房就是一个单独的游戏环境,用过的词不再出现。

(2)房内的所有用户,第一个进入房的称为房主,拥有控制发牌权,其他用户无。

(3)客户端与服务端的交互组成:①建立websocket连接;②用户进入房间;③发牌;④用户离开房间;⑤断开websocket连接。

2. websocket

客户端需要处理三个触发事件,三个监听事件。

(1)用户进入房间,通知服务端;

(2)用户离开房间,通知服务端;

(3)房主点击“开始发牌”,通知服务端。

对应的,客户端同样需要监听三个事件。

(1)服务端通知其他用户进入房间;

(2)服务端通知其他用户离开房间;

(3)服务端发牌。

看了websocket的API,它不似socket.io那般可以拆分事件去监听,去触发,而是统一接收服务端数据,统一发送数据给服务端,而且也不能传送对象数据,websocket只能传送字符串和二进制数据。

所以,监听和触发用onSocketMessage和sendSocketMessage统一处理的话,需要自定义对象参数来区分每个事件。这里用到JSON.stringify和JSON.parse,将对象转化为字符串,将字符串转化为对象。

    wx.sendSocketMessage({  // 发送消息给服务端data: JSON.stringify(obj), // 特别注意!!!websocket只接收string或ArrayBuffersuccess(res) {console.log('sendSocketMessage发送消息至服务器成功', res)},fail(err) {console.error('sendSocketMessage发送消息至服务器报错', err)}})
    wx.onSocketMessage(function (res) { // 接收服务端下发的消息let obj = JSON.parse(res.data)  // 特别注意!!res只能是string/ArrayBufferconsole.log('这是来自服务器的消息')})

“谁是卧底”websocket传送数据的结构

    let obj = {event: eventName, // 事件roomKey: roomKey, // 房间号data: data, // 数据,用户信息或者词牌信息}/** @event
* 'createRoom' 进入房间-客户端
* 'leaveRoom' 离开房间-客户端
* 'deliverPocker' 开始发牌-客户端
* 'intoRoom' 进入房间-服务端
* 'leaveRoom' 离开房间-服务端
* 'revivePocker' 发牌-服务端
*/

3. 小程序端代码实现

3.1 准备roomKey和用户信息

点击"面对面建房"按钮申请用户授权,获取用户头像和昵称,将信息保存在客户端storage中。输入四位数字房号,将房号保存在客户端storage中,跳转到游戏页面,websocket处理均在游戏页面中处理。

3.2 建立websocket链接

进入游戏页面,在onReady生命周期函数中建立websocket连接,并且通知服务端“用户进入房间”

  onReady() {console.log('onReady---------')let that = thisthat.setData({roomKey: wx.getStorageSync('roomKey'),user: wx.getStorageSync('userInfo')})that.connect()  // 建立websocket连接wx.setNavigationBarTitle({title: '发牌房间' + that.data.roomKey})},connect() {let that = thiswx.connectSocket({     // 建立websocket连接url: 'wss://www.*****.****/'   // wss地址})wx.onSocketOpen(function (res) {  // 建立连接成功that.setData({connectStatus: 1})console.log('websocket 已经连接服务器', res)that.send('createRoom', that.data.roomKey, that.data.user)  // 通知服务端用户进入房间})},

3.3 封装send函数,其作用——发送数据给服务端

  send(eventName, roomKey, data) {let obj = {event: eventName, // 事件roomKey: roomKey, // 房间号data: data, // 传送数据}wx.sendSocketMessage({data: JSON.stringify(obj),success(res) {console.log('sendSocketMessage发送消息至服务器成功', res)},fail(err) {console.error('sendSocketMessage发送消息至服务器报错', err)}})},

3.4 关闭websocket连接

在onUnload生命周期函数里处理“离开房间”事件,并且关闭websocket连接

  onUnload() {console.log('onUnload---------')this.close()},close() {this.send('leaveRoom', this.data.roomKey, this.data.user)let that = thiswx.closeSocket()  // 关闭websocket连接wx.onSocketClose(function (res) {   // 关闭成功that.setData({connectStatus: 0})console.log('websocket服务器已经断开', res)})},

3.5 在onLoad生命周期函数里监听服务端发送的消息

  onLoad() {console.log('onLoad---------')let that = thislet key = wx.getStorageSync('roomKey')wx.onSocketMessage(function (res) {  // 接收服务端下发的消息let obj = JSON.parse(res.data)  // 将字符串转化为对象console.log('这是来自服务器的消息', obj.event, obj.roomKey, obj.data)if (obj.event == 'intoRoom' && key == obj.roomKey) {that.resetUsers(obj.data)  // 更新当前房间里的用户列表视图}if (obj.event == 'leaveRoom' && key == obj.roomKey) {that.resetUsers(obj.data) // 更新当前房间里的用户列表视图}if (obj.event == 'revivePocker' && key == obj.roomKey) {that.deliverPocker(obj.data) // 更新牌面词语}})},resetUsers(users) {this.setData({users: users,isOwner: false})for (let i = 0; i < users.length; i++) {if (users[i].isOwner && (users[i].nickName == this.data.user.nickName)) { // 是否是房主this.setData({isOwner: true})}}},deliverPocker(users) {let myuser = wx.getStorageSync('userInfo')for (let i = 0; i < users.length; i++) {if (users[i].nickName == myuser.nickName) {this.setData({pocker: users[i].pocker})this.showWord()break;}}},

4.服务端代码实现

服务端主要用node的ws实现

const ws = require('ws');

(1)每个用户建立websocket连接,需要保存该连接

// https服务
const serve = https.createServer(options, app.callback()).listen(config.port, (err) => {if (err) {console.log('服务启动出错', err);} else {db.connect();  // 数据库连接console.log('guessWord-server运行在' + config.port + '端口');}
});// wss服务
let clients = []  // 客户端websocket连接队列
let userIndex = 0  // 客户数
let undercoverWords = null  // 谁是卧底词库
const wss = new ws.Server({ server: serve })
wss.on('connection', function (wxConnect) {clients.push({"ws": wxConnect,"nickname": 'userIndex' + (userIndex++) });console.log('wss connection wxConnect ------ ')WordAPI.undercover().then(res => {  // 获取“谁是卧底”词库if (res.code == 200) {let arr = res.dataarr.sort(function () { return 0.5 - Math.random() }) // 打乱题库undercoverWords = arr} else {console.error(res.message)}})wxConnect.on('message', function (msg) { // 监听客户端发送的消息let obj = JSON.parse(msg)// console.log('接收来至客户端的信息', obj.event, obj.roomKey, obj.data.nickName)if (obj.event == 'createRoom') { // 进入房间createRoom(obj.roomKey, obj.data)}if (obj.event == 'leaveRoom') { // 离开房间leaveRoom(obj.roomKey, obj.data)}if (obj.event == 'deliverPocker') { // 房主发牌deliverPocker(obj.roomKey)}})
})

(2)封装服务端的广播函数(通知所有客户端)

// 广播所有客户端消息
function broadcastSend(event, roomKey, data) {clients.forEach(function (v, i) {if (v.ws.readyState === ws.OPEN) {v.ws.send(JSON.stringify({event: event,roomKey: roomKey,data: data}));}})
}

(3)每个用户进入房内,服务端需要往该房的用户列表里增加数据,并通知客户端

function createRoom(roomKey, user) {if (rooms[roomKey]) { // 房间已存在,加入房间rooms[roomKey].users.push(user)} else { // 房间不存在,创建房间user['isOwner'] = truerooms[roomKey] = {users: [user],games: []}}broadcastSend('intoRoom', roomKey, rooms[roomKey].users)console.log(user.nickName + '进入房间' + roomKey + ',当前房间人数' + rooms[roomKey].users.length)
}

(4)房主每点击一次“开始发牌”,服务端需要进行三个随机处理:

①随机抽取一组词语作为本轮游戏词

②随机决定哪个词作为卧底词

③从房内用户随机分配卧底身份

分配完毕,每个用户拥有自己的身份词,就可以将词下发给客户端了。

function deliverPocker(roomKey) { // 发牌// 排除已发过的牌let g = rooms[roomKey].gameslet current = []for (let i = 0; i < undercoverWords.length; i++) {if (g.indexOf(undercoverWords[i].title) == -1) {current.push(undercoverWords[i].title)}}// 打乱题库current.sort(function () { return 0.5 - Math.random() })if (current.length == 0) { // 牌已发完broadcastSend('revivePocker', roomKey, '')} else {// 随机取出牌,并记录let pocker = current[0]rooms[roomKey].games.push(pocker)// 决定哪个词是卧底牌let arr = pocker.split(',')let random = Math.random() // 随机数let wodi = arr[0] // 卧底牌let pingm = arr[1] // 平民牌if (random > 0.5) {wodi = arr[1]pingm = arr[0]}// 决定哪几个人是卧底牌let ul = rooms[roomKey].users.length // 当前房间参与游戏的人数// 3-5人则1个卧底,6-8则2个卧底,9-11人则3个卧底,12人以上4个卧底let wodiIndex = []if (ul >= 3 && ul <= 5) {wodiIndex.push(randomNum(0, ul - 1))}if (ul >= 6 && ul <= 8) {wodiIndex.push(getRandomNoRepeat(0, ul - 1, wodiIndex))wodiIndex.push(getRandomNoRepeat(0, ul - 1, wodiIndex))}if (ul >= 9 && ul <= 11) {wodiIndex.push(getRandomNoRepeat(0, ul - 1, wodiIndex))wodiIndex.push(getRandomNoRepeat(0, ul - 1, wodiIndex))wodiIndex.push(getRandomNoRepeat(0, ul - 1, wodiIndex))}if (ul >= 12) {wodiIndex.push(getRandomNoRepeat(0, ul - 1, wodiIndex))wodiIndex.push(getRandomNoRepeat(0, ul - 1, wodiIndex))wodiIndex.push(getRandomNoRepeat(0, ul - 1, wodiIndex))wodiIndex.push(getRandomNoRepeat(0, ul - 1, wodiIndex))}// 分发牌面let u = rooms[roomKey].usersfor (let i = 0; i < ul; i++) {if (wodiIndex.indexOf(i) != -1) {u[i]['pocker'] = wodi} else {u[i]['pocker'] = pingm}}broadcastSend('revivePocker', roomKey, u)}
}
// 生成从minNum到maxNum的随机数
function randomNum(minNum, maxNum) {switch (arguments.length) {case 1:return parseInt(Math.random() * minNum + 1, 10);case 2:return parseInt(Math.random() * (maxNum - minNum + 1) + minNum, 10);default:return 0;}
}
// 生成从minNum到maxNum的随机数,但不含已有的数组
function getRandomNoRepeat(minNum, maxNum, had) {let i = randomNum(minNum, maxNum);if (had.indexOf(i) === -1) {return i;}return getRandomNoRepeat(minNum, maxNum, had);
}

(5)每个用户离开房,需要删除房内该用户,并通知客户端,当房内人数为0,需要销毁房

function leaveRoom(roomKey, user) {let u = rooms[roomKey].userslet index = -1for (let i = 0; i < u.length; i++) {if (u.nickName == user.nickName) {index = ibreak}}rooms[roomKey].users.splice(index, 1)if (rooms[roomKey].users.length == 0) {delete rooms[roomKey]broadcastSend('leaveRoom', roomKey, rooms[roomKey].users)} else {if (index == 0) {rooms[roomKey].users[0]['isOwner'] = true}broadcastSend('leaveRoom', roomKey, rooms[roomKey].users)}if (rooms[roomKey]) {console.log(user.nickName + '离开房间' + roomKey + ',当前房间人数' + rooms[roomKey].users.length)} else {console.log(user.nickName + '离开房间' + roomKey + ',当前房间人数为0,房间已被销毁')}
}

走到这里,全部流程已经走通~~~~

后记

服务端代码可以优化一波~~~~有待优化,等下波优化再更新

【小程序】websocket实现“谁是卧底”在线随机发牌相关推荐

  1. 微信小程序WebSocket相关问题说明

    看本帖的前提是:你的WebSocket在小程序之外是正常可用的:因为WebSocket不是小程序独有的,所有大部分问题在网上是可以找到说明的,本帖只是聚合了一些小程序中使用WebSocket中遇到的问 ...

  2. 微信在线客服 php,微信小程序中添加联系在线客服功能

    这次给大家带来微信小程序中添加联系在线客服功能,微信小程序中添加联系在线客服功能的注意事项有哪些,下面就是实战案例,一起来看一下. 1. 普通客服按钮添加客服-联系我们 2. 悬浮客服按钮添加,图片自 ...

  3. 微信小程序WebSocket接口以及在小程序中的使用。

    关于微信小程序WebSocket的使用 一.连接wx.connectSocket 二.wx.onSocketOpen和wx.onSocketError 三.wx.onSocketMessage 四.w ...

  4. 微信小程序WebSocket心跳检测与断来重连

    为什么要心跳检测 使用微信小程序WebSocket时,WebSocket在一定的时间没有进行通信就会断开连接,所以需要使用心跳检测. 那么心跳检测是什么呢,心跳检测顾名思义就是和人心脏动一样,客户端在 ...

  5. 微信小程序websocket实现即时聊天

    今天给大家分享一下本人做小程序使用websocket的一点小经验,希望对大家有所帮助. 使用之前肯定首先要了解一下websocket是什么,简单来讲websocket就是客户端与服务器之间专门建立的一 ...

  6. 微信小程序WebSocket 中实现发送文字,图片,语音以及WebSocket 常见问题解决方案

    小程序 WebSocket 常见问题:(本文已解决的) 1.自动断开链接,重连但是只能存在两个 WebSocket 的问题.   ---1兼容情况:1.1 正常聊天过一段时间 WebSocket 自动 ...

  7. 微信小程序websocket聊天前端实现

    微信小程序websocket聊天前端实现,可以发语音.图片.文字. 代码下载:https://download.csdn.net/download/cc1314_/10983195

  8. 微信预约小程序怎么制作(在线预约小程序系统开发功能)

    微信预约小程序怎么制作(在线预约小程序系统开发功能) 一般开发制作微信预约小程序系统的方式有三种. 1,是购买成品的预约小程序系统,功能固定,只需要替换里面内容信息. 2,联系专业的微信小程序开发公司 ...

  9. 【仿12348的法律咨询平台微信小程序源代码,可以在线留言、设置打赏金额、查看其他人的法律问题等功能】

    仿12348的法律咨询平台微信小程序源代码,可以在线留言.设置打赏金额.查看其他人的法律问题等功能. CSDN下载地址:https://download.csdn.net/download/u0109 ...

最新文章

  1. R语言ggplot2可视化在箱图中为箱图添加均值的标签及对应数值实战
  2. ajax获取数据自动创建分页,支持自定义显示数据量以及分页数量
  3. 如何获取html页面上的按钮列表,如何从一个html页面获取单选按钮的值到另一个?...
  4. mysql innodb count_MySQL下INNODB引擎的SELECT COUNT(*)性能优化及思考
  5. js中货币格式化方法
  6. Windows开发的内功和招式
  7. 新年第二弹|卖萌屋私藏书单大公开
  8. Kafka.net使用编程入门(一)
  9. mysql5.6 python_Centos-6.5 + python3 + mysql5.6 环境搭建
  10. 团队作业(二):项目选题
  11. 决策树随机森林adaboost理论实战
  12. java实现jsp转pdf,使用Java生成Pdf文档-JSP教程,Java技巧及代码
  13. CAD 2022卸载方法,如何完全彻底卸载删除清理干净CAD各种残留注册表和文件? 【转载】
  14. # 图书管理系统案例练习
  15. Max Core Frequency 异常显示为-1.80GHz -- Intel-Extreme-Tuning-Utility-Intel-XTU (英特尔 XTU)
  16. Unity3d UI自适应之Canvas Scaler详细说明和测试项目源码
  17. 【飞飞CMS二次开发实录】开篇:安装与运行
  18. 2023年全国最新工会考试精选真题及答案43
  19. 云和恩墨的两道Oracle面试题
  20. 【C语言】#ifdef和#endif条件编译

热门文章

  1. 循环往复 志在千里-while语句(C语言)
  2. 实施演示ppt的注意事项
  3. ABPA 对文件的存取
  4. Mybatis万字教程 (一次订阅能看所有专栏)
  5. linux挂载实验箱闹钟,Linux/Ubuntu命令行下打造一个音乐闹钟
  6. CloudComparePCL 剔除点云中的重复点
  7. python爬虫练习-爬取暖心小故事并实现定时邮箱发送
  8. 毕马威中国:证券基金经营机构信息技术审计项目发现洞察
  9. 小鹅通前端春招一面面经(2021.4.1)
  10. 计算机程序是怎样运行的