首页 > 代码库 > 网络游戏中应用可插拔工厂处理消息

网络游戏中应用可插拔工厂处理消息

嫌翻得不好的去看后面的原文吧

————————————————————————————————————————————————————————————

问题

         今天的网络多人游戏必须处理大量不同的消息。有标准的消息(创建玩家、删除玩家、聊天等等),也有游戏中特定的消息。所有这些消息都有它们自己的数据项,它们必须能够通过一个连接发送出去并在另一端重新组装。作为网络游戏开发者,你的工作就是梳理一切以便你的游戏能够以一种优雅的方式发送和接收消息。

         在C++中最明显的做到这一点方式就是用类来表示不同的消息。这些类包括一个特殊消息的所有数据,也包括将这些数据序列化和反序列化到一个字节流的方法。然而,既然所有的消息都包含一些公有的数据元素(例如,从哪里来,到那里去),实现一个抽象的基类以便每个不同的消息可以从它继承是有意义的,像下面这样:

// the net_message base classclass net_message{public:  net_message() { }  ~net_message() { clear(); }    void clear(void) { }  virtual int serializeto(byte *output) { return(0); }   virtual void serializefrom(byte *fromdata, int datasize) { }  DPID getfrom(void) { return(m_from); }  DPID getto(void)   { return(m_to);   }protected:    void setfrom(DPID id) { m_from = id; }  void setto(DPID id) { m_to = id; }  DPID m_from;  DPID m_to;};// convert a directplay message into our classvoid net_message_createplayerorgroup::serializefrom(byte *fromdata, int datasize){  LPDPMSG_CREATEPLAYERORGROUP lp = reinterpret_cast(fromdata);      m_data.setdata(lp->lpData, lp->dwDataSize);  m_isgroup = (lp->dwPlayerType == DPPLAYERTYPE_GROUP);  namestructtoplayer(lp->dpnName, m_playername);}
// another derivation.class net_message_destroyplayerorgroup : public net_message{public:  int serializeto(byte *output) { output = NULL; return(-1); }   void serializefrom(byte *fromdata, int datasize);  uti_string getplayername(void) { return(m_playername); }  bool isgroup(void) { return(m_isgroup); }private:  bool m_isgroup;  uti_string m_playername;  };// convert a directplay message into our classvoid net_message_destroyplayerorgroup::serializefrom(byte *fromdata, int datasize){  LPDPMSG_DESTROYPLAYERORGROUP lp = reinterpret_cast(fromdata);    m_isgroup = (lp->dwPlayerType == DPPLAYERTYPE_GROUP);                namestructtoplayer(lp->dpnName, m_playername);}

         发送这些消息不是问题。如果客户端想发送某个消息,它实例化某个适当的类,填充它想要发送的数据,然后调用serializeto()方法,这个方法将所有数据都插入到一个字节流中,然后将这个字节流发送出去。到目前为止,一切都很好。

         问题出现在接收端,这种基于类的表示不同消息的实现方式意味着当我们收到一个消息,我们的程序将只能使用包含在消息中的ID类型来判断具体是哪一个类的消息被收到了。换句话说,我们的接收代码必须能够看到这个消息并且说“好吧,这是一个ID___,这是一个___消息,所以我需要构造一个___类型的类。”然后,我们必须反序列化数据到这个类的成员中去。

为什么要用可插拔工厂

         可插拔工厂是这个问题的一个解决方案。想象一下你写了一个新的消息类。现在,想象一下你可以通过简单的在你的项目中添加源码来支持你的自定义消息。这很好,你不必更改你的网络引擎的任何代码,你简单的将你的文件加入项目并重新编译。

听起来好像不是真实的?不,可插拔的工厂使用一些C ++技巧,但它不是火箭科学。

         可插拔类依赖于两个关键的C++技巧:多态和静态类成员。

         让我们来看一些代码。这个代码直接来自于我即将完成的多人解密游戏中的网络引擎。我将我的基本的可插拔工厂命名为net_message_maker;按照惯例,可插拔工厂通常用单词maker作为类名的一部分,这样能够快速告诉程序员他们是什么。

