分类 江西小程序 下的文章

从零开始打造一个报名小程序

微信小程序是一种全新的连接用户与服务的方式,它可以在微信内被便捷地获取和传播,同时具有出色的使用体验。

优势

1、对用户使用上来说,确实方便,要用的时候打开,不用的时候关掉,即用即走。这点比需要下载,还要占用手机内存空间的APP要好。

2、主要的样式代码都封装在微信小程序里面,所以打开速度比普通的H5要快,接近原生APP。

3、可以调用比H5更多的手机系统功能来进行开发,例如GPS定位、录音、拍视频、重力感应等,能开发更丰富的使用场景。

4、在安卓手机上可以添加到手机桌面,看上去跟原生APP差不多,但仅限安卓手机,iphone就不行了。

5、运行速度跟APP差不多,也能做出很多H5不做到的功能,开发成本跟H5差不多,相对来说开发成本比APP要低。

劣势

1、微信小程序只有2M的大小,这样导致无法开发大型一些的小程序。所以目前你会看到很多小程序真的很小很简单。

2、小程序的技术框架还不稳定,开发方法时常有修改,导致短时间内经常要升级维护,或许这能解析为什么小程序只能2M大小,怕部署太大型的项目会出大问题。

3、不能跳转外链网址,所以间接影响了小程序的开放性。也可能是想限制其他支付方式或功能接入(或许是我想多了)。

4、不能直接分享到朋友圈,哎呀,少了一个重要的推广方式。

5、需要像APP一样审核上架,这点比HTML5即做即发布要麻烦些。

微信小程序入口在哪里?相信大家都已经使用过摩拜单车的小程序了。。。
小程序入口

小程序既不是原生开发,也不是纯html5网页,是以混合hybird的方式展示

小程序API一览

https://mp.weixin.qq.com/debug/wxadoc/dev/api/

更多教程看官方文档:https://mp.weixin.qq.com/debug/wxadoc/dev/

介绍完毕,正式开工

注册小程序

现在个人也可以注册小程序了
打开微信公众平台的登录页:https://mp.weixin.qq.com/
立即注册
TIM图片20170824170511.png
用个人邮箱就可以注册
注册完成后,登录进入后台
进入设置找到小程序的AppId,为下一步做好准备
QQ图片20170824220635.png

下载微信开发者工具

针对公众号和小程序,微信推出了“开发者工具”,集成了开发调试、代码编辑及程序发布等功能。
https://mp.weixin.qq.com/debug/wxadoc/dev/devtools/devtools.html

添加项目

打开开发者工具,选择“本地小程序项目”,添加项目
QQ图片20170824221155.png

输入AppId和项目名,选择一个本地目录作为项目目录
勾选上quick_start会在目录下新建一些示例代码
QQ图片20170824221330.png

点击添加项目以后,可以看到示例的hello world
QQ图片20170824221706.png

系统分析

我们看到示例取到了用户的头像和昵称,
我们现在要实现一个简单的报名功能,
进入小程序,就展示已报名的用户头像和昵称,
未报名的用户,点击报名,记录报名用户的头像和昵称,存入数据库

那么如何与数据库进行交互呢?
接口!
用小程序的api-【wx.request】请求事先准备好的接口
例如:
接口1:获取已报名用户列表接口(GET)
https://api.keyunq.com/getUsers.php
接口2:用户报名接口(POST)(返回0:已经报过名了,其他整数:报名成功)
https://api.keyunq.com/addUser.php

在小程序加载时,调接口1
用户点击报名时,调接口2

报名操作

在示例页面上,新建一个报名按钮
在开发者工具上,打开pages/index/index.wxml首页的页面结构文件
将显示hello world的地方,改成一个报名按钮

<text class="user-motto">{{motto}}</text>

改为

<button type="primary" bindtap="baoming"> {{motto}} </button>

bindtap就是绑定事件处理函数,motto是在data里定义的数据,
关于数据绑定这块,小程序是单向数据绑定,和react类似
通过setData改变数据,同时会刷新页面,展示新的数据

