RPC パッケージの標準レベルへのインタフェースは、RPC 通信へのさらに詳細な制御を提供します。この制御を使用するプログラムはより複雑になります。下位レベルでの効果的なプログラミングには、コンピュータネットワークの構造に対するより深い知識が必要です。トップ、中間、エキスパート、ボトムレベルは、標準インタフェースの一部です。
この節では、RPC ライブラリの下位レベルを使用して RPC プログラムを詳細に制御する方法について説明します。たとえば、単純インタフェースレベルでは NETPATH
を介してしか使用できなかったトランスポートプロトコルを自由に使用できます。これらのルーチンを使用するには、TLI
に対する知識が必要です。
表 4-3 に示したルーチンにはトランスポートハンドルの指定が必要なため、単純インタフェースからは使用できません。たとえば、単純レベルでは、XDR ルーチンでシリアライズとデシリアライズを行うときに、メモリの割り当てと解放を行うことはできません。
表 4-3 トランスポートハンドルの指定が必要な XDR ルーチン
単純インタフェースでは使用できないルーチン |
||
---|---|---|
clnt_call() |
clnt_destroy() |
clnt_control() |
clnt_perrno() |
clnt_pcreateerror() |
clnt_perror() |
svc_destroy() |
トップレベルのルーチンを使用すると、アプリケーションで使用するトランスポートタイプを指定できますが、特定のトランスポートは指定できません。このレベルは、クライアントとサーバの両方でアプリケーションが自分のトランスポートハンドルを作成する点で、単純インタフェースと異なります。
例 4-7 に示すようなヘッダファイルがあるとします。
/* 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 ((u_long)0x40000001) #define TIME_VERS ((u_long)1) #define TIME_GET ((u_long)1)
例 4-8 に、クライアント側の、トップレベルのサービスルーチンを使用する簡単な日時表示プログラムを示します。このプログラムでは、時刻を返すサービスを呼び出します。トランスポートタイプはプログラムを起動するときの引数で指定します。
#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);
例 4-9 には、トップレベルのサービスルーチンを使用したサーバ側プログラムを示します。このプログラムでは、時刻を返すサービスを実行します。
#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 つの関数
svc_getargs( SVCXPRT_handle, XDR_filter, argument_pointer); svc_freeargs( SVCXPRT_handle, XDR_filter argument_pointer );
を呼び出して、引数を取り出し、デシリアライズ (XDR 形式から復号化) し、解放します。
中間レベルのルーチンを使用するときは、使用するトランスポート自体をアプリケーションから直接選択します。
例 4-10 は、「トップレベルのインタフェース」の時刻サービスのクライアント側プログラムを、中間レベルの 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(3N) マニュアルページと『Transport Interfaces Programming Guide』を参照してください)。このレベルの 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);
これに対するサーバ側プログラムを 例 4-11 に示します。サービスを起動するコマンド行では、どのトランスポート上でサービスを提供するかを指定する必要があります。
/* * このプログラムは、サービスを呼び出したクライアントにグリニッチ標準時を * 返します。呼び出し方法: 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 についての詳細は、『Transport Interfaces Programming Guide』を参照してください。
例 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; /* 遠隔アドレス */ u_long prog; /* プログラム番号 */ u_long 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) { /* remote addr unknown */ 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) は、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 を使用できます。有効な 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 のフィールドを設定します。
例 4-14 は、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 はサーバのアドレスです。
サーバ側は 例 4-15 のようになります。
/* * 使用する変数 * 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 long cache_size;
この関数は、cache_size エントリを保持するために十分な大きさで、サービスのエンドポイント xprt に、重複要求キャッシュを割り当てます。サービスに、異なる戻り値を返す手続きが含まれる場合は、重複要求キャッシュが必要です。キャッシュをいったん有効にすると、後で無効にする方法はありません。
次のデータ構造は参考のために示しますが、変更される可能性があります。
最初に示すのは、クライアント側の RPC ハンドルで、<rpc/clnt.h> で定義されています。下位レベルの RPC を使用する場合は、例 4-16 に示したように接続ごとに 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; /* ネットワークトークン *l char *cl_tp; /* デバイス名 */ } CLIENT;
クライアント側ハンドルの第 1 フィールドは、 <rpc/auth.h> で定義された認証情報の構造体です。このフィールドはデフォルトで AUTH_NONE に設定されているため、例 4-17に示すように、必要に応じてクライアント側プログラムで 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 には資格を確認するためのデータが入っています。詳細については、「認証」を参照してください。
例 4-18 には、サーバ側のトランスポートハンドルを示します。
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;
表 4-4 は、サーバ側のトランスポートハンドルに対応するフィールドを示します。
表 4-4 RPC サーバ側のトランスポートハンドル
ハンドルに結合したファイル記述子です。複数のサーバハンドルで 1 つのファイル記述子を共有できます。 |
|
トランスポートのネットワーク ID (たとえば、udp)。ハンドルはこのトランスポート上に作成されます。xp_tp は、このトランスポートに結合したデバイス名です。 |
|
サーバ自身の結合アドレスです。 |
|
RPC の呼び出し側アドレスです (したがって、呼び出しのたびに変わります)。 |
|
svc_tli_create() のようなエキスパートレベルのルーチンで初期化されます。 |
その他のフィールドは、ボトムレベルのサーバルーチン svc_dg_create() と svc_vc_create() で初期化されます。
接続型端点では、表 4-5の各フィールドには、接続要求がサーバに受け入れられるまで正しい値が入りません。
表 4-5 RPC 接続型端点
接続が確立するまでは無効なフィールド |
||
---|---|---|
xp_fd |
xp_ops() |
xp_p1() |
xp_p2 |
xp_verf() |
xp_tp() |
xp_ltaddr |
xp_rtaddr() |
xp_netid() |