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网络传输到这里就全部完成了。