tunnel.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. const { tunnel } = require('../qcloud')
  2. const { mysql } = require('../qcloud')
  3. const option = {
  4. MAX_SCORE_GAP: 10000,//匹配的玩家最大分差不能超过10000分
  5. MATCH_SPEED: 3000,//匹配的频率:每3秒遍历匹配一次
  6. QUESTION_NUMBER: 5,//答题数5个
  7. SEND_QUESTIONS_DELAY: 3500,//匹配完成后,间隔3.5S后开始向前端发题
  8. SEND_QUESTION_TIME: 16000,//发题的频率:每16秒发送一题
  9. PING_PONG_TIME: 6000,//PING-PONG响应的PING发送频率
  10. PING_PONG_OUT_TIME: 20000,//PING-PONG响应超时时间
  11. MAX_NUMBER_TUNNEL_RESEND: 3,//每个信道允许出现无效信道时重新发送的次数
  12. }
  13. const players = {} //用户信息存储对象:openId为key
  14. const rooms = {}//房间存储对象:房间名为key
  15. const fightingRecord = {}//每局比赛战绩存储对象:房间名为key,包含:openId_winner,openId_loser,score_winner,score_loser
  16. const match = {//匹配对象:包含数据和函数
  17. queueData: [],
  18. init() {
  19. let finished = true
  20. function match_succ(player1,player2){
  21. //创建房间
  22. match.createRoom(player1.openId, player2.openId)
  23. //队列中删除匹配好的2个玩家
  24. tools.deleteQueueOpenId(player1.openId)
  25. tools.deleteQueueOpenId(player2.openId)
  26. }
  27. const loopMatch = setInterval(() => {
  28. if (finished) {
  29. finished = false
  30. for (let index1 = 0; index1 < this.queueData.length; index1++) {
  31. let player1 = players[this.queueData[index1]]
  32. //排位赛匹配
  33. if (player1.friendsFightingRoom === undefined) {
  34. for (let index2 = index1; index2 < this.queueData.length; index2++) {
  35. let player2 = players[this.queueData[index2]]
  36. if (player2.friendsFightingRoom === undefined && player2.sortId === player1.sortId && Math.abs(player2.score - player1.score) < option.MAX_SCORE_GAP && player2.openId !== player1.openId) {
  37. match_succ(player1,player2)
  38. //结束该player1的匹配
  39. break
  40. }
  41. }
  42. }
  43. //好友匹配
  44. if (player1.friendsFightingRoom !== undefined && player1.friendsFightingRoom !== null) {
  45. for (let index2 = index1; index2 < this.queueData.length; index2++) {
  46. let player2 = players[this.queueData[index2]]
  47. if (player2.friendsFightingRoom !== undefined && player2.friendsFightingRoom !== null && player2.friendsFightingRoom === player1.friendsFightingRoom && player2.openId !== player1.openId) {
  48. this.match_succ(player1,player2)
  49. //结束该player1的匹配
  50. break
  51. }
  52. }
  53. }
  54. }
  55. finished = true
  56. }
  57. }, option.MATCH_SPEED)
  58. },
  59. createRoom(openId1, openId2) {
  60. let roomName = new Date().getTime().toString() + parseInt(Math.random() * 10000000)//创建时间+随机数
  61. rooms[roomName] = {
  62. roomName,
  63. player1: openId1,
  64. player2: openId2,
  65. library: null,
  66. responseNumber: 0,//收到的响应次数
  67. finished: false,//是否完成了答题
  68. }
  69. players[openId1].roomName = roomName
  70. players[openId2].roomName = roomName
  71. console.info('创建后的总房间和玩家为:', rooms, players)
  72. //library,默认包含5道题目
  73. mysql('question_detail').where((players[openId1].sortId == 1) ? {} : { sort_id: players[openId1].sortId }).select('*').orderByRaw('RAND()').limit(option.QUESTION_NUMBER).then(res => {
  74. rooms[roomName].library = res //将查询到的题目存到房间的题库library中
  75. //房间创建完成后通知前端匹配完成
  76. tools.broadcast([players[openId1].tunnelId, players[openId2].tunnelId], 'matchNotice', {
  77. 'player1': {
  78. openId: openId1,
  79. nickName: players[openId1].nickName,
  80. avatarUrl: players[openId1].avatarUrl,
  81. roomName,
  82. },
  83. 'player2': {
  84. openId: openId2,
  85. nickName: players[openId2].nickName,
  86. avatarUrl: players[openId2].avatarUrl,
  87. roomName,
  88. }
  89. })
  90. //向客户端发题
  91. tools.sendQuestionMain(roomName)
  92. },error=>{
  93. console.log(error)
  94. })
  95. },
  96. }
  97. match.init()
  98. const tools = {//工具对象,包含常用数据和函数
  99. data: {
  100. timerSendQuestion: [],//每个房间的发题定时器
  101. numberTunnelResend: [],//key为信道ID,记录每个信道重复发送的次数
  102. },
  103. //广播到指定信道
  104. broadcast(tunnelIdsArray, type, content) {
  105. tunnel.broadcast(tunnelIdsArray, type, content)
  106. .then(result => {
  107. const invalidTunnelIds = result.data && result.data.invalidTunnelIds || []
  108. if (invalidTunnelIds.length) {
  109. console.error('======检测到无效的信道IDs======', invalidTunnelIds)
  110. invalidTunnelIds.forEach(tunnelId => {
  111. let number = this.data.numberTunnelResend[tunnelId] ? this.data.numberTunnelResend[tunnelId] : 0
  112. if (number < option.MAX_NUMBER_TUNNEL_RESEND) {//当发送次数不大于规定次数时,可以进行重发
  113. let timer = setTimeout(() => {//1.5S后重发一次数据
  114. this.data.numberTunnelResend[tunnelId] = ++number
  115. tools.broadcast([tunnelId], type, content)
  116. clearTimeout(timer)
  117. }, 2000)
  118. } else {
  119. console.log('当前的numberTunnelResend长度为:', Object.keys(this.data.numberTunnelResend).length)
  120. if (Object.keys(this.data.numberTunnelResend).length > 20) { //当累积未清理掉的无效信道达到20个时,清空一次数组
  121. this.data.numberTunnelResend = []
  122. console.info('清空后的numberTunnelResend为', this.data.numberTunnelResend)
  123. } else {
  124. this.clsTimeout(tunnelId, this.data.numberTunnelResend)
  125. console.info('删除后的numberTunnelResend为', this.data.numberTunnelResend)
  126. }
  127. }
  128. })
  129. }
  130. }, error => {
  131. console.log(error)
  132. })
  133. },
  134. //关闭指定信道
  135. closeTunnel(tunnelId) {
  136. console.info('开始关闭信道:' + tunnelId)
  137. tunnel.closeTunnel(tunnelId)
  138. let openId = this.getPlayersOpenId(tunnelId)
  139. if (players[openId]) {
  140. if (players[openId].roomName) {
  141. if (rooms[players[openId].roomName]) {
  142. if (!rooms[players[openId].roomName].finished) {//如果用户存在房号,则说明是战斗状态断线,视为逃跑
  143. tools.runAway(openId)
  144. }
  145. }
  146. }
  147. if (players[openId]) {
  148. if (players[openId].roomName) {
  149. this.clsTimeout(players[openId].roomName, tools.data.timerSendQuestion)//清除发题定时器
  150. delete rooms[players[openId].roomName]//删除房间
  151. }
  152. this.deleteQueueOpenId(openId)//删除匹配队列中的openId
  153. delete players[openId]//删除玩家信息
  154. console.info('信道关闭后的队列、玩家、房间和发题定时器为:', match.queueData, players, rooms, tools.data.timerSendQuestion)
  155. }
  156. }
  157. },
  158. //根据信道ID获取openId
  159. getPlayersOpenId(tunnelId) {
  160. for (let index in players) {
  161. if (players[index].tunnelId === tunnelId) {
  162. return index
  163. }
  164. }
  165. return null
  166. },
  167. //删除匹配队列中的指定openId
  168. deleteQueueOpenId(openId) {
  169. let index = match.queueData.indexOf(openId)
  170. if (~index) {
  171. match.queueData.splice(index, 1)
  172. }
  173. },
  174. //清除数组中延时定时器
  175. clsTimeout(index, arr) {
  176. clearTimeout(arr[index])
  177. delete arr[index]
  178. },
  179. //主控发题函数
  180. sendQuestionMain(roomName) {
  181. let sendQuestionsDelay = setTimeout(() => {
  182. this.sendQuestion(roomName)
  183. clearTimeout(sendQuestionsDelay)
  184. }, option.SEND_QUESTIONS_DELAY)
  185. },
  186. //发题函数
  187. sendQuestion(roomName) {
  188. try {
  189. let openId1 = rooms[roomName].player1
  190. let openId2 = rooms[roomName].player2
  191. this.clsTimeout(roomName, this.data.timerSendQuestion)
  192. tools.broadcast([players[openId1].tunnelId, players[openId2].tunnelId], 'sendQuestion', {
  193. question: rooms[roomName].library[0] ? rooms[roomName].library[0] : {},
  194. //choice = []//[openID:'',userChoose: '',//用户选择了第几个答案 answerColor: '',//用户是否答对 scoreMyself: 0,//用户总得分]
  195. choicePlayer1: players[openId1].choice,
  196. choicePlayer2: players[openId2].choice
  197. })
  198. console.info('已经向客户端发送一题')
  199. this.data.timerSendQuestion[roomName] = setTimeout(() => {
  200. this.sendQuestion(roomName)
  201. }, option.SEND_QUESTION_TIME)
  202. //当发送完{}时,清除掉定时器
  203. if (rooms[roomName].library[0] ? false : true) {
  204. this.clsTimeout(roomName, this.data.timerSendQuestion)
  205. }
  206. rooms[roomName].library.shift() //发送一个,原始题库就删除一个
  207. rooms[roomName].responseNumber = 0 //初始化房间响应次数
  208. players[openId1].choice[1] = ''//初始化用户选择状态:第几个答案
  209. players[openId1].choice[2] = ''//初始化用户选择状态:是否答对
  210. players[openId2].choice[1] = ''//初始化用户选择状态:第几个答案
  211. players[openId2].choice[2] = ''//初始化用户选择状态:是否答对
  212. } catch (error) {
  213. console.error('错误:' + error)
  214. }
  215. },
  216. //更新得分
  217. updateScore(openId, fightingResult) {
  218. mysql('cSessionInfo').where({ open_id: openId }).select('score').then(res => {//获取原始得分
  219. let score = res[0].score
  220. if (fightingResult === 1) {
  221. score = score + 10
  222. } else if (fightingResult === 0) {
  223. score = score - 10
  224. if (score < 0) {
  225. score = 0
  226. }
  227. } else {
  228. return
  229. }
  230. mysql('cSessionInfo').where({ open_id: openId }).update('score', score).then(res => {
  231. console.info(openId + '得分已更新:' + score)
  232. }, error => {
  233. //201803010019:此处添加数据库操作失败报错
  234. console.log(error)
  235. })
  236. })
  237. },
  238. //逃跑处理函数:
  239. runAway(openId) {
  240. //获取逃跑者和胜利者的openId
  241. console.info('开始执行逃跑函数')
  242. let openIdFail = openId, openIdWin
  243. let room = rooms[players[openIdFail].roomName]
  244. if (openIdFail === room.player1) {
  245. openIdWin = room.player2
  246. } else {
  247. openIdWin = room.player1
  248. }
  249. //更新得分
  250. this.updateScore(openIdWin, 1)
  251. this.updateScore(openIdFail, 0)
  252. //存储比赛结果
  253. this.storeFightingRecord(openIdWin, 1, true)
  254. this.storeFightingRecord(openIdFail, 0, true)
  255. //通知赢家对方已逃跑
  256. if (players[openId]) {
  257. if (players[openId].roomName) {
  258. rooms[players[openId].roomName].finished = true//先改变房间状态,再关闭,避免被认为逃跑行为
  259. }
  260. }
  261. this.broadcast([players[openIdWin].tunnelId], 'runawayNotice', {
  262. message: '对手已逃跑'
  263. })
  264. //有时出现无效信道ID,导致onclose无法删除用户信息,这里手工补充删除
  265. delete players[openId]//删除玩家信息
  266. },
  267. //保存战绩函数:
  268. //fightingResult: 0:表示输,1:表示赢,2:表示平手
  269. //runAway:true/false
  270. async storeFightingRecord(openId, fightingResult, runAway = false) {
  271. let roomName = players[openId].roomName
  272. try {
  273. if (fightingRecord[roomName] ? false : true) {
  274. fightingRecord[roomName] = {
  275. openId_winner: '',
  276. openId_loser: '',
  277. score_winner: 0,
  278. score_loser: 0,
  279. }
  280. }
  281. let myRecord = fightingRecord[roomName]
  282. //获取双方比赛数据
  283. if (fightingResult == 0) {//0为输
  284. myRecord.openId_loser = openId
  285. myRecord.score_loser = players[openId].choice[3]
  286. } else {
  287. myRecord.openId_winner = openId
  288. myRecord.score_winner = players[openId].choice[3]
  289. }
  290. //当获取到双方的数据时,开始存储到数据库
  291. if (myRecord.openId_winner && myRecord.openId_loser) {
  292. let room_name = roomName,
  293. run_away = runAway,
  294. open_id_winner = myRecord.openId_winner,
  295. open_id_loser = myRecord.openId_loser,
  296. score_winner = myRecord.score_winner,
  297. score_loser = myRecord.score_loser
  298. delete fightingRecord[room_name]
  299. try{
  300. await mysql('fighting_record').insert({ id: null, room_name, run_away, open_id_winner, open_id_loser, score_winner, score_loser, time: null })
  301. }catch(error){
  302. console.log(error)
  303. }
  304. console.log('清空后的fightingRecord为', fightingRecord)
  305. }
  306. } catch (error) {
  307. console.log(error)
  308. }
  309. },
  310. }
  311. /**
  312. * 实现 onConnect 方法
  313. * 在客户端成功连接 WebSocket 信道服务之后会调用该方法,
  314. */
  315. function onConnect(tunnelId) {
  316. tools.broadcast([tunnelId], 'tunnelIdReplaced', {
  317. newTunnelId: tunnelId
  318. })
  319. //PING-PONG机制:发送PING
  320. clearTimeout(players[tools.getPlayersOpenId(tunnelId)].timer)
  321. tools.broadcast([tunnelId], 'PING', {})
  322. }
  323. /**
  324. * 实现 onClose 方法
  325. * 客户端关闭 WebSocket 信道或者被信道服务器判断为已断开后,
  326. * 会调用该方法,此时可以进行清理及通知操作
  327. */
  328. function onClose(tunnelId) {
  329. console.info('onClose监听到信道' + tunnelId + '关闭')
  330. tools.closeTunnel(tunnelId)
  331. }
  332. /**
  333. * 实现 onMessage 方法
  334. * 客户端推送消息到 WebSocket 信道服务器上后,会调用该方法,此时可以处理信道的消息。
  335. */
  336. function onMessage(tunnelId, type, content) {
  337. console.info('onMessage监听到新消息:', { tunnelId, type, content })
  338. // if (!(tunnelId in players)) {
  339. // tools.closeTunnel(tunnelId)
  340. // }
  341. switch (type) {
  342. case 'PONG': //PING-PONG机制:监听PONG
  343. if (tunnelId) {
  344. let openId = content.openId
  345. clearTimeout(players[openId].timer)//清除掉定时器
  346. let timer = setTimeout(() => {
  347. if (players[openId]) {
  348. //再次设置一个定时器
  349. players[openId].timer = setTimeout(() => {//ping-pong机制:监听客户端是否离线
  350. console.log('开始执行PING-PONG定时器函数')
  351. tools.closeTunnel(tunnelId)//如果离线,则清空用户信息
  352. }, option.PING_PONG_OUT_TIME)
  353. //再次发送一个PING
  354. tools.broadcast([tunnelId], 'PING', {})
  355. console.log(tunnelId + '发送一个PING')
  356. clearTimeout(timer)
  357. }
  358. }, option.PING_PONG_TIME)
  359. }
  360. break
  361. case 'updateMatchInfo':
  362. if (tunnelId) {
  363. let openId = content.openId
  364. players[openId].sortId = content.sortId
  365. players[openId].friendsFightingRoom = content.friendsFightingRoom
  366. console.info('更新用户匹配条件信息:', players[openId])
  367. }
  368. break
  369. case 'answer':
  370. if (tunnelId) {
  371. tools.broadcast([tunnelId], 'getAnswer', {})//通知前端,后台已收到选项
  372. console.info('收到了' + players[content.choice.openId].nickName + '(' + tunnelId + ')的信息')
  373. let roomName = content.roomName
  374. let openId = content.choice.openId
  375. rooms[roomName].responseNumber = rooms[roomName].responseNumber + 1//房间获得了1次响应
  376. players[openId].choice[1] = content.choice.userChoose
  377. players[openId].choice[2] = content.choice.answerColor
  378. players[openId].choice[3] = content.choice.scoreMyself
  379. if (rooms[roomName].responseNumber === 2) {
  380. //当两位玩家都完成答题时,立刻向客户端发送下一题
  381. tools.sendQuestion(roomName)
  382. }
  383. }
  384. break
  385. case 'fightingResult': //用户答完题监听
  386. if (tunnelId) {
  387. console.info('进入fightingResult阶段')
  388. const fightingResult = content.fightingResult
  389. const openId = content.openId
  390. tools.updateScore(openId, fightingResult)//更新分数
  391. tools.storeFightingRecord(openId, fightingResult) //存储比赛详情数据
  392. if (rooms[players[openId].roomName]) {
  393. rooms[players[openId].roomName].finished = true//先改变房间状态,再关闭,避免被认为逃跑行为
  394. }
  395. tools.closeTunnel(tunnelId)//关闭信道连接
  396. console.info('战斗完成后的队列、玩家、房间和发题定时器为:', match.queueData, players, rooms, tools.data.timerSendQuestion)
  397. }
  398. break
  399. default:
  400. break
  401. }
  402. }
  403. module.exports = {
  404. get: async ctx => {//响应用户开始进行websocket连接,信道服务器连接成功后通知客户端
  405. //data:{tunnel:{tunnelId:xxx,connectUrl:xxx},userinfo:{openId:xxx.nickName:xxx,...}}
  406. let data = await tunnel.getTunnelUrl(ctx.req)//当用户发起信道请求的时候,会得到信道信息和用户信息
  407. let userinfo = data.userinfo
  408. let openId = userinfo.openId
  409. if (openId in players) {//如果已经存在openId,则说明只需更新信道ID
  410. players[openId].tunnelId = data.tunnel.tunnelId
  411. console.info('信道变化后的队列、玩家、房间和发题定时器为:', match.queueData, players, rooms, tools.data.timerSendQuestion)
  412. } else {
  413. let score = await mysql('cSessionInfo').where({ open_id: data.userinfo.openId }).select('score')//[{score:4324}]
  414. userinfo.score = score[0].score //number,在用户信息中加入得分
  415. userinfo.tunnelId = data.tunnel.tunnelId //在用户信息中加入tunnel_id属性
  416. userinfo.matchTime = new Date().getTime() //在用户信息中加入匹配的时间,如:1513670126897
  417. userinfo.roomName = null//在用户信息中加入当前战斗状态,默认为null,否则为对战房间号
  418. userinfo.friendsFightingRoom = null//初始值为null,匹配前会复制赋值为undefined或一个数字判断是排位赛还是好友匹配
  419. userinfo.sortId = null
  420. userinfo.choice = [openId, '', '', 0]//[openID:'',user_choose: '',//用户选择了第几个答案 answer_color: '',//用户是否答对 score_myself: '',//用户总得分]
  421. userinfo.timer = setTimeout(() => {//ping-pong机制:监听客户端是否离线
  422. //tools.closeTunnel(data.tunnel.tunnelId)//如果离线,则清空用户信息//暂时先取消此处定时器
  423. }, option.PING_PONG_OUT_TIME)
  424. players[openId] = userinfo //将此用户作为合法用户,并将openId和用户信息(不包含得分,状态等数据)关联起来
  425. console.info('新信道加入后的队列、玩家、房间和发题定时器为:', match.queueData, players, rooms, tools.data.timerSendQuestion)
  426. //在匹配队列中压入一个openId,外套一个队列限制函数
  427. match.queueData.push(openId)
  428. }
  429. ctx.state.data = data.tunnel //返回信道信息给用户
  430. },
  431. post: async ctx => {//用来处理信道传递过来的消息
  432. const packet = await tunnel.onTunnelMessage(ctx.request.body) //onTunnelMessage:当用户消息发送到信道上时,使用该函数处理信道的消息
  433. switch (packet.type) {
  434. case 'connect': //用户开始进行websocket连接,信道服务器连接成功后通知服务端
  435. onConnect(packet.tunnelId)
  436. break
  437. case 'message':
  438. onMessage(packet.tunnelId, packet.content.messageType, packet.content.messageContent)
  439. break
  440. case 'close':
  441. onClose(packet.tunnelId)
  442. break
  443. }
  444. }
  445. }