ACE Tutorial 004
A much more clever Client


では、そろそろ簡単なデータを送信するクライアントを作成してみよう。 よりクールな仕様として、データを C++ の << 演算子を利用して送信するようオーバーロードする。 これが今回の課題である。

Kirthika は述べた。


// $Id$


/*
  connector オブジェクトと string クラスが必要である。
*/

#include "ace/Log_Msg.h"
#include "ace/SOCK_Connector.h"
#include "ace/SString.h"


/*
  このチュートリアルでは send_n() メソッドの前後に処理を追加するため SOCK_Stream を拡張している。
*/

class Client : public ACE_SOCK_Stream
{
public:
  // 基本コンストラクタ
  Client (void);

  
  /*
    コンストラクトと open() を一度に呼び出す。
    これは open() が失敗した時にクリーンな終了ができないため、良い考えとは言えない。
  */

  Client (const char *server,
          u_short port);

  
  /*
    サーバとの接続を開く。
    呼び出しが ACE_SOCK_Connector と似ていることに注意してほしい。
    これは自身の open() を提供することで、呼び出し元から connector を隠し、処理を単純化するためだ。
  */

  int open (const char *server,
            u_short port);

  
  /*
    これらのメソッドはコンストラクタで open() を呼び出す場合があるため必須である。
  */

  int initialized (void) { return initialized_; }
  int error (void) { return error_; }

  
  /*
    この部分が今回の中核である。
    C++ では「cout << some-data」のような書式が使える。
    これはデータを扱うのに簡単で便利である。
    これらのメソッドを追加しておくことで、ソケット接続に対して同様の操作を可能とする。
  */

  Client &operator<< (ACE_SString &str);
  Client &operator<< (char *str);
  Client &operator<< (int n);

protected:
  u_char initialized_;
  u_char error_;
};

/* 基本コンストラクタではフラグを設定するのみだ。 */
Client::Client(void)
{
  initialized_ = 0;
  error_ = 0;
}


/*
  このコンストラクタはフラグをセットした後に open() を呼び出す。
  もし open() 呼び出しが失敗した場合、フラグが適切に変更される。
  このコンストラクタ呼び出し後、initialized() と error() の二つのインライン関数を結果の取得に利用できる。
*/

Client::Client (const char *server,
                u_short port)
{
  initialized_ = 0;
  error_ = 0;
  this->open (server, port);
}


/*
  サーバへの接続を開く。
  これは ACE_SOCK_Connector の利用を呼び出し元から隠す。
  つまり呼び出し元はどのように接続が行われるかを考えなくてよい。
  これは良い設計である。
*/

int
Client::open (const char *server,
              u_short port)
{
  
  /*
    この部分はほとんどチュートリアル3と同様である。
    違うのは成功時に initialized_ メンバ変数を設定することくらいだ。
  */


  ACE_SOCK_Connector connector;
  ACE_INET_Addr addr (port, server);

  if (connector.connect (*this, addr) == -1)
    ACE_ERROR_RETURN ((LM_ERROR,
                       "%p\n",
                       "open"),
                      -1);
  initialized_ = 1;
  return 0;
}


/*
  最初のストリーム挿入演算子は、string オブジェクトの内容を相手に送信する。
*/

Client &
Client::operator<< (ACE_SString &str)
{
  
  /*
    「server << foo << bar << stuff;」という書式を有効にする。

    このため、各 << 演算子は実行前にオブジェクトの状態が正しいか確認する。
  */


  if (initialized () && !error ())
    {
      /* string オブジェクト内部のデータを得る。 */
      const char *cp = str.fast_rep ();

      
      /*
        以前と同様に send_n() を利用してデータを送信する。
        問題が発生した場合には続く << 演算子が無効な操作を行わないために error_ 変数を設定する。
      */

      if (this->send_n (cp,
                        ACE_OS::strlen (cp)) == -1)
        error_ = 1;
    }
  else
    
    /*
      初期化されていない状態で利用しようとした場合は error_ を設定する。
    */

    error_ = 1;

  
  /*
    ストリーム挿入演算子のチェインに対応するため、自身への参照を返す。
    (例 -- "server << foo << bar")
    これを行わない場合には各ストリーム挿入を演算子ではなく文として実行しなければならない。
    それでも良いのだが、C++ 標準の iostream と同様の感覚で扱えるべきだろう。
  */

  return *this ;
}


  char* が渡された時にはどうする?
  ここでは簡単に ACE_SString コンストラクタによって char* を変換し、それを挿入することにする。
  これによって operator<<(ACE_SString&) メソッドの実装を再利用することができ、効率が良い。
  物事を成す方法は一つではないことの典型例である。
