コンピューター:C言語講座:RPCについて(2)


概要
 RPCについて(1)では高レベル関数を用いてゼロからソースを組み立ててみました。(2)ではprcgenを使用して、より簡単に高機能のRPCプログラムを作成してみます。
 高レベル関数を使用した場合に使用できなかったTCPプロトコルも使用できます。
 また、サーバの起動方法についてもよりスマートな方法を取り上げてみます。

サンプル
 いきなりサンプルですが、これもrpcgenを実際に動かしてみるのが一番わかりやすいと思います。動作は(1)と同じで、リモートホストのディレクトリの内容を見ます(芸が無い)。
(1)ではやりとりするデータは単純な文字列でしたが、今回のはもう少し実用的にします。

<プロトコル定義>
const MAXNAMELEN=512;
typedef string nametype<MAXNAMELEN>;
typedef struct namenode *namelist;

struct namenode{
    nametype    name;
    namelist    pNext;
};

union rls_res switch (int errno){
    case  0:
        namelist    list;
    default:
        void;
};

program DIRPROG{
    version DIRVERS{
        rls_res
        RLS(nametype)=1;
    }=1;
}=0x20000001;

 rpcgenに処理させるデータです。*.xと、拡張子.xで作成します。rls.xで作成したとして進めます。今回のサンプルではリスト構造の返答を得られるようにしてあります。

rpcgen rls.x

 引数に作成したファイル名を渡してrpcgenを起動すると、以下のファイルが作成されます。

rls.h:ヘッダファイル
rls_xdr.c:XDRソースファイル
rls_svc.c:サーバ用ソースファイル
rls_clnt.c:クライアント用ソースファイル

<rls.h>
/*
* Please do not edit this file.
* It was generated using rpcgen.
*/

#include <rpc/types.h>

#define MAXNAMELEN 512

typedef char *nametype;
bool_t xdr_nametype();

typedef struct namenode *namelist;
bool_t xdr_namelist();

struct namenode {
    nametype name;
    namelist pNext;
};
typedef struct namenode namenode;
bool_t xdr_namenode();

struct rls_res {
    int errno;
    union {
        namelist list;
    } rls_res_u;
};
typedef struct rls_res rls_res;
bool_t xdr_rls_res();

#define DIRPROG ((u_long)0x20000001)
#define DIRVERS ((u_long)1)
#define RLS ((u_long)1)
extern rls_res *rls_1();

 作成されたヘッダファイルです。RPCについて(1)同様にプログラム番号などの定義がされるほか、データ構造の定義などがされます。その他のファイルは少々長いのでここにはあげませんが、基本的にこれらの自動生成されたファイルは自分で編集はしません。
 残された作業は実際のサーバの処理関数の作成と、クライアントプログラムのメイン部分の作成だけです。

<サーバの処理関数>
#include    <rpc/rpc.h>
#include    <sys/dir.h>
#include    "rls.h"

extern int   errno;
extern char   *malloc();
extern char   *strdup();

rls_res *rls_1(dirname)
nametype    *dirname;
{
namelist    nl;
namelist    *nlp;
static rls_res res;   /* must be static! */
static DIR   *dirp=NULL;
struct direct  *d;

    if(dirp){
        xdr_free(xdr_rls_res,&res);
    }

    dirp=opendir(*dirname);
    if(dirp==NULL){
        res.errno=errno;
        return(&res);
    }

    nlp=(&res.rls_res_u.list);
    while(d=readdir(dirp)){
        nl=(*nlp)=(namenode *)malloc(sizeof(namenode));
        nl->name=strdup(d->d_name);
        nlp=(&nl->pNext);
    }
    (*nlp)=NULL;

    res.errno=0;
    closedir(dirp);

    return(&res);
}

 rls_svc.cから呼ばれる処理関数です。実際にディレクトリの中身を調べ、返答します。ここで注意が必要なのは、返答するべき変数はstaticにする必要がある点です。そうしないとrls_1()がreturnする際に開放されてしまい、値が不定となってしまいます。また、今回はリスト構造で、メモリを動的にmalloc(),strdup()で割り当てていますので、次回の呼び出し時に開放する必要があります。ここではdirpがNULLかどうかで前回の処理結果が残っているかどうかを判断し、ある場合はxdr_free()で開放しています。
 このファイルとrls_svc.c,rls_xdr.cをコンパイル、リンクすればサーバの出来上がりです。

<クライアントのメイン部分>
#include    <stdio.h>
#include    <rpc/rpc.h>
#include    "rls.h"

extern int   errno;

