服务端与客户端的交流,需要一套指令集,客户端发送的指令要与服务端一致,这样服务端才能理解客户端的意思。
——茂叔
上一篇,我们已经在客户端与服务端之间成功的建立了连接。
那么我们应该为客户端与服务端之间的通信设计一套规则,也就是双方都能听得懂的语言。
指令集
这里,我们采用指令+数据
的形式来定义一个类,在type.cs
里面定义:
public class CommunicationObject
{
public GameCommand command;
public string data;
public CommunicationObject(GameCommand command, string data)
{
this.command = command;
this.data = data;
}
public override string ToString()
{
return JObject.FromObject(this).ToString();
}
}
其中的GameCommand
是我们的指令集枚举,其定义如下:
public enum GameCommand : byte
{
ERROR = 0,
CONNECTED = 1,
LOGIN,
MOVE,
ATTACK,
SEARCH,
TAKE,
QUIT
}
目前暂时就这些指令,将来会不断扩充。
客户端也应该有一套完全一样的指令集,以便双方能听懂对方的意思。
我们可以直接在客户端去定义一个类似的“枚举”,尽管javascript其实没有“枚举”这个东西。
但是这样一来,每次我们扩充或者修改了指令集,就必须在两端同时修改定义,这样不仅麻烦,而且容易出错。
不如我们每次都让客户端从服务端下载指令集?
也就是说,每次客户端连接成功之后,服务端就把枚举的定义Json化,然后下发给客户端。
这样我们只需要维护服务端的指令集就可以了。
关于C#
的Json
操作,我选择使用Newtonsoft.json
。
修改GameConnection
的构造方法如下:
public GameConnection(string IP, WebSocket mSocket, Action<string> LOGFUN = null)
{
this.IP = IP;
this.mSocket = mSocket;
LOG = LOGFUN == null ? x => { } : LOGFUN;
LOG("来自 " + IP + " 连接建立成功");
JObject ISJson = JObject.FromObject(Enum.GetValues(typeof(GameCommand))
.Cast<GameCommand>()
.ToDictionary(x => x.ToString(), x => (int)x));
CommunicationObject FirstMsg = new CommunicationObject( GameCommand.CONNECTED, ISJson.ToString());
ArraySegment<byte> segment = new ArraySegment<byte>(Encoding.UTF8.GetBytes(FirstMsg.ToString()));
mSocket.SendAsync(segment, WebSocketMessageType.Text, true, CancellationToken.None);
ReceiveDataAsync();
}
然后再到微信web开发者工具里面去把game.js
内容修改如下:
wx.connectSocket({
url: 'ws://localhost:666/',
method: "GET",
success(res){
console.log("Socket successed:")
console.log(res)
wx.onSocketOpen(
(res) => {
console.log("Socket opened:")
console.log(res)
}
)
wx.onSocketClose(
(res) => {
console.log("Socket closed:")
console.log(res)
}
)
wx.onSocketError((res) => {
console.log("Socket error:")
console.log(res)
})
wx.onSocketMessage((res)=>{
var msg = JSON.parse(JSON.parse(res.data).data);
console.log("got message:")
console.log(msg)
})
},
fail(res){
console.log("Socket failed:")
console.log(res)
}
})
调试一下,先启动服务端,再启动客户端,客户端结果如下:
看,我们已经成功同步了指令集,以后只要ERROR
和CONNECTED
的指定值不变,添加删除修改其他值就不必考虑客户端的同步定义问题了。
授权与登录
客户端
指令集弄好了,那么,客户端连上以后我们就可以让用户授权客户端使用用户的公开资料,比如昵称、头像什么的……还有就是需要获取用户的登录状态,并获得微信分配我们该用户在这个小游戏的唯一标识openid
,我们用这个openid
可以直接创建用户,所以就不需要做用户注册功能了。
微信登录的机制是客户端先调用微信API,获得一个叫code的东西,然后我们在服务器端用这个code去取得用户的登录状态和openid
。
所以,客户端启动后应该先判断用户是否已经授权读取基本资料,如果没有授权,就创建授权按钮让用户授权。如果已经授权,就直接获取用户的登录code然后发送给服务端进行登录。
具体流程如下:
在微具(微信web开发者工具)里面删除之前在game.js
里面的全部测试代码。在根目录下新建global.js
和net.js
两个文件。
首先在global.js
里面导出一个initialize()
,用于生成全局变量信息。微信小游戏自带一个GameGlobal
的全局变量,我们自定义的全局变量可以直接创建为GameGlobal
成员,引用时不需要写GameGlobal
。
exports.initialize = () => {
GameGlobal._g = {
COMMAND: undefined,
WEBSOCKET_URL : 'ws://localhost:666/',
DEVICEWIDTH: undefined,
DEVICEHEIGHT:undefined
}
}
在net.js
里面导出initialize()
,定义相关的函数,代码如下:
exports.initialize = () => {
wx.connectSocket({
url: _g.WEBSOCKET_URL,
method: "GET",
success(res) {
console.log("Socket successed:")
console.log(res)
wx.onSocketOpen(
(res) => {
console.log("Socket opened:")
console.log(res)
}
)
wx.onSocketClose(
(res) => {
console.log("Socket closed:")
console.log(res)
}
)
wx.onSocketError((res) => {
console.log("Socket error:")
console.log(res)
})
wx.onSocketMessage((res) => {
console.log(res)
parseMessage(JSON.parse(res.data))
})
},
fail(res) {
console.log("Socket failed:")
console.log(res)
}
})
}
function parseMessage(msg) {
if (msg.command == 1) //这是初次连接成功传回的结果
{
_g.COMMAND = JSON.parse(msg.data);
console.log(_g.COMMAND)
login()
} else {
switch (msg.command) //其他命令的处理结果
{
}
}
}
function login() {
wx.login({
success(res) {
var data = {
command: _g.COMMAND.LOGIN,
data: res.code
}
wx.sendSocketMessage({
data: JSON.stringify(data)
})
}
})
}
最后修改我们的game.js
如下:
var globaldata = require('global.js')
var net = require('net.js')
globaldata.initialize()
wx.getSystemInfo({
success(res) {
_g.DEVICEWIDTH = res.windowWidth;
_g.DEVICEHEIGHT = res.windowHeight;
wx.getSetting({
success(res) {
if (!res.authSetting['scope.userInfo']) {
let button = wx.createUserInfoButton({
type: 'text',
text: '请允许我们使用您的基本资料',
style: {
left: _g.DEVICEWIDTH/2-150,
top: 200,
width: 300,
height: 40,
lineHeight: 40,
backgroundColor: '#ff0000',
color: '#ffffff',
textAlign: 'center',
fontSize: 16,
borderRadius: 4
}
})
button.onTap((res) => {
if (res.errMsg == "getUserInfo:ok")
{
button.hide()
net.initialize()
}
})
}
else{
net.initialize()
}
}
})
}
})
测试一下,可以看到授权按钮,点击后会出现授权对话框,如果选择允许,服务端可以收到登录code。
客户端:
服务端:
服务端
服务端收到登录code后,需要调用微信的开放接口auth.code2Session
来获取用户的openid
。该接口的调用方式为:
GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
把我们的appId
、appSecret
和code
带入即可。
首先,我们需要为我们的GameConnection
类定义一个事件,用于在接收到客户端信息后的处理工作。
在type.js
文件里面定义事件代理:
public delegate void ClientCommandHandler(object sender,GameCommand cmd, string data);
在GameConnection.cs
里面去为GameConnection
添加事件:
public event ClientCommandHandler onClientCommand;
然后在接收到客户端信息后,触发这个事件。
……
ArraySegment<byte> receivebuff = new ArraySegment<byte>(new byte[1024]);
result = await mSocket.ReceiveAsync(receivebuff, CancellationToken.None);
string receiveStr = Encoding.UTF8.GetString(receivebuff.ToArray(), 0, result.Count);
LOG("收到来自 " + IP + " 的信息:" + receiveStr);
JObject jobject = JObject.Parse(receiveStr);
onClientCommand(this,(GameCommand)byte.Parse(jobject["command"].ToString()), jobject["data"].ToString());
……
然后我们到GameServer
类里面去写这个事件的方法,并绑定给每个GameConnection
:
……
GameConnection connection = new GameConnection(request.UserHostAddress, ret.Result.WebSocket, LOG);
connection.onClientCommand +=new ClientCommandHandler(ParseClientCommand);
……
private void ParseClientCommand(object connection,GameCommand cmd, string data)
{
switch (cmd)
{
case GameCommand.ERROR:
break;
case GameCommand.CONNECTED:
break;
case GameCommand.LOGIN:
{
string requeststr = "https://api.weixin.qq.com/sns/jscode2session?appid=" + G.AppID + "&secret=" + G.Secret + "&js_code=" + data + "&grant_type=authorization_code";
HttpWebRequest wxRequest = (HttpWebRequest)WebRequest.Create(requeststr);
wxRequest.Method = "GET";
wxRequest.Timeout = 6000;
HttpWebResponse wxResponse = (HttpWebResponse)wxRequest.GetResponse();
Stream stream = wxResponse.GetResponseStream();
StreamReader reader = new StreamReader(stream);
string res = reader.ReadToEnd();
stream.Close();
reader.Close();
LOG(res);
}
break;
case GameCommand.MOVE:
break;
case GameCommand.ATTACK:
break;
case GameCommand.SEARCH:
break;
case GameCommand.TAKE:
break;
case GameCommand.QUIT:
break;
default:
break;
}
}
……
我把小游戏的appId
、appSecret
都定义在全局变量文件里面了,就是GameLib
里面G.cs
文件里面:
public static string AppID= "wx6748....";
public static string Secret = ".......";
记得换成你自己的哦~!
调试一下,我们会在服务端的日志里面看到效果。
嗯,这样我们就成功获得了用户的openid
了,在这个过程中,我们初步搭建起了客户端与服务器的通信架构,接下来的开发就方便多了。
下一步,我们要为用户创建账号信息了,关于创建账号的策略,我们下一篇再来讨论。
截止这一篇的全部代码,请大家去我的GitHub下载:
>>第十篇代码连接<<