前言
初探篇讲解Socket基础知识,和GF框架如何实现Tcp网络渠道的,后面文章会对接ET框架进行网络测试。
1.Network常用接口
程序员必须掌握的技能:算法、框架、网络,我说的...(小声bb:虽然这三个我也不会),接下来学习框架这么实现网络模块的,首先给出常用的接口表格:
网络模块 | 接口说明 |
HasNetworkChannel | 检查是否存在网络频道 |
GetNetworkChannel | 获取网络频道 |
GetAllNetworkChannels | 获取所有网络频道 |
CreateNetworkChannel | 创建网络频道 |
DestroyNetworkChannel | 销毁网络频道 |
网络渠道 | 接口说明 |
Connect | 连接到远程主机 |
Send | 发送消息包到远程主机 |
其实网络渠道相当于Socket高级封装,框架主要实现消息接收(通过队列和消息池),消息包的序列化和反序列化需要开发者继承INetworkChannelHelper实现,后面文章会讲述如何实现对接ET框架的类,这里简单讲述网络模块的框架实现原理。
2.框架实现了什么?
先从NetworkChannelBase脚本入手,它存在以下这些变量,具体代码如下:
private const float DefaultHeartBeatInterval = 30;
private readonly string m_Name;
protected readonly Queue<Packet> m_SendPacketPool;
protected readonly EventPool<Packet> m_ReceivePacketPool;
protected readonly INetworkChannelHelper m_NetworkChannelHelper;
protected AddressFamily m_AddressFamily;
protected bool m_ResetHeartBeatElapseSecondsWhenReceivePacket;
protected float m_HeartBeatInterval;
protected Socket m_Socket;
protected readonly SendState m_SendState;
protected readonly ReceiveState m_ReceiveState;
protected readonly HeartBeatState m_HeartBeatState;
protected int m_SentPacketCount;
protected int m_ReceivedPacketCount;
protected bool m_Active;
private bool m_Disposed;
详细代码自行查阅GameFramework,通过变量分析后,可以得到以下几点认知:
1、网络模块内置心跳包功能,但是具体发送心跳包的函数是使用INetworkChannelHelper发送,也就是用户自定义发送方式。
2、发送和接受消息都有队列,唯一不同点是接受消息队列使用EventPool,这样可以确保消息接受时触发对应函数。
3、发送、接受、心跳都封装到对应State类里,都内置MemoryStream对象和封装对应函数。
TcpNetworkChannel继承它,链接、发送、接受消息都是通过异步函数执行的。接下来给出异步函数表格:
BeginAccept | 开始一个异步操作来接受一个传入的连接尝试 |
BeginConnect | 开始一个对远程主机连接的异步请求 |
BeginDisconnect | 开始异步请求从远程终结点断开连接 |
EndAccept | 异步接受传入的连接尝试 |
EndConnect | 结束挂起的异步连接请求 |
EndDisconnect | 结束挂起的异步断开连接请求 |
BeginReceive | 开始从连接的Socket中异步接收数据 |
BeginReceiveFrom | 开始从指定网络设备中异步接收数据 |
BeginReceiveMessageFrom | 开始使用指定的SocketFlags将指定字节数的数据异步接收到数据缓冲区的指定位置,然后存储终结点和数据包信息 |
BeginSend | 将数据异步发送到连接的Socket |
BeginSendFile | 将文件异步发送到连接的Socket对象 |
BeginSendTo | 向特定远程主机异步发送数据 |
发送消息入栈发送队列,出栈通过BeginSend将消息发送出去,发送成功后调用m_SendCallback函数。至于接受队列如何触发对应句柄呢,如果了解事件池原理的,就跳过以下内容。需要调用RegisterHandler将回调消息函数注册到事件池中,在INetworkChannelHelper通过反射将所有继承IPacketHandler进行注册,至于继承IPacketHandler如何进行实现呢?通过心跳包回调句柄分析实现。
namespace GameMain
{
public partial class TestMessageHandle : IPacketHandler
{
public override int Id => OuterOpcode.R2C_Ping;
public override void Handle(AChannel channel, R2C_Ping message)
{
//TODO 发送C2R_Ping
}
}
}
3.对接ET框架
ET版本5.0讲解Tcp消息包头,首先展示出ET消息包的格式。
所以GF框架去实现INetworkChannelHelper也需要对应上格式,具体代码如下:
public IMessage DeserializeMessage(IMHeader header, MemoryStream source, out object customErrorData)
{
customErrorData = null;
THeader tcpheader = header as THeader;
if (tcpheader == null)
{
Log.Warning("tcp header is invalid.");
return null;
}
IMessage message = null;
if (tcpheader.IsValid)
{
message = m_Responses[header.Id];
if (message != null)
{
ProtobufHelper.FromStream(message, source);
}
else
{
Log.Warning("Can not deserialize message for message id '{0}'.", header.Id.ToString());
}
}
else
{
Log.Warning("tcp header is invalid.");
}
ReferencePool.Release(tcpheader);
return message;
}
public IMHeader DeserializeMessageHeader(MemoryStream source, out object customErrorData)
{
customErrorData = null;
THeader header = ReferencePool.Acquire<THeader>();
if (source != null)
{
byte[] bytes = source.GetBuffer();
header.Len = BitConverter.ToInt32(bytes, 0) - 2;
header.Id = BitConverter.ToUInt16(bytes, NetworkParam.TcpOpcodeIndex);
return header;
}
return null;
}
public bool Serialize<T>(T message, MemoryStream destination) where T : IMessage
{
memoryStream.Seek(HeaderLen, SeekOrigin.Begin);
memoryStream.SetLength(HeaderLen);
ProtobufHelper.ToStream(message, memoryStream);
THeader header = ReferencePool.Acquire<THeader>();
header.Id = message.Id;
header.Len = (int)(memoryStream.Length - NetworkParam.TcpOpcodeIndex);
memoryStream.Position = 0;
this.cache.WriteTo(0, header.Len);
this.cache.WriteTo(4, header.Id);
Array.Copy(cache, 0, memoryStream.GetBuffer(), 0, cache.Length);
memoryStream.WriteTo(destination);
ReferencePool.Release(header);
return true;
}
序列化消息时长度需要减去4字节,反序列化消息时需要指定大小,不然GoogleProtobuf无法成功解析。