チャットサーバを改良する(入出力制御モジュールを組み込む)


はじめに

ここでは、マニュアル「はじめに」の章末で紹介したプログラムに入出力制御モジュールを組み込んでみます。プログラムはBBS.pmパッケージに同梱されています。

入出力制御モジュールを組み込む前と組み込み後を比較するため、機能の追加や動作の変更はなるべく行わず、以前のものと同じ動作をするようにしています。

このチュートリアルでは入出力制御モジュールの機能や使い方を中心に説明するため、チャットサーバ自体は補足程度しか説明していません。
また入出力制御モジュールは汎用モジュールではないため、作成するサーバアプリケーションによっては適さない場合があります。詳しくは本チュートリアルで説明していますので、使用の際はご注意ください。


プログラムの問題点

マニュアルで紹介したプログラムはノードが入力した文字をノードにエコーバックするものですが、漢字やアルファベットなどの文字をエコーバックすると通常通り画面に表示されます。

20210322 121841

しかし、矢印キーやEnterキーなどの制御文字(制御コード)がエコーバックされるとカーソルが移動したり、表示している文字の上から文字が表示されるなどで画面が乱れたりします。

20210322 123803 20210322 122934

これは正しく伝送されている結果で誤動作ではありませんが、例えばシェル(コマンドプロンプトなど)のような対話型コマンド処理でそのような動作をするのは正常な動作とは言えません。

正常な処理を行うためには制御コードが入力されたときにエコーバックしないための処理を用意する必要があり、さらに後退(BS)キーなど、ある特定の制御文字が入力したときは入力中のデータを補正するとともに送信先ノードのコンソールに送信されている文字を打ち消すなど、多くの処理を行う必要があります。


モジュールについて

入出力制御モジュールは入力制御(Input)と出力制御(Output)に分かれていて、入力制御モジュール(Input)はノードから受信したデータからパラメータや文字列として取得できるようにデータ処理を行いながら、前述のような処理を同時に行います。

出力制御モジュール(Output)はサーバからノードに送出するデータ処理を行います。

これから、実際に対応作業を行ってみます。


対応作業

以下は変更前のプログラムです。

package App;
 
use strict;
use warnings;
use utf8;

use BBS;                            # BBSのロード
my $bbs = new BBS;                  # オブジェクト作成

$bbs->{'send_buffer'} = {};         # 送信バッファ(オブジェクト内に作る)
#            ->{node} = "{data}";


## ( onConnect : 接続時処理 )
sub onconnect {
   my $self = shift;

   printf "\n(%d) 接続しました ", $self->from();
   print "\n接続ノード: [ ".join(',', $self->nodes())." ] ";
}
 
## ( onDisconnect : 切断時処理 )
sub ondisconnect {
   my $self = shift;

   printf "\n(%d) 切断しました ", $self->from();
   print "\n接続ノード: [ ".join(',', $self->nodes())." ] ";
}
 
## ( onRecv : データ受信 )
sub onrecv {
   my $self = shift;
   my $rcvdata = join('', @_);                       # イベントから受信データを取得

   my @tonodes = $self->nodes();                     # ノードリストを取得
   map { $self->send( $_, $rcvdata ) } @tonodes;     # 受信データを全ノードに送信
}
 
## ( onSend : データ送信 )
sub onsend {
   my $self = shift;
   my $to_node = shift;                                  # イベントから送信対象のノード番号を取得
   my $send_data = join('', @_);                         # イベントから送信データを取得
 
   $self->{'send_buffer'}->{$to_node} = $send_data;      # 対象ノードの送信バッファに送信データを保存
}
 
## ( Output : データ出力 )
sub output {
   my $self = shift;

   my $to_node = $self->from();                              # 送信対象のノード番号を取得
   my $send_data = $self->{'send_buffer'}->{$to_node};       # 対象ノードの送信バッファからデータを取得
   if ( defined($send_data) ) {                              # 送信データを取得したら
      $self->{'send_buffer'}->{$to_node} = undef;                # 送信バッファを消去
      return $send_data;                                         # 送信データを返り値に設定してルーチンを抜ける
   }
   return;                                                   # ルーチンを抜ける(送信データを取得しなかったら)
}
 