*/


Client &
Client::operator<< (char *str)
{
  ACE_SString newStr (str);

  *this << newStr;

  return *this ;

  
  /*
    これは次のように書いてもよい。

     return *this << ACE_SString (str);

    しかし、この種の書き方はデバッグの際に苦痛となるので避けるべきだ。
  */

}


/*
  ACE_SString と char* は同様の処理ができた。
  他の種類のデータ型の場合はどうだろうか?

  この場合も char* 経由で ACE_SString にしてしまえば、実装済みの << 演算子が利用できる。
*/

Client &
Client::operator<< (int n)
{
  
  /*
    最大の数値よりも大きいバッファを準備する。
    BUFSIZ であれば十分な大きさとなるだろう。
  */

  char buf[BUFSIZ];

  /* 数値を確保済みのバッファへ書き出す。 */
  ACE_OS::sprintf (buf,
                   "(%d)\n",
                   n);

  /* ストリーム挿入が可能な ACE_SString 型に変換する。 */
  ACE_SString newStr (buf);

  /* そして送信 */
  *this << newStr;

  /* 他と同様に自身の参照を返す。 */
  return *this;
}


/*
  これで必要なコードは書き終えた。
  チュートリアル3と同様にコマンドラインオプションを受け付けることにしよう。
*/

int
main (int argc, char *argv[])
{
  const char *server_host = argc > 1 ? argv[1] : ACE_DEFAULT_SERVER_HOST;
  u_short server_port = argc > 2 ? ACE_OS::atoi (argv[2]) : ACE_DEFAULT_SERVER_PORT;
  int max_iterations = argc > 3 ? ACE_OS::atoi (argv[3]) : 4;

  /* もう一方よりも安全なデフォルトコンストラクタを呼び出す。 */
  Client peer;

  
  /*
    サーバとの接続を開く。
    チュートリアル3と比較して、どれほど単純な呼び出しになったか見てほしい。
  */

  if (peer.open (server_host,
                 server_port) == -1)
    ACE_ERROR_RETURN ((LM_ERROR,
                       "%p\n",
                       "open"),
                      -1);

  for (int i = 0; i < max_iterations; i++)
    {
      
      /*
        サーバに現在の繰り返し回数を送信する。
        もう sprintf をここで利用する必要は無い。
        それは下の階層に隠蔽されている。
      */

      peer << "message = " << i+1;

      /* 異常は無いか? */
      if (peer.error ())
        ACE_ERROR_RETURN ((LM_ERROR,
                           "%p\n",
                           "send"),
                          -1);
      else
        ACE_OS::sleep (1);
    }

  if (peer.close () == -1)
    ACE_ERROR_RETURN ((LM_ERROR,
                       "%p\n",
                       "close"),
                      -1);
  return 0;
}

以上で終了だ。 見てきたように、データ送信を send() や send_n() の明示的呼び出しよりも自然な形で行えるようなオブジェクトを作るのは、そう難しいことではない。 他のオブジェクトでも同様にしてやれば、ストリームへのデータ入出力ができる。 当然ながら、 C++ 標準 ostream と同レベルの型変換を実装するのは、これよりも難しくなるだろう。 それに加えて、このクライアントは、より最適化する余地も十分にある。

読者への宿題として、こいつのサーバ版を作成してみることを勧める。 詰まった時には ACE 配布物の IOStream_Test が参考になるかもしれない。

もし自力でコンパイルしてみたければここにソースがある。


[インデックスへ]