data: {
    motto: '我要报名',
    userInfo: {}
},

然后在index.js文件Page定义里,编写这个函数

baoming: function () {
    console.log('baoming')
}

保存一下,可以在调试界面看到,点击报名,打印出来了baoming字符串
进一步完善,用wx.request调用接口

baoming: function () {
    var that = this
    let avatar = app.globalData.userInfo.avatarUrl
    let name = app.globalData.userInfo.nickName
    wx.request({
      url: 'https://api.keyunq.com/addUser.php',
      data: {
        avatar: avatar ,
        name: name
      },
      method: 'POST',
      header: {
          'content-type': 'application/x-www-form-urlencoded'
      },
      success: function(res) {
        if(res.data == 0) {
          that.setData({
            motto:'您已经报过名了,基佬!'
          })
        } else {
          wx.navigateTo({
            url: '../index/index'
          })
        }
      }
    })
  }

报错"不在以下合法域名列表中",需要在后台配置服务器信息
将接口域名配置为request合法域名,
QQ图片20170824231319.png

再回到开发者工具,项目-》配置信息那刷新一下
QQ图片20170824234840.png

展示已报名人员

上一步实现了报名操作,现在把已报名的人员列表展示出来
在Page的onLoad函数里,调用接口2,读取数据,展示出来

新增data定义baomingList,报名用户列表数据

data: {
    motto: '我要报名',
    userInfo: {},
    baomingList: {}
  },

onLoad函数,后期为了用户体验,把onLoad改为了onShow函数

onLoad: function () {
    console.log('onLoad')
    var that = this
    //调用应用实例的方法获取全局数据
    app.getUserInfo(function(userInfo){
      //更新数据
      that.setData({
        userInfo:userInfo
      })
    })
    wx.request({
      url: 'https://api.keyunq.com/getUsers.php',
      header: {
        'content-type': 'application/x-www-form-urlencoded'
      },
      success: function (res) {
        if(res.data) {
          that.setData({
            baomingList: res.data
          })
        }
      }
    })
  },

调用接口2,然后setData到baomingList
然后在view视图文件index.wxml,输出baomingList

<!--index.wxml-->
<view class="container">
  <view class="">已经报名的基佬:</view>
  <view class="page__bd page__bd_spacing">
    <view class="weui-grids">
      <view class="weui-grid text_center" wx:for="{{baomingList}}" wx:key="key"><image class="userinfo-avatar" src="{{item.avatar}}" background-size="cover"></image><view class="weui-grid__label">{{item.name}}</view></view>
    </view>
  </view>
  <view class="usermotto">
    <button type="primary" bindtap="baoming"> {{motto}} </button>
  </view>
</view>

一些样式,app.wxss

/**app.wxss**/
@import "dist/style/weui.wxss";
.container {
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  box-sizing: border-box;
} 

这里我引入了weui的样式,下载weui的样式文件,放在了dist目录
首页的样式文件index.wxss

