【问题标题】:C++ design - Network packets and serializationC++ 设计 - 网络数据包和序列化
【发布时间】:2008-12-01 02:06:13
【问题描述】:

对于我的游戏,我有一个 Packet 类,它代表网络数据包,基本上由一组数据和一些纯虚函数组成

然后我希望有派生自 Packet 的类,例如:StatePacket、PauseRequestPacket 等。这些子类中的每一个都将实现虚函数 Handle(),当一个接收这些数据包中的一部分以便它可以完成它的工作,几个 get/set 函数将读取和设置数据数组中的字段。

所以我有两个问题:

  1. (抽象)Packet 类需要可复制和可分配,但无需切片,保留派生类的所有字段。派生类甚至可能没有额外的字段,只有函数,这将与基类上的数组一起使用。我怎样才能做到这一点?
  2. 序列化时,我会给每个子类一个唯一的数字ID,然后在子类自己序列化之前将其写入流。但是对于反序列化,我如何将读取的 ID 映射到适当的子类以实例化它?

如果有人想要任何澄清,请询问。

-- 谢谢


编辑:我对此不太满意,但我做到了:

Packet.h:http://pastebin.com/f512e52f1
Packet.cpp:http://pastebin.com/f5d535d19
PacketFactory.h:http://pastebin.com/f29b7d637
PacketFactory.cpp:http://pastebin.com/f689edd9b
PacketAcknowledge.h:http://pastebin.com/f50f13d6f
PacketAcknowledge.cpp:http://pastebin.com/f62d34eef

如果有人有时间查看并提出任何改进建议,我将不胜感激。


是的,我知道工厂模式,但是如何编写代码来构造每个类?一个巨大的开关声明?这也会重复每个类的 ID(一次在工厂中,一个在序列化器中),我想避免这种情况。

