《炉石传说》建筑设计欣赏(7):采用Google.ProtocolBuffers处理网络消息

时间:2022-06-01 19:15:42

这一次,琢磨了一下Unity3D网络游戏发展的网络信息处理。服务器的网络游戏一般都是自主研发,因此,相应的网络消息处理应该培养自己。client/现在使用的邮件服务器之间的价差JSON和Google.ProtocolBuffers有两种常见的方法。平炉码看其处理。代码写的还是非常好的,把它的思路分析一下。与大家分享。

总体机制描写叙述

我们想要达到的目标大概是这种:
  • 有N个网络消息,每一个消息相应一个Proto中的message描写叙述;
  • 每一个消息相应一个数字ID;
  • 底层在收到消息是,将其解析成为Google.ProtocolBuffers.IMessage对象,这个对象的详细类型应该是前面那个message生成的代码;
  • 发送消息就简单了,由于知道其类型,能够直接运行序列化。

炉石使用Google.ProtocolBuffers类库,能够看这里:http://www.nuget.org/packages/Google.ProtocolBuffers/

消息发送

发送的机制非常easy,首先使用ProtocolBuffer生成的message类构造一个消息对象,比如:ConnectAPI.SendPing()
public static void SendPing()
{
Ping.Builder body = Ping.CreateBuilder();
QueueGamePacket(0x73, body);
s_lastGameServerPacketSentTime = DateTime.Now;
}

底层会构造一个“PegasusPacket”数据包对象。加入到发送队列之中,这个数据包对象主要包括3部分:消息ID。消息大小,详细消息数据。详见PegasusPacket.Encode()函数:

public override byte[] Encode()
{
if (!(this.Body is IMessageLite))
{
return null;
}
IMessageLite body = (IMessageLite) this.Body;
this.Size = body.SerializedSize;
byte[] destinationArray = new byte[8 + this.Size];
Array.Copy(BitConverter.GetBytes(this.Type), 0, destinationArray, 0, 4);
Array.Copy(BitConverter.GetBytes(this.Size), 0, destinationArray, 4, 4);
body.WriteTo(CodedOutputStream.CreateInstance(destinationArray, 8, this.Size));
return destinationArray;
}

消息接收与解析

接下来我们重点看一下消息的接收与解析机制。首先由于TCP是流式的。所以底层应该检測数据包头,并收集到一个完整的数据包,然后再发送到上层解析。这部分逻辑是在”ClientConnection<PacketType>.BytesReceived()“中实现的。

当收到完整数据包时。会在主线程中触发”OnPacketCompleted“事件,实际上会调用到”ConnectAPI.PacketReceived()“,其内部主要是调用了”ConnectAPI.QueuePacketReceived()“,这个函数负责将TCP层接收到的byte[]解析成相应的IMessage对象。


重点来了!因为网络层发过来的数据包,仅仅包括一个消息ID。那么client就须要解决从ID找到相应的消息Type的问题。

想象中无非有两种方式去做:1是手动记录每一个ID相应的Type;2是搞一个中间的相应关系的类,附加上自己定义的Attribute,然后在使用反射机制自己主动收集这些类,事实上和前者也差点儿相同。

炉石採用了第一种方式。总体机制是这种:

  • client每一个消息相应一个PacketDecoder的派生类对象;
  • ConnectAPI类使用一个字典,用来保存<消息ID,Decoder对象>之间的相应关系:ConnectAPI.s_packetDecoders:SortedDictionary<Int32,ConnectAPI.PacketDecoder>;
  • 假设每一个消息都要写一个Decoder,而其内部代码由全然一致,岂不是非常蛋疼?!

    好吧,我们用模板来实现,详见兴许分析;

  • 在ConnectAPI.ConnectInit()初始化的时候。创建Decoder对象。并保存到上述dict之中,类似这样:

     s_packetDecoders.Add(0x74, new DefaultProtobufPacketDecoder<Pong, Pong.Builder>());
  • 最后在上述的收到完整数据包的函数中,依据数据包中记录的消息ID。去查找Decoder。然后调用其方法得到详细的消息对象。类似这样:
        if (s_packetDecoders.TryGetValue(packet.Type, out decoder))
    {
    PegasusPacket item = decoder.HandlePacket(packet);
    if (item != null)
    {
    queue.Enqueue(item);
    }
    }
    else
    {
    Debug.LogError("Could not find a packet decoder for a packet of type " + packet.Type);
    }

最后我们看一下,Decoder模板的实现技巧。首先消息解析的详细操作是有Google.ProtocolBuffers生成的代码去实现的,所以详细操作流程是全然一致的。这些写到基类的的静态模板函数中:

public abstract class PacketDecoder
{
// Methods
public abstract PegasusPacket HandlePacket(PegasusPacket p);
public static PegasusPacket HandleProtoBuf<TMessage, TBuilder>(PegasusPacket p) where TMessage: IMessageLite<TMessage, TBuilder> where TBuilder: IBuilderLite<TMessage, TBuilder>, new()
{
byte[] body = (byte[]) p.Body;
TBuilder local2 = default(TBuilder);
TBuilder local = (local2 == null) ? Activator.CreateInstance<TBuilder>() : default(TBuilder);
p.Body = local.MergeFrom(body).Build();
return p;
}
}

其次。使用一个模板派生类,实现HandlePacket()这个虚函数,基本的目的仅仅是把TMessage和TBuilder这两个类型传给那个静态函数而已:

public class DefaultProtobufPacketDecoder<TMessage, TBuilder> : ConnectAPI.PacketDecoder where TMessage: IMessageLite<TMessage, TBuilder> where TBuilder: IBuilderLite<TMessage, TBuilder>, new()
{
// Methods
public override PegasusPacket HandlePacket(PegasusPacket p)
{
return ConnectAPI.PacketDecoder.HandleProtoBuf<TMessage, TBuilder>(p);
}
}

OK,炉石是使用使用ProtocolBuffers来处理网络消息的机制就是这样,是不是已经非常清晰啦!



版权声明:本文博主原创文章,博客,未经同意不得转载。