/**index.wxss**/
.userinfo {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.userinfo-avatar {
  width: 128rpx;
  height: 128rpx;
  margin: 20rpx;
  border-radius: 50%;
}

.userinfo-nickname {
  color: #aaa;
}

.usermotto {
  margin-top: 20px;
}

.grid {
  display: flex;
}
.grid-item {
  display: inline;
}
.page__bd {
  width: 100%;
}

调试没什么问题后,可以提交到真机预览
点开发者工具-》项目-》基础信息-》预览
用微信扫描二维码,即可进行预览,效果图如下
TIM图片20170825151132.png

要邀请其他人一起体验的话,点击预览下面的上传按钮
上传后会生成一个体验版本,在微信官方后台-》开发管理,设置一下,就会生成一个体验版的二维码
然后在用户身份-》成员管理处,添加成员,邀请进行体验

江西小程序-打造一款简单猜拳小程序(go+websocket+redis+mysql+小程序前端)(五)

本节思路:
基于本系列第二节内容,实现一个websocket服务端,redis为存储介质,以自己约定的cmd命令为简单通讯协议,与小程序进行通讯,实现简单的2人猜拳功能

cmd命令按照如下流程:
login(client->server),小程序发给服务端,登录命令,服务端接收命令后,判断房间人数是否已满,未满则保存客户端websocket连接到列表,客户端用户信息存入redis,更新在线用户列表,然后发送init(server->client)命令,通知所有在线用户,更新在线用户列表。

当客户端点击准备按钮,发送ready(client->server)命令到服务端,服务端更新该用户的ready标识位(存于redis),计算房间人数是否已满,以及房间所有用户是否都已设置ready标识位,如是,则发送start(server->client)命令给所有用户,开始游戏。

客户端选择好自己的出拳信息,发送guess(client->server)命令到服务端,服务端记录该用户出拳数据,判断房间内所有人都以提交guess命令,则计算最后结果,发送result(server->client)命令给所有用户。

客户端接收到result命令后,重新进入ready流程。

如退出小程序,客户端发送logout(client->server)命令到服务端,服务端从列表中删除该用户,重新发送init(server->client)命令到所有其他在线用户,更新在线用户列表。

命令流程如下:
login(client->server)
init(server->client)
ready(client->server)
start(server->client)
guess(client->server)
result(server->client)
logout(client->server)

效果示意图:
login
QQ图片20170425143225.png

ready
QQ图片20170425143331.png

start
QQ图片20170425143419.png

result
QQ图片20170425143500.jpg

go服务端

package main
import (
    "golang.org/x/net/websocket"
    "fmt"
    "log"
    "net/http"
    "github.com/go-redis/redis"
    "encoding/json"
    "strconv"
)
const max_room_num = 2

var (  
    JSON          = websocket.JSON              // codec for JSON  
    Message       = websocket.Message           // 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.js
App({
  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;
}

源码:github
https://github.com/keyunq/ytguess

江西小程序-打造一款简单猜拳小程序(go+websocket+redis+mysql+小程序前端)(四)

本节思路
进入小程序的前端开发,验证前面构建的websocket服务能否和小程序联通。确认联通后再同步进行前后端的开发。

小程序的申请,开发者工具下载等准备工作就不具体阐述了。

打开开发者工具-选择小程序开发,
首先面临的问题就是用户登录状态的维护


小程序登录态维护实现流程

1.通过wx.login获取登录态。
2.通过登录返回的code发送给服务器,服务器用code换取session_key和openid后。
3.获取session_key后一般使用缓存框架保存登录态,服务端随机生成一串唯一字符串3rdSessionId为key,session_key为value组成键值对并存到缓存当中,缓存时间视情况自行决定。
4.将3rdSessionId返回给客户端
5.客户端将3rdSessionId缓存到localStorage中,后续接口从缓存中读取3rdSessionId,传递给服务器;服务器根据3rdSessionId来判断用户身份。
6.如果服务器根据3rdSessionId在缓存中查找是否存在session_key,如果存在正常执行;如果不存在小程序未登录,重新从第一步流程走。

以上流程是官方推荐使用的流程,并不是维护登录态的唯一途径。

另外,你也可以在小程序中使用wx.checkSession()检查登录态是否过期。如果过期重新调用wx.login接口。
session_key在微信服务器有效期是30天,建议服务端缓存session_key不超过30天。

登录时序图
login.png

而根据以上流程,将陷入多层嵌套回调,于是用promise优化流程

官方在 2017.03.28 更新日志 中 支持绝大部分的 ES6 API
其中包括了Promise

于是可以在微信小程序中使用Promise来处理登录流程

代码如下:
app.js

//app.js
App({
  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
              },
              success: function(res) {
                console.log("取得新的sessionid")
                var mysessionid = res.data
                wx.setStorageSync("mysessionid",mysessionid)
                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(sessionFlag){
          if(sessionFlag == 1) {
            console.log("sessionid有效-直接取本地userInfo")
            var userInfo = wx.getStorageSync("userInfo")
            resolve(userInfo)
          } else {
            console.log("sessionid无效-取userInfo存到本地")
            that.login().then(function(){
              that.getWxUserInfo().then(function(userInfo){
                resolve(userInfo)
              })
            })
          }
        })
        
      } else {
        console.log("sessionid不存在,重新走登录流程")
        that.login().then(function(){
          that.getWxUserInfo().then(function(userInfo){
            resolve(userInfo)
          })
        })
      }
    })
  },
  globalData:{
    userInfo:null
  }
})

