C#使用Protocol Buffer(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;
}

在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;
   }
  }
}

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

通信应该实现的功能:

  • 服务器可以时时监听多个客户端
  • 服务器可以时时监听某一个客户端消息
  • 服务器可以时时给某一个客户端发消息
  • 首先我们需要定义一个客户端对象

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());
   }
  }
}

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

?
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
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();
  }
  
}
  

三、数据包的编码和解码

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值