Unity网络编程(四)Protobuf
2019-12-15 本文已影响0人
罗卡恩
如果服务器客户端是两种语言写的 然后编码和解码工作量比较大
就有了protobuf来救我们
官方git
https://github.com/protocolbuffers/protobuf
为啥要用protobuf
1.比xml json 小 解析快
2..NET自带的二进制序列化 改变数据结构麻烦
3.自己写的 还要做不同语言的适配
缺点
基本没有可读性
数据脱离.proto文件就没有意义
protobuf流程
原来用的都是proto2 现在用最新版proto3试一试
1.定义proto结构 就类似于序列化结构 但要用proto格式去写
2.用proto提供的工具编译为对应语言的代码
3.然后就是序列化 反序列化
proto语法
//没有定义这个标志,默认认为你使用proto2版本
syntax = "proto3";
//避免不同工程间的名称冲突
package bigtalkunity;
//可以引用别的proto文件的一些结构
import "google/protobuf/timestamp.proto";
//不写就默认生成的类的命名空间就是package的名称
option csharp_namespace = "BigTalkUnity.AddressBook";
//把他想成class
message Person {
//等于号后的数字是字段的唯一编号 1-15编号使用一个字节
//16-2047的会使用2个字节
//19000-19999之间的编号不能使用,因为是protobuf内部保留使用。
string name = 1;
int32 id = 2; //C#int 在这里是int32
string email = 3;
//枚举第一个值必须为0 也可以单独定义
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
//也可以套
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
//当成list
repeated PhoneNumber phones = 4;
google.protobuf.Timestamp last_updated = 5;
}
//表示可以嵌套
message AddressBook {
repeated Person people = 1;
}
对应关系
image.png
,单行注释使用//,多行注释使用/* ... */
proto修饰符 Required 发送必须有值 接收必须识别该字段
optional 可以不设置值 无法识别就忽略 可以做到按需升级和平滑过渡
Repeated 可以传0-N的相同的元素
编译proto
从这里下载protoc
https://github.com/protocolbuffers/protobuf/releases
找到对应自己电脑的版本下载
然后在cmd控制台输入
-I等价于--protopath --csharp_out是输出语言种类 别的语言是别的指令 protoc -h 查看 然后面的就是生成位置
protoc -I=$SRC_DIR --csharp_out=$DST_DIR $SRC_DIR/addressbook.proto
当前路径cmd可以直接打开当前目录下的命令行
image.png
这样就是写入当前目录
protoc.exe --csharp_out=. Person.proto
image.png
image.png
然后之前的网站下载C#版的库
image.png
需要unity.NET版本在4.X以上才能用
在PlayerSetting里可以设置
image.png
这个东西解压到unity里
image.png
解压到unity里
然后就可以使用了
先看看protobuf生成的.cs代码
然后Person.cs放到unity里
/**
*Copyright(C) 2019 by #COMPANY#
*All rights reserved.
*FileName: #SCRIPTFULLNAME#
*Author: #AUTHOR#
*Version: #VERSION#
*UnityVersion:#UNITYVERSION#
*Date: #DATE#
*Description:
*History:
*/
using BigTalkUnity.AddressBook;
using UnityEngine;
//引用静态类型
using static BigTalkUnity.AddressBook.Person.Types;
public class ProtoTest : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
Person john = new Person {
Id=1234,
Name="john",
Email="132@qq.com",
Phones = { new PhoneNumber {Number="4564", Type= PhoneType.Home } }
};
}
}
数据就这样做好了
然后就差序列化反序列化了
这样可以储存一些数据配置表啥的
/**
*Copyright(C) 2019 by #COMPANY#
*All rights reserved.
*FileName: #SCRIPTFULLNAME#
*Author: #AUTHOR#
*Version: #VERSION#
*UnityVersion:#UNITYVERSION#
*Date: #DATE#
*Description:
*History:
*/
using BigTalkUnity.AddressBook;
using Google.Protobuf;
using System.IO;
using UnityEngine;
//引用静态类型
using static BigTalkUnity.AddressBook.Person.Types;
public class ProtoTest : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
Person john = new Person {
Id=1234,
Name="john",
Email="132@qq.com",
Phones = { new PhoneNumber {Number="4564", Type= PhoneType.Home } }
};
//序列化
// 写入stream
using (var output = File.Create("john.dat"))
{
john.WriteTo(output);
}
// 转为json字符串
var jsonStr = john.ToString();
// 转为bytestring
var byteStr = john.ToByteString();
// 转为byte数组
var byteArray = john.ToByteArray();
//反序列化
// 1. 从stream中解析
using (var input = File.OpenRead("john.dat"))
{
john = Person.Parser.ParseFrom(input);
}
// 2. 从字节串中解析
john = Person.Parser.ParseFrom(byteStr);
// 3. 从字节数组中解析
john = Person.Parser.ParseFrom(byteArray);
// 4. 从json字符串解析
john = Person.Parser.ParseJson(jsonStr);
}
}
运行脚本可以在这里看到我们的数据
image.png
image.png
改造聊天室
proto只是序列化反序列化 不能代表整个包体就不用我们管了
syntax = "proto3";
package ChatSystem;
import "google/protobuf/timestamp.proto";
message ChatMessage{
string name = 1;
string content = 2;
//用来代表发送消息的时间
google.protobuf.Timestamp send_time = 16;
}
image.png
cmd编译一下
protoc.exe --csharp_out=. chatMsg.proto
然后粘贴到unity里
/**
*Copyright(C) 2019 by #COMPANY#
*All rights reserved.
*FileName: #SCRIPTFULLNAME#
*Author: #AUTHOR#
*Version: #VERSION#
*UnityVersion:#UNITYVERSION#
*Date: #DATE#
*Description:
*History:
*/
using UnityEngine;
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine.UI;
using System;
using System.Collections.Generic;
using System.Linq;
using Google.Protobuf;
// Timestamp类型需要
using Google.Protobuf.WellKnownTypes;
using ChatSystem;
public class TalkClient : MonoBehaviour
{
public InputField input;
public InputField inputName;
public Text text;
public Button btn;
byte[] buffer = new byte[1024];
Socket socket;
//是msg的名字 不是proto生成C#的名字
List<ChatMessage> msg = new List<ChatMessage>();
// 用来存储接收到的数据
List<byte> DataCache = new List<byte>();
// Start is called before the first frame update
void Start()
{
socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
//连接服务器
socket.Connect("127.0.0.1", 9999);
socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallback, null);
btn.onClick.AddListener(() =>
{
var chatMsg = new ChatMessage()
{
Name = inputName.text,
Content = input.text,
// protobuf的Timestamp要求使用Utc时间传输
SendTime = Timestamp.FromDateTime(DateTime.UtcNow)
};
byte[] msg = Encode(chatMsg.ToByteArray());
//发送数据
socket.Send(msg);
});
}
private void Update()
{
if (msg.Count > 0)
{
foreach (var item in msg)
{
//因为unity不能在子线程调用unity大部分API Debug.Log可以 socket内部异步为我们开了线程
//UniRx插件中有一个MainThreadDispatcher类,可以很方便地用来处理子线程到主线程的转换
//// protobuf的Timestamp要求使用Utc时间传输,在这转为本地时间
text.text += item.Name + ":" + item.Content+ "" + item.SendTime.ToDateTime().ToLocalTime()+ "\n";
}
//清除处理过的消息
msg.Clear();
}
}
void ReceiveCallback(IAsyncResult ar)
{
try
{
//接受数据
int length = socket.EndReceive(ar);
if (length > 0)
{
//取接收长度个加入缓存
DataCache.AddRange(buffer.Take(length));
ProcessData();
//重新开始接受
socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallback, null);
}
else
{
OnClientDisconnect();
}
}
catch (SocketException ex)
{
if (ex.SocketErrorCode == SocketError.ConnectionReset)
OnClientDisconnect();
}
}
void ProcessData()
{
while (true)
{
var data = Decode();
if (data==null)
break;
//转json
var chatMsg = ChatMessage.Parser.ParseFrom(data);
Debug.Log($"接收到服务端的消息:{chatMsg.Content}");
msg.Add(chatMsg);
}
}
void OnClientDisconnect()
{
Debug.Log("与服务端断开连接");
socket.Close();
}
//封包
byte[] Encode(byte[] data)
{
byte[] jsonBytes = data;
var length = BitConverter.GetBytes((ushort)jsonBytes.Length);
byte[] bytes = new byte[jsonBytes.Length + 2];
//拷贝字头到前两位 然后把数据拷贝到后两位
Buffer.BlockCopy(length, 0, bytes, 0, 2);
Buffer.BlockCopy(jsonBytes, 0, bytes, 2, jsonBytes.Length);
return bytes;
}
// 解包
byte[] Decode()
{
if (DataCache.Count < 2) return null;
var length = BitConverter.ToUInt16(DataCache.Take(2).ToArray(), 0);
// 判断数据是否足够,如果不够可能原因是发生分包,下次再解析
if (DataCache.Count - 2 >= length)
{
//跳过前两位
var bytes = DataCache.Skip(2).Take(length).ToArray();
DataCache.RemoveRange(0, length + 2);
return bytes;
}
return null;
}
void OnDestroy()
{
socket.Close();
}
}
跑通了
image.png