前言
初探篇講解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無法成功解析。