index.js

//index.js
//获取应用实例
var app = getApp()
Page({
  data: {
    userInfo: {}
  },
  onLoad: function () {
    console.log("Page onLoad函数");
    var that = this
    app.getUserInfo().then(function(userInfo){
      that.setData({
        userInfo:userInfo
      })
    })
  }
})

根据console的输出内容可以看到:
第一次登录时:

QQ图片20170414115524.png

刷新页面:

QQ图片20170414115618.png

当服务器上的缓存的sessionid失效时:
QQ图片20170414115739.png

check.php(在服务器查找sessionid,可以在redis里得到session_key,本例只判断了sessionid是否存在)

<?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 1;
} 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(通过code,获取session_key和openid,并生成唯一sessionid,以sessionid为key,session_key和openid为value,存入redis,并返回sessionid给小程序)

<?php
$code = $_GET['code'];
define("APPID",'yourappid');
define("SECRET",'yoursecret');
$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 = 10*24*60*60;
$redis->setex("miniappsession:".$str,$expires_time,$arr->openid."|".$arr->session_key);

echo $str;


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);
    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;
}

测试websocket联通

登录后取得的用户昵称、用户头像,根据本系列第二节内容
我们在小程序内构造一个room=1,username=用户昵称的websocket请求,看能否连接websocket服务器

index.js

//index.js
//获取应用实例
var app = getApp()
Page({
  data: {
    userInfo: {}
  },
  onLoad: function () {
    console.log("Page onLoad函数");
    var that = this
    app.getUserInfo().then(function(userInfo){
      that.setData({
        userInfo:userInfo
      })
      //websocket
      wx.connectSocket({
        url: 'wss://xx.xxxxx.com/echo'
      })
      wx.onSocketOpen(function(res) {
        console.log('WebSocket连接已打开!')
        var msg = new Object();
        msg.Room = '1';
        msg.Cmd = 'login';
        msg.User = userInfo.nickName;
        msg.Uuid = userInfo.nickName;
        var str = JSON.stringify(msg)
        wx.sendSocketMessage({
          data:str
        })
      })

    })
  }
})

查看Network里的请求,可以看到
QQ图片20170414145616.png

证明小程序与websocket服务已经连通了

江西小程序-打造一款简单猜拳小程序(go+websocket+redis+mysql+小程序前端)(三)

本节思路
由于微信小程序的网络请求都必须走HTTPS协议,于是打算用nginx反向代理go的websocket服务,这样在go的服务端,不用处理https,提高了性能。

根据之前的文章:江西小程序-给swoole的websocket server加上ssl
我们已经得到了一个pem文件和一个key文件

配置nginx

vi /etc/nginx/conf.d/yourdomain.conf

