预序列化消息对象 - 实现?

时间:2011-12-15 14:15:26

标签: c++ sockets networking inheritance composition

我有一个TCP客户端 - 服务器设置,我需要能够使用相同的传输/接收基础设施在不同时间传递不同格式的消息。

从客户端发送到服务器的两种不同类型的消息可能是:

  • TIME_SYNC_REQUEST:请求服务器的游戏时间。除邮件类型外不包含任何其他信息。

  • UPDATE:描述自上次更新发布以来发生的所有游戏状态更改(如果这不是连接后的第一个更新),以便服务器可以更新其数据模型的位置它认为合适。

(要包含在标题中的消息类型,以及要包含在消息正文中的任何数据。)

在动态语言中,我会创建一个AbstractMessage类型,并从中派生两种不同的消息类型,TimeSyncRequestMessage不包含额外的数据成员,UpdateMessage包含所有必要的成员(玩家位置等),并使用反射来查看我需要为套接字send()实际序列化。由于类名描述了类型,我甚至不需要额外的成员。

在C ++中:出于性能原因,我不希望使用dynamic_cast来镜像上述方法。我是否应该使用组合方法,虚拟成员填写任何可能的数据,以及char messageType?我想另一种可能性是在不同类型的列表中保留不同的消息类型。这是唯一的选择吗?否则,我还可以做什么来存储消息信息,直到将其序列化为止?

6 个答案:

答案 0 :(得分:1)

除非你有一些非常高的性能特征,否则我会使用自我描述的消息格式。这通常使用通用格式(比如key = value),但没有特定的结构,而是已知的属性会描述消息的类型,然后可以使用特定于该消息类型的逻辑从该消息中提取任何其他属性。

我发现这种类型的消息传递保留了更好的向后兼容性 - 因此,如果您要添加新属性,则可以添加,而较旧的客户端将无法看到它们。使用固定结构的消息往往不太好。

编辑:有关自我描述消息格式的更多信息。基本上,这里的想法是您定义字段字典 - 这是您的通用消息包含的字段的范围。现在,默认消息必须包含一些必填字段,然后由您自己添加到消息中的其他字段。序列化/反序列化非常简单,最终构建一个包含要添加的所有字段的blob,另一端,构造一个具有所有属性的容器(想象一个映射)。必填字段可以描述类型,例如,您可以在字典中使用字段作为消息类型,并为所有消息设置此字段。您询问此字段以确定如何处理此消息。一旦处于处理逻辑中,您只需从容器中提取逻辑所需的其他属性(映射)并处理它们。

这种方法提供了最大的灵活性,允许您执行仅传输真正改变的字段的操作。现在你如何保持这种状态取决于你 - 但鉴于你在消息和处理逻辑之间有一对一的映射 - 你既不需要继承也不需要组合。这种类型的系统的智能源于你如何序列化字段(并反序列化,以便你知道字段中的字典属性)。有关这种格式的示例,请参阅FIX协议 - 现在我不会提倡将其用于游戏,但这个想法应该展示自我描述的消息是什么。

EDIT2:我无法提供完整的实现,但这是一个草图。

首先让我定义一个值类型 - 这是字段可以存在的典型值:

typedef boost::variant<int32, int64, double, std::string> value_type;

现在我描述一个字段

struct field
{
  int field_key;
  value_type field_value;    
};

现在这是我的留言容器

struct Message
{
  field type;
  field size;

  container<field> fields; // I use a generic "container", you can use whatever you want (map/vector etc. depending on how you want to handle repeating fields etc.)
};

现在让我们说我想构建一条TIME_SYNC更新的消息,使用工厂为我生成一个合适的骨架

boost::unique_ptr<Message> getTimeSyncMessage()
{
  boost::unique_ptr<Message> msg(new Message);
  msg->type = { dict::field_type, TIME_SYNC }; // set the type

  // set other default attributes for this message type

  return msg;
}

现在,我想设置更多属性,这就是我需要支持字段的字典,例如......

namespace dict
{
  static const int field_type = 1; // message type field id