$bbs->setsyshandler('onConnect',    sub { $bbs->App::onconnect(@_) });
$bbs->setsyshandler('onDisconnect', sub { $bbs->App::ondisconnect(@_) });
$bbs->setsyshandler('onRecv',       sub { $bbs->App::onrecv(@_) });
$bbs->setsyshandler('onSend',       sub { $bbs->App::onsend(@_) });
$bbs->setsyshandler('Output',       sub { $bbs->App::output(@_) });
 
print "開始しました";
$bbs->start(8888);

そして以下はモジュール組み込み後のプログラムです。

package App;
 
use strict;
use warnings;
use utf8;

use BBS;               # BBSのロード
use BBS::IO::Input;    # 入力制御モジュールのロード
use BBS::IO::Output;   # 出力制御モジュールのロード
my $bbs = new BBS;     # オブジェクト作成


## ( onConnect : 接続時処理 )
sub onconnect {
   my $self = shift;

   my $me = $self->from();
   printf "\n(%d) 接続しました ", $me;
   $self->node( $me )->{'__Input'} = new BBS::IO::Input;            # 入力制御オブジェクトを作成しノードテーブルに保存
   $self->node( $me )->{'__Output'} = new BBS::IO::Output;          # 出力制御オブジェクトを作成しノードテーブルに保存
   print "\n接続ノード: [ ".join(',', $self->nodes())." ] ";
   $self->setapphandler( sub { $bbs->App::appwork(@_) } );          # AppWorkイベントが発生したときに呼び出すルーチンを設定
}

## ( onDisconnect : 切断時処理 )
sub ondisconnect {
   my $self = shift;

   printf "\n(%d) 切断しました ", $self->from();
   print "\n接続ノード: [ ".join(',', $self->nodes())." ] ";
}

## ( onRecv : データ受信 )
sub onrecv {
   my $self = shift;
   my $recvdata = join('', @_);                     # イベントから受信データを取得

   my $me = $self->from();                          # 受信対象のノード番号を取得
   my $in = $self->node( $me )->{'__Input'};        # 対象ノードの入力制御オブジェクトを取得
   $in->store( $recvdata );                         #  入力制御オブジェクト内の受信バッファにデータを取り込む
}

## ( onSend : データ送信 )
sub onsend {
  my $self = shift;

  my $to = shift;                                   # イベントから送信先ノード番号を受け取る
  my $senddata = join('', @_);                      # イベントから送信データを受け取る
  my $out = $self->node( $to )->{'__Output'};       # 出力対象ノードの出力制御オブジェクトを取得
  $out->store( $senddata );                         # 出力制御オブジェクト内の送信バッファにデータを取り込む
}

## ( Output : データ出力 )
sub output {
  my $self = shift;

  my $me = $self->from();                           # 出力対象のノード番号を得る
  my $out = $self->node( $me )->{'__Output'};       # 対象ノードの出力制御オブジェクトを取得
    my $senddata = $out->emit();                    # 出力制御オブジェクト内の送信バッファからデータを取り出す
  if ( defined($senddata) ) {                       # 送信データを取得したら
    return $send_data;                                 # 送信データを返り値に設定してルーチンを抜ける
  }
  return;                                           # ルーチンを抜ける(送信データを取得しなかったら)
}

## ( AppWork : アプリケーション処理 )
sub appwork {
   my $self = shift;

   my $me = $self->from();                                 # 処理対象のノード番号を取得
   my $in = $self->node( $me )->{'__Input'};               # 対象ノードの入力制御オブジェクトを取得
   $in->keyin();                                           # 入力制御オブジェクト内の受信バッファからデータを取得
   my $inkey = $in->inkey();                               # 入力した文字を取得
   map { $self->send( $_, $inkey ) } $self->nodes();      # 取得した文字を全ノードに送信
}

