ONC+ 開発ガイド

rpcgen チュートリアル

rpcgen を使用すると、分散型アプリケーションを簡単に作成できます。サーバー側手続きは、手続き呼び出し規約に準拠した言語で記述します。サーバー側手続きは、rpcgen によって生成されたサーバースタブとリンクして、実行可能なサーバープログラムを形成します。クライアント側手続きも同様に記述およびリンクします。

この節では、rpcgen を使用した基本的なプログラミング例を示します。また、rpcgen(1) のマニュアルページを参照してください。

ローカル手続きを遠隔手続きに変換

単一のコンピュータ環境で実行されるアプリケーションを、ネットワーク上で実行する分散型アプリケーションに変更する場合を考えます。次の例で、システムコンソールにメッセージを表示するプログラムを分散型アプリケーションに変換する方法を、ステップ別に説明します。変換前のプログラム例 3-1 を次に示します。


例 3-1 シングルコンピュータ用の printmsg.c

/* printmsg.c: コンソールにメッセージを表示する */
#include <stdio.h>
main(argc, argv)
	int argc;
	char *argv[];
{
 char *message;
 
	if (argc != 2) {
		fprintf(stderr, "usage: %s <message>¥n",
 				argv[0]);
		exit(1);
	}
 message = argv[1];
	if (!printmessage(message)) {
 	fprintf(stderr,"%s: couldn't print your
					message¥n",argv[0]);
 exit(1);
	}
	printf("Message Delivered!¥n");
 exit(0);
}
 
/* コンソールにメッセージを表示する。
 * メッセージを表示できたかどうかを示すブール値を返す。*/
 
printmessage(msg)
 char *msg;
{
	FILE *f;
 
	f = fopen("/dev/console", "w");
	if (f == (FILE *)NULL) {
 	return (0);
	}
	fprintf(f, "%s¥n", msg);
 fclose(f);
	return(1);
}

このプログラムをシングルコンピュータ上で使用するときは、次のコマンドでコンパイルして実行できます。

$ cc printmsg.c -o printmsg
$ printmsg "Hello, there."
Message delivered!
$
 

printmessage() 関数を遠隔手続きに変換すると、ネットワーク上のどこからでも実行できるようになります。rpcgen を使用すると、簡単にこのような変換を実行できます。

最初に、手続きを呼び出すときのすべての引数と戻り値のデータ型を決定します。printmessage() の引数は文字列で、戻り値は整数です。このようなプロトコル仕様を RPC 言語で記述して、遠隔手続きとしての printmessage() を作成することができます。RPC 言語でこのプロトコル仕様を記述したソースコードは次のようになります。

/* msg.x: メッセージを表示する遠隔手続きのプロトコル */
 program MESSAGEPROG {
     version PRINTMESSAGEVERS {
        int PRINTMESSAGE(string) = 1;
  	} = 1;
} = 0x20000001;

遠隔手続きは常に遠隔プログラムの中で宣言されます。上のコードでは、PRINTMESSAGE という手続きが 1 つだけ含まれた遠隔プログラムが 1 つ宣言されています。この例では、PRINTMESSAGE という手続きが、MESSAGEPROG という遠隔プログラム内の手続き 1、バージョン 1 として宣言されています。遠隔プログラムのプログラム番号は、0x20000001 です。(プログラム番号の指定方法については、付録 B 「RPC プロトコルおよび言語の仕様」 を参照してください)。既存の手続きが変更されたり新規手続が追加されたりして、遠隔プログラムの機能が変更されると、バージョン番号が 1 つ増やされます。遠隔プログラムで複数のバージョンを定義することも、1 つのバージョンで複数の手続きを定義することもできます。

プログラム名も手続き名も共に大文字で宣言していることに注意してください。

また、引数のデータ型を C 言語で書くときのように char * としないで string としていることにも注意してください。これは、C 言語で char * と指定すると、文字型配列とも、単一の文字へのポインタとも解釈できて不明確なためです。RPC 言語では、NULL で終わる文字型配列は string 型で宣言します。

更に次の 2 つのプログラムを書く必要があります。

例 3-2 には、例 3-1 の手続きを PRINTMESSAGE という遠隔手続きに変更したものを示します。


例 3-2 RPC バージョンの printmsg.c

/*
 * msg_proc.c: 遠隔手続きバージョンの printmessage 
 */
#include <stdio.h>
#include "msg.h"				/* rpcgen が生成 */
 
int *
printmessage_1(msg, req)
 char **msg;
 struct svc_req *req;		/* 呼び出しの詳細 */
{
 static int result;			/* 必ず static で宣言 */
 FILE *f;
 
f = fopen("/dev/console", "w");
if (f == (FILE *)NULL) {
	result = 0;
	return (&result);
}
fprintf(f, "%s¥n", *msg);
fclose(f);
result = 1;
return (&result);
}

遠隔手続き printmessage_1() と、ローカル手続き printmessage() の宣言は次の 4 つの点で異なることに注意してください。

  1. 引数が文字へのポインタではなく、文字配列へのポインタになっています。-N オプションを使用しない遠隔手続きの場合は、引数自体が渡されるのではなく、常に引数へのポインタが渡されるからです。-N オプションを指定しなければ、遠隔手続きの呼び出しで引数が 1 つしか渡されません。複数の引数が必要な場合は、引数を struct 型にして渡す必要があります。

  2. 引数が 2 つあります。第 2 引数には、関数呼び出しのときのコンテキスト、すなわち、プログラム、バージョン、手続きの番号、raw および canonical の認証、SVCXPRT 構造体へのポインタが入っています。 SVCXPRT 構造体にはトランスポート情報が入っています。呼び出された手続きが要求されたサービスを実行するときに、これらの情報が必要になる場合があります。

  3. 戻り値は、整数そのものではなく整数へのポインタになっています。-N オプションを指定しない遠隔手続きの場合は、戻り値自体ではなく戻り値へのポインタが返されるためです。-M (マルチスレッド) オプション または -A (自動モード) オプションが使用されていない場合は、戻り値は static で宣言します。戻り値を遠隔手続きのローカル値にしてしまうと、遠隔手続きが戻り値を返した後、サーバー側スタブプログラムからその値を参照することができなくなります。 -M および -A が使用されている場合は、戻り値へのポインタは第 3 引数として手続きに渡されるため、戻り値は手続きで宣言されません。

  4. 手続き名に _1 が追加されています。一般に rpcgen が遠隔手続き呼び出しを生成するときは、次のように手続き名が決められます。プログラム定義で指定した手続き名 (この場合は PRINTMESSAGE) はすべて小文字に変換され、下線 (_) とバージョン番号 (この場合は 1) が追加されます。このように手続き名が決定されるので、同じ手続きの複数バージョンが使用可能になります。

例 3-3 には、この遠隔手続きを呼び出すクライアント側メインプログラムを示します。


例 3-3 printmsg.c を呼び出すクライアント側プログラム

/*
 * rprintmsg.c: printmsg.c の RPC 対応バージョン
 */
#include <stdio.h>
#include "msg.h"			/* rpcgen が生成 */
 
main(argc, argv)
	int argc;
	char *argv[];
{
	CLIENT *clnt;
	int *result;
	char *server;
	char *message;
 
	if (argc != 3) {
		fprintf(stderr, "usage: %s host 
					message¥n", argv[0]);
		exit(1);
	}
 
	server = argv[1];
	message = argv[2];
	/*
 * コマンド行で指定したサーバーの
 * MESSAGEPROG の呼び出しで使用する
 * クライアント「ハンドル」を作成
 */
	clnt = clnt_create(server, MESSAGEPROG,
								PRINTMESSAGEVERS,
								"visible");
	if (clnt == (CLIENT *)NULL) {
		/*
		 * サーバーとの接続確立に失敗したため、
		 * エラーメッセージを表示して終了
		 */
		clnt_pcreateerror(server);
		exit(1);
	}
	
	/*
	 * サーバー上の遠隔手続き printmessage を呼び出す
	 */
	result = printmessage_1(&message, clnt);
	if (result == (int *)NULL) {
		/*
		 * サーバーの呼び出しでエラーが発生したため、
		 * エラーメッセージを表示して終了
		 */
		clnt_perror(clnt, server);
		exit(1);
	}
/*
 * 遠隔手続き呼び出しは正常終了
 */
	if (*result == 0) {
		/*
		 * サーバーがメッセージの表示に失敗したため、
		 * エラーメッセージを表示して終了
		 */
		fprintf(stderr,
		"%s: could not print your message¥n",argv[0]);
		exit(1);
	}
 
/*
 * サーバーのコンソールにメッセージが出力された
 */
	printf("Message delivered to %s¥n",
				server);
	clnt_destroy( clnt );
	exit(0);
}

この例 3-3 では、次の点に注意してください。

  1. 最初に、RPC ライブラリルーチン clnt_create() を呼び出してクライアントハンドルを作成しています。クライアントハンドルは、遠隔手続きを呼び出すスタブルーチンに引き渡されます。(これ以外にもクライアントハンドルを作成する方法があります。詳細については、第 4 章「RPC プログラマインタフェース」を参照してください)。クライアントハンドルを使用する遠隔手続き呼び出しがすべて終了したら、clnt_destroy() を使用してそのクライアントハンドルを破棄し、システム資源を無駄に使用しないようにします。

  2. clnt_create() の最後の引数に visible を指定して、/etc/netconfigvisible と指定したすべてのトランスポートを使用できるようにします。詳細については、/etc/netconfig ファイルと『Transport Interfaces Programming Guide』を参照してください。

  3. 遠隔手続き printmessage_1() の呼び出しは、第 2 引数として挿入されたクライアントハンドルを除いて、msg_proc.c で宣言されたとおりに実行されています。戻り値も値ではなく、値へのポインタで返されています。

  4. 遠隔手続き呼び出しのエラーには、RPC 自体のエラーと、遠隔手続きの実行中に発生したエラーの 2 種類があります。最初のエラーの場合は、遠隔手続き printmessage_1() の戻り値が NULL になります。2 つめのエラーの場合は、アプリケーションによってエラーの返し方が異なります。この例では、*result によってエラーがわかります。

これまでに示した各コードをコンパイルする方法を次に示します。

$	rpcgen msg.x
$	cc rprintmsg.c msg_clnt.c -o rprintmsg -lnsl
$	cc msg_proc.c msg_svc.c -o msg_server -lnsl

最初に rpcgen を実行してヘッダーファイル (msg.h)、クライアント側スタブプログラム (msg_clnt.c)、サーバー側スタブプログラム (msg_svc.c) を生成します。次の 2 つのコンパイルコマンドで、クライアント側プログラム rprintmsg とサーバー側プログラム msg_server が作成されます。C のオブジェクトファイルは、ライブラリ libnsl とリンクする必要があります。ライブラリ libnsl には、RPC と XDR で必要な関数をはじめとするネットワーク関数がすべて含まれています。

この例では、アプリケーションが libnsl に含まれる基本型だけを使用しているので、XDR ルーチンは生成されません。

次に、rpcgen が入力ファイル msg.x から何を生成するかを説明します。

  1. msg.h というヘッダーファイルを作成します。msg.h には、他のモジュールで使用できるように MESSAGEPROGMESSAGEVERSPRINTMESSAGE#define 文が入っています。このヘッダーファイルは、クライアント側とサーバー側の両方のモジュールでインクルードする必要があります。

  2. クライアント側スタブルーチンを msg_clnt.c というファイルに出力します。このファイルには、クライアントプログラム rprintmsg から呼び出されるルーチン printmessage_1() が 1 つだけ入っています。rpcgen への入力ファイルが FOO.x という名前ならば、クライアント側スタブルーチンは FOO_clnt.c というファイルに出力されます。

  3. msg_proc.cprintmessage_1() を呼び出すサーバープログラムを msg_svc.c というファイルに出力します。サーバープログラムのファイル名は、クライアントプログラムのファイル名と同様の方法で決まります。rpcgen への入力ファイルが FOO.x という名前ならば、サーバープログラムは FOO_svc.c というファイルに出力されます。

サーバープログラムが作成されると、遠隔マシン上にインストールして実行することができます。(遠隔マシンが同じ機種の場合は、サーバープログラムをバイナリのままコピーすることができますが、機種が異なる場合は、サーバープログラムのソースファイルを遠隔マシンにコピーして再コンパイルする必要があります)。遠隔マシンを remote、ローカルマシンを local とすると、遠隔システムのシェルから次のコマンドでサーバープログラムを起動することができます。

remote$ msg_server

rpcgen が生成したサーバープロセスは、常にバックグラウンドで実行されます。このとき、サーバープログラムにアンパサンド(&) を付けて起動する必要はありません。また、rpcgen が生成したサーバープロセスはコマンド行からではなく、listen()inetd() などのポートモニタから起動することもできます。

以降は、local マシン上のユーザーが次のようなコマンドを実行して、remote マシンのコンソールにメッセージを表示できます。

local$ rprintmsg remote "Hello, there."

rprintmsg を使用すると、サーバープログラム msg_server が起動されているどのシステムにでも (local システムも含む)、コンソールにメッセージを表示できます。

複雑なデータ構造の引き渡し

「ローカル手続きを遠隔手続きに変換」では、クライアント側とサーバー側のRPC コードの生成について説明しました。rpcgen を使用して、XDR ルーチンを生成することもできます (XDR ルーチンは、ローカルデータ形式と XDR 形式との相互変換を行います)。

遠隔ディレクトリを一覧表示する RPC サービスの全体を 例 3-4 に示します。rpcgen を使用してスタブルーチンと XDR ルーチンの両方を生成します。


例 3-4 RPC 言語で書かれたプロトコル記述ファイル (dir.x)

/*
 * dir.x: 遠隔ディレクトリを一覧表示するサービスのプロトコル
 *
 * rpcgen の機能を説明するためのサンプルプログラム
 */
 
const MAXNAMELEN = 255;						/* ディレクトリエントリの最大長 */
typedef string nametype<MAXNAMELEN>;		/* ディレクトリエントリ */
typedef struct namenode *namelist;			/* リスト形式でリンク */
 
/* ディレクトリリスト内のノード */
struct namenode {
	nametype name;						/* ディレクトリエントリ名 */
	namelist next;						/* 次のエントリ */
};
 
/*
 * READDIR の戻り値
 *
 * どこにでも移植できるアプリケーションにするためには、
 * この例のように UNIX の errno を返さないで、
 * エラーコードリストを設定して使用する方がよい
 * 
 * このプログラムでは、次の共用体を使用して、遠隔呼び出しが
 * 正常終了したか異常終了したかを区別します。
 */
 
union readdir_res switch (int errno) {
	case 0:
		namelist list;		/* 正常終了: 戻り値はディレクトリリスト */
	default:
		void;					/* エラー発生: 戻り値なし */
};
 
/* ディレクトリを一覧表示するプログラムの定義 */
program DIRPROG {
	version DIRVERS {
		readdir_res
		READDIR(nametype) = 1;
	} = 1;
} = 0x20000076; 

上の例の readdir_res のように、RPC 言語のキーワード structunionenum を使用して型を再定義することができます。使用したキーワードは、後にその型の変数を宣言するときには指定しません。たとえば、共用体 foo を定義した場合、union foo ではなく foo で宣言します。

rpcgen でコンパイルすると、RPC の共用体は C 言語の構造体に変換されます。RPC の共用体は、キーワード union を使用して宣言しないでください。

dir.x に対して rpcgen を実行すると、次の 4 つのファイル、(1) ヘッダーファイル、(2) クライアント側のスタブルーチン、(3) サーバー側の骨組み、(4) XDR ルーチンの入った dir_xdr.c というファイルが生成されます。(4) のファイルに入っている XDR ルーチンは、宣言されたデータ型を、ホスト環境のデータ形式から XDR 形式に、またはその逆方向に変換します。

rpcgen では、.x ファイルで使用されている RPC 言語の各データ型に対して、データ型名の前に XDR ルーチンであることを示すヘッダー xdr_ が付いたルーチン (たとえば、xdr_int) が libnsl で提供されるものとみなします。.x ファイルにデータ型が定義されていると、rpcgen はそれに対するルーチンを生成します。msg.x のように、.x ソースファイルにデータ型が定義されていない場合は、_xdr.c ファイルは生成されません。

.x ソースファイルで、libnsl でサポートされていないデータ型を使用し、.x ファイルではそのデータ型を定義しないこともできます。その場合は、xdr_ ルーチンをユーザーが自分で作成することになります。こうして、ユーザー独自の xdr_ ルーチンを提供することができます。任意のデータ型を引き渡す方法についての詳細は、第 4 章「RPC プログラマインタフェース」を参照してください。例 3-5 に、サーバー側の READDIR 手続きを示します。


例 3-5 サーバー側の dir_proc.c の例

/*
 * dir_proc.c: 遠隔手続き readdir 
 */
#include <dirent.h>
#include "dir.h"                /* rpcgen が生成 */
 
extern int errno;
extern char *malloc();
extern char *strdup();
 
readdir_res *
readdir_1(dirname, req)
	nametype *dirname;
	struct svc_req *req;
 
{
	DIR *dirp;
	struct dirent *d;
	namelist nl;
	namelist *nlp;
	static readdir_res res; /* 必ず static で宣言 */
	
	/* ディレクトリのオープン */
	dirp = opendir(*dirname);
	if (dirp == (DIR *)NULL) {
		res.errno = errno;
		return (&res);
	}
	/* 直前の戻り値の領域解放 */
	xdr_free(xdr_readdir_res, &res);
/*
 * ディレクトリエントリをすべて取り出す。ここで割り当てたメモリーは、
 * 次に readdir_1 が呼び出されたときに xdr_free で解放
 */
	nlp = &res.readdir_res_u.list;
	while (d = readdir(dirp)) {
		nl = *nlp = (namenode *) 
							malloc(sizeof(namenode));
		if (nl == (namenode *) NULL) {
			res.errno = EAGAIN;
			closedir(dirp);
			return(&res);
		}
		nl->name = strdup(d->d_name);
		nlp = &nl->next;
	}
	*nlp = (namelist)NULL;
	/* 結果を返す */
	res.errno = 0;
	closedir(dirp);
	return (&res);
}

例 3-6 に、クライアント側のREADDIR 手続きを示します。


例 3-6 クライアント側のプログラム (rls.c)

/*
 * rls.c: クライアント側の遠隔ディレクトリリスト
 */
 
#include <stdio.h>
#include "dir.h"                       /* rpcgen が生成 */
 
extern int errno;
 
main(argc, argv)
	int argc;
	char *argv[];
{
	CLIENT *clnt;
	char *server;
	char *dir;
	readdir_res *result;
	namelist nl;
 
	if (argc != 3) {
		fprintf(stderr, "usage: %s host 
					directory¥n",argv[0]);
		exit(1);
	}
	server = argv[1];
	dir = argv[2];
/*
 * コマンドラインで指定したサーバーの MESSAGEPROG の呼び出しで使用する
 * クライアント「ハンドル」を作成
 */
	cl = clnt_create(server, DIRPROG,
								DIRVERS, "tcp");
	if (clnt == (CLIENT *)NULL) {
		clnt_pcreateerror(server);
		exit(1);
	}
	result = readdir_1(&dir, clnt);
	if (result == (readdir_res *)NULL) {
		clnt_perror(clnt, server);
		exit(1);
	}
/*
 * 遠隔手続き呼び出しは正常終了
 */
	if (result->errno != 0) {
   /*
    * 遠隔システム上のエラー。エラーメッセージを表示して終了
    */
		errno = result->errno;
		perror(dir);
		exit(1);
	}
/*
 * ディレクトリリストの取り出しに成功。ディレクトリリストを表示
 */
	for (nl = result->readdir_res_u.list;
								nl != NULL;
		nl = nl->next) {
		printf("%s¥n", nl->name);
	}
	xdr_free(xdr_readdir_res, result);
	clnt_destroy(cl);
	exit(0);
}

この前のサンプルプログラムと同様に、システム名を localremote とします。ファイルのコンパイルと実行は、次のコマンドで行います。

remote$ rpcgen dir.x
remote$ cc -c dir_xdr.c
remote$ cc rls.c dir_clnt.c dir_xdr.o -o rls -lnsl
remote$ cc dir_svc.c dir_proc.c dir_xdr.o -o dir_svc -lnsl
remote$ dir_svc

local システムに rls() をインストールすると、次のように remote システム上の/usr/share/lib の内容を表示できます。

local$ rls remote /usr/share/libascii
eqnchar
greek
kbd
marg8
tabclr
tabs
tabs4
local$ 

rpcgen が生成したクライアント側のコードは、RPC 呼び出しの戻り値のために割り当てたメモリーを解放しないので、必要がなくなったら xdr_free() を呼び出してメモリーを解放してください。xdr_free() の呼び出しは free() ルーチンの呼び出しに似ていますが、XDR ルーチンの戻り値のアドレスを引き渡す点が異なります。この例では、ディレクトリリストを表示した後で次のように xdr_free() を呼び出しています。

xdr_free(xdr_readdir_res,result); 


注 -

xdr_free() を使用して malloc() で割り当てたメモリーを解放します。xdr_free() を使用してメモリーを解放すると、メモリーリークを生じて失敗します。


前処理命令

rpcgen では、C 言語などの前処理命令をサポートしています。rpcgen の入力ファイルに入っている C 言語の前処理命令は、コンパイル前に処理されます。.x ソースファイルでは、標準 C のすべての前処理命令を使用できます。生成する出力ファイルのタイプによって、次の 5 つのシンボルが rpcgen によって定義されます。

rpcgen 入力ファイルのパーセント記号 (%) で始まる行はそのまま出力ファイルに書き出され、その行の内容には影響を及ぼしません。そのとき、意図した位置に出力されるとは限らないため注意が必要です。出力ファイルのどこに書き出されたか確認して、必要ならば編集し直してください。

表 3-1 rpcgen の前処理命令

シンボル 

使用目的 

RPC_HDR

ヘッダーファイルの出力 

RPC_XDR

XDR ルーチンの出力 

RPC_SVC

サーバー側スタブプログラムの出力 

RPC_CLNT

クライアント側スタブプログラムの出力 

RPC_TBL

インデックステーブルの出力 

例 3-7 に、簡単な rpcgen の例を示します。rpcgen の前処理機能の使用方法に注意してください。


例 3-7 時刻プロトコルを記述する rpcgen ソースプログラム

/*
 * time.x: 遠隔時刻のプロトコル
 */
program TIMEPROG {
	version TIMEVERS {
			unsigned int TIMEGET() = 1;
	} = 1;
} = 0x20000044;
 
#ifdef RPC_SVC
%int *
%timeget_1()
%{
%	static int thetime;
%
%	thetime = time(0);
%	return (&thetime);
%}
#endif

cpp 命令

rpcgen では、C 言語の前処理機能をサポートしています。rpcgen では、デフォルトで /usr/ccs/lib/cpp を C のプリプロセッサとして使用します。これを使用できないときは、/lib/cpp を使用します。これ以外の cpp を含むライブラリを使用するときは、rpcgen-Y フラグで指定します。

たとえば、/usr/local/bin/cpp を使用するには、次のように rpcgen を起動します

rpcgen -Y /usr/local/bin test.x