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() で初期化されます。
接続型端点では、 次の各フィールドには、接続要求がサーバーに受け入れられるまで正しい値が入りません。