メッセージクラス

メッセージクラス

メッセージクラスを直接記述する方法を述べる。

メッセージクラス実装時に行わなければならないことは次の点である:
  • メッセージID(KIND,TYPE)の指定
  • メッセージ内容の指定
  • 直列化用のコードの実装
  • メッセージクラスの登録

次のコードは、nine::Messageクラスの定義から、メッセージ定義に関連する部分を抜き出したものである:

class Message
{
   public:
      virtual int getMarshalSize() const;
      virtual void marshal(Buffer* pBuf) const;
      virtual void unmarshal(Buffer* pBuf);

      struct Header {
            int      commId;
            uint16_t messageId;
      } header;
};
 

メッセージIDの指定

まずはユニークなメッセージIDを用意する。

IDのユニークさの確保はアプリケーション側の責任である。 ここでは、ID = 0x100 (KIND=0x1, TYPE=0x00) を使おう。

この ID は、メッセージオブジェクトの header.messageId メンバの値にならなければならない。nine 内部で生成された場合でもセットされるよう、コンストラクタに記述する。

class MyMessage: public nine::Message
{
  public:
    enum { MESSAGE_ID = 0x100; }
    MyMessage() {
      header.messageId = MESSAGE_ID;
    }
};

メッセージの登録

メッセージの登録は、nine::MessageRegistry の static 関数 Register を呼び出すことで行う:
class MessageTypeRegistry
{
   public:
      typedef Message* (*t_pfFactory)(uint16_t);
      static uint16_t Register(uint16_t, t_pfFactory, bool override = false);
};
第一引数はメッセージID, 第二引数はメッセージオブジェクトの生成関数、第三引数は既存の登録と衝突した場合に上書きするかどうかである。

呼び出しはメッセージを使う前に行う必要がある。 C++ の初期化テクニックを用いるか、main() で呼びだすのが良い。

nine::Message* CreateMyMessage(uint16_t msgid) { return new MyMessage(); }

int main()
{
  nine::MessageRegistry::Register(MyMessage::MESSAGE_ID, &CreateMyMessage, false);
}

メッセージ内容と直列化関数

メッセージの内容は、メンバ変数として宣言すればよい。 メンバへのアクセスはカプセル化してもよいし、直接アクセスさせてもいい。

直列化関数の実装は、getMarshalSize(), marshal(), unmarshal() の仮想関数をオーバーライドして行う:
getMarshalSize()
直列化した際に書かれる総バイト数を返す。バイナリ直列化時にパケットサイズの事前計算をする際に呼ばれる。
marshal(Buffer*)
直列化のコードを Buffer の書き込み API を用いて記述する。
JSON 直列化を使う場合は writeTYPE() 関数のみを用い、getWriteBuffers() は使わないこと。
unmarshal(Buffer*)
直列化復元のコードを Buffer の読み込み API を用いて記述する。
JSON 直列化を使う場合は、readTYPE() 関数のみを用い、getReadBuffers() は使わないこと。
ここで、marshal() と unmarshal() での読み書きの順番は合わせる必要がある。
例として、メンバに uint16 と string を持ったメッセージの直列化コードを実装する:
class MyMessage: public nine::Message
{
  public:
      // メッセージの内容に相当するメンバ変数
      uint16_t m_u16;
      std::string m_str;

      virtual int getMarshalSize() const {
        return 2 + nine::MarshalHelper::GetMarshalSize(m_str);
      }
      virtual void marshal(Buffer* pBuf) const {
        pBuf->writeUInt16(m_u16);
        pBuf->writeString(m_str);
      }
      virtual void unmarshal(Buffer* pBuf) {
        pBuf->readUInt16(&m_u16);
        pBuf->readString(&m_str);
      }
  ...

クラステンプレートを用いた定義

メッセージ実装のうち、メッセージIDの指定とメッセージクラスの登録を簡単に記述するためのクラステンプレートが提供されている

基本的なテンプレートは MessageTmpl であり、次のような実装である:
template <int KIND, int TYPE, typename IMPL>
class MessageTmpl: public Message
{
   public:
      enum { _MESSAGE_ID   = ((KIND & 0x0F) << 12) | (TYPE & 0x0FFF) };
      static Message* Create(uint16_t msgid) { return new IMPL(); }
      enum {
         MESSAGE_KIND = KIND, //!< テンプレートパラメータKIND を参照するための列挙体値。
         MESSAGE_TYPE = TYPE, //!< テンプレートパラメータTYPE を参照するための列挙体値。
      };

      static uint16_t MESSAGE_ID; // for registration

      // 大域変数初期化がうまく動かない場合はこれを明示的に呼ぶ。
      static uint16_t Enable() {
         MESSAGE_ID = MessageTypeRegistry::Register(
            _MESSAGE_ID,  &MessageTmpl< KIND, TYPE, IMPL >::Create);
         return MESSAGE_ID;
      }

   protected:
      MessageTmpl() {
         header.messageId = MESSAGE_ID; //use parameter to instanciate template
      }
};

template <int KIND, int TYPE, typename IMPL>
uint16_t MessageTmpl< KIND, TYPE, IMPL >::MESSAGE_ID = MessageTypeRegistry::Register(
   MessageTmpl< KIND, TYPE, IMPL >::_MESSAGE_ID, &MessageTmpl< KIND, TYPE, IMPL >::Create);

メッセージID をテンプレート引数に与える。 コンストラクタでは、header.messageId にメッセージID をセットするコードが用意されている。 さらに、static メンバ変数(クラス変数)の初期化を利用したメッセージクラスの登録もされるようになっている。

実装時には次のように、直列化の実装のみを行えばよい:
class MyMessage: public nine::MessageTmpl< 0x1, 0x00, MyMessage >
{
  public:
      uint16_t m_u16;
      std::string m_str;

      virtual int getMarshalSize() const;
      virtual void marshal(Buffer* pBuf) const;
      virtual void unmarshal(Buffer* pBuf);
};