using Google.Protobuf;
//using Google.Protobuf.Examples.AddPerson;
using Google.Protobuf.WellKnownTypes;
using System;
using System.Net.Sockets;
using UnityEngine;
using ARProto;
using pb = global::Google.Protobuf;
public class NewBehaviourScript : MonoBehaviour
{
void OnGUI()
{
if (GUI.Button(new Rect(100, 10, 120, 100), new GUIContent("Button", "Go")))
{
UIGo();
}
}
public void UIGo()
{
Person person =new Person();
person.Id=01;
AddPerson addPerson =new AddPerson();
SendMsg(addPerson);
}
// Use this for initialization
void Start()
{
StartConnect();
}
TcpClient tcpClient; //
byte[] receive_buff; // 專門用來接收Socket裏面的數據的
byte[] data_buff; // 用來存當前未處理的數據
CodedOutputStream outputStream; // 用來綁定SocketStream,方便把proto對象轉換成字節流Stream輸送給服務器
void StartConnect()
{
TcpClient client = new TcpClient();
tcpClient = client;
//這裏寫上你自己服務器的ip和端口
client.Connect("106.2.124.243", 9090);
receive_buff = new byte[client.ReceiveBufferSize];
outputStream = new CodedOutputStream(client.GetStream());
// 監聽一波服務器消息
client.GetStream().BeginRead(receive_buff, 0, client.ReceiveBufferSize, ReceiveMessage, null);
}
int nFalg = 0; // 這個變量主要是爲了防止和服務端無休無止互發消息,測試代碼
void Update()
{
// 因爲ReceiveMessage接收數據是異步的方式,不是在主線程,有些方法不能用,比如ToString,所以消息處理放在這裏處理
// 但主要是因爲後面要加上消息廣播,可以添加在這裏
if (data_buff != null && ++nFalg < 5)
{
// 把數據傳給CodedInputStream計算本次包的長度
CodedInputStream inputStream = new CodedInputStream(data_buff);
int length = inputStream.ReadLength();
// 計算"包長度"佔用的字節數,後面取數據的時候扣掉這個字節數,就是真實數據長度
int lengthLength = CodedOutputStream.ComputeLengthSize(length);
// 當前數據足夠解析一個包了
if (length + lengthLength <= data_buff.Length)
{
byte[] real_data = new byte[length];
// 拷貝真實數據
Array.Copy(data_buff, lengthLength, real_data, 0, length);
// 假設服務器給你發了個AddressBook
AddPerson ab = AddPerson.Parser.ParseFrom(real_data);
// 把這個數據直接還給服務器,驗證客戶端發給服務器的情況
SendMsg(ab);
// 數據剛剛好,沒有多餘的
if (length + lengthLength == data_buff.Length)
{
data_buff = null;
}
else
{
// 數據有剩餘,保存剩餘數據,等下一個Update解析
byte[] t = new byte[data_buff.Length - length - lengthLength];
Array.Copy(data_buff, lengthLength + length, t, 0, t.Length);
data_buff = t;
}
}
}
}
// 發送數據
public void SendMsg( pb::IMessage<AddPerson> message)
{
if (outputStream != null)
{
// WriteMessage 裏面會先write一個長度,然後再write真實數據
//// outputStream.WriteMessage(message);
outputStream.Flush(); // 把buffer數據寫入到tcpClient的流裏面
}
}
public void ReceiveMessage(IAsyncResult ar)
{
Debug.Log("消息:" + receive_buff);
try
{
// 本次接收到的數據長度
int bytesRead = tcpClient.GetStream().EndRead(ar);
if (bytesRead < 1)
{
Debug.LogError("bytesRead < 1");
return;
}
else
{
if (data_buff == null)
{
// buff裏面沒有數據
data_buff = new byte[bytesRead];
Array.Copy(receive_buff, data_buff, bytesRead);
}
else
{
// buff裏面有數據,要和新數據整合起來
byte[] new_data = new byte[bytesRead + data_buff.Length];
Array.Copy(data_buff, new_data, data_buff.Length);
Array.Copy(receive_buff, 0, new_data, data_buff.Length, bytesRead);
data_buff = new_data;
}
}
// 繼續監聽下一波數據
tcpClient.GetStream().BeginRead(receive_buff, 0, tcpClient.ReceiveBufferSize, ReceiveMessage, null);
}
catch (Exception ex)
{
// 爲了防止報ex沒被使用的警告
Debug.Log(ex);
}
}
}
處理與Netty服務器通信的粘包、拆包
服務器的粘包拆包是Netty本身支持的解碼編碼器,如下圖
服務器粘包、拆包處理方式
總共四行,其中第一行作用在拆包的時候,第三行作用在粘包的時候(我猜的)。
它這個拆包粘包不是普通的那種固定4個字節標示長度的,而是有時候1個字節,有時候是2、3、4、5個字節,根據當前發送的真實數據的長度定的。
在普通的方案粘包方案,數據是這樣的:4個字節+真實數據
有的是用換行回車作爲標識符拆包、粘包
那在Netty的方案裏,包長度究竟是幾個字節呢?
其實它也是用到了Protobuff裏面的數據讀取、保存方式,感興趣的可以打開protobuf3-for-unity-3.0.0\src\Google.Protobuf.sln
工程看一下,在Google.Protobuf
項目中,打開CodedInputStream.cs
:
SlowReadRawVarint32
包頭佔用幾個字節是由下面這個函數計算的:
這個是計算一個uint數據的真實長度的方法
這也是protobuff對象編碼後數據會比較小的主要原因。比如一個對象編碼後得到的是440個字節數據,那麼調用ComputeRawVarint32Size(440)
的返回值是2,也就是服務器和客戶端發送的數據最終長度是440+2=442個字節。明白了這些,拆包和粘包就都不是問題了。
上面的代碼裏,粘包是這一段:
public void SendMsg(IMessage message)
{
if (outputStream != null)
{
// WriteMessage 裏面會先write一個長度,然後再write真實數據
outputStream.WriteMessage(message);
outputStream.Flush(); // 把buffer數據寫入到tcpClient的流裏面
}
}
乍一看,好像沒有在真實數據前面加長度啊?其實,在outputStream的WriteMessage裏面已經有WriteLength了,幫我們做好了。
image.png
再看拆包:
// 把數據傳給CodedInputStream計算本次包的長度
CodedInputStream inputStream = new CodedInputStream(data_buff);
int length = inputStream.ReadLength();
// 計算"包長度"佔用的字節數,後面取數據的時候扣掉這個字節數,就是真實數據長度
int lengthLength = CodedOutputStream.ComputeLengthSize(length);
// 當前數據足夠解析一個包了
if (length + lengthLength <= data_buff.Length)
{
byte[] real_data = new byte[length];
// 拷貝真實數據
Array.Copy(data_buff, lengthLength, real_data, 0, length);
// 假設服務器給你發了個AddressBook
AddressBook ab = AddressBook.Parser.ParseFrom(real_data);
...
}
先用CodedInputStream 看看這個“包大小”值是多少,再用CodedOutputStream.ComputeLengthSize計算這個“包大小”佔幾個字節,然後就明白真實數據從哪裏開始,佔多少字節了。