rpcgen を使用すると、分散型アプリケーションを簡単に作成できます。サーバー側手続きは、手続き呼び出し規約に準拠した言語で記述します。サーバー側手続きは、rpcgen によって生成されたサーバースタブとリンクして、実行可能なサーバープログラムを形成します。クライアント側手続きも同様に記述およびリンクします。
この節では、rpcgen を使用した基本的なプログラミング例を示します。また、rpcgen(1) のマニュアルページを参照してください。
単一のコンピュータ環境で実行されるアプリケーションを、ネットワーク上で実行する分散型アプリケーションに変更する場合を考えます。次の例で、システムコンソールにメッセージを表示するプログラムを分散型アプリケーションに変換する方法を、ステップ別に説明します。変換前のプログラム例を次に示します。
/* printmsg.c: コンソールにメッセージを表示する */ #include <stdio.h> main(argc, argv) int argc; char *argv[]; { char *message; if (argc != 2) { fprintf(stderr, "usage: %s <message>\n", argv[0]); exit(1); } message = argv[1]; if (!printmessage(message)) { fprintf(stderr,"%s: couldn't print your message\n",argv[0]); exit(1); } printf("Message Delivered!\n"); exit(0); } /* コンソールにメッセージを表示する。 * メッセージを表示できたかどうかを示すブール値を返す。 */ printmessage(msg) char *msg; { FILE *f; f = fopen("/dev/console", "w"); if (f == (FILE *)NULL) { return (0); } fprintf(f, "%s\n", msg); fclose(f); return(1);}
このプログラムをシングルコンピュータ上で使用するときは、次のコマンドでコンパイルして実行できます。
$ cc printmsg.c -o printmsg $ printmsg "Hello, there." Message delivered! $ |
printmessage() 関数をリモートプロシージャに変換すると、ネットワーク上のどこからでも実行できるようになります。
最初に、手続きを呼び出すときのすべての引数と戻り値のデータ型を決定します。printmessage() の引数は文字列で、戻り値は整数です。このようなプロトコル仕様を RPC 言語で記述して、リモートプロシージャとしての printmessage() を作成することができます。RPC 言語でこのプロトコル仕様を記述したソースコードは次のようになります。
/* msg.x: メッセージを表示するリモートプロシージャのプロトコル */ program MESSAGEPROG { version PRINTMESSAGEVERS { int PRINTMESSAGE(string) = 1; } = 1; } = 0x20000001; |
リモートプロシージャは常にリモートプログラムの中で宣言されます。上のコードでは、PRINTMESSAGE という手続きが 1 つだけ含まれたリモートプログラムが 1 つ宣言されています。この例では、PRINTMESSAGE という手続きが、MESSAGEPROG というリモートプログラム内の手続き 1、バージョン 1 として宣言されています。リモートプログラムのプログラム番号は、0x20000001 です。プログラム番号の指定方法については、付録 B 「RPC プロトコルおよび言語の仕様」を参照してください。
リモートプログラムの機能が変更されると、バージョン番号が 1 つ増やされます。つまり、既存の手続きが変更されたり、新しい手続きが追加された場合などです。リモートプログラムで複数のバージョンを定義することも、1 つのバージョンで複数の手続きを定義することもできます。
プログラム名も手続き名も共に大文字で宣言していることに注意してください。
また、引数のデータ型を C 言語で書くときのように char *
としないで string
としていることにも注意してください。これは、C 言語で char *
と指定すると、文字型配列とも、単一の文字へのポインタとも解釈できて不明確なためです。RPC 言語では、NULL で終わる文字型配列は string
型で宣言します。
更に次の 2 つのプログラムを書く必要があります。
リモートプロシージャ自体
リモートプロシージャを呼び出すクライアント側のメインプログラム
例 3–2 には、例 3–1 の手続きを PRINTMESSAGE というリモートプロシージャに変更したものを示します。
/* * msg_proc.c: リモートプロシージャバージョンの "printmessage" */ #include <stdio.h> #include "msg.h" /* msg.h rpcgen が生成 */ int * printmessage_1(msg, req) char **msg; struct svc_req *req; /* 呼び出しの詳細 */ { static int result; /* 必ず static で宣言 */ FILE *f; f = fopen("/dev/console", "w"); if (f == (FILE *)NULL) { result = 0; return (&result); } fprintf(f, "%s\n", *msg); fclose(f); result = 1; return (&result);}
リモートプロシージャ printmessage_1() と、ローカル手続き printmessage() の宣言は次の 4 つの点で異なることに注意してください。
printmessage_1() では、引数が文字へのポインタではなく、文字列配列へのポインタになっています。これは、-N オプションを使用しないリモートプロシージャの場合には常に当てはまります。その場合、常に引数自体ではなく、引数へのポインタが使われます。-N オプションを指定しなければ、リモートプロシージャの呼び出しで引数が 1 つしか渡されません。複数の引数が必要な場合は、引数を struct
型にして渡す必要があります。
printmessage_1() には、引数が 2 つあります。第 2 引数には、関数呼び出しのときのコンテキストが入っています。つまりプログラム番号、バージョン、そして手続き番号、 raw および canonical の認証、 SVCXPRT 構造体へのポインタが入っています。 SVCXPRT 構造体には、トランスポート情報が入っています。呼び出された手続きが要求されたサービスを実行するときに、これらの情報が必要になります。
printmessage_1() では、戻り値は整数自体ではなく、整数へのポインタになります。これもまた、-N オプションを使用しないリモートプロシージャの場合には常に当てはまります。そこでは、戻り値自体ではなく、戻り値へのポインタが返されるためです。-M (マルチスレッド) オプション または -A (自動モード) オプションが使用されていない場合は、戻り値は static
で宣言します。戻り値をリモートプロシージャのローカル値にしてしまうと、リモートプロシージャが戻り値を返した後、サーバー側スタブプログラムからその値を参照することができなくなります。 -M および -A が使用されている場合は、戻り値へのポインタは第 3 引数として手続きに渡されるため、戻り値は手続きで宣言されません。
手続き名を見ると、_1 が追加されてprintmessage_1 () になっています。一般に rpcgen がリモートプロシージャ呼び出しを生成するときは、次のように手続き名が決められます。プログラム定義で指定した手続き名 (この場合は PRINTMESSAGE) はすべて小文字に変換され、下線 (_) とバージョン番号 (この場合は 1) が追加されます。このように手続き名が決定されるので、同じ手続きの複数バージョンが使用可能となります。
リモートプロシージャを呼び出すクライアント側メインプログラムを次に示します。
/* * rprintmsg.c: "printmsg.c" の RPC 対応バージョン */ #include <stdio.h> #include "msg.h" /* rpcgen が生成した msg.h */ main(argc, argv) int argc; char *argv[]; { CLIENT *clnt; int *result; char *server; char *message; if (argc != 3) { fprintf(stderr, "usage: %s host message\n", argv[0]); exit(1); } server = argv[1]; message = argv[2]; /* * コマンド行で指定したサーバの * MESSAGEPROG の呼び出しで使用する * クライアント「ハンドル」を作成 */ clnt = clnt_create(server, MESSAGEPROG, PRINTMESSAGEVERS, "visible"); if (clnt == (CLIENT *)NULL) { /* * サーバーとの接続確立に失敗したため、 * エラーメッセージを表示して終了 */ clnt_pcreateerror(server); exit(1); } /* * サーバー上のリモートプロシージャ printmessage を * 呼び出す */ result = printmessage_1(&message, clnt); if (result == (int *)NULL) { /* * サーバーの呼び出しでエラーが発生したため、 * エラーメッセージを表示して終了 */ clnt_perror(clnt, server); exit(1); } /* * リモートプロシージャ呼び出しは正常終了 */ if (*result == 0) { /* * サーバーがメッセージの表示に失敗したため、 * エラーメッセージを表示して終了 */ fprintf(stderr, "%s: could not print your message\n",argv[0]); exit(1); } /* サーバーのコンソールにメッセージが出力された */ printf("Message delivered to %s\n", server); clnt_destroy( clnt ); exit(0);}
このコード例では、最初に RPC ライブラリルーチン clnt_create() を呼び出して、クライアントハンドルを作成しています。 クライアントハンドルは、リモートプロシージャを呼び出すスタブルーチンに引き渡されます。これ以外にも、クライアントハンドルを作成する方法があります。詳細については、第 4 章「RPC プログラマインタフェース」 を参照してください。クライアントハンドルを使用するリモートプロシージャ呼び出しがすべて終了したら、clnt_destroy () を使用してそのクライアントハンドルを破棄し、システム資源を無駄に使用しないようにします。
clnt_create() の最後の引数に visible を指定して、/etc/netconfig で visible と指定したすべてのトランスポートを使用できるようにします。トランスポートについての詳細については、 /etc/netconfig ファイルと 『プログラミングインタフェース』 を参照してください。
リモートプロシージャ printmessage_1() の呼び出しは、第 2 引数として挿入されたクライアントハンドルを除いて、msg_proc.c で宣言されたとおりに実行されています。戻り値も値ではなく、値へのポインタで返されています。
リモートプロシージャ呼び出しのエラーは、2 種類あります。RPC 自体のエラーと、リモートプロシージャの実行中に発生したエラーです。最初のエラーの場合は、リモートプロシージャ printmessage_1() の戻り値が NULL になります。2 つめのエラーの場合は、アプリケーションによってエラーの返し方が異なります。この例では、*result によってエラーがわかります。
printmsg 全体をコンパイルする方法を次に示します。
$ rpcgen msg.x $ cc rprintmsg.c msg_clnt.c -o rprintmsg -lnsl $ cc msg_proc.c msg_svc.c -o msg_server -lnsl
最初に rpcgen を実行してヘッダーファイル (msg.h)、クライアント側スタブプログラム (msg_clnt.c)、サーバー側スタブプログラム (msg_svc.c) を生成します。次の 2 つのコンパイルコマンドで、クライアント側プログラム rprintmsg とサーバー側プログラム msg_server が作成されます。C のオブジェクトファイルは、ライブラリ libnsl とリンクする必要があります。それには、RPC と XDR で必要な関数をはじめとするネットワーク関数がすべて含まれています。
この例では、アプリケーションが libnsl に含まれる基本型だけを使用しているので、XDR ルーチンは生成されません。
rpcgen は、入力ファイル msg.x から次のファイルを生成します。
msg.h というヘッダーファイルを作成します。msg.h には、他のモジュールで使用できるように MESSAGEPROG、MESSAGEVERS、PRINTMESSAGE の #define 文が入っています。このヘッダーファイルは、クライアント側とサーバー側の両方のモジュールでインクルードする必要があります。
クライアント側スタブルーチン、msg_clnt.c を作成します。 このファイルには、クライアントプログラム rprintmsg から呼び出されるルーチン printmessage_1() が 1 つだけ入っています。rpcgen への入力ファイルが FOO.x という名前ならば、クライアント側スタブルーチンは FOO_clnt.c というファイルに出力されます。
msg_proc.c の printmessage_1() を呼び出すサーバープログラムである、 msg_svc.c というファイルが作成されます。サーバープログラムのファイル名は、クライアントプログラムのファイル名と同様の方法で決まります。rpcgen への入力ファイルが FOO.x という名前ならば、サーバープログラムは FOO_svc.c というファイルに出力されます。
サーバープログラムが作成されると、リモートマシン上にインストールして実行できます。リモートマシンが同じ機種の場合は、サーバープログラムをバイナリのままコピーすることができます。機種が異なる場合は、サーバープログラムのソースファイルをリモートマシンにコピーして再コンパイルする必要があります。この例では、リモートマシンは remote、 ローカルマシーンは local とします。次のコマンドで、リモートシステムのシェルからサーバープログラムを起動します。
remote$ msg_server |
rpcgen が生成したサーバープロセスは、常にバックグラウンドで実行されます。このとき、サーバープログラムにアンパサンド (&) を付けて起動する必要はありません。また、rpcgen が生成したサーバープロセスはコマンド行からではなく、listen() や inetd() などのポートモニターから起動することもできます。
以降は、local マシン上のユーザーが次のようなコマンドを実行して、remote マシンのコンソールにメッセージを表示できます。
local$ rprintmsg remote "Hello, there." |
rprintmsg を使用すると、サーバープログラム msg_server が起動されているどのシステムにでも (local システムも含む)、コンソールにメッセージを表示できます。
ローカル手続きをリモートプロシージャに変換では、クライアント側とサーバー側のRPC コードの生成について説明しました。rpcgen を使用して、XDR ルーチンを生成することもできます (XDR ルーチンは、ローカルデータ形式と XDR 形式との相互変換を行います)。
次に、RPC サービスのサンプルプログラム全体を示します。これは、リモートディレクトリを一覧表示するもので、rpcgen を使用してスタブルーチンと XDR ルーチンの両方を生成します。
/* * dir.x: リモートディレクトリを一覧表示するサービスのプロトコル * * rpcgen の機能を説明するためのサンプルプログラム */ const MAXNAMELEN = 255; /* ディレクトリエントリの最大長 */ typedef string nametype<MAXNAMELEN>; /* ディレクトリエントリ */ typedef struct namenode *namelist; /* リスト形式でリンク */ /* ディレクトリリスト内のノード */ struct namenode { nametype name; /* ディレクトリエントリ名 */ namelist next; /* 次のエントリ */ }; /* * READDIR の戻り値 * * どこにでも移植できるアプリケーションにするためには、 * この例のように UNIX の errno を返さないで、 * エラーコードリストを設定して使用する方がよい * * このプログラムでは、次の共用体を使用して、リモート呼び出しが * 正常終了したか異常終了したかを区別します。 */ union readdir_res switch (int errno) { case 0: namelist list; /* 正常終了: 戻り値はディレクトリリスト */ default: void; /* エラー発生: 戻り値なし */ }; /* ディレクトリを一覧表示するプログラムの定義 */ program DIRPROG { version DIRVERS { readdir_res READDIR(nametype) = 1; } = 1; } = 0x20000076; |
上の例の readdir_res のように、RPC 言語のキーワード struct
、union
、enum
を使用して型を再定義することができます。使用したキーワードは、後にその型の変数を宣言するときには指定しません。たとえば、共用体 foo を定義した場合、union foo ではなく foo で宣言します。
rpcgen でコンパイルすると、RPC の共用体は C 言語の構造体に変換されます。RPC の共用体は、キーワード union
を使用して宣言しないでください。
dir.x に対して rpcgen を実行すると、次の 4 つのファイルが出力されます。
ヘッダーファイル
クライアント側のスタブルーチン
サーバー側の骨組み
XDR ルーチンが入った dir_xdr.c というファイル
rpcgen では、.x ファイルで使用されている RPC 言語の各データ型に対して、データ型名の前に XDR ルーチンであることを示すヘッダー xdr_ が付いたルーチン (たとえば、xdr_int) が libnsl で提供されるものとみなします。.x ファイルにデータ型が定義されていると、rpcgen はそれに対するルーチンを生成します。msg.x のように、.x ソースファイルにデータ型が定義されていない場合は、_xdr.c ファイルは生成されません。
.x ソースファイルで、libnsl でサポートされていないデータ型を使用し、.x ファイルではそのデータ型を定義しないこともできます。その場合は、xdr_ ルーチンをユーザーが自分で作成することになります。こうして、ユーザー独自の xdr_ ルーチンを提供することができます。任意のデータ型を引き渡す方法についての詳細は、第 4 章「RPC プログラマインタフェース」を参照してください。次に、サーバー側の READDIR 手続きを示します。
/* * dir_proc.c: リモートプロシージャ readdir */ #include <dirent.h> #include "dir.h" /* rpcgen が生成 */ extern int errno; extern char *malloc(); extern char *strdup(); readdir_res * readdir_1(dirname, req) nametype *dirname; struct svc_req *req; { DIR *dirp; struct dirent *d; namelist nl; namelist *nlp; static readdir_res res; /* 必ず static で宣言 */ /* ディレクトリのオープン */ dirp = opendir(*dirname); if (dirp == (DIR *)NULL) { res.errno = errno; return (&res); } /* 直前の戻り値の領域解放 */ xdr_free(xdr_readdir_res, &res); /* * ディレクトリエントリをすべて取り出す。ここで割り当てたメモリーは、 * 次に readdir_1 が呼び出されたときに xdr_free で解放 */ nlp = &res.readdir_res_u.list; while (d = readdir(dirp)) { nl = *nlp = (namenode *) malloc(sizeof(namenode)); if (nl == (namenode *) NULL) { res.errno = EAGAIN; closedir(dirp); return(&res); } nl->name = strdup(d->d_name); nlp = &nl->next; } *nlp = (namelist)NULL; /* 結果を返す */ res.errno = 0; closedir(dirp); return (&res); } |
次に、クライアント側の READDIR 手続きを次に示します。
/* * rls.c: クライアント側のリモートディレクトリリスト */ #include <stdio.h> #include "dir.h" /* rpcgen が生成 */ extern int errno; main(argc, argv) int argc; char *argv[]; { CLIENT *clnt; char *server; char *dir; readdir_res *result; namelist nl; if (argc != 3) { fprintf(stderr, "usage: %s host directory\n",argv[0]); exit(1); } server = argv[1]; dir = argv[2]; /* * コマンドラインで指定したサーバーの MESSAGEPROG の呼び出しで使用する * クライアント「ハンドル」を作成 */ cl = clnt_create(server, DIRPROG, DIRVERS, "tcp"); if (clnt == (CLIENT *)NULL) { clnt_pcreateerror(server); exit(1); } result = readdir_1(&dir, clnt); if (result == (readdir_res *)NULL) { clnt_perror(clnt, 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->next) { printf("%s\n", nl->name); } xdr_free(xdr_readdir_res, result); clnt_destroy(cl); exit(0); } |
この前のサンプルプログラムと同様に、システム名を local と remote とします。ファイルのコンパイルと実行は、次のコマンドで行います。
remote$ rpcgen dir.x remote$ cc -c dir_xdr.c remote$ cc rls.c dir_clnt.c dir_xdr.o -o rls -lnsl remote$ cc dir_svc.c dir_proc.c dir_xdr.o -o dir_svc -lnsl remote$ dir_svc
local システムに rls() をインストールすると、次のように remote システム上の/usr/share/lib の内容を表示できます。
local$ rls remote /usr/share/libascii eqnchar greek kbd marg8 tabclr tabs tabs4 local $ |
rpcgen が生成したクライアント側のコードは、RPC 呼び出しの戻り値のために割り当てたメモリーを解放しないので、必要がなくなったら xdr_free() を呼び出してメモリーを解放してください。 xdr_free() の呼び出しは free() ルーチンの呼び出しに似ていますが、XDR ルーチンの戻り値のアドレスを引き渡す点が異なります。この例では、ディレクトリリストを表示した後に、 xdr_free(xdr_readdir_res,result); を呼び出しています。
xdr_free() を使用して malloc() で割り当てたメモリーを解放します。xdr_free() を使用してメモリーを解放すると、メモリーリークを生じて失敗します。
rpcgen では、C 言語などの前処理命令をサポートしています。rpcgen の入力ファイルに入っている C 言語の前処理命令は、コンパイル前に処理されます。.x ソースファイルでは、標準 C のすべての前処理命令を使用できます。生成する出力ファイルのタイプによって、次の 5 つのシンボルが rpcgen によって定義されます。
rpcgen 入力ファイルのパーセント記号 (%) で始まる行はそのまま出力ファイルに書き出され、その行の内容には影響を及ぼしません。そのとき、意図した位置に出力されるとは限らないため注意が必要です。出力ファイルのどこに書き出されたか確認して、必要ならば編集し直してください。
rpcgen で使用される前処理命令を、次の表で示します。
表 3–1 rpcgen の前処理命令
シンボル |
使用目的 |
---|---|
ヘッダーファイルの出力 |
|
XDR ルーチンの出力 |
|
サーバー側スタブプログラムの出力 |
|
クライアント側スタブプログラムの出力 |
|
インデックステーブルの出力 |
次に、簡単な rpcgen コードの例を示します。rpcgen の前処理機能の使用方法に注意してください。
/* * time.x: リモート時刻のプロトコル */ program TIMEPROG { version TIMEVERS { unsigned int TIMEGET() = 1; } = 1; } = 0x20000044; #ifdef RPC_SVC %int * %timeget_1() %{ % static int thetime; % % thetime = time(0); % return (&thetime); %} #endif |
rpcgen では、C 言語の前処理機能をサポートしています。rpcgen では、デフォルトで /usr/ccs/lib/cpp を C のプリプロセッサとして使用します。これを使用できないときは、/lib/cpp を使用します。これ以外の cpp を含むライブラリを使用するときは、rpcgen の -Y フラグで指定します。
たとえば、/usr/local/bin/cpp を使用するには、次のように rpcgen を起動します。
rpcgen -Y /usr/local/bin test.x |