この節では、RPC プログラミングと rpcgen の使用方法に関するさまざまなテクニックを示します。
表 3–4 RPC プログラミングテクニック
テクニック |
説明 |
---|---|
ネットワークタイプ |
rpcgen は、特定のトランスポートタイプに対応したサーバーコードを生成できる |
rpcgen コマンド行で、C 言語のプリプロセッサシンボルを定義できる |
|
サーバーはブロードキャスト呼び出しにエラー応答を送る必要はない |
|
通常の関数呼び出しとしてデバッグしてから、分散型アプリケーションに変更する |
|
ポートモニターのサポート |
RPC サーバーの代わりにポートモニターで「受信待ち」することができる |
プログラムからサーバーのディスパッチテーブルにアクセスできる |
|
クライアントのタイムアウト期間のデフォルト値を変更できる |
|
クライアントはサーバーに自分自身を証明できる。関連サーバーはクライアントの認証情報を調べることができる |
rpcgen の省略可能な引数には、使用したいネットワークのタイプや特定のネットワーク識別子を指定するためのものがあります。ネットワーク選択について詳しくは、『プログラミングインタフェース』 を参照してください。
-s フラグを指定すると、指定したタイプのトランスポートからの要求に応答するサーバーが作成されます。たとえば、rpcgen -s datagram_n prot.x を実行すると、 NETPATH
環境変数で指定された、または、NETPATH
が定義されていない場合には /etc/netconfig に記述された、非接続型トランスポートすべてに応答するサーバーが標準出力に書き出されます。コマンド行では、-s フラグとネットワークタイプのペアを複数指定できます。
同様に、-n フラグを指定すると、1 つのネットワーク識別子で指定したトランスポートからの要求だけに応答するサーバーを作成することができます。
rpcgen で -n フラグを指定して作成したサーバーを使用するときは注意が必要です。ネットワーク識別子は各ホストに固有なため、作成されたサーバーは別のホストで予測どおりに機能しないことがあります。
コマンド行で、C 言語のプリプロセッサシンボルを定義し、値を割り当てることができます。コマンド行の定義文は、たとえば、DEBUG シンボルが定義されているときの条件付きデバッグコードの生成に使用できます。たとえば :
$ rpcgen -DDEBUG proto.x |
手続きがブロードキャストRPC を通して呼び出され、有効な応答を返せないときは、サーバーはクライアントに応答しないでください。その方がネットワークが混雑しません。 サーバーが応答を返さないようにするには、リモートプロシージャの戻り値を NULL にします。rpcgen が生成したサーバープログラムは、NULL を受け取った場合は応答しません。
NFS サーバーの場合だけ応答する手続きを示します。
void * reply_if_nfsserver() { char notnull; /* *この場所のみで、そのアドレスが使用可能 */ if( access( "/etc/dfs/sharetab", F_OK ) < 0 ) { /* RPC の応答を禁止 */ return( (void *) NULL ); } /* * NULL 以外の値 notnull を指定したので、RPC は応答する */ return( (void *) ¬null ); } |
RPC ライブラリルーチンが応答するには、手続きが NULL 以外のポインタ値を返す必要があります 。
この例では、手続き reply_if_nfsserver() が NULL 以外の値を返すように定義されているならば、戻り値 (¬null ) は静的変数を指していなければなりません。
inetd や listen のようなポートモニターは、特定の RPC サービスに対するネットワークアドレスを監視することができます。特定のサービスに対する要求が到着すると、ポートモニターは、サーバープロセスを生成します。サービスを提供したら、サーバーは終了できます。この方法によりシステム資源を節約することができます。rpcgen で生成するサーバー関数 main() は inetd で呼び出すことができます。詳細は、 inetd の使用 を参照してください。
サーバープロセスがサービス要求に答えた後、続けて要求が来る場合に備えて一定時間待つことには意味があります。一定時間内に次の呼び出しが起こらなければ、サーバーは終了し、inetd のようなポートモニターがサーバーのための監視を続けます。サーバーが終了しないうちに次の要求が来れば、ポートモニターは新たなプロセスを生成することなく待ち状態のサーバーにその要求を送ります。
listen() などのポートモニターの場合は、サーバーのための監視を行い、サービス要求が来れば必ず新たなプロセスを生成します。このようなモニタからサーバープロセスを起動する場合は、サーバープロセスはサービス提供後すぐに終了するようにしなければなりません。
rpcgen がデフォルトで生成したサービスは、サービス提供後 120 秒間待ってから終了します。待ち時間を変更するには、-K フラグを使用します。たとえば、次のコマンドでは、サーバーは 20 秒待ってから終了します。サービス提供後すぐに終了させるには、次のように待ち時間に対して 0 を指定します。
rpcgen -K 20 proto.x
rpcgen -K 0 proto.x
ずっと待ち状態を続けて終了しないようにするには、-K -1 と指定します。
ポートモニターについての詳細は、付録 F 「SAF を使用したポートモニタープログラムの作成」を参照してください。
クライアントプログラムはサーバーに要求を送った後、デフォルトで 25 秒間応答を待ちます。応答待ちのタイムアウト値は clnt_control() ルーチンを使用して変更できます。clnt_control() ルーチンの使用方法についての詳細は、標準インタフェース を参照してください。また、rpc(3NSL) マニュアルページも参照してください。 タイムアウト値を変更する場合に、ネットワークを往復するのに必要な最短時間より長くなるようにする必要があります。次のプログラム例で、clnt_control () の使用方法を示します。
struct timeval tv; CLIENT *clnt; clnt = clnt_create( "somehost", SOMEPROG, SOMEVERS, "visible" ); if (clnt == (CLIENT *)NULL) exit(1); tv.tv_sec = 60; /* * タイムアウト値を 60 秒に変更 */ tv.tv_usec = 0; clnt_control(clnt, CLSET_TIMEOUT, &tv); |
クライアント作成ルーチンにはクライアント認証機能はありません。クライアントによっては、サーバーに対して自分自身を証明する必要があります。
次の例では、セキュリティレベルが最も低いクライアント認証方法のうち、一般に使用される方法を示します。よりセキュリティレベルが高い認証方法の詳細については、認証 を参照してください。
CLIENT *clnt; clnt = clnt_create( "somehost", SOMEPROG, SOMEVERS, "visible" ); if (clnt != (CLIENT *)NULL) { /* AUTH_SYS 形式の認証情報を設定 */ clnt->cl_auth = authsys_createdefault(); } |
一定のセキュリティレベルを保持しなければならないサーバーではクライアント認証情報が必要になります。クライアント認証情報は、第 2 引数でサーバーに渡されます。
次のプログラム例では、クライアントの認証データをチェックするサーバープログラムが記述されています。これは、rpcgen チュートリアル で説明した printmessage_1() を修正したものです。スーパーユーザーにだけコンソールへのメッセージの表示を許可します。
int * printmessage_1(msg, req) char **msg; struct svc_req *req; { static int result; /* 必ず static で宣言 */ FILE *f; struct authsys_parms *aup; aup = (struct authsys_parms *)req->rq_clntcred; if (aup->aup_uid != 0) { result = 0; return (&result) } /* 元のコードと同じ */ } |
RPC パッケージで使用するディスパッチテーブルにプログラムからアクセスしたい場合があります。たとえば、サーバーディスパッチルーチンで権限を調べてからサービスルーチンを呼び出したいときなどです。または、クライアントライブラリで記憶管理や XDR データ変換の詳細を扱う場合です。
-T オプションを指定して rpcgen を起動すると、プロトコル記述ファイル proto.x で定義した各プログラムごとの RPC ディスパッチテーブルがファイル proto_tbl.i に出力されます。接尾辞.i は index を表します。-t オプションを指定して rpcgen を起動した場合は、ヘッダーファイルだけが生成されます。rpcgen を起動するときは、C 形式モード (-N オプション) と同時に -T または -t フラグを指定することはできません。
ディスパッチテーブルの各エントリは struct
rpcgen_table で、この構造体はヘッダーファイル proto.h で次のように定義されています。
struct rpcgen_table { char *(*proc)(); xdrproc_t xdr_arg; unsigned len_arg; xdrproc_t xdr_res; xdrproc_t len_res }; |
ここでの定義は、次のとおりです。
proc は、サービスルーチンへのポインタ
xdr_arg は、入力(引数) の xdr ルーチンへのポインタ
len_arg は、入力引数の長さ(バイト数)
xdr_res は、出力(結果) の xdr ルーチンへのポインタ
len_res は、力結果の長さ(バイト数)
サンプルプログラム dir.x のディスパッチテーブル dirprog_1_table は、手続き番号がインデックスになっています。変数 dirprog_1_nproc には、テーブル内のエントリ数が入っています。
ディスパッチテーブルから手続きを探し出すルーチンfind_proc() を次に示します。
struct rpcgen_table * find_proc(proc) rpcproc_t proc; { if (proc>= dirprog_1_nproc) /* error */ else return (&dirprog_1_table[proc]); } |
ディスパッチテーブル内の各エントリは対応するサービスルーチンへのポインタです。ところが、サービスルーチンは一般にクライアント側のコードでは定義されていません。未解決の外部参照を起こさないため、また、ディスパッチテーブルに対するソースファイルを 1 つだけ要求にするために RPCGEN_ACTION(proc_ver) で、rpcgen サービスルーチンの初期化を行います。
これを使用して、クライアント側とサーバー側に同一のディスパッチテーブルを持たせることができます。クライアント側プログラムをコンパイルするときは、次の define 文を使用します。
#define RPCGEN_ACTION(routine) 0 |
サーバー側プログラムを作成するときは、次の define 文を使用します。
#define RPCGEN_ACTION(routine)routine |
例 3–27 では、proc はタイプ rpcproc_t として宣言されていることに注意してください。 正式には、RPC のプログラム、バージョン、手続き、およびポートは、タイプ u_long として宣言されていました。32ビットマシン上では、u_long の量は 4 バイト (int として) で、64 ビットシステム上では u_long の量は 8 バイトになります。RPC のプログラム、バージョン、手続き、およびポートを宣言できる場合には必ず、u_long と long の代わりに Solaris 7 で導入されたデータタイプ rpcprog_t、rpcvers_t、rpc_proc_t、rpcport_t を使用する必要があります。これらの新しいタイプを使用すると、32 ビットシステムとの下位互換性があるからです。つまり、この新しいデータタイプによって、rpcgen を実行するシステムに関係なく、4 バイトの量が保証されます。プログラム、バージョン、および手続きの u_longバージョンを使用する rpcgen プログラムを引き続き実行すると、32 ビットマシンと 64 ビットマシンでは、異なる結果になる場合があります。そのため、これらを該当する新しいデータタイプに置き換えることをお勧めします。実際、可能な限り long と u_long の使用は避けた方が賢明です。
Solaris 7 以降の rpcgen を起動する場合、rpcgen によって作成されたソースファイルには XDR ルーチンが組み込まれていて、このソースファイルは、そのコードを32 ビットマシンと64 ビットマシン上のどちらで実行するかによって、異なるインラインマクロが使用されます。特にIXDR_GETLONG() と IXDR_PUTLONG() の代わりに、IXDR_GET_INT32() と IXDR_PUT_INT32() マクロが使用されます。たとえば、rpcgen ソースファイル foo.x に以下のコードが組み込まれている場合を考えます。この場合生成されるファイル foo_xdr.c ファイルでは、適切なインラインマクロが使用されているかどうか確認されます。
struct foo { char c; int i1; int i2; int i3; long l; short s; }; |
#if defined(_LP64) || defined(_KERNEL) register int *buf; #else register long *buf; #endif . . . #if defined(_LP64) || defined(_KERNEL) IXDR_PUT_INT32(buf, objp->i1); IXDR_PUT_INT32(buf, objp->i2); IXDR_PUT_INT32(buf, objp->i3); IXDR_PUT_INT32(buf, objp->l); IXDR_PUT_SHORT(buf, objp->s); #else IXDR_PUT_LONG(buf, objp->i1); IXDR_PUT_LONG(buf, objp->i2); IXDR_PUT_LONG(buf, objp->i3); IXDR_PUT_LONG(buf, objp->l); IXDR_PUT_SHORT(buf, objp->s); #endif |
このコードにより、buf は int または long のどちらかになるように宣言されますが、これはマシンが 64 ビットであるか、または 32 ビットであるかによって決まるということに注意してください。
現在は、RPC を通じて転送されるデータタイプのサイズは、4 バイトの量 (32 ビット) に制限されています。8 バイトの long は、アプリケーションが 64 ビットのアーキテクチャを最大限に使用できるようにする場合に提供されます。ただし、プログラマは、 int のために long や、x_putlong() などの long を使用する関数の使用は、可能な限り避ける必要があります。上述したように、RPC プログラム、バージョン、手続きおよびポートにはそれぞれ専用のタイプがあります。データ値が INT32_MIN と INT32_MAX の間にない場合、xdr_long() は失敗します。また、IXDR_GET_LONG() や IXDR_PUT_LONG() などのインラインマクロが使用されると、そのデータは切り捨てられます。u_long 変数についても同様です。詳しくは、xdr_long(3NSL) マニュアルページを参照してください。
IPv6 トランスポートをサポートするのは、TI-RPC だけです。現在または将来的に、IPv6 を使用してアプリケーションを実行する予定がある場合は、下位互換スイッチは使用する必要はありません。IPv4 と IPv6 のどちらを選択するかは、 /etc/netconfig 内の関連エントリのそれぞれの順序によって決まります。
作成したアプリケーションのテストとデバッグは、簡単に実行できます。最初は、クライアント側とサーバー側の手続きをリンクして全体をシングルプロセスとしてテストします。最初は、各手続きをそれぞれクライアント側とサーバー側のスケルトンとはリンクしません。クライアントを作成するRPC ライブラリルーチン (rpc_clnt_create(3NSL) のマニュアルページを参照) と認証ルーチンの呼び出し部分はコメントにします。この段階では、libnsl をリンクしないでください。
これまでに説明したサンプルプログラムの手続きを、コマンド cc rls.c dir_clnt.c dir_proc.c -o rls でリンクします。
RPC と XDR の関数をコメントにすると、手続き呼び出しは通常のローカル関数呼び出しとなり、プログラムは dbxtool のようなローカルデバッガでデバッグ可能になります。プログラムが正しく機能することが確認されたら、クライアント側プログラムを rpcgen が生成したクライアント側のスケルトンとリンクし、サーバー側プログラムを rpcgen が生成したクライアント側のスケルトンとリンクします。
また、Raw PRC モードを使用して XDR ルーチンをテストすることもできます。詳細については、下位レベルの Raw RPC を使用したプログラムテスト を参照してください。
RPC 呼び出しで発生するエラーには2 種類あります。1 つは、リモートプロシージャ呼び出し過程で起こるエラーです。例を示します。
手続きが実行できない
リモートサーバーが応答しない
リモートサーバーが引数を復号化できない
例 3–26で考えると、result が NULL の場合は RPC エラーです。エラーの原因を調べるには、clnt_perror() を使用してエラーの原因を表示するか、clnt_sperror() を使用してエラー文字列を取り出します。
もう 1 つのエラーは、サーバー自体のエラーです。例 3–26で考えると、opendir() からエラーが返された場合です。このようなエラーの処理はアプリケーションによって異なるため、プログラマの責任で対応します。
-C オプションを指定した場合はサーバー側ルーチンに _svc という接尾辞が付くため、上の説明がそのまま当てはまらないことに注意してください。