本节思路:基于本系列第二节的内容,实现一个websocket服务器,使用redis作为存储介质,使用自己约定的cmd命令作为简单的通信协议,与小程序进行通信,实现简单的2人猜测功能。
Cmd命令遵循以下过程:登录(client-server),小程序将登录命令发送到服务器。服务器收到命令后,判断房间号是否已满。如果未满,则保存客户端websocket并连接到列表,将客户端用户信息存储在redis中,更新在线用户列表,然后发送init(server-client)命令通知所有在线用户并更新在线用户列表。
当客户端单击ready按钮时,它会向服务器发送ready(client-server)命令,服务器会更新用户的ready标志(存储在redis中),计算房间中的人数是否已满,以及房间中的所有用户是否都设置了ready标志。如果是这样,它会向所有用户发送start(服务器-客户端)命令来启动游戏。
客户端选择自己的打孔信息,向服务器发送猜测(client-server)命令,服务器记录用户的打孔数据,判断房间里的每个人都在提交猜测命令,计算最终结果,向所有用户发送结果(server-client)命令。
接收到结果命令后,客户端重新进入就绪流程。
如果小程序退出,客户端向服务器发送注销(客户端-服务器)命令,服务器从列表中删除用户,向所有其他在线用户重新发送init(服务器-客户端)命令,并更新在线用户列表。
命令流程如下:登录(客户端-服务器)初始化(服务器-客户端)就绪(客户端-服务器)启动(服务器-客户端)猜测(客户端-服务器)结果(服务器-客户端)注销(客户端-服务器)
示意图:登录
准备好的
开始
结果
Go服务器
package main import(' golang . org/x/net/web socket ' ' fmt ' ' log ' ' net/http ' ' github . com/go-redis/redis ' ' encoding/JSON ' ' strconv ')const max _ room _ num=2var(JSON=web socket。JSON消息=websocket的JSON///编解码器。混乱
age // codec for string, []byte ActiveClients = make(map[string]ClientConn) // map containing clients //在线websocket列表 User = make(map[string]string)) type ClientConn struct { websocket *websocket.Conn }type UserMsg struct { Room string Cmd string User string AvatarUrl string Content string Uuid string HandNum string GuessNum string}type UserInfo struct { User string AvatarUrl string Uuid string}type ReplyMsg struct { Room string Cmd string Data string}type GuessResult struct { Result string CurrentNum int HandRecord map[string]string GuessRecord map[string]string}func echoHandler(ws *websocket.Conn) { var err error var userMsg UserMsg for { var data []byte if err = websocket.Message.Receive(ws, &data); err != nil { fmt.Println("can't receive") break } err = json.Unmarshal(data, &userMsg) fmt.Println(userMsg) go wsHandler(ws,userMsg) } }func wsHandler(ws *websocket.Conn,userMsg UserMsg) { sockCli := ClientConn{ws} var err error redisClient := redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", // no password set DB: 0, // use default DB }) //登录 if userMsg.Cmd == "login" { fmt.Println("login") //判断房间人数是否已满 checkNumTmp := redisClient.SCard(userMsg.Room) checkNum := checkNumTmp.Val() if(checkNum < max_room_num) { fmt.Println("checkNum success") //socket用户列表新增当前用户websocket连接 ActiveClients[userMsg.Uuid] = sockCli //用户uuid保存到redis房间set集合内 redisClient.SAdd("ROOM:"+userMsg.Room,userMsg.Uuid) var me UserInfo me.User = userMsg.User me.AvatarUrl = userMsg.AvatarUrl me.Uuid = userMsg.Uuid //生成用户信息json串 b, err := json.Marshal(me) if err != nil { fmt.Println("Encoding User Faild") } else { //保存用户信息到redis redisClient.Set("USER:"+me.Uuid,b,0) //初始化用户 initOnlineMsg(redisClient,userMsg) } } else { var rm ReplyMsg rm.Room = userMsg.Room rm.Cmd = "loginFailed" rm.Data = "登录失败,人数已满" sendMsg,err2 := json.Marshal(rm) sendMsgStr := string(sendMsg) fmt.Println(sendMsgStr) if err2 != nil { } else { if err = websocket.Message.Send(ws, sendMsgStr); err != nil { log.Println("Could not send UsersList to ", userMsg.User, err.Error()) } } } //准备 } else if userMsg.Cmd == "ready" { redisClient.Set("READY:"+userMsg.Uuid,"ready",0) //从redis取房间内的所有用户uuid roomSlice := redisClient.SMembers("ROOM:"+userMsg.Room) //用户uuid保存到一个go切片online online := roomSlice.Val() i := 0 //循环取在线用户个人信息 if len(online) != 0 { for _, na := range online { if na != "" { userJson := redisClient.Get("READY:"+na) userJson2 := userJson.Val() if userJson2 == "ready" { i++ } } } } if i == len(online) && i == max_room_num { var rm ReplyMsg rm.Room = userMsg.Room rm.Cmd = "start" rm.Data = "" broadcast(redisClient,userMsg,rm) } //退出 } else if userMsg.Cmd == "logout" { fmt.Println("logout") //socket用户列表删除该用户websocket连接 delete(ActiveClients,userMsg.Uuid) //从redis房间set集合内删除该用户uuid redisClient.SRem("ROOM:"+userMsg.Room,userMsg.Uuid) //初始化用户 initOnlineMsg(redisClient,userMsg) //出拳 } else if userMsg.Cmd == "guess" { var result string fmt.Println("guess") fmt.Println(userMsg.HandNum) fmt.Println(userMsg.GuessNum) myHandNum,_ := strconv.Atoi(userMsg.HandNum) myGuessNum,_ := strconv.Atoi(userMsg.GuessNum) redisClient.Set("HANDNUM:"+userMsg.Uuid,myHandNum,0) redisClient.Set("GUESSNUM:"+userMsg.Uuid,myGuessNum,0) //从redis取房间内的所有用户uuid roomSlice := redisClient.SMembers("ROOM:"+userMsg.Room) //用户uuid保存到一个go切片online online := roomSlice.Val() i := 0 //循环取在线用户 if len(online) != 0 { for _, na := range online { if na != "" { handnumCmd := redisClient.Get("HANDNUM:"+na) handnum := handnumCmd.Val() if handnum != "" { i++ } } } } //房间内所有人都已提交,则计算最后结果 if i == len(online) && i == max_room_num { var handRecordList map[string]string handRecordList = make(map[string]string) var guessRecordList map[string]string guessRecordList = make(map[string]string) //计算正确结果currentNum currentNum := 0 //循环取在线用户 if len(online) != 0 { for _, na := range online { if na != "" { //取某用户的出拳数据,已用户名为key,存入结果map handnumCmd := redisClient.Get("HANDNUM:"+na) handnum := handnumCmd.Val() guessnumCmd := redisClient.Get("GUESSNUM:"+na) guessnum := guessnumCmd.Val() userJson := redisClient.Get("USER:"+na) userJson2 := userJson.Val() var user UserInfo json.Unmarshal([]byte(userJson2), &user) handRecordList[user.User] = handnum guessRecordList[user.User] = guessnum //计算结果 thandnum,_ := strconv.Atoi(handnum) currentNum = currentNum + thandnum } } } //给各个用户发送结果消息 if len(online) != 0 { for _, na := range online { if na != "" { guessnumCmd := redisClient.Get("GUESSNUM:"+na) guessnum := guessnumCmd.Val() tguessnum ,_ := strconv.Atoi(guessnum) if tguessnum == currentNum { result = "1" } else { result = "0" } var guessResult GuessResult guessResult.Result = result guessResult.CurrentNum = currentNum guessResult.HandRecord = handRecordList guessResult.GuessRecord = guessRecordList resultTmp,_ := json.Marshal(guessResult) resultData := string(resultTmp) //删除用户准备状态 redisClient.Del("READY:"+na) //删除用户猜拳数据 redisClient.Del("HANDNUM:"+na) redisClient.Del("GUESSNUM:"+na) var rm ReplyMsg rm.Room = userMsg.Room rm.Cmd = "result" rm.Data = resultData sendMsg,_ := json.Marshal(rm) sendMsgStr := string(sendMsg) if err = websocket.Message.Send(ActiveClients[na].websocket, sendMsgStr); err != nil { log.Println("Could not send UsersList to ", "", err.Error()) } } } } } //发消息 } else { /* //从redis取房间内的所有用户uuid roomSlice := redisClient.SMembers(userMsg.Room) //用户uuid保存到一个go切片online online := roomSlice.Val() //循环给房间内用户发送消息 if len(online) != 0 { for _, na := range online { if na != "" { //ActiveClients[na].websocket就是用户对应的websocket链接 if err = websocket.Message.Send(ActiveClients[na].websocket, userMsg.User+"说:"+userMsg.Content); err != nil { log.Println("Could not send message to ", userMsg.User, err.Error()) } } } }*/ }}//房间成员初始化,有人加入或者退出都要重新初始化,相当于聊天室的在线用户列表的维护func initOnlineMsg(redisClient *redis.Client,userMsg UserMsg) { var err error //从redis取房间内的所有用户uuid roomSlice := redisClient.SMembers("ROOM:"+userMsg.Room) //用户uuid保存到一个go切片online online := roomSlice.Val() var onlineList []string //循环取在线用户个人信息 if len(online) != 0 { for _, na := range online { if na != "" { userJson := redisClient.Get("USER:"+na) userJson2 := userJson.Val() onlineList = append(onlineList,userJson2) } } } fmt.Println("get online success") //生成在线用户信息json串 //c, err := json.Marshal(onlineList) onlineListStr,err2 := json.Marshal(onlineList) var rm ReplyMsg rm.Room = userMsg.Room rm.Cmd = "init" rm.Data = string(onlineListStr) sendMsg,err2 := json.Marshal(rm) sendMsgStr := string(sendMsg) fmt.Println("init") if err2 != nil { } else { //给所有用户发初始化消息 if len(online) != 0 { for _, na := range online { if na != "" { if err = websocket.Message.Send(ActiveClients[na].websocket, sendMsgStr); err != nil { log.Println("Could not send UsersList to ", "", err.Error()) } } } } //若房间人数满,发送就绪消息 if len(online) >= max_room_num { fmt.Println("full") var rm ReplyMsg rm.Room = userMsg.Room rm.Cmd = "full" rm.Data = "" sendMsg,_ := json.Marshal(rm) sendMsgStr := string(sendMsg) for _, na := range online { if na != "" { if err = websocket.Message.Send(ActiveClients[na].websocket, sendMsgStr); err != nil { log.Println("Could not send UsersList to ", "", err.Error()) } } } } } }//广播消息func broadcast(redisClient *redis.Client,userMsg UserMsg,rm ReplyMsg) { var err error //从redis取房间内的所有用户uuid roomSlice := redisClient.SMembers("ROOM:"+userMsg.Room) //用户uuid保存到一个go切片online online := roomSlice.Val() sendMsg,err2 := json.Marshal(rm) sendMsgStr := string(sendMsg) fmt.Println("broadcast") if err2 != nil { } else { //给所有用户发消息 if len(online) != 0 { for _, na := range online { if na != "" { if err = websocket.Message.Send(ActiveClients[na].websocket, sendMsgStr); err != nil { log.Println("Could not send UsersList to ", "", err.Error()) } } } } }}func main() { http.Handle("/echo", websocket.Handler(echoHandler)) http.Handle("/", http.FileServer(http.Dir("."))) err := http.ListenAndServe(":8929", nil) if err != nil { panic("ListenAndServe: " + err.Error()) }}小程序代码:app.js
//app.jsApp({ onLaunch: function () { console.log("App生命周期函数——onLaunch函数"); }, checkSession:function(mysessionid) { return new Promise(function(resolve, reject) { wx.request({ url: 'https://xxx.xxxxx.com/check.php', header: { sessionid:mysessionid }, success: function(res) { console.log("检查sessionid是否有效") resolve(res.data) }, fail: function(e) { reject(e) } }) }) }, login:function() { return new Promise(function(resolve, reject) { wx.login({ success: function (res0) { if (res0.code) { wx.request({ url: 'https://xxx.xxxxx.com/login.php', data: { code: res0.code }, header: { 'content-type': 'application/json' }, success: function(res) { console.log("取得新的sessionid") console.log(res.data) var mysessionid = res.data.k wx.setStorageSync("mysessionid",mysessionid) var myuuid = res.data.v wx.setStorageSync("myuuid",myuuid) resolve(mysessionid) }, fail: function(e) { reject(e) } }) } } }) }) }, getWxUserInfo:function() { return new Promise(function(resolve, reject) { wx.getUserInfo({ withCredentials: false, success: function(res) { console.log("取得新的userInfo") var userInfo = res.userInfo wx.setStorageSync("userInfo",userInfo) console.log("setUserInfo") resolve(userInfo) } }) }) }, getUserInfo:function() { var that = this return new Promise(function(resolve, reject) { var mysessionid = wx.getStorageSync('mysessionid') if(mysessionid) { console.log("sessionid存在") that.checkSession(mysessionid).then(function(sessionContent){ if(sessionContent == 0) { console.log("sessionid无效-取userInfo存到本地") that.login().then(function(){ that.getWxUserInfo().then(function(userInfo){ resolve(userInfo) }) }) } else { console.log("sessionid有效-直接取本地userInfo") var userInfo = wx.getStorageSync("userInfo") resolve(userInfo) } }) } else { console.log("sessionid不存在,重新走登录流程") that.login().then(function(){ that.getWxUserInfo().then(function(userInfo){ resolve(userInfo) }) }) } }) }, globalData:{ userInfo:null, onlineList:[], onlineStatus:false, myHandNum:0, myGuessNum:0 }})
page/index.js
//index.js//获取应用实例var app = getApp()Page({ data: { userInfo: {}, onlineList:{}, status:0, statusStr:"等待中", guessBoxStatus:"hideBox", handList:['0','1','2','3','4','5'], handStyleList:['primary','default','default','default','default','default'], guessList:['0','1','2','3','4','5','6','7','8','9','10'], guessStyleList:['primary','default','default','default','default','default','default','default','default','default','default'], buttonList:['0','1','2'], buttonStrList:['准备','开始','提交'], buttonStyleList:['btnShow','btnHide','btnHide'], buttonFuncList:['ready','start','guess'] }, onLoad: function () { console.log("Page onLoad函数"); wx.playBackgroundAudio({ dataUrl: 'https://xxx.xxxxx.com/8585.mp3', title: '古琴音效', coverImgUrl: 'https://xxx.xxxxx.com/logo.png', success: function() { console.log("播放音效") } }) }, onHide: function() { console.log('发送注销消息') var myuuid = wx.getStorageSync('myuuid') var msg = new Object(); msg.Room = '1'; msg.Cmd = 'logout'; msg.Uuid = myuuid; var str = JSON.stringify(msg) wx.sendSocketMessage({ data:str }) wx.closeSocket() app.globalData.onlineStatus = false }, onShow: function() { var that = this app.getUserInfo().then(function(userInfo){ that.setData({ userInfo:userInfo }) that.wsHandler(userInfo) that.initBox() }) }, wsHandler: function(userInfo) { var that = this //websocket wx.connectSocket({ url: 'wss://xx.xxxxx.com/echo' }) wx.onSocketOpen(function(res) { console.log('WebSocket连接已打开!') var myuuid = wx.getStorageSync('myuuid') var msg = new Object(); msg.Room = '1'; msg.Cmd = 'login'; msg.User = userInfo.nickName; msg.AvatarUrl = userInfo.avatarUrl; msg.Uuid = myuuid; var str = JSON.stringify(msg) wx.sendSocketMessage({ data:str }) }) wx.onSocketMessage(function(res) { var msg = JSON.parse(res.data) if(msg.Cmd == 'init') { var userList = JSON.parse(msg.Data) app.globalData.onlineList = [] for(var i=0;i<userList.length;i++){ var user = JSON.parse(userList[i]) app.globalData.onlineList.push(user) } that.setData({ onlineList:app.globalData.onlineList, status:0, statusStr:'等待中' }) } if(msg.Cmd == 'full') { that.setData({ status:1, statusStr:'准备开始' }) } if(msg.Cmd == 'result') { var result = JSON.parse(msg.Data) var content = "总数为"+result.CurrentNum+"n" for (var value in result.HandRecord) { content = content+value+"出拳:"+result.HandRecord[value]+"n"; } for (var value in result.GuessRecord) { content = content+value+"猜拳:"+result.GuessRecord[value]+"n"; } if(result.Result == 1) { content = "恭喜你,猜中啦n" + content wx.showModal({ content: content, showCancel: false, success: function (res) { if (res.confirm) { that.initBox() } } }); } if(result.Result == 0) { content = "很遗憾,猜错啦n" + content wx.showModal({ content: content, showCancel: false, success: function (res) { if (res.confirm) { that.initBox() } } }); } } if(msg.Cmd == 'start') { that.setData({ status:2, statusStr:'游戏中', guessBoxStatus:'showBox', buttonStyleList:['btnHide','btnHide','btnShow'], }) } }) }, setHandNum: function(event) { var that = this console.log(event.target.dataset.handnum) app.globalData.myHandNum = event.target.dataset.handnum var myList = that.data.handStyleList for(var i=0;i<myList.length;i++) { if(i == event.target.dataset.handnum) { myList[i] = 'primary' } else { myList[i] = 'default' } } that.setData({ handStyleList:myList }) }, setGuessNum: function(event) { var that = this console.log(event.target.dataset.guessnum) app.globalData.myGuessNum = event.target.dataset.guessnum var myList = that.data.guessStyleList for(var i=0;i<myList.length;i++) { if(i == event.target.dataset.guessnum) { myList[i] = 'primary' } else { myList[i] = 'default' } } that.setData({ guessStyleList:myList }) }, guess: function() { var that = this var userInfo = that.data.userInfo var myuuid = wx.getStorageSync('myuuid') var msg = new Object(); msg.Room = '1'; msg.Cmd = 'guess'; msg.User = userInfo.nickName; msg.AvatarUrl = userInfo.avatarUrl; msg.Uuid = myuuid; msg.HandNum = app.globalData.myHandNum msg.GuessNum = app.globalData.myGuessNum var str = JSON.stringify(msg) wx.sendSocketMessage({ data:str }) }, ready: function() { var that = this var userInfo = that.data.userInfo var myuuid = wx.getStorageSync('myuuid') var msg = new Object(); msg.Room = '1'; msg.Cmd = 'ready'; msg.User = userInfo.nickName; msg.AvatarUrl = userInfo.avatarUrl; msg.Uuid = myuuid; var str = JSON.stringify(msg) wx.sendSocketMessage({ data:str }) that.setData({ status:1, statusStr:'等待对手,准备开始', buttonStyleList:['btnHide','btnHide','btnHide'], }) }, start: function() { var that = this var userInfo = that.data.userInfo var myuuid = wx.getStorageSync('myuuid') var msg = new Object(); msg.Room = '1'; msg.Cmd = 'start'; msg.User = userInfo.nickName; msg.AvatarUrl = userInfo.avatarUrl; msg.Uuid = myuuid; var str = JSON.stringify(msg) wx.sendSocketMessage({ data:str }) }, initBox: function() { var that = this that.setData({ status:0, statusStr:"等待中", guessBoxStatus:"hideBox", handList:['0','1','2','3','4','5'], handStyleList:['primary','default','default','default','default','default'], guessList:['0','1','2','3','4','5','6','7','8','9','10'], guessStyleList:['primary','default','default','default','default','default','default','default','default','default','default'], buttonList:['0','1','2'], buttonStrList:['准备','开始','提交'], buttonStyleList:['btnShow','btnHide','btnHide'], buttonFuncList:['ready','start','guess'] }) }, getAudioStatus: function() { wx.getBackgroundAudioPlayerState({ success: function(res) { var status = res.status var dataUrl = res.dataUrl var currentPosition = res.currentPosition var duration = res.duration var downloadPercent = res.downloadPercent console.log("音乐状态"+status) console.log("音乐长度"+duration) } }) }})
ps:播放音乐的功能,在开发工具可以看到,真机上没有听到声音,暂时还没找到解决办法代码的健壮性,页面效果还需要再优化
check.php
<?php$post_data = $_POST;$header = get_all_headers();$sessionid = $header['sessionid'];$host = '127.0.0.1';$port = '6379';$timeout = 0;$redis = new Redis();$redis->connect($host, $port, $timeout);$session_content = $redis->get("miniappsession:".$sessionid);if($session_content){ echo $session_content;} else { echo 0;}/** * 获取自定义的header数据 */function get_all_headers(){ // 忽略获取的header数据 $ignore = array('host','accept','content-length','content-type'); $headers = array(); foreach($_SERVER as $key=>$value){ if(substr($key, 0, 5)==='HTTP_'){ $key = substr($key, 5); $key = str_replace('_', ' ', $key); $key = str_replace(' ', '-', $key); $key = strtolower($key); if(!in_array($key, $ignore)){ $headers[$key] = $value; } } } return $headers;}
login.php
<?php$code = $_GET['code'];define("APPID",'xxxxxxxxxxxxxxxxxxxxx');define("SECRET",'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx');$url = "https://api.weixin.qq.com/sns/jscode2session?appid=".APPID."&secret=".SECRET."&js_code=".$code."&grant_type=authorization_code";$rs = curlGet($url);$arr = json_decode($rs);$str = randomFromDev(32);$host = '127.0.0.1';$port = '6379';$timeout = 0;$redis = new Redis();$redis->connect($host, $port, $timeout);$expires_time = 15*24*60*60;$session_content = md5($arr->openid.$expires_time);$redis->setex("miniappsession:".$str,$expires_time,$session_content);$sessionObj['k'] = $str;$sessionObj['v'] = $session_content;echo json_encode($sessionObj);function randomFromDev($len){ $fp = @fopen('/dev/urandom','rb'); $result = ''; if ($fp !== FALSE) { $result .= @fread($fp, $len); @fclose($fp); } else { trigger_error('Can not open /dev/urandom.'); } $result = md5($result); // convert from binary to string //$result = base64_encode($result); // remove none url chars //$result = strtr($result, '+/', '-_'); // Remove = from the end //$result = str_replace('=', ' ', $result); return $result;}function curlGet($url, $method = 'get', $data = ''){ $ch = curl_init(); $header = 'Accept-Charset: utf-8'; curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, strtoupper($method)); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_HTTPHEADER, $header); curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (compatible; MSIE 5.01; Windows NT 5.0)'); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); curl_setopt($ch, CURLOPT_AUTOREFERER, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $data); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $temp = curl_exec($ch); return $temp;}