3. 建立与服务端通信连接,使用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里面的数据的
byte[] data_buff; // 用来存当前未处理的数据
CodedOutputStream outputStream; // 用来绑定SocketStream,方便把proto对象转换成字节流Stream输送给服务器
void StartConnect()
{
TcpClient client = new TcpClient();
tcpClient = client;
//这里写上你自己服务器的ip和端口
client.Connect("192.168.1.1", 8800);
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
AddressBook ab = AddressBook.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(IMessage message)
{
if (outputStream != null)
{
// WriteMessage 里面会先write一个长度,然后再write真实数据
outputStream.WriteMessage(message);
outputStream.Flush(); // 把buffer数据写入到tcpClient的流里面
}
}
public void ReceiveMessage(IAsyncResult ar)
{
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);
}
}
}
4. 处理与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
:
包头占用几个字节是由下面这个函数计算的:
这也是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了,帮我们做好了。
再看拆包:
// 把数据传给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计算这个“包大小”占几个字节,然后就明白真实数据从哪里开始,占多少字节了。
结束语
测试并不是非常非常充分,仅供参考。
参考:http://blog.csdn.net/u010841296/article/details/50957471?locationNum=2&fps=1