この章では、RPC との C インタフェースについて取り上げ、RPC を使用してネットワークアプリケーションを書く方法を説明します。RPC ライブラリにおけるルーチンの完全な仕様については、rpc(3NSL) のマニュアルページおよび関連するマニュアルページを参照してください。
この章では、次のトピックについて説明します。
この章で説明するクライアントおよびサーバーインタフェースは、特に注意書きがある場合 (raw モードなど) 以外は、マルチスレッド対応です。すなわち、RPC 関数を呼び出すアプリケーションはマルチスレッド環境で自由に実行することができます。
単純インタフェースでは、その他の 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() を呼び出してもよいことに注意してください。
RPC パッケージの標準レベルへのインタフェースは、RPC 通信へのさらに詳細な制御を提供します。この制御を使用するプログラムはより複雑になります。下位レベルでの効果的なプログラミングには、コンピュータネットワークの構造に対するより深い知識が必要です。トップ、中間、エキスパート、ボトムレベルは、標準インタフェースの一部です。
この節では、RPC ライブラリの下位レベルを使用して RPC プログラムを詳細に制御する方法について説明します。たとえば、単純インタフェースレベルでは NETPATH
を介してしか使用できなかったトランスポートプロトコルを自由に使用できます。これらのルーチンを使用するには、TLI (top-level interface) に関する知識が必要です。
次に示したルーチンでは、トランスポートハンドルの指定が必要なため、単純インタフェースからは使用できません。たとえば、単純レベルでは、XDR ルーチンでシリアライズとデシリアライズを行うときに、メモリーの割り当てと解放を行うことはできません。
トップレベルのルーチンを使用すると、アプリケーションで使用するトランスポートタイプを指定できますが、特定のトランスポートは指定できません。このレベルは、クライアントとサーバーの両方でアプリケーションが自分のトランスポートハンドルを作成する点で、単純インタフェースと異なります。
次のようなヘッダファイルがあるとします。
/* time_prot.h */ #include <rpc/rpc.h> #include <rpc/types.h> struct timev { int second; int minute; int hour; }; typedef struct timev timev; bool_t xdr_timev(); #define TIME_PROG 0x40000001 #define TIME_VERS 1 #define TIME_GET 1
次に、クライアント側の、トップレベルのサービスルーチンを使用する簡単な日時表示プログラムを示します。トランスポートタイプはプログラムを起動するときの引数で指定します。
#include <stdio.h> #include "time_prot.h" #define TOTAL (30) /* * 時刻を返すサービスを呼び出すプログラム * 使用方法: calltime ホスト名 */ main(argc, argv) int argc; char *argv[]; { struct timeval time_out; CLIENT *client; enum clnt_stat stat; struct timev timev; char *nettype; if (argc != 2 && argc != 3) { fprintf(stderr,”usage:%s host[nettype]\n” ,argv[0]); exit(1); } if (argc == 2) nettype = "netpath"; /* デフォルト */ else nettype = argv[2]; client = clnt_create(argv[1], TIME_PROG, TIME_VERS, nettype); if (client == (CLIENT *) NULL) { clnt_pcreateerror(“Couldn't create client”); exit(1); } time_out.tv_sec = TOTAL; time_out.tv_usec = 0; stat = clnt_call( client, TIME_GET, xdr_void, (caddr_t)NULL, xdr_timev, (caddr_t)&timev, time_out); if (stat != RPC_SUCCESS) { clnt_perror(client, "Call failed"); exit(1); } fprintf(stderr,"%s: %02d:%02d:%02d GMT\n", nettype timev.hour, timev.minute, timev.second); (void) clnt_destroy(client); exit(0); } |
プログラムを起動するときに nettype を指定しなかった場合は、代わりに 「netpath」という文字列が使用されます。RPC ライブラリルーチンは、この文字列を見つけると、環境変数 NETPATH
値によって使用するトランスポートを決めます。
クライアントハンドルが作成できない場合は、clnt_pcreateerror() でエラー原因を表示します。または、グローバル変数 rpc_createerr の値としてエラーステータスを取り出します。
クライアントハンドルが作成できたら、clnt_call() を使用してリモート呼び出しを行います。clnt_call() の引数は、クライアントハンドル、リモートプロシージャ番号、入力引数に対する XDR フィルタ、引数へのポインタ、戻り値に対する XDR フィルタ、戻り値へのポインタ、呼び出しのタイムアウト値です。この例では、リモートプロシージャに渡す引数はないので、XDR ルーチンとしては xdr_void() を指定しています。最後に clnt_destroy() を使用して使用済みメモリーを解放します。
上記の例でプログラマがクライアントハンドル作成に許される時間を30 秒に設定したいとすると、次のコード例の一部のように、clnt_create() への呼び出しは clnt_create_timed() への呼び出しに替わります。
struct timeval timeout; timeout.tv_sec = 30; /* 30 秒 */ timeout.tv_usec = 0; client = clnt_create_timed(argv[1], TIME_PROG, TIME_VERS, nettype, &timeout); |
次に、トップレベルのサービスルーチンを使用したサーバー側プログラムを示します。
#include <stdio.h> #include <rpc/rpc.h> #include "time_prot.h" static void time_prog(); main(argc,argv) int argc; char *argv[]; { int transpnum; char *nettype; if (argc> 2) { fprintf(stderr, "usage: %s [nettype]\n", argv[0]); exit(1); } if (argc == 2) nettype = argv[1]; else nettype = "netpath"; /* デフォルト */ transpnum = svc_create(time_prog,TIME_PROG,TIME_VERS,nettype); if (transpnum == 0) { fprintf(stderr,”%s: cannot create %s service.\n”, argv[0], nettype); exit(1); } svc_run(); } /* * サーバーのディスパッチ関数 */ static void time_prog(rqstp, transp) struct svc_req *rqstp; SVCXPRT *transp; { struct timev rslt; time_t thetime; switch(rqstp->rq_proc) { case NULLPROC: svc_sendreply(transp, xdr_void, NULL); return; case TIME_GET: break; default: svcerr_noproc(transp); return; } thetime = time((time_t *) 0); rslt.second = thetime % 60; thetime /= 60; rslt.minute = thetime % 60; thetime /= 60; rslt.hour = thetime % 24; if (!svc_sendreply( transp, xdr_timev, &rslt)) { svcerr_systemerr(transp); } } |
svc_create() は、サーバーハンドルを作成したトランスポートの個数を返します。サービス関数 time_prog() は、対応するプログラム番号とバージョン番号を指定したサービス要求がきたときに svc_run() に呼び出されます。サーバーは、svc_sendreply() を使用して戻り値をクライアントに返します。
rpcgen を使用してディスパッチ関数を生成する場合は、 svc_sendreply() は手続きが戻り値を返してから呼び出されます。そのため、戻り値 (この例では rslt) は実際の手続き内で static 宣言しなければなりません。この例では、svc_sendreply() はディスパッチ関数の中で呼び出されているので、rslt は static で宣言されていません。
この例では、リモートプロシージャには引数がありません。引数を渡す必要がある場合は次の 2 つの関数を呼び出して、引数を取り出し、デシリアライズ (XDR 形式から復号化) し、解放します。
svc_getargs( SVCXPRT_handle, XDR_filter, argument_pointer); svc_freeargs( SVCXPRT_handle, XDR_filter argument_pointer );
中間レベルのルーチンを使用するときは、使用するトランスポート自体をアプリケーションから直接選択します。
次のプログラムは、トップレベルのインタフェースの時刻サービスのクライアント側プログラムを、中間レベルの RPC で書いたものです。この例のプログラムを実行するときは、どのトランスポートで呼び出しを行うか、コマンド行で指定する必要があります。
#include <stdio.h> #include <rpc/rpc.h> #include <netconfig.h> /* 構造体 netconfig を使用するため */ #include "time_prot.h" #define TOTAL (30) main(argc,argv) int argc; char *argv[]; { CLIENT *client; struct netconfig *nconf; char *netid; /* 以前のサンプルプログラムの宣言と同じ */ if (argc != 3) { fprintf(stderr, "usage: %s host netid\n”, argv[0]); exit(1); } netid = argv[2]; if ((nconf = getnetconfigent( netid)) == (struct netconfig *) NULL) { fprintf(stderr, "Bad netid type: %s\n", netid); exit(1); } client = clnt_tp_create(argv[1], TIME_PROG, TIME_VERS, nconf); if (client == (CLIENT *) NULL) { clnt_pcreateerror("Could not create client"); exit(1); } freenetconfigent(nconf); /* これ以降は以前のサンプルプログラムと同じ */ } |
この例では、getnetconfigent(netid) を呼び出して netconfig 構造体を取り出しています 。詳細については、 getnetconfig(3NSL) マニュアルページと 『プログラミングインタフェース』 を参照してください。このレベルの RPC を使用する場合は、プログラムで直接ネットワーク (トランスポート) を選択できます。
上記の例でプログラマがクライアントハンドル作成に許される時間を 30 秒に設定したいとすると、次のコード例の一部のように、clnt_tp_create() への呼び出しは clnt_tp_create_timed() への呼び出しに替わります。
struct timeval timeout; timeout.tv_sec = 30; /* 30 秒 */ timeout.tv_usec = 0; client = clnt_tp_create_timed(argv[1], TIME_PROG, TIME_VERS, nconf, &timeout); |
次に、対応するサーバー側プログラムを示します。サービスを起動するコマンド行では、どのトランスポート上でサービスを提供するかを指定する必要があります。
/* * このプログラムは、サービスを呼び出したクライアントにグリニッチ標準時を * 返します。呼び出し方法: server netid */ #include <stdio.h> #include <rpc/rpc.h> #include <netconfig.h> /* 構造体 netconfig を使用するため */ #include "time_prot.h" static void time_prog(); main(argc, argv) int argc; char *argv[]; { SVCXPRT *transp; struct netconfig *nconf; if (argc != 2) { fprintf(stderr, "usage: %s netid\n", argv[0]); exit(1); } if ((nconf = getnetconfigent( argv[1])) == (struct netconfig *) NULL) { fprintf(stderr, "Could not find info on %s\n", argv[1]); exit(1); } transp = svc_tp_create(time_prog, TIME_PROG, TIME_VERS, nconf); if (transp == (SVCXPRT *) NULL) { fprintf(stderr, "%s: cannot create %s service\n", argv[0], argv[1]); exit(1) } freenetconfigent(nconf); svc_run(); } static void time_prog(rqstp, transp) struct svc_req *rqstp; SVCXPRT *transp; { /* トップレベルの RPC を使用したコードと同じ */ |
エキスパートレベルのネットワーク選択は、中間レベルと同じです。中間レベルとの唯一の違いは、アプリケーションから CLIENT と SVCXPRT のハンドルをより詳細に制御できる点です。次の例では、clnt_tli_create() と svc_tli_create() の 2 つのルーチンを使用した制御方法を示します。TLI についての詳細は、『プログラミングインタフェース』 を参照してください。
例 4–12 には、clnt_tli_create() を使用して UDP トランスポートに対するクライアントを作成するルーチン clntudp_create() を示します。このプログラムでは、指定したトランスポートファミリに基づいたネットワークの選択方法を示します。clnt_tli_create() には、クライアントハンドルの作成のほかに次の 3 つの機能があります。
#include <stdio.h> #include <rpc/rpc.h> #include <netconfig.h> #include <netinet/in.h> /* * 旧バージョンの RPC では、TCP/IP と UDP/IP だけがサポートされていました。 * 現バージョンの clntudp_create() は TLI/STREAMS に基づいています。 */ CLIENT * clntudp_create(raddr, prog, vers, wait, sockp) struct sockaddr_in *raddr; /* リモートアドレス */ rpcprog_t prog; /* プログラム番号 */ prcvers_t vers; /* バージョン番号 */ struct timeval wait; /* 待ち時間 */ int *sockp; /* ファイル記述子 (fd) のポインタ */ { CLIENT *cl; /* クライアントハンドル */ int madefd = FALSE; /* fd はオープンされているか */ int fd = *sockp; /* TLI の fd */ struct t_bind *tbind; /* 結合アドレス */ struct netconfig *nconf; /* netconfig 構造体 */ void *handlep; if ((handlep = setnetconfig() ) == (void *) NULL) { /* ネットワーク設定開始でのエラー */ rpc_createerr.cf_stat = RPC_UNKNOWNPROTO; return((CLIENT *) NULL); } /* * 非接続型で、プロトコルファミリが INET、名前が UDP の * トランスポートが見つかるまで探す。 */ while (nconf = getnetconfig( handlep)) { if ((nconf->nc_semantics == NC_TPI_CLTS) && (strcmp( nconf->nc_protofmly, NC_INET ) == 0) && (strcmp( nconf->nc_proto, NC_UDP ) == 0)) break; } if (nconf == (struct netconfig *) NULL) rpc_createerr.cf_stat = RPC_UNKNOWNPROTO; goto err; } if (fd == RPC_ANYFD) { fd = t_open(nconf->nc_device, O_RDWR, &tinfo); if (fd == -1) { rpc_createerr.cf_stat = RPC_SYSTEMERROR; goto err; } } if (raddr->sin_port == 0) { /* 未知のリモートアドレス */ u_short sport; /* * ユーザー作成のルーチン rpcb_getport() は * rpcb_getaddr を呼び出して、 * netbuf アドレスをホストのバイト順序に従って * ポート番号に変換する */ sport = rpcb_getport(raddr, prog, vers, nconf); if (sport == 0) { rpc_createerr.cf_stat = RPC_PROGUNAVAIL; goto err; } raddr->sin_port = htons(sport); } /* sockaddr_in を netbuf に変換 */ tbind = (struct t_bind *) t_alloc(fd, T_BIND, T_ADDR); if (tbind == (struct t_bind *) NULL) rpc_createerr.cf_stat = RPC_SYSTEMERROR; goto err; } if (t_bind->addr.maxlen < sizeof( struct sockaddr_in)) goto err; (void) memcpy( tbind->addr.buf, (char *)raddr, sizeof(struct sockaddr_in)); tbind->addr.len = sizeof(struct sockaddr_in); /* fd を結合 */ if (t_bind( fd, NULL, NULL) == -1) { rpc_createerr.ct_stat = RPC_TLIERROR; goto err; } cl = clnt_tli_create(fd, nconf, &(tbind->addr), prog, vers, tinfo.tsdu, tinfo.tsdu); /* netconfig ファイルを閉じる */ (void) endnetconfig( handlep); (void) t_free((char *) tbind, T_BIND); if (cl) { *sockp = fd; if (madefd == TRUE) { /* fd はハンドルの破棄と同時に閉じる */ (void)clnt_control(cl,CLSET_FD_CLOSE, (char *)NULL); } /* リトライ時間の設定 */ (void) clnt_control( l, CLSET_RETRY_TIMEOUT, (char *) &wait); return(cl); } err: if (madefd == TRUE) (void) t_close(fd); (void) endnetconfig(handlep); return((CLIENT *) NULL); } |
ネットワーク (トランスポート) 選択には、setnetconfig()、getnetconfig()、endnetconfig() を使用します。endnetconfig() の呼び出しは、プログラムの終り近くの clnt_tli_create() の呼び出しの後で行なっていることに注意してください。
clntudp_create() には、オープンしている TLI ファイル記述子を渡すことができます。ファイル記述子が渡されなかった場合 (fd == RPC_ANYFD)、 clntudp_create() は、t_open() に渡すデバイス名を UDP の netconfig 構造体から取り出して自分でオープンします。
リモートアドレスがわからない場合 (raddr->sin_port == 0) は、リモートの rpcbind デーモンを使って取り出します。
クライアントハンドルが作成されれば、clnt_control() を使用してさまざまな変更を加えることができます。RPC ライブラリは、ハンドルを破棄するときにファイル記述子を閉じます。fd 自体をオープンしたときには、clnt_destroy () を呼び出して閉じます。 そして、リトライのタイムアウト値を設定します。
例 4–13 に、対応するサーバー側プログラム svcudp_create() を示します。サーバー側では svc_tli_create() を使用します。
svc_tli_create() は、アプリケーションで次のように詳細な制御を行う必要があるときに使用します。
送信バッファと受信バッファのサイズを指定します。引数 fd は、渡された時に結合されていないことがあります。その場合、fd は指定されたアドレスに結合され、そのアドレスがハンドルに格納されます。結合されたアドレスが NULL に設定されていて、fd が最初から結合されていない場合、任意の最適なアドレスへ結合されます。
サービスを rpcbind により登録するには、rpcb_set() を使用します。
#include <stdio.h> #include <rpc/rpc.h> #include <netconfig.h> #include <netinet/in.h> SVCXPRT * svcudp_create(fd) register int fd; { struct netconfig *nconf; SVCXPRT *svc; int madefd = FALSE; int port; void *handlep; struct t_info tinfo; /* どのトランスポートも使用不可の場合 */ if ((handlep = setnetconfig() ) == (void *) NULL) { nc_perror("server"); return((SVCXPRT *) NULL); } /* * 非接続型で、プロトコルファミリが INET、名前が UDP の * トランスポートが見つかるまで探す。 */ while (nconf = getnetconfig( handlep)) { if ((nconf->nc_semantics == NC_TPI_CLTS) && (strcmp( nconf->nc_protofmly, NC_INET) == 0 )&& (strcmp( nconf->nc_proto, NC_UDP) == 0 )) break; } if (nconf == (struct netconfig *) NULL) { endnetconfig(handlep); return((SVCXPRT *) NULL); } if (fd == RPC_ANYFD) { fd = t_open(nconf->nc_device, O_RDWR, &tinfo); if (fd == -1) { (void) endnetconfig(); return((SVCXPRT *) NULL); } madefd = TRUE; } else t_getinfo(fd, &tinfo); svc = svc_tli_create(fd, nconf, (struct t_bind *) NULL, tinfo.tsdu, tinfo.tsdu); (void) endnetconfig(handlep); if (svc == (SVCXPRT *) NULL) { if (madefd) (void) t_close(fd); return((SVCXPRT *)NULL); } return (svc); } |
この例では、clntudp_create() と同じ方法でネットワーク選択を行なっています。svc_tli_create() で結合しているため、ファイル記述子は明示的にはトランスポートアドレスと結合されません。
svcudp_create() はオープンしている fd を使用できます。有効なファイル記述子が渡されなければ、選択された netconfig 構造体を使用してこのルーチン内でオープンします。
アプリケーションで RPC のボトムレベルインタフェースを使用すると、すべてのオプションを使用して通信を制御できます。clnt_tli_create() などのエキスパートレベルの RPC インタフェースは、ボトムレベルのルーチンを使用しています。ユーザーがこのレベルのルーチンを直接使用することはほとんどありません。
ボトムレベルのルーチンは内部データ構造を作成し、バッファを管理し、RPC ヘッダーを作成します。ボトムレベルルーチンの呼び出し側 (エキスパートレベルのルーチンで言えば、clnt_tli_create() ) では、クライアントハンドルの cl_netid と cl_tp の両フィールドを初期化する必要があります。作成したハンドルの cl_netid にはトランスポートのネットワーク ID (たとえば udp) を設定し、cl_tp にはトランスポートのデバイス名 (たとえば /dev/udp) を設定します。clnt_dg_create() と clnt_vc_create() のルーチンは、clnt_ops と cl_private のフィールドを設定します。
次に、clnt_vc_create() と clnt_dg_create() の呼び出し方法を示します。
/* * 使用する変数 : * cl: CLIENT * * tinfo: struct t_info (t_open() または t_getinfo() からの戻り値) * svcaddr: struct netbuf * */ switch(tinfo.servtype) { case T_COTS: case T_COTS_ORD: cl = clnt_vc_create(fd, svcaddr, prog, vers, sendsz, recvsz); break; case T_CLTS: cl = clnt_dg_create(fd, svcaddr, prog, vers, sendsz, recvsz); break; default: goto err; } |
これらのルーチンを使用するときは、ファイル記述子がオープンされて結合されている必要があります。svcaddr はサーバーのアドレスです。
次に、ボトムレベルのサーバーを作成する例を示します。
/* * 使用する変数 * xprt: SVCXPRT * */ switch(tinfo.servtype) { case T_COTS_ORD: case T_COTS: xprt = svc_vc_create(fd, sendsz, recvsz); break; case T_CLTS: xprt = svc_dg_create(fd, sendsz, recvsz); break; default: goto err; } |
svc_dg_enablecache() はデータグラムトランスポートのキャッシュを開始します。キャッシュは、サーバー手続きが「一度だけ」行われるバージョンにのみ、使用されるべきです。これは、キャッシュされたサーバー手続きを何回も実行すると、異なる別の結果を生じるためです。
svc_dg_enablecache(xprt, cache_size) SVCXPRT *xprt; unsigned int cache_size; |
この関数は、cache_size エントリを保持するために十分な大きさで、サービスのエンドポイント xprt に、重複要求キャッシュを割り当てます。サービスに、異なる戻り値を返す手続きが含まれる場合は、重複要求キャッシュが必要です。キャッシュをいったん有効にすると、後で無効にする方法はありません。
次のデータ構造は参考のために示します。この実装は、変更される可能性があります。
最初に示す構造体はクライアント側の RPC ハンドルで、 <rpc/clnt.h> で定義されています。下位レベルの RPC を使用する場合は、次に示すように接続ごとに1 つのハンドルを作成して初期化する必要があります。
typedef struct { AUTH *cl_auth; /* 認証情報 */ struct clnt_ops { enum clnt_stat (*cl_call)(); /* リモートプロシージャ呼び出し */ void (*cl_abort)(); /* 呼び出しの中止 */ void (*cl_geterr)(); /* 特定エラーコードの取得 */ bool_t (*cl_freeres)(); /* 戻り値の解放*/ void (*cl_destroy)(); /* この構造体の破棄 */ bool_t (*cl_control)(); /* RPC の ioctl() */ } *cl_ops; caddrt_t cl_private; /* プライベートに使用 */ char *cl_netid; /* ネットワークトークン */ char *cl_tp; /* デバイス名 */ } CLIENT; |
クライアント側ハンドルの第 1 フィールドは、 <rpc/auth.h> で定義された認証情報の構造体です。このフィールドはデフォルトで、AUTH_NONE に設定されています。次に示すように、必要に応じてクライアント側プログラムで cl_auth を初期化する必要があります。
typedef struct { struct opaque_auth ah_cred; struct opaque_auth ah_verf; union des_block ah_key; struct auth_ops { void (*ah_nextverf)(); int (*ah_marshal)(); /* nextverf とシリアライズ */ int (*ah_validate)(); /* 妥当性検査の確認 */ int (*ah_refresh)(); /* 資格のリフレッシュ */ void (*ah_destroy)(); /* この構造体の破棄 */ } *ah_ops; caddr_t ah_private; } AUTH; |
AUTH 構造体の ah_cred には呼び出し側の資格が、ah_verf には資格を確認するためのデータが入っています。詳細については、認証 を参照してください
typedef struct { int xp_fd; #define xp_sock xp_fd u_short xp_port; /* 結合されたポート番号、旧形式 */ struct xp_ops { bool_t (*xp_recv)(); /* 要求の受信 */ enum xprt_stat (*xp_stat)(); /* トランスポートステータスの取得 */ bool_t (*xp_getargs)(); /* 引数の取り出し */ bool_t (*xp_reply)(); /* 応答の送信 */ bool_t (*xp_freeargs)(); /* 引数に割り当てたメモリーの解放 */ void (*xp_destroy)(); /* この構造体の破棄 */ } *xp_ops; int xp_addrlen; /* リモートアドレスの長さ、旧形式 */ char *xp_tp; /* トランスポートプロバイダのデバイス名 */ char *xp_netid; /* ネットワークトークン */ struct netbuf xp_ltaddr; /* ローカルトランスポートアドレス */ struct netbuf xp_rtaddr; /* リモートトランスポートアドレス */ char xp_raddr[16]; /* リモートアドレス、旧形式 */ struct opaque_auth xp_verf; /* raw 応答の確認 */ caddr_t xp_p1; /* プライベート: svc ops で使用 */ caddr_t xp_p2; /* プライベート: svc ops で使用 */ caddr_t xp_p3; /* プライベート: svc lib で使用 */ } SVCXPRT; |
次の表では、サーバー側のトランスポートハンドルに対応するフィールドが示されています。
xp_fd |
ハンドルに結合したファイル記述子。複数のサーバーハンドルで 1 つのファイル記述子を共有できる |
xp_netid |
トランスポートのネットワーク ID (たとえば、udp)。ハンドルはこのトランスポート上に作成される。xp_tp は、このトランスポートに結合したデバイス名 |
xp_ltaddr |
サーバー自身の結合アドレス |
xp_rtaddr |
RPC の呼び出し側アドレス (したがって、呼び出しのたびに変る) |
xp_netid xp_tp xp_ltaddr |
svc_tli_create() のようなエキスパートレベルのルーチンで初期化される |
その他のフィールドは、ボトムレベルのサーバールーチン svc_dg_create() と svc_vc_create() で初期化されます。
接続型端点では、 次の各フィールドには、接続要求がサーバーに受け入れられるまで正しい値が入りません。
デバッグツールとして、ネットワーク機能をすべてバイパスする 2 つの擬似 RPC インタフェースがあります。ルーチン clnt_raw_create() と svc_raw_create() は、実際のトランスポートを使用しません。
製品システムで RAW モードは使用しないでください。RAW モードは、デバッグを行い易くするために使用します。RAW モードはマルチスレッド対応ではありません。
このプログラムは、次の Makefile を使用してコンパイルとリンクを行います。
all: raw CFLAGS += -g raw: raw.o cc -g -o raw raw.o -lnsl
/* * 数値を 1 増加させる簡単なプログラム */ #include <stdio.h> #include <rpc/rpc.h> #include <rpc/raw.h> #define prognum 0x40000001 #define versnum 1 #define INCR 1 struct timeval TIMEOUT = {0, 0}; static void server(); main (argc, argv) int argc; char **argv; { CLIENT *cl; SVCXPRT *svc; int num = 0, ans; int flag; if (argc == 2) num = atoi(argv[1]); svc = svc_raw_create(); if (svc == (SVCXPRT *) NULL) { fprintf(stderr, "Could not create server handle\n"); exit(1); } flag = svc_reg( svc, prognum, versnum, server, (struct netconfig *) NULL ); if (flag == 0) { fprintf(stderr, "Error: svc_reg failed.\n"); exit(1); } cl = clnt_raw_create( prognum, versnum ); if (cl == (CLIENT *) NULL) { clnt_pcreateerror("Error: clnt_raw_create"); exit(1); } if (clnt_call(cl, INCR, xdr_int, (caddr_t) &num, xdr_int, (caddr_t) &ans, TIMEOUT) != RPC_SUCCESS) { clnt_perror(cl, "Error: client_call with raw"); exit(1); } printf("Client: number returned %d\n", ans); exit(0); } static void server(rqstp, transp) struct svc_req *rqstp; SVCXPRT *transp; { int num; fprintf(stderr, "Entering server procedure.\n"); switch(rqstp->rq_proc) { case NULLPROC: if (svc_sendreply( transp, xdr_void, (caddr_t) NULL) == FALSE) { fprintf(stderr, "error in null proc\n"); exit(1); } return; case INCR: break; default: svcerr_noproc(transp); return; } if (!svc_getargs( transp, xdr_int, &num)) { svcerr_decode(transp); return; } fprintf(stderr, "Server procedure: about to increment.\n"); num++; if (svc_sendreply(transp, xdr_int, &num) == FALSE) { fprintf(stderr, "error in sending answer\n"); exit (1); } fprintf(stderr, "Leaving server procedure.\n"); } |
次の点に注意してください。
サーバーはクライアントより先に作成しなければなりません。
svc_raw_create() には引数がありません。
サーバーは rpcbind デーモンに登録されません。svc_reg() の最後の引数は (struct
netconfig *) NULL です。
svc_run() が呼び出されません。
RPC 呼び出しはすべて同一の制御スレッド内で行われます。
例 4–20 に示すサンプルプログラムは、あるホストのファイルを別のホストにコピーするプログラムです。RPC の send() 呼び出しで標準入力から読み込まれたデータがサーバー receive() に送信され、サーバーの標準出力に書き出されます。また、このプログラムでは、1 つの XDR ルーチンでシリアライズとデシリアライズの両方を実行する方法も示します。ここでは、接続型トランスポートを使用します。
/* * XDR ルーチン: * 復号化時にネットワークを読み取り fp に書き込む * 符号化時に fp を読み取りネットワークに書き込む */ #include <stdio.h> #include <rpc/rpc.h> bool_t xdr_rcp(xdrs, fp) XDR *xdrs; FILE *fp; { unsigned long size; char buf[BUFSIZ], *p; if (xdrs->x_op == XDR_FREE) /* 解放するものなし */ return(TRUE); while (TRUE) { if (xdrs->x_op == XDR_ENCODE) { if ((size = fread( buf, sizeof( char ), BUFSIZ, fp)) == 0 && ferror(fp)) { fprintf(stderr, "can't fread\n"); return(FALSE); } else return(TRUE); } p = buf; if (! xdr_bytes( xdrs, &p, &size, BUFSIZ)) return(0); if (size == 0) return(1); if (xdrs->x_op == XDR_DECODE) { if (fwrite( buf, sizeof(char), size, fp) != size) { fprintf(stderr, "can't fwrite\n"); return(FALSE); } else return(TRUE); } } } |
例 4–21 と 例 4–22 には、例 4–20 に示した xdr_rcp()ルーチンだけでシリアライズとデシリアライズを行うプログラムを示します。
/* 送信側のルーチン */ #include <stdio.h> #include <netdb.h> #include <rpc/rpc.h> #include <sys/socket.h> #include <sys/time.h> #include "rcp.h" main(argc, argv) int argc; char **argv; { int xdr_rcp(); if (argc != 2 7) { fprintf(stderr, "usage: %s servername\n", argv[0]); exit(1); } if( callcots( argv[1], RCPPROG, RCPPROC, RCPVERS, xdr_rcp, stdin, xdr_void, 0 ) != 0 ) exit(1); exit(0); } callcots(host, prognum, procnum, versnum, inproc, in, outproc, out) char *host, *in, *out; xdrproc_t inproc, outproc; { enum clnt_stat clnt_stat; register CLIENT *client; struct timeval total_timeout; if ((client = clnt_create( host, prognum, versnum, "circuit_v") == (CLIENT *) NULL)) { clnt_pcreateerror("clnt_create"); return(-1); } total_timeout.tv_sec = 20; total_timeout.tv_usec = 0; clnt_stat = clnt_call(client, procnum, inproc, in, outproc, out, total_timeout); clnt_destroy(client); if (clnt_stat != RPC_SUCCESS) clnt_perror("callcots"); return((int)clnt_stat); } |
次に、受信側のルーチンを定義します。サーバー側では、xdr_rcp() がすべての処理を自動的に実行することに注意してください。
/* * 受信側ルーチン */ #include <stdio.h> #include <rpc/rpc.h #include "rcp.h" main() { void rcp_service(); if (svc_create(rpc_service,RCPPROG,RCPVERS,"circuit_v") == 0) { fprintf(stderr, "svc_create: errpr\n"); exit(1); } svc_run(); /* この関数は戻らない */ fprintf(stderr, "svc_run should never return\n"); } void rcp_service(rqstp, transp) register struct svc_req *rqstp; register SVCXPRT *transp; { switch(rqstp->rq_proc) { case NULLPROC: if (svc_sendreply(transp, xdr_void, (caddr_t) NULL) == FALSE) fprintf(stderr, "err: rcp_service"); return; case RCPPROC: if (!svc_getargs( transp, xdr_rcp, stdout)) { svcerr_decode(transp); return(); } if(!svc_sendreply(transp, xdr_void, (caddr_t) NULL)) { fprintf(stderr, "can't reply\n"); return(); } return(); default: svcerr_noproc(transp); return(); } } |
XDR ルーチンは通常、データのシリアライズとデシリアライズに使用します。XDR ルーチンは、多くの場合、メモリーを自動的に割り当て、そのメモリーを解放します。一般に、配列や構造体へのポインタの代わりに NULL
ポインタを渡されると、XDR ルーチンはデシリアライズを行うときに自分でメモリーを割り当てるようにします。次の例の xdr_chararr1() では、長さが SIZE の固定長配列を処理するようになっており、必要に応じてメモリーを割り当てることができません。
xdr_chararr1(xdrsp, chararr) XDR *xdrsp; char chararr[]; { char *p; int len; p = chararr; len = SIZE; return (xdr_bytes(xdrsp, &p, &len, SIZE)); } |
chararr にすでに領域が確保されている場合は、サーバー側から次のように呼び出すことができます。
char chararr[SIZE]; svc_getargs(transp, xdr_chararr1, chararr); |
XDR ルーチンや RPC ルーチンにデータを引き渡すための構造体は、基底アドレスが、アーキテクチャで決められた境界になるようなメモリーの割り当てにならなければなりません。XDR ルーチンでメモリーを割り当てるときも、次の点に注意して割り当てます。
呼び出し側が要求した場合にメモリー割り当てを行う
割り当てたメモリーへのポインタを返す
次の例では、第 2 引数が NULL ポインタの場合、デシリアライズされたデータを入れるためのメモリーが割り当てられます。
xdr_chararr2(xdrsp, chararrp) XDR *xdrsp; char **chararrp; { int len; len = SIZE; return (xdr_bytes(xdrsp, charrarrp, &len, SIZE)); } |
これに対する RPC 呼び出しを次に示します。
char *arrptr; arrptr = NULL; svc_getargs(transp, xdr_chararr2, &arrptr); /* * ここで戻り値を使用 */ svc_freeargs(transp, xdr_chararr2, &arrptr); |
文字配列は、使用後に svc_freeargs() を使用して解放します。svc_freeargs() は、第 2 引数に NULL ポインタを渡された場合は何もしません。
これまでに説明したことをまとめると、次のようになります。