map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}
upstream gowebsocket {
    server 127.0.0.1:8929;
}
server {
    listen       443;
    server_name  yourdomain;

    ssl on;
    ssl_certificate /usr/local/ca/xxxxxxxxxxxxx.pem;
    ssl_certificate_key /usr/local/ca/xxxxxxxxxxxxxx.key;

    #charset koi8-r;
    access_log  /var/log/nginx/log/yourdomain.access.log  main;

    location / {
        proxy_pass http://gowebsocket;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

重启nginx

江西小程序-打造一款简单猜拳小程序(go+websocket+redis+mysql+小程序前端)(二)

本节思路
1、redis的go客户端安装
2、基于redis的set集合,实现房间的概念,一个房间对应一个set集合,集合内保存该房间内用户的唯一标识
我们给每个用户生成了唯一标识uuid(后期接入微信小程序,则可以使用微信用户openid代替),于是set集合大致如下:

房间1
用户A-uuid
用户B-uuid
......
房间2
用户C-uuid
用户D-uuid
......
......

3、用户的uuid,又对应着go服务端里面的一个map

ActiveClients = make(map[string]ClientConn)

该map以用户的uuid为key,在线用户的websocket链接为value
于是在发送消息时,取到redis里某房间内所有的uuid,就可以得到对应的websocket链接,实现房间内的广播
如果限制房间内只有2个用户,则实现了一对一私聊

安装redis的go客户端

go get -u github.com/go-redis/redis

服务器端go代码:

package main
import (
    "golang.org/x/net/websocket"
    "fmt"
    "log"
    "net/http"
    "github.com/go-redis/redis"
    "encoding/json" 
)

var (  
    JSON          = websocket.JSON              // codec for JSON  
    Message       = websocket.Message           // codec for string, []byte  
    ActiveClients = make(map[string]ClientConn) // map containing clients  
    User          = make(map[string]string)
)  

type ClientConn struct {
    websocket *websocket.Conn  
}

type UserMsg struct {
    Room string
    Cmd string
    User string
    Content string
    Uuid 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") 
        //用户列表新增当前用户
        ActiveClients[userMsg.Uuid] = sockCli
        //保存到redis房间set集合内
        redisClient.SAdd(userMsg.Room,userMsg.Uuid)
    //退出
    } else if userMsg.Cmd == "logout" {
        fmt.Println("logout") 

        delete(ActiveClients,userMsg.Uuid)

        redisClient.SRem(userMsg.Room,userMsg.Uuid)

    //发消息
    } 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 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())
    }
}

为了生成唯一uuid,选择房间号和用户名,客户端代码由php实现(chat.php):

<?php
$room = $_GET["room"];
$username = $_GET["username"];
$uuid = md5(time().$username);
?>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8"/>
    <title>Sample of websocket with golang</title>
    <script src="//cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>

    <script>
      $(function() {
        var userAgent = navigator.userAgent;

        var ws = new WebSocket("ws://xxx.xx.xxx.xx:8929/echo");
        ws.onopen = function (evt) {
          //连接成功
          console.log("Connected to WebSocket server.");

          //发送登录信息
          msg = new Object();
          msg.Room = '<?php echo $room;?>';
          msg.Cmd = 'login';
          msg.User = '<?php echo $username;?>';
          msg.Uuid = '<?php echo $uuid;?>';
          ws.send(JSON.stringify(msg));
        };

        ws.onclose = function (evt) {
          
          console.log("Close WebSocket server.");

          //发送退出信息
          msg = new Object();
          msg.Room = '<?php echo $room;?>';
          msg.Cmd = 'logout';
          msg.User = '<?php echo $username;?>';
          msg.Uuid = '<?php echo $uuid;?>';
          ws.send(JSON.stringify(msg));
        };

        ws.onmessage = function(e) {
          $('<li>').text(event.data).appendTo($ul);
        };
        var $ul = $('#msg-list');
        $('#sendBtn').click(function(){

          //发送信息
          msg = new Object();
          msg.Room = '<?php echo $room;?>';
          msg.Cmd = 'send';
          msg.User = '<?php echo $username;?>';
          msg.Uuid = '<?php echo $uuid;?>';
          msg.Content = $('#name').val();
          ws.send(JSON.stringify(msg));

        });
      });
    </script>
</head>
<body>
<input id="name" type="text"/>
<input type="button" id="sendBtn" value="send"/>
<ul id="msg-list"></ul>
</body>
</html>

两个不同浏览器下,分别访问http://yourdomain/chat.php?room=1&username=keyunq1
http://yourdomain/chat.php?room=1&username=keyunq2
则可实现一对一私聊。
不完善之处:对于redis里房间的set集合,程序没有实现删除元素功能,如某用户的websocket链接关闭,set集合里未删除该用户的uuid,则发消息时,继续从set集合里取数据循环,则会发送数据到一个不存在的websocket,产生错误,此处待完善。