class net_message_maker{public:  net_message_maker(int type) {     m_registry.insert(std::make_pair(type, this));   }  static net_message *constructmessage(byte *data, int datasize);protected:  typedef std::map net_message_maker_map;  static net_message_maker_map m_registry;  virtual net_message *makemessage(byte *data, int datasize) const = 0;};

         net_message_make是一个相当简单,相当小的类。constructmessage()方法是我们感兴趣的;这个方法用一个原始的字节流创建适当的net_message的继承类。注意这个方法是静态的,所以不必实际的实例化一个net_message_make来使用它。

      注意makemessage()纯虚方法。makemessage()与constructmessage()不同;makemessage()只在继承类中实现,用于生成消息并反序列化它。

      我们有一个构造方法,这个构造方法有一个表示消息类型(例如DPSYS_SESSIONLOST等等)的参数。注意这个构造方法只是简单的将消息类型和消息本身组成一对插入到一个map中。注意构造方法插入的map名为m_registry是静态的,这意味着它被所有的类共享,当然也被所有的继承类共享。

      这就是maker基类所有的:一个静态map,一个静态方法和一个纯虚方法。

         现在来看看maker的继承类。你要为你想支持的每个消息创建一个不同的maker。你可以使用模板,也可以使用旧风格的#define技巧,甚至可以通过剪切和复制来创建他们。

class net_message_createplayerorgroup_maker : public net_message_maker { public:   net_message_createplayerorgroup_maker() : net_message_maker(DPSYS_CREATEPLAYERORGROUP) { } private:   net_message *makemessage(byte *data, int datasize) const   {     net_message_createplayerorgroup *msg = NULL;     try {       // construct the appropriate message type      msg = new net_message_createplayerorgroup;       // tell the message to populate itself using the byte stream      msg->serializefrom(data, datasize);     } catch(...) {       // handle errors!    }     return(msg);   }   static const net_message_createplayerorgroup_maker m_registerthis; };

         注意m_registerthis变量。这是Culp先生指出的一个技巧,我在前面已经暗示过。C++语言程序开始的时候初始化类的静态变量。所以,如果这个代码在程序开始的时候执行,m_registerthis得构造方法就会被调用。m_registerthis的构造方法调用基类net_message_maker的构造方法,这样这个指针将与指定的ID(在这种情况下是DPSYS_CREATEPLAYERORGROUP)组成一对。我们从来不必在代码的任何地方显示使用m_registerthis;它唯一的目的就是骗编译器在程序开始的时候运行构造方法。(当然,如果我们有多个静态变量,C++规范没有明确规定,其中构造函数的调用顺序,但对我们来说这不要紧)。

         这意味着在WinMain()代码的第一行执行之前,m_registry的成员已经包含了一个有效的map,链接到所有已注册的message_maker到它们的消息ID。这就是为什么能够在不改动网络代码的前提下可以支持新的消息。

如何工作的