void main(argc,argv)
int   argc;
char  *argv[];
{
CLIENT *cl;
char  *server;
char  *dir;
readdir_res   *result;
namelist    nl;

    if(argc!=3){
        fprintf(stderr,"rls hostname path\n");
        exit(1);
    }

    server=argv[1];
    dir=argv[2];

    cl=clnt_create(server,DIRPROG,DIRVERS,"tcp");

    if(cl==NULL){
        clnt_pcreateerror(server);
        exit(1);
    }

    result=rls_1(&dir,cl);
    if(result==NULL){
        clnt_perror(cl, server);
        exit(1);
    }

    if(result->errno!=0){
        errno=result->errno;
        perror(dir);
        exit(1);
    }

    for(nl=result->readdir_res_u.list;nl!=NULL;nl=nl->pNext){
        printf("%s\n",nl->name);
    }
    exit(0);
}

 clnt_create()でサーバに対するクライアントを生成します。"tcp"を"udp"とすればUDPプロトコルで接続も出来ます。rls_1()でサーバの関数を呼び出して結果を得ますが、これはrpcgenが生成したrls_clnt.cにすでに準備されています。成功した場合に内容を表示するプログラムとなっています。
 このファイルとrls_clnt.c,rls_xdr.cをコンパイル、リンクすればクライアントプログラムが出来上がります。

サーバの起動方法
 RPCについて(1)ではサーバはリモートホストでコマンドラインから起動していました。今回のプログラムももちろんそれでかまいませんが、多くのホストにサービスを置こうとすると、毎回起動を行なうのが大変です。クライアントを起動する時にrshでサーバを起動しても良いのですが、それにはrshが使える環境を準備しなくてはなりません。接続してみてエラーだった場合にrexec()を使用して起動することも可能ですが、rexecが使える環境が必要です。
 UNIXで一般的に提供されているrusersコマンド用サーバのrusersdなどはどうなっているかというと、inetdによって起動されるようになっています。具体的には/etc/inetd.confにサービスを記述し、/etc/rpcにプログラム番号などの情報を登録しておけば、クライアントが接続する際にinetdが起動してくれたり、接続してくれたりします。ただし、inetd.confなどへの登録はrootの権限が必要です。
 そこで、今回のサーバをinetdに登録してみます。/etc/inetd.confに以下の行を追加します。サーバプログラムの名前はrls_svcとします。

rls/1  stream rpc/tcp nowait root プログラムのある場所   rls_svc
rls/1  dgram  rpc/udp nowait root プログラムのある場所   rls_svc

 rootとあるのはこのプログラムをrootユーザで起動するという意味です。どこでもみれるので、rootにしてみましたが、危険であれば一般ユーザで全くかまいません。
 /etc/rpcに以下の行を追加します。

rls       536870913

536870913はプログラム番号の10進表記です。NISを使用している場合は/var/ypでmakeして情報を更新させます。inetdのプロセスIDをpsコマンドで調べ、HUPシグナルを送ると情報が更新されます。
 このようにしておくと、クライアントが接続しようとするとサーバが起動されていない場合はinetdによって起動され、サービスが使用できるようになります。
 なお、登録されているサービスの一覧はrpcinfo -pで見れますし、削除も出来ます。登録されていても使用不可能な場合ももちろんあります。

まとめ
 だいぶ長くなりましたが、RPCがどういうものかはわかりましたか?サンプルではサーバもクライアントも1つの処理しか行なわないシンプルなものでしたが、マルチクライアント処理などが必要な場合や、X-Windowsプログラムで、他のイベントを処理しながらブロックせずにサービスを使用したい場合などもあると思います。そこまでは記述しきれませんでしたので、文献などを調査しみてください。
 RPCを使用する効果が大きいものとしては画像処理などで時間のかかる処理を多数のホストに分散処理し、高速化をはかりたい場合などでしょう。あるいは、XDR,rpcgenなどにより、通信やデータ変換などを考えずに信頼性・汎用性の高いプログラムを作成したい場合にも有効でしょう。私は以前、CADシステムをクライアント・サーバ形式で分散処理させるシステムを作成したことがありますが、通信やデータの管理を全てソケットを使用して自分で構築し、安定させるのに非常に苦労した経験がありますが、RPCを使いこなせていればはるかに簡単に実現できたかも知れません。
 C言語講座で取り上げた他の話題に比べ、奥が深い面もあり、十分な説明にはなりませんでしたが、まずは動かしてみて、それからmanで調べたり、文献を調べてみてください。


よろしければブログもご覧ください
ipv400045001 from 1998/3/4