3. 建立與服務(wù)端通信連接,使用protobuff編碼解碼
廢話少說,先上代碼,注釋的也比較清晰了
using Google.Protobuf;
using Google.Protobuf.Examples.AddressBook;
using Google.Protobuf.WellKnownTypes;
using System;
using System.Net.Sockets;
using UnityEngine;
public class NewBehaviourScript : MonoBehaviour {
// Use this for initialization
void Start () {
StartConnect();
}
TcpClient tcpClient; //
byte[] receive_buff; // 專門用來接收Socket里面的數(shù)據(jù)的
byte[] data_buff; // 用來存當(dāng)前未處理的數(shù)據(jù)
CodedOutputStream outputStream; // 用來綁定SocketStream,方便把proto對象轉(zhuǎn)換成字節(jié)流Stream輸送給服務(wù)器
void StartConnect()
{
TcpClient client = new TcpClient();
tcpClient = client;
//這里寫上你自己服務(wù)器的ip和端口
client.Connect("192.168.1.1", 8800);
receive_buff = new byte[client.ReceiveBufferSize];
outputStream = new CodedOutputStream(client.GetStream());
// 監(jiān)聽一波服務(wù)器消息
client.GetStream().BeginRead(receive_buff, 0, client.ReceiveBufferSize, ReceiveMessage, null);
}
int nFalg = 0; // 這個變量主要是為了防止和服務(wù)端無休無止互發(fā)消息,測試代碼
void Update () {
// 因?yàn)镽eceiveMessage接收數(shù)據(jù)是異步的方式,不是在主線程,有些方法不能用,比如ToString,所以消息處理放在這里處理
// 但主要是因?yàn)楹竺嬉由舷V播,可以添加在這里
if (data_buff != null && ++nFalg < 5)
{
// 把數(shù)據(jù)傳給CodedInputStream計(jì)算本次包的長度
CodedInputStream inputStream = new CodedInputStream(data_buff);
int length = inputStream.ReadLength();
// 計(jì)算"包長度"占用的字節(jié)數(shù),后面取數(shù)據(jù)的時候扣掉這個字節(jié)數(shù),就是真實(shí)數(shù)據(jù)長度
int lengthLength = CodedOutputStream.ComputeLengthSize(length);
// 當(dāng)前數(shù)據(jù)足夠解析一個包了
if (length + lengthLength <= data_buff.Length)
{
byte[] real_data = new byte[length];
// 拷貝真實(shí)數(shù)據(jù)
Array.Copy(data_buff, lengthLength, real_data, 0, length);
// 假設(shè)服務(wù)器給你發(fā)了個AddressBook
AddressBook ab = AddressBook.Parser.ParseFrom(real_data);
// 把這個數(shù)據(jù)直接還給服務(wù)器,驗(yàn)證客戶端發(fā)給服務(wù)器的情況
SendMsg(ab);
// 數(shù)據(jù)剛剛好,沒有多余的
if (length + lengthLength == data_buff.Length)
{
data_buff = null;
}
else
{
// 數(shù)據(jù)有剩余,保存剩余數(shù)據(jù),等下一個Update解析
byte[] t = new byte[data_buff.Length - length - lengthLength];
Array.Copy(data_buff, lengthLength + length, t, 0, t.Length);
data_buff = t;
}
}
}
}
// 發(fā)送數(shù)據(jù)
public void SendMsg(IMessage message)
{
if (outputStream != null)
{
// WriteMessage 里面會先write一個長度,然后再write真實(shí)數(shù)據(jù)
outputStream.WriteMessage(message);
outputStream.Flush(); // 把buffer數(shù)據(jù)寫入到tcpClient的流里面
}
}
public void ReceiveMessage(IAsyncResult ar)
{
try
{
// 本次接收到的數(shù)據(jù)長度
int bytesRead = tcpClient.GetStream().EndRead(ar);
if (bytesRead < 1)
{
Debug.LogError("bytesRead < 1");
return;
}
else
{
if (data_buff == null)
{
// buff里面沒有數(shù)據(jù)
data_buff = new byte[bytesRead];
Array.Copy(receive_buff, data_buff, bytesRead);
}
else
{
// buff里面有數(shù)據(jù),要和新數(shù)據(jù)整合起來
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;
}
}
// 繼續(xù)監(jiān)聽下一波數(shù)據(jù)
tcpClient.GetStream().BeginRead(receive_buff, 0, tcpClient.ReceiveBufferSize, ReceiveMessage, null);
}
catch (Exception ex)
{
// 為了防止報(bào)ex沒被使用的警告
Debug.Log(ex);
}
}
}
4. 處理與Netty服務(wù)器通信的粘包、拆包
服務(wù)器的粘包拆包是Netty本身支持的解碼編碼器,如下圖
總共四行,其中第一行作用在拆包的時候,第三行作用在粘包的時候(我猜的)。
它這個拆包粘包不是普通的那種固定4個字節(jié)標(biāo)示長度的,而是有時候1個字節(jié),有時候是2、3、4、5個字節(jié),根據(jù)當(dāng)前發(fā)送的真實(shí)數(shù)據(jù)的長度定的。
在普通的方案粘包方案,數(shù)據(jù)是這樣的:4個字節(jié)+真實(shí)數(shù)據(jù)
有的是用換行回車作為標(biāo)識符拆包、粘包
那在Netty的方案里,包長度究竟是幾個字節(jié)呢?
其實(shí)它也是用到了Protobuff里面的數(shù)據(jù)讀取、保存方式,感興趣的可以打開protobuf3-for-unity-3.0.0\src\Google.Protobuf.sln
工程看一下,在Google.Protobuf
項(xiàng)目中,打開CodedInputStream.cs
:
包頭占用幾個字節(jié)是由下面這個函數(shù)計(jì)算的:
這也是protobuff對象編碼后數(shù)據(jù)會比較小的主要原因。比如一個對象編碼后得到的是440個字節(jié)數(shù)據(jù),那么調(diào)用
ComputeRawVarint32Size(440)
的返回值是2,也就是服務(wù)器和客戶端發(fā)送的數(shù)據(jù)最終長度是440+2=442個字節(jié)。明白了這些,拆包和粘包就都不是問題了。
上面的代碼里,粘包是這一段:
public void SendMsg(IMessage message)
{
if (outputStream != null)
{
// WriteMessage 里面會先write一個長度,然后再write真實(shí)數(shù)據(jù)
outputStream.WriteMessage(message);
outputStream.Flush(); // 把buffer數(shù)據(jù)寫入到tcpClient的流里面
}
}
乍一看,好像沒有在真實(shí)數(shù)據(jù)前面加長度啊?其實(shí),在outputStream的WriteMessage里面已經(jīng)有WriteLength了,幫我們做好了。
再看拆包:
// 把數(shù)據(jù)傳給CodedInputStream計(jì)算本次包的長度
CodedInputStream inputStream = new CodedInputStream(data_buff);
int length = inputStream.ReadLength();
// 計(jì)算"包長度"占用的字節(jié)數(shù),后面取數(shù)據(jù)的時候扣掉這個字節(jié)數(shù),就是真實(shí)數(shù)據(jù)長度
int lengthLength = CodedOutputStream.ComputeLengthSize(length);
// 當(dāng)前數(shù)據(jù)足夠解析一個包了
if (length + lengthLength <= data_buff.Length)
{
byte[] real_data = new byte[length];
// 拷貝真實(shí)數(shù)據(jù)
Array.Copy(data_buff, lengthLength, real_data, 0, length);
// 假設(shè)服務(wù)器給你發(fā)了個AddressBook
AddressBook ab = AddressBook.Parser.ParseFrom(real_data);
...
}
先用CodedInputStream 看看這個“包大小”值是多少,再用CodedOutputStream.ComputeLengthSize計(jì)算這個“包大小”占幾個字節(jié),然后就明白真實(shí)數(shù)據(jù)從哪里開始,占多少字節(jié)了。
結(jié)束語
測試并不是非常非常充分,僅供參考。
參考:http://blog.csdn.net/u010841296/article/details/50957471?locationNum=2&fps=1