         现在看一下整个系统的核心:方法使用一个消息ID返回合适的类。

net_message *net_message_maker::constructmessage(byte *data, int datasize){  // cast the raw memory to a generic message to determine its type  LPDPMSG_GENERIC lpMsg = (LPDPMSG_GENERIC)data;    try {    // find the appropriate factory in the map of factories...    net_message_maker *maker =      (*m_registry.find(lpMsg->dwType)).second;    // use that factory to construct the net_message derivative    return maker->makemessage(data, datasize);  } catch(...) {    err_printf("net_message_maker::constructmessage: logic error, I don‘t know                how to (or cant) construct message ID %d!", lpMsg->dwType);  }  return(NULL);}

         比方说,我从receive方法中收到了一大块数据,现在我想把这块数据转成合适的net_message继承类。我调用net_message_maker::constructmessage(),将数据和数据的大小给它。

      constructmessage()做的第一件事是将原始字节转换成通用的消息。一旦完成了转型,我们就知道了消息的类型:lpMsg->dwType。我们看一下m_registry变量,取出正确的键值对,然后得到程序开始时放入的指针。(如果我们不能找到这种类型,m_registry.find()将返回NULL,下一行将会产生一个异常,进而进入异常处理,不是很干净的处理方式,但是它能完成任务)。

      假设一切正常,本地变量maker将指向合适的工厂类(我们用这个工厂类来构造消息)。然后我们调用工厂类的makemessage()方法(我们能这么做,因为我们可以访问我们自己的娶她实例的私有方法)。makemessage()是一个纯虚方法,所以我们最终会在适当的maker内部结束掉它。

      makemessage()实例化合适的net_message的继承类,然后告诉这个实例从给定的字节块中反序列化其自身。现在我们有了一个完整的net_message,一切准备就绪。

      到这,你可以做任何你想做的事了。可能你的网络系统和我一样,在一个vector中存储所有到来的消息;或者做一些线程动作,在另一个线程中处理消息。这都不是问题。最要紧的是,只要一个简单的方法调用,constructmessage(),你就能将字节块转换为一个C++类。

—————————————————————— 原文在此 ————————————————————————————————————————————

Why Pluggable Factories Rock My Multiplayer World
by Mason McCuskey

Introduction

I‘ve developed a nasty habit over the years. Whenever I come across a business programming article, I instinctively assume that it won‘t be relevant to anything cool. My initial reaction is usually "OK, wow, this is great for writing middleware, but probably useless in game programming."

Most of the time this turns out to be true (when was the last time you used a SQL database to store saved games?), however, there are always a few articles that describe something that can be useful for game programming. One of those articles recently appeared in the magazine "C++ Report." (http://www.creport.com). Timothy R. Culp wrote an article entitled "Industrial Strength Pluggable Factories." In it, he describes a very valuable trick, not only in the business world, but in game programming as well.

This article is an attempt to take Mr. Culp‘s work and bring it down into the scary mosh pit of game development. Before continuing, head over to the C++ Report website and read the pluggable factories article. I‘m not going to duplicate what‘s already been said; I‘m going to assume you‘ve read the article and know the basics, and I‘m going to dive straight into showing how Pluggable Factories can be used to simplify DirectPlay communications.

The Problem

Networked multiplayer apps today must deal with a wide variety of messages. There‘s the standard set of DirectPlay messages (Create Player, Delete Player, Chat, etc.), as well as the army of messages your game needs to communicate. All of these messages have their own data items, and they all must be able to send themselves through a DirectPlay connection and reassemble themselves on the other side. It‘s your job as a network game programmer to sort everything out so that your game has an elegant way to send and receive its information.

The obvious way to do it in C++ is to use classes to represent the different messages. These classes contain all of the data for a particular message, as well as methods that serialize and deserialize the data into a byte stream (suitable for sending over a DirectPlay connection). Also, since all of the messages have certain data elements in common (like, who the message was from, and who it‘s going to), it makes sense to implement an abstract base class and then derive each different message type from it, like so:

// the net_message base classclass net_message{public:  net_message() { }  ~net_message() { clear(); }    void clear(void) { }  virtual int serializeto(byte *output) { return(0); }   virtual void serializefrom(byte *fromdata, int datasize) { }  DPID getfrom(void) { return(m_from); }  DPID getto(void)   { return(m_to);   }protected:    void setfrom(DPID id) { m_from = id; }  void setto(DPID id) { m_to = id; }  DPID m_from;  DPID m_to;};// a specific message derived from the base class ? this// example corresponds to DPSYS_CREATEPLAYERORGROUP.class net_message_createplayerorgroup : public net_message{public:  int serializeto(byte *output);  void serializefrom(byte *fromdata, int datasize);  uti_string getplayername(void) { return(m_playername); }  bool isgroup(void) { return(m_isgroup); }  net_byteblob &getdata(void) { return(m_data); }private:  net_byteblob m_data;  uti_string m_playername;  bool m_isgroup;};// convert a directplay message into our classvoid net_message_createplayerorgroup::serializefrom(byte *fromdata, int datasize){  LPDPMSG_CREATEPLAYERORGROUP lp = reinterpret_cast(fromdata);	  m_data.setdata(lp->lpData, lp->dwDataSize);  m_isgroup = (lp->dwPlayerType == DPPLAYERTYPE_GROUP);  namestructtoplayer(lp->dpnName, m_playername);}// another derivation.class net_message_destroyplayerorgroup : public net_message{public:  int serializeto(byte *output) { output = NULL; return(-1); }   void serializefrom(byte *fromdata, int datasize);  uti_string getplayername(void) { return(m_playername); }  bool isgroup(void) { return(m_isgroup); }private:  bool m_isgroup;  uti_string m_playername;  };// convert a directplay message into our classvoid net_message_destroyplayerorgroup::serializefrom(byte *fromdata, int datasize){  LPDPMSG_DESTROYPLAYERORGROUP lp = reinterpret_cast(fromdata);	m_isgroup = (lp->dwPlayerType == DPPLAYERTYPE_GROUP);	            namestructtoplayer(lp->dpnName, m_playername);}

Sending these messages isn‘t a problem ? if the client wants to send a certain message, it instantiates the appropriate class, fills up the class with the data it wants to send, and then calls the serializeto() method, which squishes everything into a byte stream, which is then sent using IDirectPlay->Send(). So far, so good.

The problem is on the receiving end. Developing this class-based approach to messaging means that when we receive a message, our program will have to conjure up the appropriate class using nothing but the ID byte contained within the received message. In other words, our receive code must be able to look at a message and say, "OK, that‘s ID ___? that‘s a ____ message, so I need to construct a class of type ____." Then, we must deserialize the data back into the members of the class.

Why Pluggable Factories Rock

Pluggable factories are a solution to that problem. Imagine you write a new message class that you want to use in your program. Now, imagine that you can add support for your custom messages by simply adding the source files to the project. That‘s right ? you don‘t change any lines in your networking engine? you simply add your files to the project and recompile.

Sound too good to be true? It‘s not. Pluggable factories use a few C++ tricks, but it‘s not rocket science.

Meet Your Maker

"Blessed are the game programmers, for they shalt not have to deal with legacy file formats."

The pluggable factory relies on two key C++ tricks: polymorphism (derived classes and virtual functions), and static class members.

Let‘s look at some code. This code is straight from the networking engine of my upcoming multiplayer puzzle game, Quaternion (see my homepage for more information). I‘ve called my base pluggable factory net_message_maker; by convention, pluggable factories usually have the word "maker" somewhere in their class name. This not only quickly tells any programmer what they are, but it also allows us writers to amuse ourselves by creating clever names for the sections of our articles.

class net_message_maker{public:  net_message_maker(int type) {     m_registry.insert(std::make_pair(type, this));   }  static net_message *constructmessage(byte *data, int datasize);protected:  typedef std::map net_message_maker_map;  static net_message_maker_map m_registry;  virtual net_message *makemessage(byte *data, int datasize) const = 0;};

For its power, net_message_maker is a fairly simple little class. The constructmessage() function is the one we‘re interested in; this function takes a raw byte stream and creates the appropriate net_message derivative instance. Note that this function is static, so you don‘t need to actually instantiate a net_message_maker to use it (simply say net_message_maker::constructmessage(?)).

Notice the makemessage() pure virtual function. makemessage() is not the same thing as constructmessage(); makemessage() is only implemented in the derivitive classes, and is responsible for newing the message and deserializing it.

We have one constructor, which takes one argument ? the type of message (i.e. DPSYS_SESSIONLOST, etc.) Notice that this constructor simply hands off to the base class constructor, which takes the message type, pairs it with a pointer to itself, and inserts the pair into a map (if you‘re not familiar with STL, you might want to learn about maps before continuing). Notice that the map the constructor inserts into ? m_registry -- is static, which means it‘s shared by all classes, and by all derivative classes as well.

That‘s all there is to the base maker class. One static map, one static function, one pure virtual function.

Now let‘s look at a maker derivation. You‘ll need to derive a different maker for each message you want to support ? you can either use templates, or some old-fashioned #define trickery, or even (horror of horrors) cut and paste to create them.

class net_message_createplayerorgroup_maker : public net_message_maker { public:   net_message_createplayerorgroup_maker() : net_message_maker(DPSYS_CREATEPLAYERORGROUP) { } private:   net_message *makemessage(byte *data, int datasize) const   {     net_message_createplayerorgroup *msg = NULL;     try {       // construct the appropriate message type      msg = new net_message_createplayerorgroup;       // tell the message to populate itself using the byte stream      msg->serializefrom(data, datasize);     } catch(...) {       // handle errors!    }     return(msg);   }   static const net_message_createplayerorgroup_maker m_registerthis; };

Notice the m_registerthis variable. This is one of the tricks Mr. Culp pointed out, and I hinted at eariler. The C++ language says that static members of classes are initialized at program startup. So, if this code is part of the program when it starts up, the constructor for the m_registerthis variable is going to get called. The m_registerthis constructor calls the base net_message_maker class constructor, which pairs the this pointer with the ID given (in this case, DPSYS_CREATEPLAYERORGROUP). We never explicitly use m_registerthis anywhere else in the code; it‘s sole purpose is to trick the compiler into running the constructor at program startup. (Granted, if we have multiple static variables, the C++ spec doesn‘t specify in which order the constructors are called, but that doesn‘t matter to us).

What this means is that before the first line of our WinMain() is executed, the m_registry member is going to contain a valid map, linking all registered message_makers to their message IDs. This is how it‘s possible to add support for a new message without changing one line of the networking code.

How It Works

Now let‘s take a look at the heart of the whole system: the function that takes a message ID and returns the appropriate class.

net_message *net_message_maker::constructmessage(byte *data, int datasize){  // cast the raw memory to a generic message to determine its type  LPDPMSG_GENERIC lpMsg = (LPDPMSG_GENERIC)data;    try {    // find the appropriate factory in the map of factories...    net_message_maker *maker =      (*m_registry.find(lpMsg->dwType)).second;    // use that factory to construct the net_message derivative    return maker->makemessage(data, datasize);  } catch(...) {    err_printf("net_message_maker::constructmessage: logic error, I don‘t know                how to (or can‘t) construct message ID %d!", lpMsg->dwType);  }  return(NULL);}

Let‘s say I‘ve just received a big blob of data from DirectPlay‘s receive function, and now I want to convert that blob of data into the appropriate net_message derivative. I call net_message_maker::constructmessage(), giving it the blob of data, and the size of the blob of data.

The first thing constructmessage() does is cast the raw data to a generic message. This is the sort of "blind casting" that should make any good C++ programmer freeze in terror, but it‘s a necessary evil. The DirectX docs even tell us to do it this way.

Once we‘ve cast the blob, we know the type of the message: lpMsg->dwType. We look in our m_registry variable, and pull out the correct pair. Then we get the second member of that pair, which is really the this pointer that the constructor registered at program start. (If we can‘t find the type, m_registry.find() is going to return NULL (or, in debug, 0xcdcdcdcd), which will generate an exception on the next line, and will land us in the exception handler for the function. Not the cleanest way to do things, but it gets the job done).

Assuming nothing goes wrong, the local variable "maker" now points to the appropriate factory we should use to construct the message. We then call the makemessage() function of that factory (we can do so, because we have access to the private methods of other instances of ourselves). makemessage() is a pure virtual function, so we‘ll end up inside of the appropriate maker.

makemessage() news up the appropriate net_message derivative, and then tells that instance to deserialize itself from the provided byte blob. Now we have a perfect net_message, all ready to go.

From here, you can do whatever you want. Maybe your networking system is like mine, and stores all of the incoming messages in a vector? or maybe you‘ve got some thread action happening, and have a secondary thread processing the messages. That really doesn‘t matter ? what matters is that with one simple function call, constructmessage(), you‘ve transformed a byte blob into a C++ class.

Conclusion

Congratulations, you now know about pluggable factories. Keep in mind that this technique, as Mr. Culp explains, isn‘t just for networking messages. Basically any place in your code where you need to turn an ID byte into a class is a great place for pluggable factories. There‘s a lot more power contained in this pattern than I‘m illustrating; the purpose of this article was to show you how to apply a theoretical concept directly to your code.

And, just maybe, to make you think twice before you cast off that "business programming journal" as useless. :)


Mason McCuskey is the leader of Spin Studios, an indie development group looking to break into the industry by creating a great game, Quaternion, and getting it published. He can be reached via the Spin Studios website (http://www.spin-studios.com), and doesn‘t mind answering your questions by email at mason@spin-studios.com.

Discuss this article in the forums

 

网络游戏中应用可插拔工厂处理消息