这篇文章为进阶内容,之前的数据传输机制已经可以很好的适用于大部分项目了。但为了更快、更清晰的传输,这篇我们来拓展学习一下Protobuf数据的传输
介绍
protobuf是google的一个开源项目,可用于以下两种用途:
(1)数据的存储(序列化和反序列化),类似于xml、json等;
(2)制作网络通信协议。
protobuf比XML、比JSON更为强悍,语言无关、平台无关、更小的存储、更少的歧义、更高的性能
同时为了更方便的保证客户端和服务器端的传输内容一致性,便于以后维护、代码阅读。我们将消息传输内容统一使用类的结构
这里我们来继续第六节内容,将下面内容以类对象形式传输
一、将客户端发送给服务器的该玩家位置信息
二、服务器发给客户端的其它玩家位置信息
分析
我们先回顾一下这两条涉及的代码内容:
第一条是客户端发给服务器,是在我们客户端SyncTransformRequest实现的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class SyncTransformRequest : Singleton<SyncTransformRequest> { //发起位置信息请求 public void SendSyncPositionRequest(Vector3 pos) { //把位置信息x,y,z传递给服务器端 Dictionary<byte, object> data = new Dictionary<byte, object>(); data.Add(1, pos.x); data.Add(2, pos.y); data.Add(3, pos.z); PhotonEngine.Peer.OpCustom((byte)OperationCode.SyncPosition, data, true);//把Player位置传递给服务器 } } |
这里我们发给服务器三条数据,分别是x\y\z的坐标
第二条是服务器发给客户端,在服务器的SyncPositionThread实现的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
private void SendPosition() { //装载PlayerData里面的信息 List<PlayerData> playerDatraList = new List<PlayerData>(); foreach (ClientPeer peer in MyGameServer.Instance.peerList)//遍历所有客户段 { if (string.IsNullOrEmpty(peer.username) == false)//取得当前已经登陆的客户端 { PlayerData playerdata = new PlayerData(); playerdata.Username = peer.username;//设置playerdata里面的username playerdata.x = peer.x;//设置playerdata里面的Position playerdata.y = peer.y; playerdata.z = peer.z; playerDatraList.Add(playerdata);//把playerdata放入集合 } } //进行Xml序列化成String StringWriter sw = new StringWriter(); XmlSerializer serializer = new XmlSerializer(typeof(List<PlayerData>)); serializer.Serialize(sw, playerDatraList); sw.Close(); string playerDataListString = sw.ToString(); Dictionary<byte, object> data = new Dictionary<byte, object>(); data.Add(1, playerDataListString);//把所有的playerDataListString装载进字典里面 //把Xml序列化的信息装在字典里发送给各个客户端 foreach (ClientPeer peer in MyGameServer.Instance.peerList) { if (string.IsNullOrEmpty(peer.username) == false) { EventData ed = new EventData((byte)EventCode.SyncPosition); ed.Parameters = data; peer.SendEvent(ed, new SendParameters()); } } } |
这里服务器发给客户端的是一条string类型的事件,但实际是由List<PlayerData>格式转XML生成的string格式,这样看来,其实是List<PlayerData>格式的
PlayerData的内容也是包含x\y\z和username四个字段
创建Proto文件
根据我们分析出的数据格式,我们来创建对应的proto文件
没有了解过proto的可以学习下:Protobuf语言指南
我们打开记事本,创建一个文件,名称:SyncTransform.proto,这个文件下步会用到,但项目里用不到,所以可以先临时保存在桌面
在文件里我们创建SyncPositionC2S和SyncPositionEvtS2C两个类
SyncPositionC2S:客户端发给服务器的位置消息
SyncPositionEvtS2C:服务器发给客户端的位置消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
message SyncPositionC2S{ required float x = 1; required float y = 2; required float z = 3; } message SyncPositionEvtS2C{ repeated PositionData dataList = 1; message PositionData { required string username = 2; required float x = 3; required float y = 4; required float z = 5; } } |
通过.proto文件生成.cs文件
这个是proto文件格式,通过ProtoGen工具可以将其转成cs格式文件
proto转cs工具:Protobuf-net使用ProtoGen批量转换成cs文件
转换后的cs文件SyncTransform.cs
因为我们要把消息用到的数据类使用DLL库封装起来用,所以这个cs文件项目里也用不到,我们也先保存在桌面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
//------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // </auto-generated> //------------------------------------------------------------------------------ // Generated from: Proto/SyncTransform.proto namespace ProtoData { [global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"SyncPositionC2S")] public partial class SyncPositionC2S : global::ProtoBuf.IExtensible { public SyncPositionC2S() {} private int _x; [global::ProtoBuf.ProtoMember(1, IsRequired = true, Name=@"x", DataFormat = global::ProtoBuf.DataFormat.TwosComplement)] public int x { get { return _x; } set { _x = value; } } private int _y; [global::ProtoBuf.ProtoMember(2, IsRequired = true, Name=@"y", DataFormat = global::ProtoBuf.DataFormat.TwosComplement)] public int y { get { return _y; } set { _y = value; } } private int _z; [global::ProtoBuf.ProtoMember(3, IsRequired = true, Name=@"z", DataFormat = global::ProtoBuf.DataFormat.TwosComplement)] public int z { get { return _z; } set { _z = value; } } private global::ProtoBuf.IExtension extensionObject; global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing) { return global::ProtoBuf.Extensible.GetExtensionObject(ref extensionObject, createIfMissing); } } [global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"SyncPositionEvtS2C")] public partial class SyncPositionEvtS2C : global::ProtoBuf.IExtensible { public SyncPositionEvtS2C() {} private readonly global::System.Collections.Generic.List<SyncPositionEvtS2C.PositionData> _dataList = new global::System.Collections.Generic.List<SyncPositionEvtS2C.PositionData>(); [global::ProtoBuf.ProtoMember(1, Name=@"dataList", DataFormat = global::ProtoBuf.DataFormat.Default)] public global::System.Collections.Generic.List<SyncPositionEvtS2C.PositionData> dataList { get { return _dataList; } } [global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"PositionData")] public partial class PositionData : global::ProtoBuf.IExtensible { public PositionData() {} private string _username; [global::ProtoBuf.ProtoMember(2, IsRequired = true, Name=@"username", DataFormat = global::ProtoBuf.DataFormat.Default)] public string username { get { return _username; } set { _username = value; } } private int _x; [global::ProtoBuf.ProtoMember(3, IsRequired = true, Name=@"x", DataFormat = global::ProtoBuf.DataFormat.TwosComplement)] public int x { get { return _x; } set { _x = value; } } private int _y; [global::ProtoBuf.ProtoMember(4, IsRequired = true, Name=@"y", DataFormat = global::ProtoBuf.DataFormat.TwosComplement)] public int y { get { return _y; } set { _y = value; } } private int _z; [global::ProtoBuf.ProtoMember(5, IsRequired = true, Name=@"z", DataFormat = global::ProtoBuf.DataFormat.TwosComplement)] public int z { get { return _z; } set { _z = value; } } private global::ProtoBuf.IExtension extensionObject; global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing) { return global::ProtoBuf.Extensible.GetExtensionObject(ref extensionObject, createIfMissing); } } private global::ProtoBuf.IExtension extensionObject; global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing) { return global::ProtoBuf.Extensible.GetExtensionObject(ref extensionObject, createIfMissing); } } } |
将SyncTransform.cs生成DLL库
为了方便客户端和服务器端消息数据类能够统一管理,方便维护,我们将所有消息类生成DLL
这里我们暂时只有SyncTransform数据类,所以就将这一个文件生成DLL
关于生成DLL库,继续这个教程:使用VS将cs文件生成DLL
生成后,我们将该教程里生成的ProtoData.dll文件导入到客户端Unity的Plugins目录和在服务器端添加引用
由于缺少protobuf库的引用,所以会有报错
添加protobuf-net.dll库
protobuf-net是谷歌的产品,可以从google下载
1.翻墙从谷歌下载:https://code.google.com/archive/p/protobuf-net/downloads
2.如果不方便翻墙的可以从:https://github.com/654306663/ProtoGenToCs.git
第二个链接在使用proto转cs工具这个教程里面已经给链接了
下载完后打开Full文件夹
1.客户端:将unity/protobuf-net.dll放入Plugins目录
2.服务器端:根据服务器选择的目标框架版本,默认添加net30/protobuf-net.dll引用
当添加完引用后,随便打开客户端和服务器端脚本,测试下我们自己生成的DLL是否生效
1 2 3 4 |
ProtoData.SyncPositionC2S syncPositionC2S = new ProtoData.SyncPositionC2S(); syncPositionC2S.x = pos.x; syncPositionC2S.y = pos.y; syncPositionC2S.z = pos.z; |
添加ProtoBuf消息序列化与反序列化
在客户端和服务器的Tools文件夹里,都添加BinSerializer,作用是
1.将类序列化为二进制流格式进行消息传输
2.接收二进制流,将其反序列化为类类型
BinSerializer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
using ProtoBuf; using System; using System.IO; namespace MyGameServer.Tools { public class BinSerializer { /// <summary> /// 将消息序列化为二进制的方法 /// </summary> /// <param name="model">要序列化的对象</param> public static byte[] Serialize<T>(T t) { try { //涉及格式转换,需要用到流,将二进制序列化到流中 using (MemoryStream ms = new MemoryStream()) { //使用ProtoBuf工具的序列化方法 Serializer.Serialize<T>(ms, t); //定义二级制数组,保存序列化后的结果 byte[] result = new byte[ms.Length]; //将流的位置设为0,起始点 ms.Position = 0; //将流中的内容读取到二进制数组中 ms.Read(result, 0, result.Length); return result; } } catch (Exception ex) { MyGameServer.log.Info("序列化失败: " + ex.ToString()); return null; } } /// <summary> /// 将收到的消息反序列化成对象 /// </summary> /// <returns>The serialize.</returns> /// <param name="msg">收到的消息.</param> public static T DeSerialize<T>(byte[] msg) { try { using (MemoryStream ms = new MemoryStream(msg)) { //将消息写入流中 ms.Write(msg, 0, msg.Length); //将流的位置归0 ms.Position = 0; //使用工具反序列化对象 T result = Serializer.Deserialize<T>(ms); return result; } } catch (Exception ex) { MyGameServer.log.Info("反序列化失败: " + ex.ToString()); return default(T); } } } } |
至此我们才完成了准备工作,下面将修改客户端和服务器的位移同步消息内容
客户端修改位移同步消息
SyncTransformRequest 客户端发给服务器端的该客户端玩家的位移同步消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
using UnityEngine; using System.Collections; using System.Collections.Generic; namespace Net { public class SyncTransformRequest : Singleton<SyncTransformRequest> { //发起位置信息请求 public void SendSyncPositionRequest(Vector3 pos) { ProtoData.SyncPositionC2S syncPositionC2S = new ProtoData.SyncPositionC2S(); syncPositionC2S.x = pos.x; syncPositionC2S.y = pos.y; syncPositionC2S.z = pos.z; byte[] bytes = BinSerializer.Serialize(syncPositionC2S); //把位置信息x,y,z传递给服务器端 Dictionary<byte, object> data = new Dictionary<byte, object>(); data.Add(1, bytes); PhotonEngine.Peer.OpCustom((byte)OperationCode.SyncPosition, data, true);//把Player位置传递给服务器 } } } |
SyncTransformEvent 客户端接收服务器发来的所有玩家位移同步消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
using UnityEngine; using ExitGames.Client.Photon; using Tools; namespace Net { public class SyncTransformEvent : EventBase { public override void AddListener() { EventMediat.AddListener(EventCode.SyncPosition, OnSyncPositionReceived); } public override void RemoveListener() { EventMediat.RemoveListener(EventCode.SyncPosition, OnSyncPositionReceived); } void OnSyncPositionReceived(EventData eventData) { byte[] bytes = (byte[])DictTool.GetValue<byte, object>(eventData.Parameters, 1); ProtoData.SyncPositionEvtS2C syncPositionEvtS2C = BinSerializer.DeSerialize<ProtoData.SyncPositionEvtS2C>(bytes); GameObject.FindGameObjectWithTag("Player").GetComponent<Player>().OnSyncPositionEvent(syncPositionEvtS2C.dataList); } } } |
Player 同步所有所有玩家位置的方法OnSyncPositionEvent
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public void OnSyncPositionEvent(List<ProtoData.SyncPositionEvtS2C.PositionData> positionDataList) { foreach (ProtoData.SyncPositionEvtS2C.PositionData pd in positionDataList)//遍历所有的数据 { GameObject go = DictTool.GetValue<string, GameObject>(playerDic, pd.username);//根据传递过来的Username去找到所对应的实例化出来的Player //如果查找到了相应的角色,就把相应的位置信息赋值给这个角色的position if (go != null) { go.transform.position = new Vector3() { x = pd.x, y = pd.y, z = pd.z }; } } } |
至此,客户端修改内容完成,之前用到的PlayerData数据类已经不需要了。可以把Common目录删掉了~
服务器端修改位移同步消息
SyncTransformHandler 用来接收客户端发来的位移同步消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
using MyGameServer.Tools; using Photon.SocketServer; namespace MyGameServer.Handler { class SyncTransformHandler : IHandlerBase { public void AddListener() { HandlerMediat.AddListener(OperationCode.SyncPosition, OnSyncPositionReceived); } public void RemoveListener() { HandlerMediat.RemoveListener(OperationCode.SyncPosition, OnSyncPositionReceived); } //获取客户端位置请求的处理的代码 public void OnSyncPositionReceived(ClientPeer peer, OperationRequest operationRequest, SendParameters sendParameters) { //接收位置并保持起来 byte[] bytes = (byte[])DictTool.GetValue<byte, object>(operationRequest.Parameters, 1); ProtoData.SyncPositionC2S syncPositionC2S = BinSerializer.DeSerialize<ProtoData.SyncPositionC2S>(bytes); peer.x = syncPositionC2S.x; peer.y = syncPositionC2S.y; peer.z = syncPositionC2S.z; } } } |
SyncPositionThread 用来服务器端每隔指定时间发送给所有客户端的位移同步消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
using Photon.SocketServer; using ProtoData; using System.Collections.Generic; using System.Threading; namespace MyGameServer.Threads { class SyncPositionThread { private Thread t; //启动线程的方法 public void Run() { t = new Thread(UpdataPosition);//UpdataPosition表示线程要启动的方法 t.IsBackground = true;//后台运行 t.Start();//启动线程 } private void UpdataPosition() { Thread.Sleep(5000);//开始的时候休息5秒开始同步 while (true)//死循环 { Thread.Sleep(30);//没隔0.03秒同步一次位置信息 //进行同步 SendPosition(); } } //把所有客户端的位置信息发送到各个客户端 //封装位置信息,封装到字典里,然后利用Xml序列化去发送 private void SendPosition() { SyncPositionEvtS2C syncPositionEvtS2C = new SyncPositionEvtS2C(); //装载PlayerData里面的信息 foreach (ClientPeer peer in MyGameServer.Instance.peerList)//遍历所有客户段 { if (string.IsNullOrEmpty(peer.username) == false)//取得当前已经登陆的客户端 { SyncPositionEvtS2C.PositionData positionData = new SyncPositionEvtS2C.PositionData(); positionData.username = peer.username;//设置playerdata里面的username positionData.x = peer.x;//设置playerdata里面的Position positionData.y = peer.y; positionData.z = peer.z; syncPositionEvtS2C.dataList.Add(positionData);//把playerdata放入集合 } } byte[] bytes = Tools.BinSerializer.Serialize(syncPositionEvtS2C); Dictionary<byte, object> data = new Dictionary<byte, object>(); data.Add(1, bytes);//把所有的playerDataListString装载进字典里面 //把信息装在字典里发送给各个客户端 foreach (ClientPeer peer in MyGameServer.Instance.peerList) { if (string.IsNullOrEmpty(peer.username) == false) { EventData ed = new EventData((byte)EventCode.SyncPosition); ed.Parameters = data; peer.SendEvent(ed, new SendParameters()); } } } //关闭线程 public void Stop() { t.Abort();//终止线程 } } } |
服务器端的修改也完成了,同样PlayerData数据类已经不需要了,Common目录可以直接删除掉
结束
这个系列课程已经完成,希望对大家有帮助~~
Unity使用版本:Unity5.3.4
ProtoServer使用版本:4.0.28.2962
MySQL使用版本:5.7
GitHub下载地址:
https://github.com/654306663/PhotonServer
- 本文固定链接: http://www.u3d8.com/?p=1528
- 转载请注明: 网虫虫 在 u3d8.com 发表过