  // fields that you want
  static const int field_time = 2;
  :
}

现在我可以说,

boost::unique_ptr<Message> msg = getTimeSyncMessage();

msg->setField(field_time, some_value);
msg->setField(field_other, some_other_value);
: // etc.

现在,当您准备发送时,此消息的序列化只是单步执行容器并添加到blob。您可以使用ASCII编码或二进制编码(我先从前者开始,然后再转到后者 - 具体取决于要求)。所以上面的ASCII编码版本可能是这样的:

1=1|2=10:00:00.000|3=foo 

为了论证,我使用|来分隔字段,您可以使用其他可以保证不会出现在您的值中的字段。使用二进制格式 - 这是不相关的,每个字段的大小可以嵌入数据中。

反序列化将逐步执行blob,适当地提取每个字段(例如,通过|分隔),使用工厂方法生成骨架(一旦获得类型 - 字段{{1然后填写容器中的所有属性。稍后当您想要获取特定属性时 - 您可以执行以下操作:

1

我知道这只是一个草图,但希望它传达了自我描述格式背后的想法。一旦你掌握了基本的想法,就可以做很多优化 - 但这是另一回事......

答案 1 :(得分:1)

也许您可以让消息类进行序列化 - 定义序列化接口,每条消息都实现此接口。因此,在您想要序列化和发送时,您可以调用AbstractMessage :: Serialize()来获取序列化数据。

答案 2 :(得分:0)

一种常见的方法是在所有邮件上添加标题。例如,您可能有一个如下所示的标题结构:

struct header
{
  int msgid;
  int len;
};

然后,流将包含标头和消息数据。您可以使用标题中的信息从流中读取正确的数据量并确定它的类型。

如何对其余数据进行编码以及如何设置类结构,这在很大程度上取决于您的体系结构。如果您使用的是每个主机相同且运行相同代码的专用网络,则可以使用结构的二进制转储。否则,更可能的情况是,您将拥有每种类型的可变长度数据结构,可能使用Google Protobuf或Boost序列化进行序列化。

在伪代码中,消息的接收端如下所示:

  read_header( header );
  switch( header.msgid )
  {
     case TIME_SYNC:
       read_time_sync( ts );
       process_time_sync( ts );
       break;

     case UPDATE:
       read_update( up );
       process_update( up );
       break;

     default:
       emit error
       skip header.len;
       break;
  }

“读取”功能的外观取决于您的序列化。如果你有基本的数据结构并且需要使用各种语言,谷歌protobuf是相当不错的。如果仅使用C ++并且所有代码都可以共享相同的数据结构头,那么Boost序列化就很好。

答案 3 :(得分:0)

正常的方法是发送消息类型,然后发送序列化数据 在接收方,您将收到消息类型,并根据该类型,通过factory method(使用映射或switch-case)实例化该类,然后让对象反序列化数据。

答案 4 :(得分:0)

您的性能要求是否足以排除dynamic_cast?我没有看到测试一般结构上的字段可能比这更快,因此只留下不同消息的不同列表:您必须通过其他方式知道每个案例中对象的类型。但是你可以指向一个抽象类并对这些指针进行静态转换。

我建议您重新评估dynamic_cast的使用情况,我不认为它对网络应用程序来说是致命的慢。

答案 5 :(得分:0)

在连接的发送端,为了构造我们的消息,我们将消息ID和标题与消息数据分开:

  • Message是仅包含messageCategorymessageID的类型。
  • 每个此类Message都会推送到统一的messageQueue
  • 保留与每个messageCategory相关的数据的单独哈希值。在这些中,有一个由messageID键入的该类型的每条消息的数据记录。值类型取决于邮件类别,因此对于TIME_SYNC邮件,我们会有struct TimeSyncMessageData

序列化:

  • messageQueue弹出消息,通过messageID引用该消息类型的相应哈希值,以检索我们要序列化的数据&amp;发送。
  • Serialise&amp;发送数据。

优点:

  • 单个通用Message对象中没有可能未使用的数据成员。
  • 在序列化的时候进行数据检索的直观设置。