C#使用Protocol Buffer(ProtoBuf)进行Unity中的Socket通信
首先来说一下本文中例子所要实现的功能:
- 基于ProtoBuf序列化对象
- 使用Socket实现时时通信
- 数据包的编码和解码
下面来看具体的步骤:
一、Unity中使用ProtoBuf
导入DLL到Unity中,
创建网络传输的模型类:
usingSystem; usingProtoBuf; //添加特性,表示可以被ProtoBuf工具序列化 [ProtoContract] publicclassNetModel{ //添加特性,表示该字段可以被序列化,1可以理解为下标 [ProtoMember(1)] publicintID; [ProtoMember(2)] publicstringCommit; [ProtoMember(3)] publicstringMessage; } usingSystem; usingProtoBuf; //添加特性,表示可以被ProtoBuf工具序列化 [ProtoContract] publicclassNetModel{ //添加特性,表示该字段可以被序列化,1可以理解为下标 [ProtoMember(1)] publicintID; [ProtoMember(2)] publicstringCommit; [ProtoMember(3)] publicstringMessage; }
在Unity中添加测试脚本,介绍ProtoBuf工具的使用。
usingSystem; usingSystem.IO; publicclassTest:MonoBehaviour{ voidStart(){ //创建对象 NetModelitem=newNetModel(){ID=1,Commit="LanOu",Message="Unity"}; //序列化对象 byte[]temp=Serialize(item); //ProtoBuf的优势一:小 Debug.Log(temp.Length); //反序列化为对象 NetModelresult=DeSerialize(temp); Debug.Log(result.Message); } //将消息序列化为二进制的方法 //<paramname="model">要序列化的对象</param> privatebyte[]Serialize(NetModelmodel) { try{ //涉及格式转换,需要用到流,将二进制序列化到流中 using(MemoryStreamms=newMemoryStream()){ //使用ProtoBuf工具的序列化方法 ProtoBuf.Serializer.Serialize<NetModel>(ms,model); //定义二级制数组,保存序列化后的结果 byte[]result=newbyte[ms.Length]; //将流的位置设为0,起始点 ms.Position=0; //将流中的内容读取到二进制数组中 ms.Read(result,0,result.Length); returnresult; } }catch(Exceptionex){ Debug.Log("序列化失败:"+ex.ToString()); returnnull; } } //将收到的消息反序列化成对象 //<returns>Theserialize.</returns> //<paramname="msg">收到的消息.</param> privateNetModelDeSerialize(byte[]msg) { try{ using(MemoryStreamms=newMemoryStream()){ //将消息写入流中 ms.Write(msg,0,msg.Length); //将流的位置归0 ms.Position=0; //使用工具反序列化对象 NetModelresult=ProtoBuf.Serializer.Deserialize<NetModel>(ms); returnresult; } }catch(Exceptionex){ Debug.Log("反序列化失败:"+ex.ToString()); returnnull; } } } usingSystem; usingSystem.IO; publicclassTest:MonoBehaviour{ voidStart(){ //创建对象 NetModelitem=newNetModel(){ID=1,Commit="LanOu",Message="Unity"}; //序列化对象 byte[]temp=Serialize(item); //ProtoBuf的优势一:小 Debug.Log(temp.Length); //反序列化为对象 NetModelresult=DeSerialize(temp); Debug.Log(result.Message); } //将消息序列化为二进制的方法 //<paramname="model">要序列化的对象</param> privatebyte[]Serialize(NetModelmodel) { try{ //涉及格式转换,需要用到流,将二进制序列化到流中 using(MemoryStreamms=newMemoryStream()){ //使用ProtoBuf工具的序列化方法 ProtoBuf.Serializer.Serialize<NetModel>(ms,model); //定义二级制数组,保存序列化后的结果 byte[]result=newbyte[ms.Length]; //将流的位置设为0,起始点 ms.Position=0; //将流中的内容读取到二进制数组中 ms.Read(result,0,result.Length); returnresult; } }catch(Exceptionex){ Debug.Log("序列化失败:"+ex.ToString()); returnnull; } } //将收到的消息反序列化成对象 //<returns>Theserialize.</returns> //<paramname="msg">收到的消息.</param> privateNetModelDeSerialize(byte[]msg) { try{ using(MemoryStreamms=newMemoryStream()){ //将消息写入流中 ms.Write(msg,0,msg.Length); //将流的位置归0 ms.Position=0; //使用工具反序列化对象 NetModelresult=ProtoBuf.Serializer.Deserialize<NetModel>(ms); returnresult; } }catch(Exceptionex){ Debug.Log("反序列化失败:"+ex.ToString()); returnnull; } } }
二、Unity中使用Socket实现时时通信
通信应该实现的功能:
- 服务器可以时时监听多个客户端
- 服务器可以时时监听某一个客户端消息
- 服务器可以时时给某一个客户端发消息
- 首先我们需要定义一个客户端对象
usingSystem; usingSystem.Net.Sockets; //表示一个客户端 publicclassNetUserToken{ //连接客户端的Socket publicSocketsocket; //用于存放接收数据 publicbyte[]buffer; publicNetUserToken() { buffer=newbyte[1024]; } //接受消息 //<paramname="data">Data.</param> publicvoidReceive(byte[]data) { UnityEngine.Debug.Log("接收到消息!"); } //发送消息 //<paramname="data">Data.</param> publicvoidSend(byte[]data) { } } usingSystem; usingSystem.Net.Sockets; //表示一个客户端 publicclassNetUserToken{ //连接客户端的Socket publicSocketsocket; //用于存放接收数据 publicbyte[]buffer; publicNetUserToken() { buffer=newbyte[1024]; } //接受消息 //<paramname="data">Data.</param> publicvoidReceive(byte[]data) { UnityEngine.Debug.Log("接收到消息!"); } //发送消息 //<paramname="data">Data.</param> publicvoidSend(byte[]data) { } }
然后实现我们的服务器代码
usingSystem.Collections; usingSystem.Collections.Generic; usingSystem.Net; usingSystem; usingSystem.Net.Sockets; publicclassNetServer{ //单例脚本 publicstaticreadonlyNetServerInstance=newNetServer(); //定义tcp服务器 privateSocketserver; privateintmaxClient=10; //定义端口 privateintport=35353; //用户池 privateStack<NetUserToken>pools; privateNetServer() { //初始化socket server=newSocket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp); server.Bind(newIPEndPoint(IPAddress.Any,port)); } //开启服务器 publicvoidStart() { server.Listen(maxClient); UnityEngine.Debug.Log("ServerOK!"); //实例化客户端的用户池 pools=newStack<NetUserToken>(maxClient); for(inti=0;i<maxClient;i++) { NetUserTokenusertoken=newNetUserToken(); pools.Push(usertoken); } //可以异步接受客户端,BeginAccept函数的第一个参数是回调函数,当有客户端连接的时候自动调用 server.BeginAccept(AsyncAccept,null); } //回调函数,有客户端连接的时候会自动调用此方法 privatevoidAsyncAccept(IAsyncResultresult) { try{ //结束监听,同时获取到客户端 Socketclient=server.EndAccept(result); UnityEngine.Debug.Log("有客户端连接"); //来了一个客户端 NetUserTokenuserToken=pools.Pop(); userToken.socket=client; //客户端连接之后,可以接受客户端消息 BeginReceive(userToken); //尾递归,再次监听是否还有其他客户端连入 server.BeginAccept(AsyncAccept,null); }catch(Exceptionex){ UnityEngine.Debug.Log(ex.ToString()); } } //异步监听消息 privatevoidBeginReceive(NetUserTokenuserToken) { try{ //异步方法 userToken.socket.BeginReceive(userToken.buffer,0,userToken.buffer.Length,SocketFlags.None, EndReceive,userToken); }catch(Exceptionex){ UnityEngine.Debug.Log(ex.ToString()); } } //监听到消息之后调用的函数 privatevoidEndReceive(IAsyncResultresult) { try{ //取出客户端 NetUserTokenuserToken=result.AsyncStateasNetUserToken; //获取消息的长度 intlen=userToken.socket.EndReceive(result); if(len>0) { byte[]data=newbyte[len]; Buffer.BlockCopy(userToken.buffer,0,data,0,len); //用户接受消息 userToken.Receive(data); //尾递归,再次监听客户端消息 BeginReceive(userToken); } }catch(Exceptionex){ UnityEngine.Debug.Log(ex.ToString()); } } } usingSystem.Collections; usingSystem.Collections.Generic; usingSystem.Net; usingSystem; usingSystem.Net.Sockets; publicclassNetServer{ //单例脚本 publicstaticreadonlyNetServerInstance=newNetServer(); //定义tcp服务器 privateSocketserver; privateintmaxClient=10; //定义端口 privateintport=35353; //用户池 privateStack<NetUserToken>pools; privateNetServer() { //初始化socket server=newSocket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp); server.Bind(newIPEndPoint(IPAddress.Any,port)); } //开启服务器 publicvoidStart() { server.Listen(maxClient); UnityEngine.Debug.Log("ServerOK!"); //实例化客户端的用户池 pools=newStack<NetUserToken>(maxClient); for(inti=0;i<maxClient;i++) { NetUserTokenusertoken=newNetUserToken(); pools.Push(usertoken); } //可以异步接受客户端,BeginAccept函数的第一个参数是回调函数,当有客户端连接的时候自动调用 server.BeginAccept(AsyncAccept,null); } //回调函数,有客户端连接的时候会自动调用此方法 privatevoidAsyncAccept(IAsyncResultresult) { try{ //结束监听,同时获取到客户端 Socketclient=server.EndAccept(result); UnityEngine.Debug.Log("有客户端连接"); //来了一个客户端 NetUserTokenuserToken=pools.Pop(); userToken.socket=client; //客户端连接之后,可以接受客户端消息 BeginReceive(userToken); //尾递归,再次监听是否还有其他客户端连入 server.BeginAccept(AsyncAccept,null); }catch(Exceptionex){ UnityEngine.Debug.Log(ex.ToString()); } } //异步监听消息 privatevoidBeginReceive(NetUserTokenuserToken) { try{ //异步方法 userToken.socket.BeginReceive(userToken.buffer,0,userToken.buffer.Length,SocketFlags.None, EndReceive,userToken); }catch(Exceptionex){ UnityEngine.Debug.Log(ex.ToString()); } } //监听到消息之后调用的函数 privatevoidEndReceive(IAsyncResultresult) { try{ //取出客户端 NetUserTokenuserToken=result.AsyncStateasNetUserToken; //获取消息的长度 intlen=userToken.socket.EndReceive(result); if(len>0) { byte[]data=newbyte[len]; Buffer.BlockCopy(userToken.buffer,0,data,0,len); //用户接受消息 userToken.Receive(data); //尾递归,再次监听客户端消息 BeginReceive(userToken); } }catch(Exceptionex){ UnityEngine.Debug.Log(ex.ToString()); } } }
在Unity中开启服务器,并使用C#控制台模拟客户端连接、发送消息操作。测试OK了,Unity中可以时时监听到消息。
usingUnityEngine; usingSystem.Collections; publicclassCreateServer:MonoBehaviour{ voidStartServer(){ NetServer.Instance.Start(); } } //C#控制台工程 usingSystem; usingSystem.Net; usingSystem.Net.Sockets; usingSystem.IO; usingSystem.Text; namespaceTemp { classMainClass { publicstaticvoidMain(string[]args) { TcpClienttc=newTcpClient(); IPEndPointip=newIPEndPoint(IPAddress.Parse("127.0.0.1"),35353); tc.Connect(ip); if(tc.Connected) { while(true) { stringmsg=Console.ReadLine(); byte[]result=Encoding.UTF8.GetBytes(msg); tc.GetStream().Write(result,0,result.Length); } } Console.ReadLine(); } } } usingUnityEngine; usingSystem.Collections; publicclassCreateServer:MonoBehaviour{ voidStartServer(){ NetServer.Instance.Start(); } } //C#控制台工程 usingSystem; usingSystem.Net; usingSystem.Net.Sockets; usingSystem.IO; usingSystem.Text; namespaceTemp { classMainClass { publicstaticvoidMain(string[]args) { TcpClienttc=newTcpClient(); IPEndPointip=newIPEndPoint(IPAddress.Parse("127.0.0.1"),35353); tc.Connect(ip); if(tc.Connected) { while(true) { stringmsg=Console.ReadLine(); byte[]result=Encoding.UTF8.GetBytes(msg); tc.GetStream().Write(result,0,result.Length); } } Console.ReadLine(); } } }
三、数据包的编码和解码
首先,举个例子,这个月信用卡被媳妇刷爆了,面对房贷车贷的压力,我只能选择分期付款。。。
那么OK了,现在我想问一下,当服务器向客户端发送的数据过大时怎么办呢?
当服务器需要向客户端发送一条很长的数据,也会“分期付款!”,服务器会把一条很长的数据分成若干条小数据,多次发送给客户端。
可是,这样就又有另外一个问题,客户端接受到多条数据之后如何解析?
这里其实就是客户端的解码。server发数据一般采用“长度+内容”的格式,Client接收到数据之后,先提取出长度来,然后根据长度判断内容是否发送完毕。
再次重申,用户在发送序列化好的消息的前,需要先编码后再发送消息;用户在接受消息后,需要解码之后再解析数据(反序列化)。
usingUnityEngine; usingSystem.Collections.Generic; usingSystem.IO; //编码和解码 publicclassNetEncode{ //将数据编码长度+内容 ///<paramname="data">内容</param> publicstaticbyte[]Encode(byte[]data) { //整形占四个字节,所以声明一个+4的数组 byte[]result=newbyte[data.Length+4]; //使用流将编码写二进制 MemoryStreamms=newMemoryStream(); BinaryWriterbr=newBinaryWriter(ms); br.Write(data.Length); br.Write(data); //将流中的内容复制到数组中 System.Buffer.BlockCopy(ms.ToArray(),0,result,0,(int)ms.Length); br.Close(); ms.Close(); returnresult; } //将数据解码 //<paramname="cache">消息队列</param> publicstaticbyte[]Decode(refList<byte>cache) { //首先要获取长度,整形4个字节,如果字节数不足4个字节 if(cache.Count<4) { returnnull; } //读取数据 MemoryStreamms=newMemoryStream(cache.ToArray()); BinaryReaderbr=newBinaryReader(ms); intlen=br.ReadInt32(); //根据长度,判断内容是否传递完毕 if(len>ms.Length-ms.Position) { returnnull; } //获取数据 byte[]result=br.ReadBytes(len); //清空消息池 cache.Clear(); //讲剩余没处理的消息存入消息池 cache.AddRange(br.ReadBytes((int)ms.Length-(int)ms.Position)); returnresult; } } usingUnityEngine; usingSystem.Collections.Generic; usingSystem.IO; //编码和解码 publicclassNetEncode{ //将数据编码长度+内容 ///<paramname="data">内容</param> publicstaticbyte[]Encode(byte[]data) { //整形占四个字节,所以声明一个+4的数组 byte[]result=newbyte[data.Length+4]; //使用流将编码写二进制 MemoryStreamms=newMemoryStream(); BinaryWriterbr=newBinaryWriter(ms); br.Write(data.Length); br.Write(data); //将流中的内容复制到数组中 System.Buffer.BlockCopy(ms.ToArray(),0,result,0,(int)ms.Length); br.Close(); ms.Close(); returnresult; } //将数据解码 //<paramname="cache">消息队列</param> publicstaticbyte[]Decode(refList<byte>cache) { //首先要获取长度,整形4个字节,如果字节数不足4个字节 if(cache.Count<4) { returnnull; } //读取数据 MemoryStreamms=newMemoryStream(cache.ToArray()); BinaryReaderbr=newBinaryReader(ms); intlen=br.ReadInt32(); //根据长度,判断内容是否传递完毕 if(len>ms.Length-ms.Position) { returnnull; } //获取数据 byte[]result=br.ReadBytes(len); //清空消息池 cache.Clear(); //讲剩余没处理的消息存入消息池 cache.AddRange(br.ReadBytes((int)ms.Length-(int)ms.Position)); returnresult; } }
用户接受数据代码如下:
usingSystem; usingSystem.Collections.Generic; usingSystem.Net.Sockets; //表示一个客户端 publicclassNetUserToken{ //连接客户端的Socket publicSocketsocket; //用于存放接收数据 publicbyte[]buffer; //每次接受和发送数据的大小 privateconstintsize=1024; //接收数据池 privateList<byte>receiveCache; privateboolisReceiving; //发送数据池 privateQueue<byte[]>sendCache; privateboolisSending; //接收到消息之后的回调 publicAction<NetModel>receiveCallBack; publicNetUserToken() { buffer=newbyte[size]; receiveCache=newList<byte>(); sendCache=newQueue<byte[]>(); } //服务器接受客户端发送的消息 //<paramname="data">Data.</param> publicvoidReceive(byte[]data) { UnityEngine.Debug.Log("接收到数据"); //将接收到的数据放入数据池中 receiveCache.AddRange(data); //如果没在读数据 if(!isReceiving) { isReceiving=true; ReadData(); } } //读取数据 privatevoidReadData() { byte[]data=NetEncode.Decode(refreceiveCache); //说明数据保存成功 if(data!=null) { NetModelitem=NetSerilizer.DeSerialize(data); UnityEngine.Debug.Log(item.Message); if(receiveCallBack!=null) { receiveCallBack(item); } //尾递归,继续读取数据 ReadData(); } else { isReceiving=false; } } //服务器发送消息给客户端 publicvoidSend() { try{ if(sendCache.Count==0){ isSending=false; return; } byte[]data=sendCache.Dequeue(); intcount=data.Length/size; intlen=size; for(inti=0;i<count+1;i++){ if(i==count){ len=data.Length-i*size; } socket.Send(data,i*size,len,SocketFlags.None); } UnityEngine.Debug.Log("发送成功!"); Send(); }catch(Exceptionex){ UnityEngine.Debug.Log(ex.ToString()); } } publicvoidWriteSendDate(byte[]data){ sendCache.Enqueue(data); if(!isSending) { isSending=true; Send(); } } } usingSystem; usingSystem.Collections.Generic; usingSystem.Net.Sockets; //表示一个客户端 publicclassNetUserToken{ //连接客户端的Socket publicSocketsocket; //用于存放接收数据 publicbyte[]buffer; //每次接受和发送数据的大小 privateconstintsize=1024; //接收数据池 privateList<byte>receiveCache; privateboolisReceiving; //发送数据池 privateQueue<byte[]>sendCache; privateboolisSending; //接收到消息之后的回调 publicAction<NetModel>receiveCallBack; publicNetUserToken() { buffer=newbyte[size]; receiveCache=newList<byte>(); sendCache=newQueue<byte[]>(); } //服务器接受客户端发送的消息 //<paramname="data">Data.</param> publicvoidReceive(byte[]data) { UnityEngine.Debug.Log("接收到数据"); //将接收到的数据放入数据池中 receiveCache.AddRange(data); //如果没在读数据 if(!isReceiving) { isReceiving=true; ReadData(); } } //读取数据 privatevoidReadData() { byte[]data=NetEncode.Decode(refreceiveCache); //说明数据保存成功 if(data!=null) { NetModelitem=NetSerilizer.DeSerialize(data); UnityEngine.Debug.Log(item.Message); if(receiveCallBack!=null) { receiveCallBack(item); } //尾递归,继续读取数据 ReadData(); } else { isReceiving=false; } } //服务器发送消息给客户端 publicvoidSend() { try{ if(sendCache.Count==0){ isSending=false; return; } byte[]data=sendCache.Dequeue(); intcount=data.Length/size; intlen=size; for(inti=0;i<count+1;i++){ if(i==count){ len=data.Length-i*size; } socket.Send(data,i*size,len,SocketFlags.None); } UnityEngine.Debug.Log("发送成功!"); Send(); }catch(Exceptionex){ UnityEngine.Debug.Log(ex.ToString()); } } publicvoidWriteSendDate(byte[]data){ sendCache.Enqueue(data); if(!isSending) { isSending=true; Send(); } } }
ProtoBuf网络传输到这里就全部完成了。