$bbs->setsyshandler('onConnect',    sub { $bbs->App::onconnect(@_) });
$bbs->setsyshandler('onDisconnect', sub { $bbs->App::ondisconnect(@_) });
$bbs->setsyshandler('onRecv',       sub { $bbs->App::onrecv(@_) });
$bbs->setsyshandler('onSend',       sub { $bbs->App::onsend(@_) });
$bbs->setsyshandler('Output',       sub { $bbs->App::output(@_) });

print "開始しました";
$bbs->start(8888);

変更前よりも20行弱程度コードが追加されていますが、大きな変更は変わっていないように見えると思います。

では、変更点を説明します。


(1) 入出力制御モジュールのロード

まず、入力制御(Input)と出力制御(Output)の2つのモジュールをロードします。

use BBS::IO::Input;
use BBS::IO::Output;

モジュール組み込み前のプログラムでは、送信データを蓄えるための送信バッファを用意 入出力制御モジュール(Input)には受信データを蓄えるための受信バッファ、出力制御モジュール(Output)には送信データを蓄えるためのバッファが備わっているので、通信用バッファを用意する必要はありません。


(2) onConnect

## ( onConnect : 接続時処理 )
sub onconnect {
   my $self = shift;

   my $me = $self->from();
   printf "\n(%d) 接続しました ", $me;
   $self->node( $me )->{'__Input'} = new BBS::IO::Input;       # 入力制御オブジェクトを作成しノードテーブルに保存
   $self->node( $me )->{'__Output'} = new BBS::IO::Output;     # 出力制御オブジェクトを作成しノードテーブルに保存
   print "\n接続ノード: [ ".join(',', $self->nodes())." ] ";
   $self->setapphandler( sub { $bbs->App::appwork(@_) } );     # AppWorkイベントが発生したときに呼び出すルーチンを設定
}

入出力制御モジュールは、入力(Input)、出力(Output)とも、ノードごとに用意する必要がありますので、新たなノードが接続したときにそれぞれのモジュールのオブジェクトを作成し、対象ノードのノードテーブルに保存します。

ルーチンの最後にはアプリケーション処理ハンドラ(AppWork)で呼び出すルーチン(App::appwork())を設定しています。

appwork()の説明の前に、変更後のonrecv()を見てみます。


(3) onRecv

## ( onRecv : データ受信 )
sub onrecv {
   my $self = shift;
   my $recvdata = join('', @_);                 # イベントから受信データを取得

   my $me = $self->from();                      # 受信対象のノード番号を取得
   my $in = $self->node( $me )->{'__Input'};    # 対象ノードの入力制御オブジェクトを取得
   $in->store( $recvdata );                     #  入力制御オブジェクト内の受信バッファにデータを取り込む
}

接続するノードからデータを受信するとonRecvイベントが発生し、ハンドラに設定したルーチンが呼び出されます。

onRecvハンドラは呼び出しの際、受信データが引数として渡されるので、onRecvハンドラの中で受け取っています。

モジュール組み込み前のプログラムでは、受け取った受信データは直ちに関数send()を呼び出して、全てのノードにデータを引き渡していますが、モジュール組み込み後のプログラムでは、受け取ったデータは関数store()を呼び出して送信元ノードの入力制御オブジェクト内の受信バッファに取り込んでいます。

次は、appwork()を見てみます。


(4) AppWork

AppWorkハンドラはアプリケーション処理を行うためのハンドラで、モジュール組み込み前のプログラムでは定義していませんでしたが、モジュール組み込み後のプログラムで新たに追加しています。

このハンドラに定義されているコードはモジュール組み込み前にonRecvハンドラで定義されていたものが移動したものです。

