这篇文章为进阶内容,之前的数据传输机制已经可以很好的适用于大部分项目了。但为了更快、更清晰的传输,这篇我们来拓展学习一下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 发表过






