単純インタフェースでは、その他の RPC ルーチンは不要なため最も簡単に使用できます。しかし、利用できる通信メカニズムの制御は制限されます。このレベルでのプログラム開発は早く、rpcgen によって直接サポートされます。大部分のアプリケーションに対しては、rpcgen が提供する機能で十分です。
RPC サービスの中には C の関数としては提供されていないものがありますが、それも RPC プログラムとして使用できます。単純インタフェースライブラリルーチンは、詳細な制御を必要としないプログラムでは RPC 機能を直接使用できます。rnusers() のようなルーチンは、RPC サービスライブラリ librpcsvc にあります。次の例は、リモートホスト上のユーザー数を表示するプログラムです。RPC ライブラリルーチン rusers() を呼び出して、リモートホスト上のユーザー数を表示します。
#include <rpc/rpc.h> #include <rpcsvc/rusers.h> #include <stdio.h> /* * rnusers() サービスを * 呼び出すプログラム */ main(argc, argv) int argc; char **argv; { int num; if (argc != 2) { fprintf(stderr, "usage: %s hostname\n", argv[0]); exit(1); } if ((num = rnusers(argv[1])) < 0) { fprintf(stderr, "error: rnusers\n"); exit(1); } fprintf(stderr, "%d users on %s\n", num, argv[1] ); exit(0); } |
次のコマンドを使用して 例 4–1 のプログラムをコンパイルします。
cc program.c -lrpcsvc -lnsl
単純インタフェースのクライアント側には、 rpc_call() という関数が 1 つだけあります。次の 9 個のパラメータがあります。
int 0 or error code rpc_call ( char *host /* サーバーホストの名前 */ rpcprog_t prognum /* サーバープログラム番号 */ rpcvers_t versnum /* サーバーバージョン番号 */ rpcproc_t procnum /* サーバー手続き番号 */ xdrproc_t inproc /* 引数を符号化する XDR フィルタ */ char *in /* 引数へのポインタ */ xdr_proc_t outproc /* 結果を復号化するフィルタ */ char *out /* 結果を格納するアドレス */ char *nettype /* トランスポートの選択 */ ); |
関数 rpc_call() は、host 上で、prognum、versum、procnum によって指定する手続きを呼び出します。リモートプロシージャに渡される引数は、in パラメータによって指定され、inproc はこの引数を符号化するための XDR フィルタです。out パラメータは、リモートプロシージャから戻される結果が置かれるアドレスです。outproc は、結果を復号化してこのアドレスに置く XDR フィルタです。
クライアントプログラムは、サーバーから応答を得るまで rpc_call() のところで停止します。サーバーが呼び出しを受け入れると、0 の値で RPC_SUCCESS を返します。呼び出しが失敗した場合は、0 以外の値が返されます。この値は、 clnt_stat で指定される型に型変換されます。これは RPC インクルードファイルの中で定義される列挙型で、clnt_sperrno() 関数により解釈されます。この関数は、このエラーコードに対応する標準 RPC エラーメッセージへのポインタを返します。
この例では、/etc/netconfig に列挙されているすべての選択可能な可視トランスポートが試されます。試行回数を指定するには、下位レベルの RPC ライブラリを使用する必要があります。
複数の引数と複数の結果は、構造体の中で処理されます。
次のプログラムは、単純インタフェースを使用するために例 4–1 を変更したものです。
#include <stdio.h> #include <utmpx.h> #include <rpc/rpc.h> #include <rpcsvc/rusers.h> /* *RUSERSPROG RPC プログラムを呼び出すプログラム */ main(argc, argv) int argc; char **argv; { unsigned int nusers; enum clnt_stat cs; if (argc != 2) { fprintf(stderr, "usage: rusers hostname\n"); exit(1); } if( cs = rpc_call(argv[1], RUSERSPROG, RUSERSVERS, RUSERSPROC_NUM, xdr_void, (char *)0, xdr_u_int, (char *)&nusers, "visible") != RPC_SUCCESS ) { clnt_perrno(cs); exit(1); } fprintf(stderr, "%d users on %s\n", nusers, argv[1] ); exit(0); } |
マシンが異なれば、データ型の表現も異なります。したがって、 rpc_call() は、RPC 引数の型と RPC 引数へのポインタを必要とします。また、サーバーから返される結果についても同様です。RUSERSPROC_NUM の場合、戻り値は unsigned int 型であるため、rpc_call() の最初の戻りパラメータは xdr_u_int (unsigned int 用) で、2 番目は &nusers (unsigned int 型の値があるメモリーへのポインタ) です。RUSERSPROC_NUM には引数がないため、rpc_call() の XDR 符号化関数は xdr_void() で、その引数は NULL です。
単純インタフェースを使用するサーバープログラムは、理解しやすいものです。サーバーは、呼び出される手続きを登録するために rpc_reg() を呼び出します。次に、RPC ライブラリのリモートプロシージャディスパッチャである svc_run() を呼び出し、要求が来るのを待機します。
rpc_reg() には次の引数があります。
rpc_reg ( rpcprog_t prognum /* サーバープログラム番号 */ rpcvers_t versnum /* サーバーバージョン番号 */ rpcproc_t procnum /* サーバー手続き番号 */ char *procname /* リモート関数の名前 */ xdrproc_t inproc /* 引数を符号化するフィルタ */ xdrproc_t outproc /* 結果を復号化するフィルタ */ char *nettype /* トランスポートの選択 */ ); |
svc_run() は RPC 呼び出しメッセージに応えてサービス手続きを起動します。rpc_reg() のディスパッチャはリモートプロシージャが登録されたときに指定された XDR フィルタを使用して、リモートプロシージャの引数の復号化と、結果の符号化を行います。サーバープログラムについての注意点をいくつか挙げます。
ほとんどの RPC アプリケーションが、関数名の後に _1 を付ける命名規則に従っています。手続き名に _n 番号を付けることにより、サービスのバージョン番号 n を表します。
引数と結果はアドレスで渡されます。リモートで呼び出される関数はすべてこうなります。関数の結果として NULL を渡すと、クライアントには応答が送信されません。 NULL は、送信する応答がないことを示します。
結果は固定のデータ領域に存在します。これは、その値が実際の手続きが終了したあとにアクセスされるからです。RPC 応答メッセージを作成する RPC ライブラリ関数は結果にアクセスして、その値をクライアントに戻します。
引数は 1 つだけ使用できます。データに複数の要素がある場合、構造体の中に入れると、1 つの引数として渡すことができます。
手続きは、指定するタイプのトランスポートごとに登録されます。タイプのパラメータが (char *)NULL
の場合、手続きは NETPATH により指定されるすべてのトランスポートに登録されます。
rpcgen を使用するより、ユーザーが自分で書いた方が効率のよい短いコードにできる場合があります。rpcgen は、汎用のコードジェネレータであるためです。そのような登録ルーチンの例を次に示します。次の例では、手続きを 1 つ登録してから、svc_run() に入ってサービス要求を待ちます。
#include <stdio.h> #include <rpc/rpc.h> #include <rpcsvc/rusers.h> void *rusers(); main() { if(rpc_reg(RUSERSPROG, RUSERSVERS, RUSERSPROC_NUM, rusers, xdr_void, xdr_u_int, "visible") == -1) { fprintf(stderr, "Couldn't Register\n"); exit(1); } svc_run(); /* この関数は値を戻さない */ fprintf(stderr, "Error: svc_run returned!\n"); exit(1); } |
rpc_reg() は、異なるプログラム、バージョン、手続きを登録するごとに何度でも呼び出すことができます。
リモートプロシージャへ渡すデータ型とリモートプロシージャから受け取るデータ型は、事前に定義した型あるいはプログラマが定義する型の任意のものが可能です。RPC では、個々のマシンに固有のバイト順序や構造体のデータレイアウトに関係なく、任意のデータ構造を扱うことができます。データは、XDR (external data representation: 外部データ表現) 形式という標準データ形式に変換してからトランスポートに送信されます。マシン固有のデータ形式から XDR 形式に変換することをシリアライズといい、反対に XDR 形式からマシン固有のデータ形式に変換することをデシリアライズといいます。
rpc_call() と rpc_reg() の引数で変換ルーチンを指定するときは、 xdr_u_int() のような XDR プリミティブを指定することも、引数として渡された構造体全体を処理するようなユーザーが作成した変換ルーチンを指定することもできます。引数の変換ルーチンは 2 つの引数を取ります。1 つは変換結果へのポインタで、もう 1 つは XDR ハンドルへのポインタです。
int_types.h 内にある固定幅の整数タイプに慣れている ANSI C プログラマにとって都合がよいように、ルーチン xdr_char()、 xdr_short()、 xdr_int()、およびxdr_hyper() (および、それぞれの符号なしバージョン) には、次の表で示すように、ANSI C を連想させる名前の付いた同等の関数があります。
表 4–1 プリミティブタイプの等価関数
関数名 |
等価関数名 |
---|---|
xdr_char() |
xdr_int8_t() |
xdr_u_char() |
xdr_u_int8_t() |
xdr_short() |
xdr_int16_t() |
xdr_u_short() |
xdr_u_int16_t() |
xdr_int() |
xdr_int32_t() |
xdr_u_int() |
xdr_u_int32_t() |
xdr_hyper() |
xdr_int64_t() |
xdr_u_hyper() |
xdr_u_int64_t() |
xdr_wrapstring() から呼び出す xdr_string() はプリミティブではなく、3 つ以上の引数を取ります。
ユーザーが作成する変換ルーチンの例を次に示します。
struct simple { int a; short b; } simple;
この構造体で渡された引数を変換する XDR ルーチン xdr_simple() は、次に示すようになります。
#include <rpc/rpc.h> #include "simple.h" bool_t xdr_simple(xdrsp, simplep) XDR *xdrsp; struct simple *simplep; { if (!xdr_int(xdrsp, &simplep->a)) return (FALSE); if (!xdr_short(xdrsp, &simplep->b)) return (FALSE); return (TRUE); }
rpcgen でも、同じ機能を持つ変換ルーチンを自動生成できます。
XDR ルーチンは、データ変換に成功した場合はゼロ以外の値 (C では TRUE) を返し、失敗した場合はゼロを返します。XDR についての詳細は、付録 C 「XDR プロトコル仕様」を参照してください。
たとえば、可変長の整数配列を送るときは、配列へのポインタと配列サイズを次のような構造体にパックします。
struct varintarr { int *data; int arrlnth; } arr; |
この配列を変換するルーチン xdr_varintarr() を次に示します。
bool_t xdr_varintarr(xdrsp, arrp) XDR *xdrsp; struct varintarr *arrp; { return(xdr_array(xdrsp, (caddr_t)&arrp->data, (u_int *)&arrp->arrlnth, MAXLEN, sizeof(int), xdr_int)); } |
xdr_array() に渡す引数は、XDR ハンドル、配列へのポインタ、配列サイズへのポインタ、配列サイズの最大値、配列要素のサイズ、配列要素を変換する XDR ルーチンへのポインタです。配列サイズが前もってわかっている場合は、次のように xdr_vector() を使用します。
int intarr[SIZE]; bool_t xdr_intarr(xdrsp, intarr) XDR *xdrsp; int intarr[]; { return (xdr_vector(xdrsp, intarr, SIZE, sizeof(int), xdr_int)); } |
XDR ルーチンでシリアライズすると、データが 4 バイトの倍数になるように変換されます。たとえば、文字配列を変換すると、各文字が 32 ビットを占有するようになります。xdr_bytes() は、文字をパックするルーチンです。これは、xdr_array() の最初の 4 つの引数と同様の引数を取ります。
NULL で終わる文字列は xdr_string() で変換します。このルーチンは、長さの引数がない xdr_bytes() ルーチンのようなものです。xdr_string() は、文字列をシリアライズするときは strlen() で長さを取り出し、デシリアライズするときは NULL で終わる文字列を生成します。
次の例では、組み込み関数 xdr_string () とxdr_reference() を呼び出して、文字列へのポインタと、前の例で示した struct simple へのポインタを変換しています。
struct finalexample { char *string; struct simple *simplep; } finalexample; bool_t xdr_finalexample(xdrsp, finalp) XDR *xdrsp; struct finalexample *finalp; { if (!xdr_string(xdrsp, &finalp->string, MAXSTRLEN)) return (FALSE); if (!xdr_reference( xdrsp, &finalp->simplep, sizeof(struct simple), xdr_simple)) return (FALSE); return (TRUE); } |
ここで、xdr_reference() の代わりに xdr_simple() を呼び出してもよいことに注意してください。