## ( onRecv : データ受信 )
sub onrecv {
   my $self = shift;
   my $rcvdata = join('', @_);                       # イベントから受信データを取得

   my @tonodes = $self->nodes();                     # ノードリストを取得
   map { $self->send( $_, $rcvdata ) } @tonodes;     # 受信データを全ノードに送信
}

具体的には取得した受信データを関数send()を呼び出して各ノードに引き渡していますが、モジュール組み込み後のプログラムでは、関数keyin()を呼び出して入力制御モジュール(Input)内の受信バッファからデータを取り出し、取り出したデータを関数send()を呼び出して全てのノードに引き渡しています。

## ( AppWork : アプリケーション処理 )
sub appwork {
   my $self = shift;

   my $me = $self->from();                             # 処理対象のノード番号を取得
   my $in = $self->node( $me )->{'__Input'};           # 対象ノードの入力制御オブジェクトを取得
   my $echo = $in->keyin();                            # 入力制御オブジェクト内の受信バッファからデータを取り出し生成したエコーバックデータを取得
   my $inkey = $in->inkey();                           # 入力した文字を取得
   map { $self->send( $_, $inkey ) } $self->nodes();   # 取得した文字を全ノードに送信
}

次にonsend()を見てみます。


(5) onSend

sub onsend {                                                # 【 送信関数 】
  my $self = shift;
  my $to = shift;                                             # 送信先ノード番号
  my $senddata = join('', @_);                                # 送信データ
  my $out = $self->node( $to )->{'__Output'};
  $out->store( $senddata );                                   # 送信バッファにデータを取り込む
}

関数send()を呼び出すと、このonSendハンドラが呼び出されます。

呼び出しの際、引数として送信データと送信先ノード番号が渡されるのでonSendハンドラから受け取ります。

モジュール組み込み前のプログラムでは、BBS.pmのオブジェクト内に確保した送信バッファにデータを保存していますが、モジュール組み込み後のプログラムでは送信先ノードの出力制御モジュール(Output)内の送信バッファにデータを取り込みます。

次にoutput()を見てみます。


(6) Output

sub output {                                                # 【 出力処理 】
  my $self = shift;
  my $me = $self->from();                                   # 送信先ノード
  my $out = $self->node( $me )->{'__Output'};
  my $senddata = $out->emit();                              # 送信バッファからデータを取り出す
  if ( defined($senddata) ) {
    return $senddata;                                      # 取り出したデータを呼び出し元に返す
  }
}

Outputハンドラが呼び出されたときに、送出するデータを返り値に設定してハンドラを抜けるとノードにデータが送出されます。

モジュール組み込み前のプログラムではBBS.pmオブジェクト内の送信バッファからデータを取り出して、ハンドラの返り値に設定しています。

モジュール組み込み後のプログラムでは関数emit()を呼び出して、出力制御モジュール(Output)内の送信バッファからデータを取り出し、ハンドラの返り値に設定しています。

最後にonDisconnectハンドラを見てみます。


(7) onDisconnect

sub ondisconnect {                                          # 【 切断時処理 】
  my $self = shift;
  my $me = $self->from();
  printf "\n(%d) 切断しました ", $me;
  print "\n接続ノード: [ ".join(',', grep { $me != $_ } $self->nodes())." ] ";
}

このルーチンはモジュール組み込み前後で何も変更していませんが、onConnectが呼び出されたときにノードテーブルに作成した入出力制御モジュールのオブジェクトは、ondisconnectハンドラを抜けたあとで自動的に廃棄されます。

以上、変更点の説明はここで終わります。

モジュール組み込み後のプログラムを実行してもモジュール組み換え前と全く同じ動作をしますので、相違点はありませんが、前述、プログラムの問題点はこの段階では解決していません。


次回予定

次回は今回変更したプログラムを実用的なチャットサーバにレベルアップしてみます。
特に入力制御モジュールが大活躍します。

興味がありましたら、次回もよろしくお願いします。

プログラムの不具合、ご意見、ご質問などがありましたら、下のコメントへお願いします。

Task Runner