zoukankan      html  css  js  c++  java
  • C#使用ProtocolBuffer(ProtoBuf)进行Unity中的Socket通信

    首先来说一下本文中例子所要实现的功能:

    • 基于ProtoBuf序列化对象
    • 使用Socket实现时时通信
    • 数据包的编码和解码

    下面来看具体的步骤:

    一、Unity中使用ProtoBuf

    导入DLL到Unity中,
    创建网络传输的模型类:

    using System;
    using ProtoBuf;
    
    //添加特性,表示可以被ProtoBuf工具序列化
    [ProtoContract]
    public class NetModel {
     //添加特性,表示该字段可以被序列化,1可以理解为下标
     [ProtoMember(1)] 
     public int ID;
     [ProtoMember(2)]
     public string Commit;
     [ProtoMember(3)]
     public string Message;
    }
    
    using System;
    using ProtoBuf;
     
    //添加特性,表示可以被ProtoBuf工具序列化
    [ProtoContract]
    public class NetModel {
     //添加特性,表示该字段可以被序列化,1可以理解为下标
     [ProtoMember(1)] 
     public int ID;
     [ProtoMember(2)]
     public string Commit;
     [ProtoMember(3)]
     public string Message;
    }
    
    

    在Unity中添加测试脚本,介绍ProtoBuf工具的使用。

    using System;
    using System.IO;
    
    public class Test : MonoBehaviour {
    
     void Start () {
      //创建对象
      NetModel item = new NetModel(){ID = 1, Commit = "LanOu", Message = "Unity"};
      //序列化对象
      byte[] temp = Serialize(item);
      //ProtoBuf的优势一:小
      Debug.Log(temp.Length);
      //反序列化为对象
      NetModel result = DeSerialize(temp);
      Debug.Log(result.Message);
    
     }
    
     // 将消息序列化为二进制的方法
     // < param name="model">要序列化的对象< /param>
     private byte[] Serialize(NetModel model)
     {
      try {
       //涉及格式转换,需要用到流,将二进制序列化到流中
       using (MemoryStream ms = new MemoryStream()) {
        //使用ProtoBuf工具的序列化方法
        ProtoBuf.Serializer.Serialize<NetModel> (ms, model);
        //定义二级制数组,保存序列化后的结果
        byte[] result = new byte[ms.Length];
        //将流的位置设为0,起始点
        ms.Position = 0;
        //将流中的内容读取到二进制数组中
        ms.Read (result, 0, result.Length);
        return result;
       }
      } catch (Exception ex) {
       Debug.Log ("序列化失败: " + ex.ToString());
       return null;
      }
     }
    
     // 将收到的消息反序列化成对象
     // < returns>The serialize.< /returns>
     // < param name="msg">收到的消息.</param>
     private NetModel DeSerialize(byte[] msg)
     {
      try {
       using (MemoryStream ms = new MemoryStream()) {
        //将消息写入流中
        ms.Write (msg, 0, msg.Length);
        //将流的位置归0
        ms.Position = 0;
        //使用工具反序列化对象
        NetModel result = ProtoBuf.Serializer.Deserialize<NetModel> (ms);
        return result;
       }
      } catch (Exception ex) {  
        Debug.Log("反序列化失败: " + ex.ToString());
        return null;
      }
     }
    }
    
    using System;
    using System.IO;
     
    public class Test : MonoBehaviour {
     
     void Start () {
      //创建对象
      NetModel item = new NetModel(){ID = 1, Commit = "LanOu", Message = "Unity"};
      //序列化对象
      byte[] temp = Serialize(item);
      //ProtoBuf的优势一:小
      Debug.Log(temp.Length);
      //反序列化为对象
      NetModel result = DeSerialize(temp);
      Debug.Log(result.Message);
     
     }
     
     // 将消息序列化为二进制的方法
     // < param name="model">要序列化的对象< /param>
     private byte[] Serialize(NetModel model)
     {
      try {
       //涉及格式转换,需要用到流,将二进制序列化到流中
       using (MemoryStream ms = new MemoryStream()) {
        //使用ProtoBuf工具的序列化方法
        ProtoBuf.Serializer.Serialize<NetModel> (ms, model);
        //定义二级制数组,保存序列化后的结果
        byte[] result = new byte[ms.Length];
        //将流的位置设为0,起始点
        ms.Position = 0;
        //将流中的内容读取到二进制数组中
        ms.Read (result, 0, result.Length);
        return result;
       }
      } catch (Exception ex) {
       Debug.Log ("序列化失败: " + ex.ToString());
       return null;
      }
     }
     
     // 将收到的消息反序列化成对象
     // < returns>The serialize.< /returns>
     // < param name="msg">收到的消息.</param>
     private NetModel DeSerialize(byte[] msg)
     {
      try {
       using (MemoryStream ms = new MemoryStream()) {
        //将消息写入流中
        ms.Write (msg, 0, msg.Length);
        //将流的位置归0
        ms.Position = 0;
        //使用工具反序列化对象
        NetModel result = ProtoBuf.Serializer.Deserialize<NetModel> (ms);
        return result;
       }
      } catch (Exception ex) {  
        Debug.Log("反序列化失败: " + ex.ToString());
        return null;
      }
     }
    }
    
    

    二、Unity中使用Socket实现时时通信

    通信应该实现的功能:

    • 服务器可以时时监听多个客户端
    • 服务器可以时时监听某一个客户端消息
    • 服务器可以时时给某一个客户端发消息
    • 首先我们需要定义一个客户端对象
    using System;
    using System.Net.Sockets;
    
    // 表示一个客户端
    public class NetUserToken {
     //连接客户端的Socket
     public Socket socket;
     //用于存放接收数据
     public byte[] buffer;
    
     public NetUserToken()
     {
      buffer = new byte[1024];
     }
    
     // 接受消息
     // < param name="data">Data.< /param>
     public void Receive(byte[] data)
     {
      UnityEngine.Debug.Log("接收到消息!");
     }
    
     // 发送消息
     //< param name="data">Data.< /param>
     public void Send(byte[] data)
     {  
    
     }
    }
    
    using System;
    using System.Net.Sockets;
     
    // 表示一个客户端
    public class NetUserToken {
     //连接客户端的Socket
     public Socket socket;
     //用于存放接收数据
     public byte[] buffer;
     
     public NetUserToken()
     {
      buffer = new byte[1024];
     }
     
     // 接受消息
     // < param name="data">Data.< /param>
     public void Receive(byte[] data)
     {
      UnityEngine.Debug.Log("接收到消息!");
     }
     
     // 发送消息
     //< param name="data">Data.< /param>
     public void Send(byte[] data)
     {  
     
     }
    }
    
    

    然后实现我们的服务器代码

    using System.Collections;
    using System.Collections.Generic;
    using System.Net;
    using System;
    using System.Net.Sockets;
    
    public class NetServer{
     //单例脚本
     public static readonly NetServer Instance = new NetServer();
     //定义tcp服务器
     private Socket server;
     private int maxClient = 10;
     //定义端口
     private int port = 35353;
     //用户池
     private Stack<NetUserToken> pools;
     private NetServer()
     {
      //初始化socket
      server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
      server.Bind(new IPEndPoint(IPAddress.Any, port));
    
     }
    
     //开启服务器
     public void Start()
     {
      server.Listen(maxClient);
      UnityEngine.Debug.Log("Server OK!");
      //实例化客户端的用户池
      pools = new Stack<NetUserToken>(maxClient);
      for(int i = 0; i < maxClient; i++)
      {
       NetUserToken usertoken = new NetUserToken();
       pools.Push(usertoken);
      }
      //可以异步接受客户端, BeginAccept函数的第一个参数是回调函数,当有客户端连接的时候自动调用
      server.BeginAccept (AsyncAccept, null);
     }
    
     //回调函数, 有客户端连接的时候会自动调用此方法
     private void AsyncAccept(IAsyncResult result)
     {
      try {
       //结束监听,同时获取到客户端
       Socket client = server.EndAccept(result);
       UnityEngine.Debug.Log("有客户端连接");
       //来了一个客户端
       NetUserToken userToken = pools.Pop();
       userToken.socket = client;
       //客户端连接之后,可以接受客户端消息
       BeginReceive(userToken);
    
       //尾递归,再次监听是否还有其他客户端连入
       server.BeginAccept(AsyncAccept, null);
      } catch (Exception ex) {
       UnityEngine.Debug.Log(ex.ToString());
      }
     }
    
     //异步监听消息
     private void BeginReceive(NetUserToken userToken)
     {
      try {
       //异步方法
       userToken.socket.BeginReceive(userToken.buffer, 0, userToken.buffer.Length, SocketFlags.None,
               EndReceive, userToken);
      } catch (Exception ex) {
       UnityEngine.Debug.Log(ex.ToString());
      }
     }
    
     //监听到消息之后调用的函数
     private void EndReceive(IAsyncResult result)
     {
      try {
       //取出客户端
       NetUserToken userToken = result.AsyncState as NetUserToken;
       //获取消息的长度
       int len = userToken.socket.EndReceive(result);
       if(len > 0)
       { 
        byte[] data = new byte[len];
        Buffer.BlockCopy(userToken.buffer, 0, data, 0, len);
        //用户接受消息
        userToken.Receive(data);
        //尾递归,再次监听客户端消息
        BeginReceive(userToken);
       }
    
      } catch (Exception ex) {
       UnityEngine.Debug.Log(ex.ToString());
      }
     }
    }
    
    using System.Collections;
    using System.Collections.Generic;
    using System.Net;
    using System;
    using System.Net.Sockets;
     
    public class NetServer{
     //单例脚本
     public static readonly NetServer Instance = new NetServer();
     //定义tcp服务器
     private Socket server;
     private int maxClient = 10;
     //定义端口
     private int port = 35353;
     //用户池
     private Stack<NetUserToken> pools;
     private NetServer()
     {
      //初始化socket
      server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
      server.Bind(new IPEndPoint(IPAddress.Any, port));
     
     }
     
     //开启服务器
     public void Start()
     {
      server.Listen(maxClient);
      UnityEngine.Debug.Log("Server OK!");
      //实例化客户端的用户池
      pools = new Stack<NetUserToken>(maxClient);
      for(int i = 0; i < maxClient; i++)
      {
       NetUserToken usertoken = new NetUserToken();
       pools.Push(usertoken);
      }
      //可以异步接受客户端, BeginAccept函数的第一个参数是回调函数,当有客户端连接的时候自动调用
      server.BeginAccept (AsyncAccept, null);
     }
     
     //回调函数, 有客户端连接的时候会自动调用此方法
     private void AsyncAccept(IAsyncResult result)
     {
      try {
       //结束监听,同时获取到客户端
       Socket client = server.EndAccept(result);
       UnityEngine.Debug.Log("有客户端连接");
       //来了一个客户端
       NetUserToken userToken = pools.Pop();
       userToken.socket = client;
       //客户端连接之后,可以接受客户端消息
       BeginReceive(userToken);
     
       //尾递归,再次监听是否还有其他客户端连入
       server.BeginAccept(AsyncAccept, null);
      } catch (Exception ex) {
       UnityEngine.Debug.Log(ex.ToString());
      }
     }
     
     //异步监听消息
     private void BeginReceive(NetUserToken userToken)
     {
      try {
       //异步方法
       userToken.socket.BeginReceive(userToken.buffer, 0, userToken.buffer.Length, SocketFlags.None,
               EndReceive, userToken);
      } catch (Exception ex) {
       UnityEngine.Debug.Log(ex.ToString());
      }
     }
     
     //监听到消息之后调用的函数
     private void EndReceive(IAsyncResult result)
     {
      try {
       //取出客户端
       NetUserToken userToken = result.AsyncState as NetUserToken;
       //获取消息的长度
       int len = userToken.socket.EndReceive(result);
       if(len > 0)
       { 
        byte[] data = new byte[len];
        Buffer.BlockCopy(userToken.buffer, 0, data, 0, len);
        //用户接受消息
        userToken.Receive(data);
        //尾递归,再次监听客户端消息
        BeginReceive(userToken);
       }
     
      } catch (Exception ex) {
       UnityEngine.Debug.Log(ex.ToString());
      }
     }
    }
    
    

    在Unity中开启服务器,并使用C#控制台模拟客户端连接、发送消息操作。测试OK了,Unity中可以时时监听到消息。

    using UnityEngine;
    using System.Collections;
    
    public class CreateServer : MonoBehaviour {
    
     void StartServer () {
      NetServer.Instance.Start();
     }
    
    }
    
    //C#控制台工程
    
    using System;
    using System.Net;
    using System.Net.Sockets;
    using System.IO;
    using System.Text;
    
    namespace Temp
    {
     class MainClass
     {
      public static void Main (string[] args)
      {
       TcpClient tc = new TcpClient();
       IPEndPoint ip = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 35353);
       tc.Connect(ip);
    
       if(tc.Connected)
       {
        while(true)
        {
    
         string msg = Console.ReadLine();
         byte[] result = Encoding.UTF8.GetBytes(msg);
         tc.GetStream().Write(result, 0, result.Length);
        }
       }
       Console.ReadLine();
      }
     }
    }
    
    using UnityEngine;
    using System.Collections;
     
    public class CreateServer : MonoBehaviour {
     
     void StartServer () {
      NetServer.Instance.Start();
     }
     
    }
     
    //C#控制台工程
     
    using System;
    using System.Net;
    using System.Net.Sockets;
    using System.IO;
    using System.Text;
     
    namespace Temp
    {
     class MainClass
     {
      public static void Main (string[] args)
      {
       TcpClient tc = new TcpClient();
       IPEndPoint ip = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 35353);
       tc.Connect(ip);
     
       if(tc.Connected)
       {
        while(true)
        {
     
         string msg = Console.ReadLine();
         byte[] result = Encoding.UTF8.GetBytes(msg);
         tc.GetStream().Write(result, 0, result.Length);
        }
       }
       Console.ReadLine();
      }
     }
    }
    
    

    三、数据包的编码和解码

    首先,举个例子,这个月信用卡被媳妇刷爆了,面对房贷车贷的压力,我只能选择分期付款。。。

    那么OK了,现在我想问一下,当服务器向客户端发送的数据过大时怎么办呢?

    当服务器需要向客户端发送一条很长的数据,也会“分期付款!”,服务器会把一条很长的数据分成若干条小数据,多次发送给客户端。

    可是,这样就又有另外一个问题,客户端接受到多条数据之后如何解析?

    这里其实就是客户端的解码。server发数据一般采用“长度+内容”的格式,Client接收到数据之后,先提取出长度来,然后根据长度判断内容是否发送完毕。

    再次重申,用户在发送序列化好的消息的前,需要先编码后再发送消息;用户在接受消息后,需要解码之后再解析数据(反序列化)。

    using UnityEngine;
    using System.Collections.Generic;
    using System.IO;
    
    // 编码和解码
    public class NetEncode {
    
     // 将数据编码 长度+内容
     /// < param name="data">内容< /param>
     public static byte[] Encode(byte[] data)
     {
      //整形占四个字节,所以声明一个+4的数组
      byte[] result = new byte[data.Length + 4];
      //使用流将编码写二进制
      MemoryStream ms = new MemoryStream();
      BinaryWriter br = new BinaryWriter(ms);
      br.Write(data.Length);
      br.Write(data);
      //将流中的内容复制到数组中
      System.Buffer.BlockCopy(ms.ToArray(), 0, result, 0, (int)ms.Length);
      br.Close();
      ms.Close();
      return result;
     }
    
     // 将数据解码
     // < param name="cache">消息队列< /param>
     public static byte[] Decode(ref List<byte> cache)
     {
      //首先要获取长度,整形4个字节,如果字节数不足4个字节
      if(cache.Count < 4)
      {
       return null;
      }
      //读取数据
      MemoryStream ms = new MemoryStream(cache.ToArray());
      BinaryReader br = new BinaryReader(ms);
      int len = br.ReadInt32();
      //根据长度,判断内容是否传递完毕
      if(len > ms.Length - ms.Position)
      {
       return null;
      }
      //获取数据
      byte[] result = br.ReadBytes(len);
      //清空消息池
      cache.Clear();
      //讲剩余没处理的消息存入消息池
      cache.AddRange(br.ReadBytes((int)ms.Length - (int)ms.Position));
    
      return result;
     }
    }
    
    using UnityEngine;
    using System.Collections.Generic;
    using System.IO;
     
    // 编码和解码
    public class NetEncode {
     
     // 将数据编码 长度+内容
     /// < param name="data">内容< /param>
     public static byte[] Encode(byte[] data)
     {
      //整形占四个字节,所以声明一个+4的数组
      byte[] result = new byte[data.Length + 4];
      //使用流将编码写二进制
      MemoryStream ms = new MemoryStream();
      BinaryWriter br = new BinaryWriter(ms);
      br.Write(data.Length);
      br.Write(data);
      //将流中的内容复制到数组中
      System.Buffer.BlockCopy(ms.ToArray(), 0, result, 0, (int)ms.Length);
      br.Close();
      ms.Close();
      return result;
     }
     
     // 将数据解码
     // < param name="cache">消息队列< /param>
     public static byte[] Decode(ref List<byte> cache)
     {
      //首先要获取长度,整形4个字节,如果字节数不足4个字节
      if(cache.Count < 4)
      {
       return null;
      }
      //读取数据
      MemoryStream ms = new MemoryStream(cache.ToArray());
      BinaryReader br = new BinaryReader(ms);
      int len = br.ReadInt32();
      //根据长度,判断内容是否传递完毕
      if(len > ms.Length - ms.Position)
      {
       return null;
      }
      //获取数据
      byte[] result = br.ReadBytes(len);
      //清空消息池
      cache.Clear();
      //讲剩余没处理的消息存入消息池
      cache.AddRange(br.ReadBytes((int)ms.Length - (int)ms.Position));
     
      return result;
     }
    }
    
    

    用户接受数据代码如下:

    using System;
    using System.Collections.Generic;
    using System.Net.Sockets;
    
    // 表示一个客户端
    public class NetUserToken {
     //连接客户端的Socket
     public Socket socket;
     //用于存放接收数据
     public byte[] buffer;
     //每次接受和发送数据的大小
     private const int size = 1024;
    
     //接收数据池
     private List<byte> receiveCache;
     private bool isReceiving;
     //发送数据池
     private Queue<byte[]> sendCache;
     private bool isSending;
    
     //接收到消息之后的回调
     public Action<NetModel> receiveCallBack;
    
    
     public NetUserToken()
     {
      buffer = new byte[size];
      receiveCache = new List<byte>();
      sendCache = new Queue<byte[]>();
     }
    
     // 服务器接受客户端发送的消息
     // < param name="data">Data.< /param>
     public void Receive(byte[] data)
     {
      UnityEngine.Debug.Log("接收到数据");
      //将接收到的数据放入数据池中
      receiveCache.AddRange(data);
      //如果没在读数据
      if(!isReceiving)
      {
       isReceiving = true;
       ReadData();
      }
     }
    
     // 读取数据
     private void ReadData()
     {
      byte[] data = NetEncode.Decode(ref receiveCache);
      //说明数据保存成功
      if(data != null)
      {
       NetModel item = NetSerilizer.DeSerialize(data);
       UnityEngine.Debug.Log(item.Message);
       if(receiveCallBack != null)
       {
        receiveCallBack(item);
       }
       //尾递归,继续读取数据
       ReadData();
      }
      else
      {
       isReceiving = false;
      }
     }
    
     // 服务器发送消息给客户端
     public void Send()
     {
      try {
       if (sendCache.Count == 0) {
        isSending = false;
        return; 
       }
       byte[] data = sendCache.Dequeue ();
       int count = data.Length / size;
       int len = size;
       for (int i = 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 (Exception ex) {
       UnityEngine.Debug.Log(ex.ToString());
      }
     }
    
     public void WriteSendDate(byte[] data){
      sendCache.Enqueue(data);
      if(!isSending)
      {
       isSending = true;
       Send();
      }
     }
    }
    
    using System;
    using System.Collections.Generic;
    using System.Net.Sockets;
     
    // 表示一个客户端
    public class NetUserToken {
     //连接客户端的Socket
     public Socket socket;
     //用于存放接收数据
     public byte[] buffer;
     //每次接受和发送数据的大小
     private const int size = 1024;
     
     //接收数据池
     private List<byte> receiveCache;
     private bool isReceiving;
     //发送数据池
     private Queue<byte[]> sendCache;
     private bool isSending;
     
     //接收到消息之后的回调
     public Action<NetModel> receiveCallBack;
     
     
     public NetUserToken()
     {
      buffer = new byte[size];
      receiveCache = new List<byte>();
      sendCache = new Queue<byte[]>();
     }
     
     // 服务器接受客户端发送的消息
     // < param name="data">Data.< /param>
     public void Receive(byte[] data)
     {
      UnityEngine.Debug.Log("接收到数据");
      //将接收到的数据放入数据池中
      receiveCache.AddRange(data);
      //如果没在读数据
      if(!isReceiving)
      {
       isReceiving = true;
       ReadData();
      }
     }
     
     // 读取数据
     private void ReadData()
     {
      byte[] data = NetEncode.Decode(ref receiveCache);
      //说明数据保存成功
      if(data != null)
      {
       NetModel item = NetSerilizer.DeSerialize(data);
       UnityEngine.Debug.Log(item.Message);
       if(receiveCallBack != null)
       {
        receiveCallBack(item);
       }
       //尾递归,继续读取数据
       ReadData();
      }
      else
      {
       isReceiving = false;
      }
     }
     
     // 服务器发送消息给客户端
     public void Send()
     {
      try {
       if (sendCache.Count == 0) {
        isSending = false;
        return; 
       }
       byte[] data = sendCache.Dequeue ();
       int count = data.Length / size;
       int len = size;
       for (int i = 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 (Exception ex) {
       UnityEngine.Debug.Log(ex.ToString());
      }
     }
     
     public void WriteSendDate(byte[] data){
      sendCache.Enqueue(data);
      if(!isSending)
      {
       isSending = true;
       Send();
      }
     }
    }
    
    

    ProtoBuf网络传输到这里就全部完成了。

  • 相关阅读:
    git切换分支报错解决
    网上看到的一个布局神器
    js获取滚动条,文档,浏览器高度
    使用iView-cli创建一个pc端网页(1)
    ajax详解与用途
    JavaScript中for/in和for的区别
    排序
    Java中getResourceAsStream的用法
    java程序打包成exe的总结
    使用7zip把jre集成到绿色运行程序内
  • 原文地址:https://www.cnblogs.com/zeroone/p/8486743.html
Copyright © 2011-2022 走看看