【问题讨论】:

    标签: c++ inheritance serialization


    【解决方案1】:

    为了复制,你需要编写一个克隆函数,因为构造函数不能是虚拟的:

    virtual Packet * clone() const = 0;
    

    每个 Packet 实现都是这样实现的:

    virtual Packet * clone() const {
        return new StatePacket(*this);
    }
    

    以 StatePacket 为例。数据包类应该是不可变的。一旦收到一个数据包,它的数据可以被复制出来,也可以被丢弃。所以不需要赋值运算符。将赋值运算符设为私有且不定义它,这将有效地禁止分配包。

    对于反序列化,您使用工厂模式:创建一个类,该类在给定消息 ID 的情况下创建正确的消息类型。为此,您可以在已知消息 ID 上使用 switch 语句,或者像这样的映射:

    struct MessageFactory {
        std::map<Packet::IdType, Packet (*)()> map;
    
        MessageFactory() {
            map[StatePacket::Id] = &StatePacket::createInstance;
            // ... all other
        }
    
        Packet * createInstance(Packet::IdType id) {
            return map[id](); 
        }
    } globalMessageFactory;
    

    确实,您应该添加检查,例如 id 是否真的已知等等。这只是粗略的想法。

    【讨论】:

    • 为了可扩展性(超出您的需要),您可以考虑不在 MessageFactory 构造函数中添加指针,而是提供 registerMessageFactory( id, functor ) 方法来动态添加新类型的消息。
    【解决方案2】:

    您需要查找工厂模式。

    工厂查看传入的数据并为您创建正确类的对象。

    【讨论】:

      【解决方案3】:

      要拥有一个事先不知道所有类型的工厂类,您需要提供一个单例,每个类都在其中注册自己。我总是弄错定义模板类的静态成员的语法,所以不要只是剪切和粘贴:

      class Packet { ... };
      
      typedef Packet* (*packet_creator)();
      
      class Factory {
      public:
        bool add_type(int id, packet_creator) {
          map_[id] = packet_creator; return true;
        }
      };
      
      template<typename T>
      class register_with_factory {
      public:
        static Packet * create() { return new T; }
        static bool registered;
      };
      
      template<typename T>
      bool register_with_factory<T>::registered = Factory::add_type(T::id(), create);
      
      class MyPacket : private register_with_factory<MyPacket>, public Packet {
      //... your stuff here...
      
        static int id() { return /* some number that you decide */; }
      };
      

      【讨论】:

      • 除非您在某处使用已注册的,否则不会调用您的 add_type 函数。例如获取它的地址: (void) &register_with_factory::registered;由于跨翻译单元的静态初始化顺序,我也避免了这种情况。
      • 两者都好。静态初始化顺序问题可以(大部分)通过使用单例来解决,但是您正在处理单例的破坏顺序问题。
      【解决方案4】:

      为什么我们,包括我自己,总是把这么简单的问题搞得这么复杂?


      也许我不在这儿了。但我不得不怀疑:这真的是满足您需求的最佳设计吗?

      总的来说,仅函数继承可以通过函数/方法指针,或聚合/委托以及数据对象的传递来更好地实现,而不是通过多态。

      多态是一个非常强大和有用的工具。但这只是我们可用的众多工具之一。


      看起来 Packet 的每个子类都需要自己的编组和解组代码。也许继承了 Packet 的 Marshalling/Unmarshalling 代码?也许延长它?一切都在 handle() 和其他任何需要的基础之上。

      代码太多了。

      虽然更加笨拙,但将 Packet 的数据实现为 Packet 类的结构/联合属性可能会更短、更快。

      然后,编组和解组将集中化。

      根据您的架构,它可以像 write(&data) 一样简单。假设您的客户端/服务器系统之间没有大/小端问题,也没有填充问题。 (例如 sizeof(data) 在两个系统上都是相同的。)

      Write(&data)/read(&data) 是一种容易出错的技术。但这通常是编写初稿的一种非常快速的方法。稍后,如果时间允许,您可以将其替换为基于每个属性类型的编组/解组代码。

      另外:我已经开始将作为结构发送/接收的数据存储起来。您可以使用 operator=() 按位复制结构,这有时非常有用!虽然在这种情况下可能没有那么多。


      最终,您将在该 subclass-id 类型的某处有一个 switch 语句。工厂技术(它本身就非常强大和有用)会为您完成此切换,查找必要的 clone() 或 copy() 方法/对象。

      或者你可以在 Packet 中自己做。您可以使用如下简单的东西:

      (getHandlerPointer(id))(这个)


      除了快速的开发时间之外,这种方法的另一个优点(函数指针)是您不需要不断地为每个数据包分配和删除一个新对象。您可以一遍又一遍地重复使用单个数据包对象。或者,如果您想将它们排队,则为数据包向量。 (请注意,我会在再次调用 read() 之前清除 Packet 对象!为了安全起见......)

      根据您游戏的网络流量密度,分配/解除分配可能会变得昂贵。话又说回来,过早的优化是万恶之源。而且你总是可以滚动你自己的新/删除操作符。 (还有更多的编码开销......)


      您失去的(使用函数指针)是每种数据包类型的干净隔离。特别是能够在不更改现有代码/文件的情况下添加新的数据包类型。


      示例代码:

      class Packet
      {
      public:
        enum PACKET_TYPES
        {
          STATE_PACKET = 0,
          PAUSE_REQUEST_PACKET,
      
          MAXIMUM_PACKET_TYPES,
          FIRST_PACKET_TYPE = STATE_PACKET
        };
      
        typedef  bool ( * HandlerType ) ( const Packet & );
      
      protected:
              /* Note: Initialize handlers to NULL when declared! */
        static HandlerType  handlers [ MAXIMUM_PACKET_TYPES ];
      
        static HandlerType  getHandler( int thePacketType )
          {   // My own assert macro...
            UASSERT( thePacketType, >=, FIRST_PACKET_TYPE    );
            UASSERT( thePacketType, <,  MAXIMUM_PACKET_TYPES );
            UASSERT( handlers [ thePacketType ], !=, HandlerType(NULL) );
            return handlers [ thePacketType ];
          }
      
      protected:
         struct Data
         {
                  // Common data to all packets.
           int  number;
           int  type;
      
           union
           {
             struct
             {
               int foo;
             } statePacket;
      
             struct
             {
               int bar;
             } pauseRequestPacket;
      
           } u;
      
         } data;
      
      
      public:
      
        //...
        bool readFromSocket() { /*read(&data); */ }  // Unmarshal
        bool writeToSocket()  { /*write(&data);*/ }  // Marshal
      
        bool handle() { return ( getHandler( data.type ) ) ( * this ); }
      
      }; /* class Packet */
      

      PS:你可能会用谷歌搜索并下载 cdecl/c++decl。它们是非常有用的程序。尤其是在玩弄函数指针时。

      例如:

      c++decl> declare foo as function(int) returning pointer to function returning void
      void (*foo(int ))()
      c++decl> explain void (* getHandler( int  ))( const int & );
      declare getHandler as function (int) returning pointer to function (reference to const int) returning void
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-08-03
        • 2011-08-29
        • 1970-01-01
        • 2019-07-05
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多