Part 2 では RPC について説明します。
この章では Sun RPC としても知られている TI-RPC について概要を説明します。RPC に初めて接するユーザに役立つ情報を記載しています (用語の定義は、用語集を参照してください)。
TI-RPC はクライアントサーバをベースにした分散型アプリケーションを構築するための強力な技術です。従来のローカルの手続き呼び出しの概念を拡張し、呼び出された手続きが呼び出す手続きと同じアドレス空間に存在する必要がないようにしています。2 つのプロセスが同じシステム上に存在することもあり、また、ネットワーク上で接続された異なるシステム上に存在する場合もあります。
RPC を使用すると、分散型アプリケーションを作成するプログラマはネットワークとの詳細なインタフェースを意識する必要がありません。 RPC はトランスポート層に依存しないため、データ通信の物理的および論理的な機構からアプリケーションを切り離して作成することができ、したがって、アプリケーションはさまざまなトランスポートを使用できます。
RPC は関数呼び出しに類似したものです。RPC を実行すると、呼び出し時の引数が遠隔手続きに渡され、呼び出し側は遠隔手続きからの応答を待ちます。
図 2-1 に、2 つのネットワーク上のシステム間でのRPC 呼び出し時に実行される動作のフローを示します。クライアントは、サーバに要求を送信して応答を待つ手続き呼び出しを行います。応答が受信されるかまたはタイムアウトになるまで、スレッドの実行は停止されます。要求が届くと、サーバは要求されたサービスを実行するディスパッチルーチンを呼び出し、その結果をクライアントに返します。RPC 呼び出しが終了すると、クライアントはプログラムの続きを実行します。
RPC はネットワークアプリケーションをサポートします。TI-RPC は TCP/IP のようなネットワーク機構上で実行されます。その他の標準の RPC としては、OSF DCE (Apollo の NCS システムをベースにしています)、Xerox Courier、Netwise があります。
特定の RPC を実装する場合には次の注意点があります。
パラメタと結果が渡される方法
結合が行われる方法
トランスポートプロトコルが使用される方法
呼び出しセマンティクス
使用されるデータ表現
TI-RPC では 1 つのパラメタをクライアントからサーバに渡すことができます。複数のパラメタが必要なときは、1 つの要素とみなされる 1 つの構造体に含めて渡されます。サーバからクライアントに渡される情報は、関数の戻り値として渡されます。サーバからクライアントにパラメタリストを通して情報を戻すことはできません。
クライアントは、利用したいサービスの使用方法を知っていなければなりません。サーバのホスト名を知ることと、実際のサーバのプロセスに接続することが必要です。各ホストでは、rpcbind と呼ばれるサービスが RPC サービスを管理します。TI-RPC は hostsファイル、NIS+、DNS などのホストネームサービスを使用してホストの位置を確認します。
トランスポートプロトコルは、クライアントとサーバとの間で呼び出しおよび返答メッセージがどのように送信されるかを指定します。TS-RPC はトランスポートプロトコルとして TCP と UDP を使用しますが、現在の TI-RPC バージョンはトランスポートに依存しません。つまり、TI-RPC は Solaris 2.x がサポートする任意のトランスポートプロトコルで動作します。
呼び出しセマンティクスは、遠隔手続きの実行に関し、特にその手続きが何回実行されたかについてクライアントが仮定することに関係があります。これはエラー条件をあつかう場合に重要です。この場合、「1 回」、「多くても 1 回」、「少なくとも 1 回」、の 3 つのセマンティクスがあります。ONC+ では「少なくとも 1 回」のセマンティクスを提供します。遠隔で呼び出される手順は一貫しています。つまり、たとえ数回にわたって呼び出されても同じ結果を返す必要があります。
データ表現とは、プロセス間でパラメタと結果が渡されるときに使用されるフォーマットのことです。さまざまなシステムアーキテクチャ上で RPCが機能するためには、標準データ形式が必要です。TI-RPC では、標準データ形式として外部データ表現 (XDR: external Data Representation) を使用します。XDR はマシンに依存しないデータ形式と符号化のためのプロトコルです。TI-RPC では、XDR を使用することによって、各ホストのバイト順序や構造体の配置方法にわずらわされることなく、任意のデータ構造を扱うことができます。XDR の詳細については、付録 C 「XDR プロトコル仕様」および 付録 A 「XDR テクニカルノート」を参照してください。
(プログラム番号、バージョン番号、手続き番号)
プログラム番号とは、関連する遠隔手続きがグループ化された 1 つのプログラムを示します。プログラム内の各手続きは固有の手続き番号を持っています。
プログラムは 1 つまたは複数のバージョンを持つ場合があります。各バージョンは遠隔で呼び出せる手続きの集まりです。バージョン番号を利用することにより、1 つの RPC プロトコルの複数のバージョンを同時に使用できます。
各バージョンには遠隔で呼び出せる多くの手続きが含まれます。各手続きは、手続き番号を持っています。
「プログラムと手続き番号」では、値の範囲と意味を示し、プログラム番号を RPC プログラムに割り当てる方法を説明しています。RPC サービス名とプログラム番号との対応リストは、rpc ネットワークデータベースの /etc/rpc にあります。
RPC が提供するサービスには、さまざまなレベルのアプリケーションインタフェースがあります。レベルごとに制御の度合いが異なるため、インタフェースのコーディング量との兼ね合いで適当なレベルを使用してください。この節では、制御の度合いとプログラムの複雑さの順に、各レベルで利用できるルーチンについて要約します。
単純インタフェースは、使用するトランスポートタイプだけを指定して、他のマシン上のルーチンを遠隔手続き呼び出しにより実行します。ほとんどのアプリケーションで、このレベルのルーチンを使用します。説明とコード例は、「単純インタフェース」を参照してください。
表 2-1 RPC ルーチン - 単純レベル
ルーチン |
説明 |
---|---|
手続きを RPC プログラムとして、指定したタイプのトランスポートすべてに登録する。 |
|
指定した遠隔ホスト上の、指定した手続きを遠隔呼び出しする。 |
|
指定したタイプのトランスポートすべてに呼び出しメッセージをブロードキャストする。 |
標準インタフェースはトップレベル、中間レベル、エキスパートレベル、ボトムレベルの 4 つのレベルにわけられます。開発者はこれらのインタフェースを使用して、トランスポートの選択、エラーへの応答または要求の再送まで待つ時間の指定などのパラメタをかなり詳細に制御できます。
トップレベルのインタフェースも簡単に使用できますが、RPC 呼び出しを行う前にクライアントハンドルを作成し、RPC 呼び出しを受ける前にサーバハンドルを作成しなければなりません。アプリケーションをすべてのトランスポート上で実行したい場合は、このインタフェースを使用してください。このルーチンの使用方法とコード例は、「トップレベルのインタフェース」を参照してください。
表 2-2 RPC ルーチン - トップレベル
ルーチン |
説明 |
---|---|
汎用のクライアント作成ルーチン。このルーチンは、サーバの位置と、使用するトランスポートのタイプを指定して呼び出す。 |
|
clnt_create() に似ているが、クライアントの作成を試みる間、各トランスポートタイプに許される最長時間を指定できる。 |
|
指定したタイプのトランスポートすべてに対しサーバハンドルを作成する。このルーチンは、使用するディスパッチ関数を svc_create() に指定して呼び出す。 |
|
要求をサーバに送信するための手続きをクライアント側から呼び出すルーチン。 |
RPC の中間レベルのインタフェースを使用すると、通信を詳細に制御できます。このような下位レベルのインタフェースを使用すると、プログラムは複雑になりますが、効率はよくなります。中間レベルでは特定のトランスポートを指定できます。このルーチンの使用方法とコード例は、「中間レベルのインタフェース」を参照してください。
表 2-3 RPC ルーチン - 中間レベル
ルーチン |
説明 |
---|---|
指定したトランスポートに対するクライアントハンドルを作成する。 |
|
clnt_tp_create() に似ているが、許される最長時間を指定できる。 |
|
指定したトランスポートに対するサーバハンドルを作成する。 |
|
要求をサーバに送信するための手続きをクライアント側から呼び出す。 |
エキスパートレベルには、トランスポートに関連するパラメタを指定するさまざまなルーチンがあります。このルーチンの使用方法とコード例は、「エキスパートレベルのインタフェース」を参照してください。
表 2-4 RPC ルーチン - エキスパートレベル
ルーチン |
説明 |
---|---|
指定したトランスポートに対するクライアントハンドルを作成する。 |
|
指定したトランスポートに対するサーバハンドルを作成する。 |
|
rpcb_set() で作成したマップを削除する。 |
|
rpcbind デーモンを呼び出して、指定した RPC サービスのトランスポートアドレスを取り出す。 |
|
指定したプログラム番号とバージョン番号のペアを、指定したディスパッチルーチンに関連付ける。 |
|
svc_reg() で設定した関連付けを解除する。 |
|
要求をサーバに送信するための手続きをクライアント側から呼び出す。 |
ボトムレベルには、トランスポートを完全に制御することができるルーチンがあります。これらのルーチンについては、「ボトムレベルのインタフェース」を参照してください。
表 2-5 RPC ルーチン - ボトムレベル
ルーチン |
説明 |
---|---|
非接続型トランスポートを使用して、指定した遠隔プログラムに対する RPC クライアントハンドルを作成する。 |
|
非接続型トランスポートを使用して、RPC サーバハンドルを作成する。 |
|
接続型トランスポートを使用して、指定した遠隔プログラムに対する RPC クライアントハンドルを作成する。 |
|
接続型トランスポートを使用して、RPC サーバハンドルを作成する。 |
|
clnt_call() |
特定のトランスポートまたはトランスポートタイプで実行されるプログラムを書くことができます。あるいは、システムが選択するトランスポート、またはユーザが選択するトランスポート上で実行されるプログラムを書くこともできます。ネットワークの選択では、/etc/netconfig データベースと環境変数 NETPATH を使用します。これにより希望するトランスポートを指定したり、また可能であればアプリケーションがそれを使用できます。指定したトランスポートが不適当な場合、アプリケーションは自動的に適切な機能を持つ他のトランスポートを試みます。
/etc/netconfig には、ホストで使用できるトランスポートが記載されていて、タイプによって識別されます。NETPATH はオプションで、ユーザはこれを使用してトランスポートを指定したり、/etc/netconfig にあるリストからトランスポートを選択したりできます。NETPATH を設定するとユーザは、アプリケーションが利用できるトランスポートを試みる順序を指定できます。NETPATH を設定しないと、システムはデフォルトで /etc/netconfig に指定されているすべての選択可能なトランスポート (visible (可視) トランスポート) について、ファイルに現れる順番で選択を試みます。
ネットワーク選択についての詳細は、『Transport Interfaces Programming Guide』または getnetconfig(3N) と netconfig(4) のマニュアルページを参照してください。
RPC では、選択可能なトランスポートを次のタイプに分類します。
表 2-6 nettype パラメタ
値 |
説明 |
---|---|
netpath と同じ。 |
|
/etc/netconfig ファイルのエントリのうち、可視フラグ (v フラグ) の付いたトランスポートが使用される。 |
|
visible と同じ。ただし、接続型トランスポートに限定される。/etc/netconfig ファイルに記載された順に選択される。 |
|
visible と同じ。ただし、非接続型トランスポートに限定される。 |
|
接続型トランスポートが、NETPATH で設定された順に使用される。 |
|
非接続型トランスポートが、NETPATH で設定された順に使用される。 |
|
インターネット・ユーザデータグラム・プロトコル (UDP) が指定される。 |
|
インターネット・トランスミッション・コントロール・プロトコル (TCP) が指定される。 |
RPC サービスは、接続型と非接続型の両方のトランスポートでサポートされています。トランスポート選択は、アプリケーションの性質で決まります。
アプリケーションが次のすべてに当てはまる場合は、非接続型トランスポートの方が適当です。
手続きの呼び出しによってサーバ内部の状態や関連データが変更されない。
引数と戻り値のサイズがトランスポートのパケットサイズより小さい。
サーバは非常に多くのクライアントを扱う必要がある。非接続型サーバは各クライアントの状態データを保持する必要がないため、本質的に数多くのクライアントを処理することができる。これに対して、接続型サーバではオープンしているクライアント接続すべての状態データを保持するため、処理できるクライアント数はホストの資源によって制限される。
アプリケーションが次のどれかに当てはまる場合は、接続型トランスポートの方が適当です。
非接続型トランスポートと比較して、アプリケーションで接続の確立により多くの手間をかけることができる。
手続きの呼び出しによってサーバ内部の状態や関連データが変更される可能性がある。
引数または戻り値のサイズがデータグラムパケットの最大サイズより大きい。
各トランスポートには固有の変換ルーチンがあり、汎用ネットワークアドレス (トランスポートアドレスを文字列で表現したもの) とローカルアドレスとの相互変換を行います。RPC システム内 (例えば、rpcbind とクライアントの間) では、汎用アドレスが使用されます。各トランスポートには、名前からアドレスへの変換ルーチンの入った実行時リンクライブラリがあります。表 2-7 に、主な変換ルーチンを示します。
上の各ルーチンについての詳細は、netdir(3N) のマニュアルページと『Transport Interfaces Programming Guide』を参照してください。どのルーチンの場合も、netconfig 構造体が名前からアドレスへの変換のコンテキストを提供していることに注意してください。
表 2-7 名前からアドレスへの変換ルーチン
netdir_getbyname() |
ホストとサービスのペア(例えば、server1、rpcbind) と netconfig 構造体から、netbuf アドレスのセットに変換する。netbuf は TLI 構造体で、実行時にトランスポート固有のアドレスが入る。 |
|
netbuf アドレスと netconfig 構造体から、ホストとサービスのペアに変換する。 |
uaddr2taddr() |
汎用アドレスと netconfig 構造体から、netbuf アドレスに変換する。 |
taddr2uaddr () |
netbuf アドレスと netconfig 構造体から、汎用アドレスに変換する。 |
トランスポートサービスにはアドレスルックアップサービスは含まれていません。トランスポートサービスはネットワーク上のメッセージ転送だけを行います。クライアントプログラムは、使用するサーバプログラムのアドレスを知る必要があります。旧バージョンの SunOS では、portmap デーモンがそのサービスを実行していました。現バージョンでは rpcbind デーモンを使用します。
RPC では、ネットワークアドレスの構造を考慮する必要がありません。RPC では、NULL で終わる ASCII 文字列で指定される汎用アドレスを使用するためです。RPC はトランスポート固有の変換ルーチンを使用して、汎用アドレスをローカルトランスポートアドレスに変換します。変換ルーチンについての詳細は、netdir(3N) と rpcbind(3N) のマニュアルページを参照してください。
rpcbind の機能を次に示します。
登録リスト全体を取り出す。
クライアントのための遠隔呼び出しを実行する。
時刻を返す。
rpcbind が RPC サービスをアドレスにマップするので、rpcbind 自身のアドレスはその利用者に知られていなければなりません。全トランスポートの名前からアドレスへの変換ルーチンが、トランスポートが使用する各タイプのためのアドレスを保有している必要があります。たとえば、インターネットドメインでは、TCPでも UDP でも rpcbind のポート番号は 111 です。rpcbind は、起動されるとホストがサポートしている全トランスポートに自分のアドレスを登録します。RPC サービスのうち rpcbind だけは、前もってアドレスが知られていなければなりません。
rpcbind は、ホストがサポートしている全トランスポートに RPC サービスのアドレスを登録して、クライアントがそれらを使用できるようにします。各サービスは、rpcbind デーモンでアドレスを登録し、クライアントから利用できるようになります。そこで、サービスのアドレスが rpcinfo(1M) と rpcbind(3N) のマニュアルページで指定されているライブラリルーチンを使用するプログラムとで利用可能になります。クライアントやサーバからは RPC サービスのネットワークアドレスを知ることはできません。
クライアントプログラムとサーバプログラム、および、クライアントホストとサーバホストとは通常別のものですが、同じであってもかまいません。サーバプログラムもまたクライアントプログラムになることができます。あるサーバが別の rpcbind サーバを呼び出す場合は、クライアントとして呼び出したことになります。
クライアントが遠隔プログラムのアドレスを調べるには、ホストの rpcbind デーモンに RPC メッセージを送信します。サービスがホスト上にあれば、デーモンは RPC 応答メッセージにアドレスを入れて返します。そこで、クライアントプログラムは RPC メッセージをサーバのアドレスに送ることができます。(クライアントプログラムから rpcbind を頻繁に呼び出さなくて済むように、最後に呼び出した遠隔プログラムのネットワークアドレスを保存しておきます)。
rpcbind の RPCBPROC_CALLIT 手続きを使用すると、クライアントはサーバのアドレスがわからなくても遠隔手続きを呼び出すことができます。クライアントは目的の手続きのプログラム番号、バージョン番号、手続き番号、引数を RPC 呼び出しメッセージで引き渡します。rpcbind は、アドレスマップから目的の手続きのアドレスを探し出し、RPC 呼び出しメッセージにクライアントから受け取った引数を入れて、その手続きに送信します。
目的の手続きから結果が返されると、RPCBPROC_CALLIT はクライアントプログラムにその結果を引き渡します。そのとき、目的の手続きの汎用アドレスも同時に渡されますので、次からはクライアントが直接その手続きを呼び出すことができます。
RPC ライブラリは rpcbind の全手続きのインタフェースを提供します。RPC ライブラリの手続きには、クライアントとサーバのプログラムのために rpcbind を自動的に呼び出すものもあります。詳細については、付録 B 「RPC プロトコルおよび言語の仕様」を参照してください。
rpcinfo は、rpcbind で登録した RPC の最新情報を取り出すユーティリティです。rpcbind または portmap ユーティリティと共に rpcinfo を使用して、あるホストに登録されたすべての RPC サービスの汎用アドレスとトランスポートを知ることができます。指定したホスト上で指定したプログラムの特定バージョンを呼び出して、応答が返ったかどうかを調べることができます。詳細については、rpcinfo(1M) のマニュアルページを参照してください。
この章では、rpcgen ツールについて紹介します。コード例および使用可能なコンパイル時のフラグの使用方法を記載したチュートリアルです。この章で使用する用語の定義については、用語集を参照してください。
rpcgen ツールは、RPC 言語で書かれたソースコードをコンパイルして、遠隔プログラムインタフェースモジュールを生成します。RPC 言語は構文も構造も C 言語に似ています。rpcgen は C 言語ソースモジュールを生成しますので、次に C コンパイラでコンパイルします。
オプションを指定すれば、rpcgen で次のことを行うことができます。
さまざまなトランスポートの選択
サーバのタイムアウトの指定
main プログラム以外のサーバ側スタブプログラムの生成
C 形式で引数を受け渡す ANSI C 準拠のコードの生成
権限をチェックしてサービスルーチンを呼び出す RPC ディスパッチ
rpcgen を使用すると、下位レベルのルーチンを作成する手間が省けるのでアプリケーション開発時間を大幅に短縮できます。rpcgen の出力コードとユーザ作成コードとは、簡単にリンクできます。(rpcgen を使用しないでRPC プログラムを作成する方法については、第 4 章「RPC プログラマインタフェース」を参照してください)。
この節では、SunOS 4.x では提供されなかった機能で、SunOS 5.x rpcgen コード生成プログラムで追加されたものについて説明します。
rpcgen では、クライアント側、サーバ側、および makefile の各テンプレートを生成することができます。オプションのリストについては、「クライアント側とサーバ側のテンプレート」を参照してください。
rpcgen には、C 形式モードとデフォルトモードという 2 つのコンパイルモードがあります。C 形式モードでは、引数は構造体へのポインタではなく値で渡されます。また、C 形式モードでは複数の引数を渡すこともできます。デフォルトモードは旧バージョンと同じです。両方のモードのコード例については、「C 形式モード」を参照してください。
現バージョンでは、マルチスレッド環境で実行可能なマルチスレッド対応コードを生成することができるようになりました。デフォルトでは、rpcgen によって生成されたコードはマルチスレッド対応ではありません。詳細およびコード例については、「マルチスレッド対応のコード」を参照してください。
rpcgen では、マルチスレッド自動モードで実行するマルチスレッド対応サーバスタブを生成します。定義およびコーディング例については、「自動マルチスレッド対応モード」を参照ください。
rpcgenでは、TS-RPC ライブラリか TI-RPC ライブラリのどちらかを使用してコードを生成します。「TI-RPC または TS-RPC のライブラリ選択」を参照してください。
rpcgen では、ANSI C に準拠したコードを生成します。また、ANSI C 準拠のコードは、SPARCompilerTM C++ 3.0 環境で使用することができます。「ANSI C に準拠したコードの生成」を参照してください。
rpcgen を使用すると、分散型アプリケーションを簡単に作成できます。サーバ側手続きは、手続き呼び出し規約に準拠した言語で記述します。サーバ側手続きは、rpcgen によって生成されたサーバスタブとリンクして、実行可能なサーバプログラムを形成します。クライアント側手続きも同様に記述およびリンクします。
この節では、rpcgen を使用した基本的なプログラミング例を示します。また、rpcgen(1) のマニュアルページを参照してください。
単一のコンピュータ環境で実行されるアプリケーションを、ネットワーク上で実行する分散型アプリケーションに変更する場合を考えます。次の例で、システムコンソールにメッセージを表示するプログラムを分散型アプリケーションに変換する方法を、ステップ別に説明します。変換前のプログラム例 3-1を次に示します。
/* 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 という遠隔手続きに変更したものを示します。
/* * 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 つの点で異なることに注意してください。
引数が文字へのポインタではなく、文字配列へのポインタになっています。-N オプションを使用しない遠隔手続きの場合は、引数自体が渡されるのではなく、常に引数へのポインタが渡されるからです。-N オプションを指定しなければ、遠隔手続きの呼び出しで引数が 1 つしか渡されません。複数の引数が必要な場合は、引数を struct
型にして渡す必要があります。
引数が 2 つあります。第 2 引数には、関数呼び出しのときのコンテキスト、すなわち、プログラム、バージョン、手続きの番号、raw および canonical の認証、SVCXPRT 構造体へのポインタが入っています。(SVCXPRT 構造体にはトランスポート情報が入っています)。呼び出された手続きが要求されたサービスを実行するときに、これらの情報が必要になる場合があります。
戻り値は、整数そのものではなく整数へのポインタになっています。-N オプションを指定しない遠隔手続きの場合は、戻り値自体ではなく戻り値へのポインタが返されるためです。-M (マルチスレッド)オプション または -A (自動モード) オプションが使用されている限り、戻り値は static
で宣言します。戻り値を遠隔手続きのローカル値にしてしまうと、遠隔手続きがリターンした後、サーバ側スタブプログラムからその値を参照することができなくなります。 -M および -A を使用している場合は、戻り値へのポインタは第 3 引数として手続きに渡されるため、戻り値手続きで宣言されません。
手続き名に _1 が追加されています。一般に rpcgen が遠隔手続き呼び出しを生成するときは、次のように手続き名が決められます。プログラム定義で指定した手続き名 (この場合は PRINTMESSAGE) はすべて小文字に変換され、下線 (_) とバージョン番号 (この場合は 1) が追加されます。このように手続き名が決定されるので、同じ手続きの複数バージョンが使用可能になります。
例 3-3 には、この遠隔手続きを呼び出すクライアント側メインプログラムを示します。
/* * 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 では、次の点に注意してください。
最初に、RPC ライブラリルーチン clnt_create() を呼び出してクライアントハンドルを作成しています。クライアントハンドルは、遠隔手続きを呼び出すスタブルーチンに引き渡されます。(これ以外にもクライアントハンドルを作成する方法があります。詳細については、第 4 章「RPC プログラマインタフェース」を参照してください)。クライアントハンドルを使用する遠隔手続き呼び出しがすべて終了したら、clnt_destroy() を使用してそのクライアントハンドルを破棄し、システム資源を無駄に使用しないようにします。
clnt_create() の最後の引数に "visible" を指定して、/etc/netconfig で visible と指定したすべてのトランスポートを使用できるようにします。詳細については、/etc/netconfig ファイルと『Transport Interfaces Programming Guide』を参照してください。
遠隔手続き printmessage_1() の呼び出しは、第 2 引数として挿入されたクライアントハンドルを除いて、msg_proc.c で宣言された通りに実行されています。戻り値も値ではなく、値へのポインタで返されています。
遠隔手続き呼び出しのエラーには、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 から何を生成するかを説明します。
msg.h というヘッダファイルを作成します。msg.h には、他のモジュールで使用できるように MESSAGEPROG、MESSAGEVERS、PRINTMESSAGE の #define 文が入っています。このヘッダファイルは、クライアント側とサーバ側の両方のモジュールでインクルードする必要があります。
クライアント側スタブルーチンを msg_clnt.c というファイルに出力します。このファイルには、クライアントプログラム rprintmsg から呼び出されるルーチン printmessage_1() が 1 つだけ入っています。rpcgen への入力ファイルが FOO.x という名前ならば、クライアント側スタブルーチンは FOO_clnt.c というファイルに出力されます。
msg_svc.c の printmessage_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 ルーチンの両方を生成します。
/* * 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 言語のキーワード struct
、union
、enum
を使用して型を再定義することができます。使用したキーワードは、後にその型の変数を宣言するときには指定しません。たとえば、共用体 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 手続きを示します。
/* * 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 手続きを示します。
/* * 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); }
この前のサンプルプログラムと同様に、システム名を local と remote とします。ファイルのコンパイルと実行は、次のコマンドで行います。
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 の前処理命令
シンボル |
使用目的 |
---|---|
ヘッダファイルの出力 |
|
XDR ルーチンの出力 |
|
サーバ側スタブプログラムの出力 |
|
クライアント側スタブプログラムの出力 |
|
インデックステーブルの出力 |
例 3-7 に、簡単な rpcgen の例を示します。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
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
この節では、コンパイル時に使用可能な rpcgen オプションについて説明します。次の表に、この節で説明するオプションを要約します。
表 3-2 rpcgen コンパイル時に指定するフラグ
オプション |
フラグ |
コメント |
---|---|---|
-a, -Sc, -Ss, -Sm |
表 3-3 を参照 |
|
-N |
新しい形式のモードを呼び出す |
|
-C |
-N オプションとともに使用 |
|
-M |
マルチスレッド環境で使用 |
|
-A |
このオプションを指定すると、-M も自動的に指定される |
|
-b |
デフォルトは TI-RPC ライブラリ |
|
-i |
rpcgen で次のフラグを指定して、クライアント側とサーバ側のテンプレートを生成することができます。
表 3-3 rpcgen テンプレート選択フラグ
フラグ |
機能 |
---|---|
すべてのテンプレートを生成 |
|
クライアント側のテンプレートを生成 |
|
サーバ側のテンプレートを生成 |
|
makefile のテンプレートを生成 |
生成されたテンプレートファイルを参考にしてプログラムを書くか、テンプレートに抜けている部分を直接書き込んで使用します。rpcgen は、スタブプログラムのほかにこれらのテンプレートファイルを生成します。
ソースプログラム add.x から C 形式モードでサーバ側テンプレートを生成するときは、次のコマンドを実行します。
rpcgen -N -Ss -o add_server_template.c add.x
生成されたテンプレートファイルは add_server_template.c という名前になります。同じソースプログラム add.x から C 形式モードでクライアント側テンプレートを生成するときは、次のコマンド行を実行します。
rpcgen -N -Sc -o add_client_template.c add.x
生成されたテンプレートファイルは add_client_template.c という名前になります。同じソースプログラム add.x から makefile テンプレートを生成するときは、次のコマンド行を実行します。
rpcgen -N -Sm -o mkfile_template add.x
生成されたテンプレートファイルは mkfile_template という名前になります。このファイルを使用して、サーバ側とクライアント側のプログラムをコンパイルできます。次のように、-a フラグを指定した場合は、
rpcgen -N -a add.x
3 つのテンプレートファイルがすべて生成されます。クライアント側テンプレートは add_client.c、サーバ側テンプレートは add_server.c、makefile テンプレートは makefile.a という名前になります。このうち1 つでも同名のファイルが存在していれば、rpcgen はエラーメッセージを表示して終了します。
テンプレートファイルを生成する際には、次に rpcgen が実行された時に上書きされないように新しい名前を付けてください。
-N フラグを指定して rpcgen を起動すると、C 形式モード (Newstyle モードとも呼ばれる)で処理が行われます。このモードでは、引数は値で渡され、複数の引数も構造体にしないで渡すことができます。この機能を使用すると、RPC コードを、C 言語やその他の高級言語に近い形式で書くことができます。既存のプログラムや makefile との互換性を保つため、従来モード (標準モード) の方がデフォルトになっています。次の例では、-N フラグにより利用できる機能を示します。従来モードと C 形式モードの両方のソースモジュールを、例 3-8 と 例 3-9に示します。
/* * このプログラムには、2 つの数値を加える手続きが入っています。 * ここでは、C 形式モードによる引数の引き渡し方法を示します。 * 関数 add() が 2 つの引数を取ることに注意してください。 */ program ADDPROG { /* プログラム番号 */ version ADDVER { /* バージョン番号 */ int add(int, int) = 1; /* 手続き */ } = 1; } = 0x20000199;
/* * このプログラムには、2 つの数値を加える手続きが入っています。 * ここでは、デフォルトモードによる引数の引き渡し方法を示します。 * デフォルトモードの場合、rpcgen は引数を 1 つしか処理しないことに * 注意してください。 */ struct add_arg { int first; int second; }; program ADDPROG { /* プログラム番号 */ version ADDVER { /* バージョン番号 */ int add (add_arg) = 1; /* 手続き */ } = 1; } = 0x20000199;
例 3-10 から例 3-13 には、生成されるクライアント側テンプレートを示します。
/* * C 形式のクライアント側メインルーチン。 * 遠隔 RPC サーバ上の関数 add() を呼び出します。 */ #include <stdio.h> #include "add.h" main(argc, argv) int argc; char *argv[]; { CLIENT *clnt; int *result,x,y; if(argc != 4) { printf("usage: %s host num1 num2¥n" argv[0]); exit(1); } /* * クライアントハンドルの作成 - サーバに結合 */ clnt = clnt_create(argv[1], ADDPROG, ADDVER, "udp"); if (clnt == NULL) { clnt_pcreateerror(argv[1]); exit(1); } x = atoi(argv[2]); y = atoi(argv[3]); /* 遠隔手続きの呼び出し: add_1() には、ポインタではなく、 * 複数の引数が渡されていることに注意してください。 */ result = add_1(x, y, clnt); if (result == (int *) NULL) { clnt_perror(clnt, "call failed:"); exit(1); } else { printf("Success: %d + %d = %d¥n", x, y, *result); } exit(0); }
例 3-11 に、デフォルトモードと C 形式モードとのコードの相違点を示します。
arg.first = atoi(argv[2]); arg.second = atoi(argv[3]); /* * 遠隔手続きの呼び出し -- クライアント側スタブプログラムには、 * 引数へのポインタを渡さなければならないことに注意してください。 */ result = add_1(&arg, clnt);
例 3-12 に、C 形式モードのサーバ側手続きを示します。
#include "add.h" int * add_1(arg1, arg2, rqstp) int arg1; int arg2; struct svc_req *rqstp; { static int result; result = arg1 + arg2; return(&result); }
例 3-13 に、デフォルトモードのサーバ側手続きを示します。
#include "add.h" int * add_1(argp, rqstp) add_arg *argp; struct svc_req *rqstp; { static int result; result = argp->first + argp->second; return(&result); }
デフォルトでは、rpcgen で生成されるコードはマルチスレッド対応になりません。グローバル変数は保護されず、戻り値も静的変数で返されます。マルチスレッド環境で実行できるマルチスレッド対応コードを生成するには、-M フラグを指定します。-M フラグは、-N か -C のどちらか (または両方) のフラグと共に指定します。
この機能を使用したマルチスレッド対応プログラムの例を示します。例 3-14 に rpcgen のプロトコルファイル msg.x を示します。
program MESSAGEPROG { version PRINTMESSAGE { int PRINTMESSAGE(string) = 1; } = 1; } = 0x4001;
文字列が遠隔手続きに渡され、遠隔手続きでは文字列を表示してから文字数をクライアントに返します。マルチスレッド対応のテンプレートを生成するには、次のコマンドを実行します。
% rpcgen -M msg.x
例 3-15 に、クライアント側のコードを示します。
#include "msg.h" void messageprog_1(host) char *host; { CLIENT *clnt; enum clnt_stat retval_1; int result_1; char * printmessage_1_arg; clnt = clnt_create(host, MESSAGEPROG, PRINTMESSAGE, "netpath"); if (clnt == (CLIENT *) NULL) { clnt_pcreateerror(host); exit(1); } printmessage_1_arg = (char *) malloc(256); strcpy(printmessage_1_arg, "Hello World"); retval_1 = printmessage_1(&printmessage_1_arg, &result_1,clnt); if (retval_1 != RPC_SUCCESS) { clnt_perror(clnt, "call failed"); } printf("result = %d¥n", result_1); clnt_destroy(clnt); } main(argc, argv) int argc; char *argv[]; { char *host; if (argc < 2) { printf("usage: %s server_host¥n", argv[0]); exit(1); } host = argv[1]; messageprog_1(host); }
ここで、rpcgen が生成したコードには、引数も戻り値もポインタで渡さなければならないことに注意してください。これはプログラムを再入可能にするために必要です。スタブ関数の戻り値は、遠隔手続きの呼び出しが正常終了したかエラーが起こったかを示します。正常終了した場合は、RPC_SUCCESS が返されます。例 3-16 に示すマルチスレッド対応のクライアント側スタブプログラム (-M で生成) とマルチスレッド対応でないクライアント側スタブプログラムを比較してください。マルチスレッド未対応のクライアント側スタブプログラムは、静的変数を使用して戻り値を格納し、一度に 1 つしかスレッドを使用することができません。
int * printmessage_1(argp, clnt) char **argp; CLIENT *clnt; { static int clnt_res; memset((char *)&clnt_res, 0, sizeof (clnt_res)); if (clnt_call(clnt, PRINTMESSAGE, (xdrproc_t) xdr_wrapstring, (caddr_t) argp, (xdrproc_t) xdr_int, (caddr_t) &clnt_res, TIMEOUT) != RPC_SUCCESS) { return (NULL); } return (&clnt_res); }
例 3-17 に、サーバ側コードを示します。
マルチスレッド対応モードを使用するサーバプログラムをコンパイルする場合は、スレッドライブラリをリンクしなければなりません。そのためには、コンパイルコマンドに -lthread オプションを指定します。
#include "msg.h" #include <syslog.h> bool_t printmessage_1_svc(argp, result, rqstp) char **argp; int *result; struct svc_req *rqstp; { int retval; if (*argp == NULL) { syslog(LOG_INFO, "argp is NULL¥n"); *result = 0; } else { syslog("argp is %s¥n", *argp); *result = strlen (*argp); } retval = 1; return (retval); } int messageprog_1_freeresult(transp, xdr_result, result) SVCXPRT *transp; xdrproc_t xdr_result; caddr_t result; { /* * 必要に応じてメモリ解放のためのコードを挿入 */ (void) xdr_free(xdr_result, result); }
サーバ側のコードでは、静的変数を使用して戻り値を格納してはいけません。呼び出し側のルーチンから戻り値へのポインタが渡されますので、戻り値はそこに返します。正常終了の場合は 1 を返し、エラーが起こった場合は 0 を返します。
rpcgen が生成するコードには、手続きの呼び出しで割り当てたメモリを解放するルーチンの呼び出しも含まれています。メモリの不正使用を避けるため、サービスルーチンで割り当てたメモリはすべてそのルーチンで解放する必要があります。上の例では、messageprog_1_freeresult() でメモリの解放を行います。
通常は、xdr_free() を使用して割り当てたメモリを解放します。(上の例では、メモリ割り当てを行っていないので、メモリの解放は実行されません)。
-M フラグを -N と -C のフラグと共に指定する例として、例 3-18 の add.x を見てみます。
program ADDPROG { version ADDVER { int add(int, int) = 1; } = 1; }= 199;
このプログラムでは、2 つの数値を加えてその結果をクライアントに返します。次のコマンドで、このファイルに対して rpcgen を実行します。
% rpcgen -N -M -C add.x
このプログラムを呼び出す例 3-19は次のようになります。
/* * このクライアント側メインルーチンでは複数のスレッドを起動します。 * 各スレッドから同時にサーバルーチンを呼び出します。 */ #include "add.h" CLIENT *clnt; #define NUMCLIENTS 5 struct argrec { int arg1; int arg2; }; /* * 現在実行中のスレッド数をカウント */ int numrunning; mutex_t numrun_lock; cond_t condnum; void addprog(struct argrec *args) { enum clnt_stat retval; int result; /* サーバルーチンの呼び出し */ retval = add_1(args->arg1, args->arg2, &result, clnt); if (retval != RPC_SUCCESS) { clnt_perror(clnt, "call failed"); } else printf("thread #%x call succeeded, result = %d¥n", thr_getself(), result); /* * 実行中のスレッド数をデクリメント */ mutex_lock(&numrun_lock); numrunning--; cond_signal(&condnum); mutex_unlock(&numrun_lock); thr_exit(NULL); } main(int argc, char *argv[]) { char *host; struct argrec args[NUMCLIENTS]; int i; thread_t mt; int ret; if (argc < 2) { printf("usage: %s server_host¥n", argv[0]); exit(1); } host = argv[1]; clnt = clnt_create(host, ADDPROG, ADDVER, "netpath"); if (clnt == (CLIENT *) NULL) { clnt_pcreateerror(host); exit(1); }; mutex_init(&numrun_lock, USYNC_THREAD, NULL); cond_init(&condnum, USYNC_THREAD, NULL); numrunning = 0; /* 個々のスレッドの起動 */ for (i = 0; i < NUMCLIENTS; i++) { args[i].arg1 = i; args[i].arg2 = i + 1; ret = thr_create(NULL, NULL, addprog, (char *) &args[i], THR_NEW_LWP, &mt); if (ret == 0) numrunning++; } mutex_lock(&numrun_lock); /* 全スレッドの終了を待つ */ while (numrunning != 0) cond_wait(&condnum, &numrun_lock); mutex_unlock(&numrun_lock); clnt_destroy(clnt); }
サーバ側の手続きは例 3-20 のようになります。
マルチスレッド対応モードを使用するサーバ側プログラムをコンパイルする場合は、スレッドライブラリにリンクしなければなりません。そのためには、コンパイルコマンドに -lthread オプションを指定します
add_1_svc(int arg1, int arg2, int *result, struct svc_req *rqstp) { bool_t retval; /* 結果の計算 */ *result = arg1 + arg2; retval = 1; return (retval); } /* * サーバ手続きで割り当てたメモリを解放するルーチン */ int addprog_1_freeresult(SVCXPRT *transp, xdrproc_t xdr_result, caddr_t result) { (void) xdr_free(xdr_result, result); }
自動マルチスレッド対応モードにより、クライアントの要求を同時に処理するために Solaris スレッドが自動的に使用されます。-A オプションを指定して、RPC コードを自動マルチスレッド対応モードで生成します。また、-A を指定すると自動的に -M が指定されるため、-M を明示的に指定する必要はありません。生成されたコードはマルチスレッド対応でなければならないため、-M が (明示的ではなくても) 必要です。
マルチスレッド対応 RPC の詳細については 「マルチスレッド RPC プログラミング」、および 「マルチスレッド自動モード」 を参照してください。
次に、rpcgen によって生成される自動モードのプログラムの例を示します。例 3-21 は、rpcgen のプロトコルファイルである time.x のコードです。文字列は遠隔手続きに引き渡されます。遠隔手続きは、文字列を表示してクライアントの文字列長を返します。マルチスレッド対応スタブを生成するには、次のコマンドを実行します。
program TIMEPROG { version TIMEVERS { unsigned int TIMEGET(void) = 1; void TIMESET(unsigned) = 2; } = 1; } = 0x20000044;
% rpcgen -A time.x
-A オプションを使用すると、生成されたサーバ側のコードには、サーバの自動マルチスレッド対応モードを使用するための命令が含まれます。
マルチスレッド対応モードを使用するサーバ側プログラムをコンパイルする場合は、スレッドライブラリにリンクしなければなりません。そのためには、コンパイルコマンドに -lthread オプションを指定します。
旧バージョンの rpcgen では、ソケット関数を使用してスタブプログラムを作成していました。SunOS 5.4 では、トランスポート独立の RPC ルーチン (TI-RPC) か、特定のトランスポート固有のソケットルーチン (TS-RPC) のどちらを使用するか選択できます。この機能は、旧バージョンとの互換性を保つために提供されています。デフォルトでは TI-RPC ルーチンが使用されます。TS-RPC ルーチンを使用したソースコードを生成するには、rpcgenで -b フラグを指定します。
rpcgen では、ANSI C に準拠したコードを出力するか、SPARCompiler C++3.0 に準拠したコードを選択するか指定できます。ANSI C に準拠したコードを生成するには、-C フラグを指定します。ほとんどの場合、「C 形式モード」指定フラグ -N も同時に指定します。
add.x のサーバ側テンプレート例は、次のコマンドで生成できます。
rpcgen -N -C -Ss -o add_server_template.c add.x
ここで、C++ 3.0 で記述されたサーバ上では遠隔手続き名が接尾辞 _svc で終わっていなければならないことに特に注意してください。次の例では、add.x に対して、コンパイルフラグ -C を指定してクライアント側の add_1 とサーバ側の add_1_svc が生成されています。
/* * このファイルはテンプレートです。これを基にしてユーザ独自の関数を * 作成してください。 */ #include <c_varieties.h> #include "add.h" int * add_1_svc(int arg1, int arg2, struct svc_req *rqstp) { static int result; /* * ここにサーバプログラムのコードを挿入 */ return(&result); }
この出力ファイルは、構文も構造も ANSI C に準拠しています。-C フラグを指定して生成したヘッダファイルは、ANSI C でも SPARCompiler C++ でも使用できます。
rpcgen は、可能な限り xdr_inline() (xdr_admin(3) マニュアルページを参照) を使用して、より効率の良いコードを生成しようとします。構造体の中に xdr_inline() を使用できるような要素 (たとえば、integer
、long
、bool
) があれば、構造体のその部分は xdr_inline() を使用してパックされます。デフォルトでは、パックされる要素が 5 つ以上連続していれば、インラインコードが生成されます。-i フラグを使用してインラインコードを生成する個数を変更することができます。たとえば、次のコマンド
rpcgen -i 3 test.x
では、パックできる要素が 3 つ以上連続していれば、インラインコードが生成されます。次のコマンド
rpcgen -i 0 test.x
では、インラインコードの生成が禁止されます。
ほとんどの場合、-i フラグを指定する必要はありません。このフラグの対象となるのは _xdr.c スタブプログラムだけです。
この節では、RPC プログラミングと rpcgen の使用方法に関するさまざまなテクニックを示します。
rpcgen の省略可能な引数には、使用したいネットワークのタイプや特定のネットワーク識別子を指定するためのものがあります。(ネットワーク選択についての詳細は、『Transport Interfaces Programming Guide』を参照してください)
-s フラグを指定すると、指定したタイプのトランスポートからの要求に応答するサーバが作成されます。たとえば、次のコマンド
rpcgen -s datagram_n prot.x
を実行すると、NETPATH
環境変数で指定した非接続型トランスポートすべてに応答するサーバが標準出力に書き出されます。(NETPATH
環境変数が定義されていない場合は、/etc/netconfig で指定した非接続型トランスポートすべてに応答するサーバが標準出力に書き出されます)。コマンドラインでは、-s フラグとネットワークタイプのペアを複数指定できます。
同様に、-n フラグを指定すると、1 つのネットワーク識別子で指定したトランスポートからの要求だけに応答するサーバを作成することができます。
rpcgen で -n フラグを指定して作成したサーバを使用するときは注意が必要です。ネットワーク識別子は各ホストに固有なため、作成されたサーバは別のホストで予測通りに機能しないことがあります。
コマンド行で、C 言語のプリプロセッサシンボルを定義し、値を割り当てることができます。コマンド行の定義文は、たとえば、DEBUG シンボルが定義されているときの条件付きデバッグコードの生成に使用できます。
$ rpcgen -DDEBUG proto.x
手続きがブロードキャスト RPC を通して呼び出され、有効な応答を返せないときは、サーバはクライアントに応答しないでください。その方がネットワークが混雑しません。サーバが応答を返さないようにするには、遠隔手続きの戻り値を NULL にします。rpcgen が生成したサーバプログラムは、NULL を受け取った場合は応答しません。
例 3-23 に、NFS サーバの場合だけ応答する手続きを示します。
void * reply_if_nfsserver() { char notnull; /* *この場所のみで、そのアドレスが使用可能 */ if( access( "/etc/dfs/sharetab", F_OK ) < 0 ) { /* RPC の応答を禁止 */ return( (void *) NULL ); } /* * NULL 以外の値 notnull を指定したので、RPC は応答する */ return( (void *) ¬null ); }
RPC ライブラリルーチンが応答するには、手続きが NULL 以外のポインタ値を返す必要があります
例 3-23 で手続き reply_if_nfsserver() が NULL 以外の値を返すように定義されているならば、戻り値 (¬null) は静的変数を指していなければなりません。
inetd や listen のようなポートモニタは、特定の RPC サービスに対するネットワークアドレスを監視することができます。特定のサービスに対する要求が到着すると、ポートモニタは、サーバプロセスを生成します。サービスを提供したら、サーバは終了できます。この技法はシステム資源を節約するためのものです。rpcgen で生成するサーバ関数 main() は inetd で呼び出すことができます。その方法についての詳細は、「inetd の使用」を参照してください。
サーバプロセスがサービス要求に答えた後、続けて要求が来る場合に備えて一定時間待つことには意味があります。一定時間内に次の呼び出しが起こらなければ、サーバは終了し、inetd のようなポートモニタがサーバのための監視を続けます。サーバが終了しないうちに次の要求が来れば、ポートモニタは新たなプロセスを生成することなく待ち状態のサーバにその要求を送ります。
listen() などのポートモニタの場合は、サーバのための監視を行い、サービス要求が来れば必ず新たなプロセスを生成します。このようなモニタからサーバプロセスを起動する場合は、サーバプロセスはサービス提供後すぐに終了するようにしなければなりません。
rpcgen がデフォルトで生成したサービスは、サービス提供後 120 秒間待ってから終了します。待ち時間を変更するには、-K フラグを使用します。たとえば、次のコマンド
$ rpcgen -K 20 proto.x
では、サーバは 20 秒待ってから終了します。サービス提供後すぐに終了させるには、次のように待ち時間に対して 0 を指定します。
$ rpcgen -K 0 proto.x.
ずっと待ち状態を続けて終了しないようにするには、-K -1 と指定します。
クライアントプログラムはサーバに要求を送った後、デフォルトで 25 秒間応答を待ちます。応答待ちのタイムアウト値は、clnt_control() ルーチンを使用して変更できます。clnt_control() ルーチンの使用方法についての詳細は、「標準インタフェース」および rpc(3N) のマニュアルページを参照してください)。タイムアウト値を変更するときは、ネットワークを往復するのに必要な最低時間以上になるように注意します。例 3-24にclnt_control () の使用方法を示します。
struct timeval tv; CLIENT *clnt; clnt = clnt_create( "somehost", SOMEPROG, SOMEVERS, "visible" ); if (clnt == (CLIENT *)NULL) exit(1); tv.tv_sec = 60; /* * タイムアウト値を 60 秒に変更 */ tv.tv_usec = 0; clnt_control(clnt, CLSET_TIMEOUT, &tv);
クライアント作成ルーチンにはクライアント認証機能はありません。クライアントによっては、サーバに対して自分自身を証明する必要があります。
次の例では、セキュリティレベルが最低限のクライアント認証方法のうち、一般に使用できる方法を示します。上位レベルの DES 認証方法の詳細については、「認証」を参照してください。
CLIENT *clnt; clnt = clnt_create( "somehost", SOMEPROG, SOMEVERS, "visible" ); if (clnt != (CLIENT *)NULL) { /* AUTH_SYS 形式の認証情報を設定 */ clnt->cl_auth = authsys_createdefault(); }
一定のセキュリティレベルを保持しなければならないサーバではクライアント認証情報が必要になります。クライアント認証情報は、第 2 引数でサーバに渡されます。
例 3-26 に、クライアント認証情報をチェックするサーバプログラムを示します。これは、「rpcgen チュートリアル」で説明した printmessage_1() を修正したもので、スーパーユーザにだけコンソールへのメッセージの表示を許可します。
int * printmessage_1(msg, req) char **msg; struct svc_req *req; { static int result; /* 必ず static で宣言 */ FILE *f; struct authsys_parms *aup; aup = (struct authsys_parms *)req->rq_clntcred; if (aup->aup_uid != 0) { result = 0; return (&result) } /* 元のコードと同じ */ }
RPC パッケージで使用するディスパッチテーブルにプログラムからアクセスしたい場合があります。たとえば、サーバディスパッチルーチンで権限を調べてからサービスルーチンを呼び出したり、クライアントライブラリで記憶管理や XDR データ変換の詳細を扱う場合です。
-T オプションを指定して rpcgen を起動すると、プロトコル記述ファイル proto.x で定義した各プログラムごとの RPC ディスパッチテーブルがファイル proto_tbl.i に出力されます。接尾辞.i は index を表します。-t オプションを指定して rpcgen を起動した場合は、ヘッダファイルだけが生成されます。rpcgen を起動するときは、C 形式モード (-N オプション) と同時に -T または -t フラグを指定することはできません。
ディスパッチテーブルの各エントリは struct
rpcgen_tableで、この構造体はヘッダファイル proto.h で次のように定義されています。
struct rpcgen_table { char *(*proc)(); xdrproc_t xdr_arg; unsigned len_arg; xdrproc_t xdr_res; xdrproc_t len_res };
ここで
proc サービスルーチンへのポインタ xdr_arg 入力 (引数) の xdrルーチンへのポインタ len_arg 入力引数の長さ (バイト数) xdr_res 出力 (結果) の xdrルーチンへのポインタ len_res 出力結果の長さ (バイト数)
サンプルプログラム dir.x のディスパッチテーブル dirprog_1_table は、手続き番号がインデックスになっています。変数 dirprog_1_table には、テーブル内のエントリ数が入っています。
ディスパッチテーブルから手続きを探し出すルーチン find_proc() を次に示します。
struct rpcgen_table * find_proc(proc) u_long proc; { if (proc >= dirprog_1_nproc) /* エラー */ else return (&dirprog_1_table[proc]); }
ディスパッチテーブル内の各エントリは対応するサービスルーチンへのポインタです。ところが、サービスルーチンは一般にクライアント側のコードでは定義されていません。未解決の外部参照を起こさないため、また、ディスパッチテーブルに対するソースファイルを 1 つだけ要求にするために RPCGEN_ACTION(proc_ver) で、rpcgen サービスルーチン の初期化を行います。
これを使用して、クライアント側とサーバ側に同一のディスパッチテーブルを持たせることができます。クライアント側プログラムをコンパイルするときは、次の define 文を使用します。
#define RPCGEN_ACTION(routine) 0
サーバ側プログラムを作成するときは、次の define 文を使用します。
#define RPCGEN_ACTION(routine)routine
作成したアプリケーションのテストとデバッグは、簡単に実行できます。最初は、クライアント側とサーバ側の手続きをリンクして全体をシングルプロセスとしてテストします。(最初は、各手続きをそれぞれクライアント側とサーバ側のスケルトンとはリンクしません)。クライアントを作成するRPC ライブラリルーチン (rpc_clnt_create(3N) のマニュアルページを参照) と認証ルーチンの呼び出し部分はコメントにします。この段階では、libnsl をリンクしてはいけません。
これまでに説明したサンプルプログラムの手続きを、次のコマンドでリンクします。
cc rls.c dir_clnt.c dir_proc.c -o rls
RPC と XDR の関数をコメントにすると、手続き呼び出しは通常のローカル関数呼び出しとなり、プログラムは dbxtool のようなローカルデバッガでデバッグ可能になります。プログラムが正しく機能することが確認されたら、クライアント側プログラムを rpcgen が生成したクライアント側のスケルトンとリンクし、サーバ側プログラムを rpcgen が生成したクライアント側のスケルトンとリンクします。
また、Raw PRC モードを使用して XDR ルーチンをテストすることもできます。その方法についての詳細は、「下位レベルの Raw RPC を使用したプログラムテスト」を参照してください。
RPC 呼び出しで発生するエラーには 2 種類あります。1 つは、遠隔手続き呼び出し過程で起こるエラーです。これには、(1) 手続きが実行できない、(2)遠隔サーバが応答しない、(3) 遠隔サーバが引数を復号化できない、などがあります。例 3-26 で考えると、result が NULL の場合は RPC エラーです。エラーの原因を調べるには、clnt_perror() を使用してエラー原因を表示するか、clnt_sperror() を使用してエラー文字列を取り出します。
もう 1 つのエラーは、サーバ自体のエラーです。例 3-26 で考えると、opendir() からエラーが返された場合です。このようなエラーの処理はアプリケーションによって異なるため、プログラマの責任で対応します。
-C オプションを指定した場合はサーバ側ルーチンに _svc という接尾辞が付くため、上の説明がそのまま当てはまらないことに注意してください。
この章では、RPC との C インタフェースについて取り上げ、RPC を使用してネットワークアプリケーションを書く方法を説明します。RPC ライブラリにおけるルーチンの完全な仕様については、rpc(3N) のマニュアルページおよび関連するマニュアルページを参照してください。
この章で説明するクライアントおよびサーバインタフェースは、特に注意書きがある場合 (raw モードなど) 以外は、マルチスレッド対応です。すなわち、RPC 関数を呼び出すアプリケーションはマルチスレッド環境で自由に実行することができます。
単純インタフェースでは、その他の RPC ルーチンは不要なため最も簡単に使用できるレベルです。しかし、利用できる通信メカニズムの制御は制限されます。このレベルでのプログラム開発は、早く行うことができ、rpcgen コンパイラによって直接サポートされます。大部分のアプリケーションに対しては、rpcgen が提供する機能で十分でしょう。
RPC サービスの中には C の関数としては提供されていないものがありますが、それも RPC プログラムとして使用できます。単純インタフェースライブラリルーチンは、詳細な制御を必要としないプログラムでは RPC 機能を直接使用できます。rusers() のようなルーチンは、RPC サービスライブラリ librpcsvc にあります。例 4-1は、RPC ライブラリルーチン rusers() を呼び出して、遠隔ホスト上のユーザ数を表示します。
#include <rpc/rpc.h> #include <rpcsvc/rusers.h> #include <stdio.h> /* * a program that calls the * rusers() service */ 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: rusers¥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 /* サーバホストの名前 */ u_long prognum /* サーバプログラム番号 */ u_long versnum /* サーババージョン番号 */ xdrproc_t inproc /* 引数を符号化する XDR フィルタ */ char *in /* 引数へのポインタ */ xdr_proc_t outproc /* 結果を復号化するフィルタ */ char *out /* 結果を格納するアドレス */ char *nettype /* トランスポートの選択 */ );
この関数は、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 を単純インタフェースを使用するために変更すると、例 4-2 のようになります。
#include <stdio.h> #include <utmp.h> #include <rpc/rpc.h> #include <rpcsvc/rusers.h> /* *RUSERSPROG RPC プログラムを呼び出すプログラム */ main(argc, argv) int argc; char **argv; { unsigned long 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_long, (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
long
型であるため、rpc_call() の最初の戻りパラメタは xdr_u_long (unsigned
long
用) で、2 番目は &nusers (unsigned
long
型の値があるメモリへのポインタ)です。RUSERSPROC_NUM には引数がないため、rpc_call() の XDR 符号化関数は xdr_void() で、その引数は NULL です。
単純インタフェースを使用するサーバプログラムは、大変理解しやすいものです。これは単に、呼び出される手続きを登録するため rpc_reg() を呼び出し、次に、RPC ライブラリの遠隔手続きディスパッチャである svc_run() を呼び出して、入ってくる要求を待ちます。
rpc_reg() には次の引数があります。
rpc_reg ( u_long prognum /* サーバプログラム番号r */ u_long versnum /* サーババージョン番号 */ u_long procnum /* サーバ手続き番号 */ char *procname /* 遠隔関数の名前 */ xdrproc_t inproc /* 引数を符号化するフィルタ */ xdrproc_t outproc /* 結果を復号化するフィルタ */ char *nettype /* トランスポートの選択 */ );
svc_run() は RPC 呼び出しメッセージに応えてサービス手続きを起動します。rpc_reg() のディスパッチャは遠隔手続きが登録されたときに指定された XDR フィルタを使用して、遠隔手続きの引数の復号化と、結果の符号化を行います。サーバプログラムについての注意点をいくつか挙げます。
ほとんどの RPC アプリケーションが、関数名の後に _1 を付ける命名規則に従っています。手続き名に _n 番号を付けることにより、サービスのバージョン番号 n を表します。
引数と結果はアドレスで渡されます。遠隔で呼び出される関数はすべてこうなります。関数の結果として NULL を渡すと、クライアントには応答が送信されません。送信する応答がないと仮定されます。
結果は固定のデータ領域に存在します。これは、その値が実際の手続きが終了したあとにアクセスされるからです。RPC 応答メッセージを作成する RPC ライブラリ関数は結果にアクセスして、その値をクライアントに戻します。
引数は 1 つだけ使用できます。データに複数の要素がある場合、構造体の中に入れると、1 つの引数として渡すことができます。
手続きは、指定するタイプのトランスポートごとに登録されます。タイプのパラメタが (char *)NULL
の場合、手続きは NETPATH により指定されるすべてのトランスポートに登録されます。
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_long, "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_long() のような XDR プリミティブを指定することもできますし、引数として渡された構造体全体を処理するようなユーザ作成の変換ルーチンを指定することもできます。引数の変換ルーチンは 2 つの引数を取ります。1 つは変換結果へのポインタで、もう 1 つは XDR ハンドルへのポインタです。
表 4-1 XDR プリミティブタイプのルーチン
XDR プリミティブ・ルーチン |
|||
---|---|---|---|
xdr_wrapstring() から呼び出す xdr_string() はプリミティブではなく、3 つ以上の引数を取ります。
ユーザが作成する変換ルーチンの例を次に示します。手続きに渡す引数は次の構造体に入れます。
struct simple { int a; short b; } simple;
この構造体で渡された引数を変換する XDR ルーチン xdr_simple() は、例 4-3に示すようになります。
#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 プロトコル仕様」を参照してください。
表 4-2 XDR ブロック構築ルーティン基本のルーチン | ||
---|---|---|
xdr_array() |
xdr_bytes() |
xdr_reference() |
xdr_vector() |
xdr_union() |
xdr_pointer() |
xdr_string() |
xdr_opaque() |
たとえば、可変長の整数配列を送るときは、配列へのポインタと配列サイズを次のような構造体にパックします。
struct varintarr { int *data; int arrlnth; } arr;
この配列を変換するルーチン xdr_varintarr() は 例 4-4に示すようになります。
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 ルーチンへのポインタです。配列サイズが前もってわかっている場合は、例 4-5のように 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() ルーチンのようなものです。文字列をシリアライズするときは strlen() で長さを取り出し、デシリアライズするときは NULL で終わる文字列を生成します。
例 4-6 では、組み込み関数 xdr_string() と xdr_reference() を呼び出して、文字列へのポインタと、前の例で示した構造体 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
に対する知識が必要です。
表 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() |
デバッグツールFailed Cross Reference Format として、ネットワーク機能をすべてバイパスする 2 つの擬似 RPC インタフェースがあります。ルーチン clnt_raw_create() と svc_raw_create() は、実際のトランスポートを使用しません。
製品システムで RAW モードは使用しないでください。RAW モードは、デバッグを行い易くするために使用します。RAW モードはマルチスレッド対応ではありません。
例 4-19 は、次の 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"); }
Failed Cross Reference Format の次の点に注意してください。
サーバはクライアントより先に作成しなければなりません。
svc_raw_create() には引数がありません。
サーバは rpcbind デーモンに登録されません。svc_reg() の最後の引数は (struct
netconfig *) NULL ですので、rpcbind デーモンに登録されないことがわかります。
svc_run() が呼び出されません。
RPC 呼び出しはすべて同一の制御スレッド内で行われます。
この節では、RPC の下位レベルインタフェースを使用するさまざまな開発テクニックを説明します。この章で説明する項目を次に示します。
サーバ上の poll() - サーバ上で svc_run() が呼び出せない場合に、サーバから直接ディスパッチャを呼び出す方法
ブロードキャスト RPC - ブロードキャストの使用方法
バッチ処理 - 一連の呼び出しをバッチ処理にして、パフォーマンスを向上する方法
認証 - 今回のリリースで使用される認証方法
ポートモニタの使用 - ポートモニタ inetd と listener のインタフェース
サーバのバージョン - 複数のプログラムバージョンのサービス方法
この節で説明する内容は、(デフォルトで) シングルスレッドのモードで RPC を実行する場合にだけ適用されます。
RPC 要求をサービスしたり、その他のアクティビティを実行したりするプロセスでは、svc_run() を呼び出せない場合があります。他のアクティビティで定期的にデータ構造を更新する場合は、プロセスから svc_run() を呼び出す前に SIGALRM 信号をセットできます。そうすると、シグナルハンドラがデータ構造を処理してから svc_run() に制御を戻します。
プロセスから svc_run() をバイパスして直接ディスパッチャにアクセスするには、svc_getreqset() を呼び出します。待っているプログラムに結合したトランスポート端点のファイル記述子がわかれば、プロセスは自分で poll() を呼び出して、RPC ファイル記述子と自身の記述子の両方で要求を待つことができます。
例 4-20 には svc_run() を示します。svc_pollset
は、__rpc_select_to_poll() の呼び出しを通して svc_fdset() から派生した pollfd 構造体の配列です。この配列は、RPC ライブラリルーチンのどれかが呼び出されるたびに変わる可能性があります。そのたびに記述子がオープンされ、クローズされるからです。poll() がいくつかの RPC ファイル記述子への RPC 要求の到着を確認すると、svc_getreq_poll() が呼び出されます。
関数 __rpc_dtbsize() と __rpc_select_to_poll() は、SVID の一部ではありませんが、libnsl ライブラリで使用できます。Solaris 以外でも実行できるように、これらの関数を作成する方のために、関数の仕様を説明します。
int __rpc_select_to_poll(int fdmax, fd_set *fdset, struct pollfd *pollset)
ビットフラグとして fd_set ポインタとチェックすべきビット数が指定されます。この関数内で、指定された pollfd 配列を RPC が使用するために初期化するようにします。RPC は、入力イベントだけをポーリングします。初期化された pollfd スロット数が返されます。
int __rpc_dtbsize()
この関数は、getrlimit() 関数を呼び出し、新しく作成された記述子にシステムが割り当てる最大値を決定します。結果は、効率化のためにキャッシュされます。
この節のSVIDルーチンについての詳細は、 rpc_svc_calls(3N) および poll(2) のマニュアルページを参照してください。
void svc_run() { int nfds; int dtbsize = __rpc_dtbsize(); int i; struct pollfd svc_pollset[fd_setsize]; for (;;) { /* * 要求待ちするサーバ fd があるかどうかをチェック */ nfds = __rpc_select_to_poll(dtbsize, &svc_fdset, svc_pollset); if (nfds == 0) break; /* 要求待ちの fd がないので終了 */ switch (i = poll(svc_pollset, nfds, -1)) { case -1: /* * エラーが起こった場合は、poll() ではなく、シグナルハンドラなど * 外部イベントによるものと考えて、無視して継続する */ case 0: continue; default: svc_getreq_poll(svc_pollset, i); } } }
RPC のブロードキャストが要求されると、メッセージはネットワーク上の rpcbind デーモンに送られます。要求されたサービスが登録されている rpcbind デーモンは、その要求をサーバに送ります。ブロードキャスト RPC と通常の RPC 呼び出しとの主な相違点を次に示します。
通常の RPC では応答は 1 つですが、ブロードキャスト RPC には複数の応答があります (メッセージに応答するすべてのマシンから応答が返されます)。
ブロードキャスト RPC は、UDP のようにブロードキャスト RPC をサポートする非接続型プロトコルでしか使用できません。
ブロードキャスト RPC では、正常終了以外の応答は返されません。したがって、ブロードキャスタと遠隔のサービスでバージョンの不一致があれば、サービスからブロードキャスタには何も返されません。
ブロードキャスト RPC では、rpcbind で登録されたデータグラムサービスだけがアクセス可能です。サービスアドレスはホストごとに異なりますので、rpc_broadcast() は、rpcbind のネットワークアドレスにメッセージを送信します。
ブロードキャスト要求のサイズはローカルネットワークの最大伝送ユニット (MTU:maximum trasfer unit) により制限されます。イーサネットの MTU は 1500 バイトです。
例 4-21 では、rpc_broadcast() の使用方法を示し、引数を説明します。
/* * コード例 4-21 RPC ブロードキャスト */ #include <stdio.h> #include <rpc/rpc.h> main(argc, argv) int argc; char *argv[]; { enum clnt_stat rpc_stat; u_long prognum, vers; struct rpcent *re; if(argc != 3) { fprintf(stderr, "usage : %s RPC_PROG VERSION¥n", argv[0]); exit(1); } if (isdigit( *argv[1])) prognum = atoi(argv[1]); else { re = getrpcbyname(argv[1]); if (! re) { fprintf(stderr, "Unknown RPC service %s¥n", argv[1]); exit(1); } prognum = re->r_number; } vers = atoi(argv[2]); rpc_stat = rpc_broadcast(prognum, vers, NULLPROC, xdr_void, (char *)NULL, xdr_void, (char *)NULL, bcast_proc, NULL); if ((rpc_stat != RPC_SUCCESS) && (rpc_stat != RPC_TIMEDOUT)) { fprintf(stderr, "broadcast failed: %s¥n", clnt_sperrno(rpc_stat)); exit(1); } exit(0); }
例 4-22 の関数 bcast_proc() では、ブロードキャストに対する応答を収集します。通常は、最初の応答だけを取り出すか、応答をすべて収集します。bcast_proc() は、応答を返したサーバの IP アドレスを表示します。この関数は FALSE を返して応答の収集を続け、RPC クライアントコードはタイムアウトが来るまでブロードキャストを再送信し続けます。
bool_t bcast_proc(res, t_addr, nconf) void *res; /* 応答なし */ struct t_bind *t_addr; /* 応答したアドレス */ struct netconfig *nconf; { register struct hostent *hp; char *naddr; naddr = taddr2naddr(nconf, &taddr->addr); if (naddr == (char *) NULL) { fprintf(stderr,"Responded: unknown¥n"); } else { fprintf(stderr,"Responded: %s¥n", naddr); free(naddr); } return(FALSE); }
TRUE が返されるとブロードキャストは終了し、rpc_broadcast() は正常終了します。FALSE が返された場合は、次の応答を待ちます。数秒間待ってから、要求が再びブロードキャストされます。応答が返されない場合は、rpc_broadcast() は RPC_TIMEDOUT を返します。
RPC の設計方針では、クライアントは呼び出しメッセージを送信して、サーバがそれに応答するのを待ちます。すなわち、サーバが要求を処理する間、クライアントは停止していることになります。これは、クライアントが各メッセージへの応答を待つ必要がないときには非効率です。
RPC のバッチ処理を使用すると、クライアントは非同期に処理を進めることができます。RPC メッセージは呼び出しパイプラインに入れてサーバに送られます。バッチ処理では次のことが必要になります。
サーバはどのような中間メッセージにも応答しません。
呼び出しパイプラインは、信頼性の高いトランスポート (たとえば、TCP) で伝送されなければなりません。
呼び出し時に指定する、戻り値に対する XDR ルーチンは NULL でなければなりません。
RPC 呼び出しのタイムアウト値はゼロでなければなりません。
サーバはそれぞれの呼び出しに対しては応答しないので、クライアントは、サーバが前の呼び出しを処理している間に平行して次の呼び出しを送信できます。トランスポートは複数の呼び出しメッセージをバッファリングし、システムコール write() で一度にサーバに送信します。こうすることにより、プロセス間通信のオーバヘッドを減らし、一連の呼び出しに要する総時間を短縮します。クライアントは終了前に、パイプラインをフラッシュする呼び出しをバッチにしないで実行します。
例 4-23には、バッチ処理を使用しないクライアント側プログラムを示します。文字配列 buf を走査して文字列を順に取り出し、1 つずつサーバに送信します。
#include <stdio.h> #include <rpc/rpc.h> #include "windows.h" main(argc, argv) int argc; char **argv; { struct timeval total_timeout; register CLIENT *client; enum clnt_stat clnt_stat; char buf[1000], *s = buf; if ((client = clnt_create( argv[1], WINDOWPROG, WINDOWVERS, "circuit_v")) == (CLIENT *) NULL) { clnt_pcreateerror("clnt_create"); exit(1); } total_timeout.tv_sec = 20; total_timeout.tv_usec = 0; while (scanf( "%s", s ) != EOF) { if (clnt_call(client, RENDERSTRING, xdr_wrapstring, &s, xdr_void, (caddr_t) NULL, total_timeout) != RPC_SUCCESS) { clnt_perror(client, "rpc"); exit(1); } } clnt_destroy( client ); exit(0); }
例 4-24には、このクライアントプログラムでバッチ処理を使用する場合を示します。各文字列の送信ごとには応答を待たず、サーバからの終了応答だけを待ちます
#include <stdio.h> #include <rpc/rpc.h> #include "windows.h" main(argc, argv) int argc; char **argv; { struct timeval total_timeout; register CLIENT *client; enum clnt_stat clnt_stat; char buf[1000], *s = buf; if ((client = clnt_create( argv[1], WINDOWPROG, WINDOWVERS, "circuit_v")) == (CLIENT *) NULL) { clnt_pcreateerror("clnt_create"); exit(1); } timerclear(&total_timeout); while (scanf("%s", s) != EOF) clnt_call(client, RENDERSTRING_BATCHED, xdr_wrapstring, &s, xdr_void, (caddr_t) NULL, total_timeout); /* ここでパイプラインをフラッシュe */ total_timeout.tv_sec = 20; clnt_stat = clnt_call(client, NULLPROC, xdr_void, (caddr_t) NULL, xdr_void, (caddr_t) NULL, total_timeout); if (clnt_stat != RPC_SUCCESS) { clnt_perror(client, "rpc"); exit(1); } clnt_destroy(client); exit(0); }
例 4-25 には、バッチ処理を使用した場合のサーバのディスパッチ部分を示します。サーバは、メッセージを送信しないので、クライアント側は、失敗に気付きません。
#include <stdio.h> #include <rpc/rpc.h> #include "windows.h" void windowdispatch(rqstp, transp) struct svc_req *rqstp; SVCXPRT *transp; { char *s = NULL; switch(rqstp->rq_proc) { case NULLPROC: if (!svc_sendreply( transp, xdr_void, NULL)) fprintf(stderr, "can't reply to RPC call¥n"); return; case RENDERSTRING: if (!svc_getargs( transp, xdr_wrapstring, &s)) { fprintf(stderr, "can't decode arguments¥n"); /* 呼び出し側にエラーを通知 */ svcerr_decode(transp); break; } /* 文字列 s を処理するコードs */ if (!svc_sendreply( transp, xdr_void, (caddr_t) NULL)) fprintf( stderr, "can't reply to RPC call¥n"); break; case RENDERSTRING_BATCHED: if (!svc_getargs(transp, xdr_wrapstring, &s)) { fprintf(stderr, "can't decode arguments¥n"); /* プロトコルエラーのため何も返さない */ break; } /* 文字列 s を処理するコード。ただし応答はしない。 */ break; default: svcerr_noproc(transp); return; } /* 引数の復号化で割り当てた文字列を解放 */ svc_freeargs(transp, xdr_wrapstring, &s); }
バッチ処理によるパフォーマンスの向上を調べるために、例 4-23、例 4-24、例 4-25で 25144 行のファイルを処理しました。このサービスは、ファイルの各行を引き渡すだけの簡単なサービスです。バッチ処理を使用した方が、使用しない場合の 4 倍の速さで終了しました。
この章でこれまでに示した例では、呼び出し側は自分自身の ID をサーバに示さず、サーバも呼び出し側の ID を要求しませんでした。ネットワークサービスによっては、ネットワークファイルシステムのように、呼び出し側の ID が要求される場合があります。『Solaris のシステム管理』を参照して、この節で説明したいずれかの認証の方法を実行してください。
RPC のクライアントとサーバを作成するときにさまざまなトランスポートを指定できるように、RPC クライアントにもさまざまなタイプの認証メカニズムを採用することができます。RPCの認証サブシステムは端点が開かれているので、認証はさまざな使用法がサポートされます。認証プロトコルは、付録 B 「RPC プロトコルおよび言語の仕様」で詳細に定義されています。
RPC が現在サポートしている認証タイプを 表 4-6 に示します。
表 4-6 RPC が現在サポートしている認証タイプ
デフォルト。認証は実行されない。 |
|
UNIX オペレーティングシステムのプロセスパーミッションを基にした認証タイプ。 |
|
サーバよっては効率を上げるために AUTH_SYS の代わりに AUTH_SHORT を使用できる。AUTH_SYS 認証を使用するクライアントプログラムは、サーバからの AUTH_SHORT 応答ベリファイアを受信できる。詳細については、付録 B 「RPC プロトコルおよび言語の仕様」を参照のこと。 |
|
DES 暗号化技法を基にした認証タイプ。 |
|
呼び出し側が次の方法で RPC クライアントハンドルを新規作成すると、
clnt = clnt_create(host, prognum, versnum, nettype);
対応するクライアント作成ルーチンが次のように認証ハンドルを設定します。
clnt->cl_auth = authnone_create();
新たな認証インスタンスを作成するときは、auth_destroy(clnt->cl_auth) を使用して現在のインスタンスを破棄します。この操作はメモリの節約のために必要です。
サーバ側では、RPC パッケージがサービスディスパッチルーチンに、任意の認証スタイルが結合されている要求を渡します。サービスディスパッチルーチンに渡された要求ハンドルには、rq_cred という構造体が入っています。その構成は、認証資格タイプを示すフィールドを除いて、ユーザから隠されています。
/* * 認証データ */ struct opaque_auth { enum_t oa_flavor; /* 資格スタイル */ caddr_t oa_base; /* より詳細な認証データのアドレス */ u_int oa_length; /* 最大 MAX_AUTH_BYTES まで */ };
RPC パッケージでは、サービスディスパッチルーチンに対して次のことを保証しています。
svc_req 構造内の rq_cred フィールドは完全に設定済みです。したがって、rq_cred.oa_flavor を調べて認証タイプを得ることができます。得られた認証タイプが RPC でサポートされていない場合は、rq_cred のその他のフィールドも調べることができます。
サービス手続きに引き渡される rq_clntcred フィールドには NULL が入っているか、サポートされている認証資格タイプの設定済み構造体へのポインタが入っています。AUTH_NONE タイプには認証データはありません。rq_clntcred は、authdes_cred、short_hand_verf、 authkerb_cred、authdes_cred の各構造体へのポインタにだけキャストできます。
クライアント側で AUTH_SYS (旧バージョンでは AUTH_UNIX) タイプの認証を使用するには、RPC クライアントハンドルの作成後に clnt->cl_auth を次のように設定します。
clnt->cl_auth = authsys_create_default();
以降は、この clnt を使用した RPC 呼び出しでは、clnt とともに以下に示す資格 - 認証構造体Failed Cross Reference Formatが渡されます。
/* * AUTH_SYS タイプの資格 */ struct authsys_parms { u_long aup_time; /* 資格作成時刻 */ char *aup_machname; /* クライアント側のホスト名 */ uid_t aup_uid; /* クライアント側の実効 uid */ gid_t aup_gid; /* クライアント側の現在のグループ ID */ u_int aup_len; /* aup_gids の配列の長さ */ gid_t *aup_gids; /* ユーザが所属するグループの配列 */ };
rpc.broadcast では、デフォルトで AUTH_SYS タイプの認証になります。
例 4-27 には、手続きを使用し、ネットワーク上のユーザ数を返すサーバプログラムである RUSERPROC_1() を示します。認証の例として AUTH_SYS タイプの資格をチェックし、呼び出し側の uid が 16 の場合は要求に応じないようにしてあります。
nuser(rqstp, transp) struct svc_req *rqstp; SVCXPRT *transp; { struct authsys_parms *sys_cred; uid_t uid; unsigned long nusers; /* NULLPROC の場合は認証データなし */ if (rqstp->rq_proc == NULLPROC) { if (!svc_sendreply( transp, xdr_void, (caddr_t) NULL)) fprintf(stderr, "can't reply to RPC call¥n"); return; } /* ここで uid を取得 */ switch(rqstp->rq_cred.oa_flavor) { case AUTH_SYS: sys_cred = (struct authsys_parms *) rqstp->rq_clntcred; uid = sys_cred->aup_uid; break; default: svcerr_weakauth(transp); return; } switch(rqstp->rq_proc) { case RUSERSPROC_1: /* 呼び出し側が、この手続きの呼び出し資格を持っているかどうか確認 */ if (uid == 16) { svcerr_systemerr(transp); return; } /* * ユーザ数を求めて変数 nusers に設定するコード */ if (!svc_sendreply( transp, xdr_u_long, &nusers)) fprintf(stderr, "can't reply to RPC call¥n"); return; default: svcerr_noproc(transp); return; } }
このプログラムでは次の点に注意してください。
NULLPROC (手続き番号はゼロ) に結合した認証パラメタは、通常はチェックされません。
サービスプロトコルでは、アクセスが拒否された場合のステータスを返さなければなりません。例 4-27のプロトコルでは、その代わりに サービスプリミティブ svcerr_systemerr() を呼び出しています。
最後の点で重要なのは、RPC の認証パッケージとサービスの関係です。RPC は認証を処理しますが、個々のサービスへのアクセス制御は行いません。サービス自体でアクセス制御の方針を決め、それがプロトコル内で戻り値として反映されるようにしなければなりません。
AUTH_SYS タイプより厳しいセキュリティレベルが要求されるプログラムでは、AUTH_DES タイプの認証を使用します。AUTH_SYS タイプは AUTH_DES タイプに簡単に変更できます。たとえば、authsys_create_default() を使用する代わりに、プログラムから authsys_create() を呼び出し、RPC 認証ハンドルを変更して目的のユーザ ID とホスト名を設定することができます。
AUTH_DES タイプの認証を使用するには、サーバ側とクライアント側の両方のホストで、keyserv() デーモンと NIS また NIS+ ネームサービスが実行されている必要があります。また、両方のホスト上のユーザに対してネットワーク管理者が割り当てたパブリックキー / シークレットのキーペアが、publickey() のデータベースに入っていなければなりません。ユーザは keylogin() のコマンドを実行して自分のシークレットキーを暗号化しておく必要があります (通常は、ログインパスワードとセキュア RPC パスワードが同一の場合には、これを login() で行います)。
AUTH_DES タイプの認証を使用するには、クライアントが認証ハンドルを正しく設定しなければなりません。その例を次に示します。
cl->cl_auth = authdes_seccreate(servername, 60, server, (char *)NULL);
最初の引数は、サーバプロセスのネットワーク名か、サーバプロセスの所有者のネット名です。サーバプロセスは通常 root プロセスで、次の関数呼び出しでネット名を得ることができます。
char servername[MAXNETNAMELEN]; host2netname(servername, server, (char *)NULL);
servername は受信文字列へのポインタで、server はサーバプロセスが実行されているホスト名です。サーバプロセスがスーパーユーザ以外のユーザから起動されている場合は、次のように user2netname() を呼び出します。
char servername[MAXNETNAMELEN]; user2netname(servername, serveruid(), (char *)NULL);
serveruid() はサーバプロセスのユーザ id です。どちらの関数も最後の引数は、サーバを含むドメイン名です。NULL を指定すると、ローカルドメイン名が使用されます。
authdes_seccreate() の第 2 引数では、このクライアントの資格の存在時間 (ウィンドウとも呼ばれる) を指定します。この例では 60 秒が指定されているので、この資格はクライアント側が RPC 呼び出しを行なってから、60 秒間で失われます。プログラムから再びこの資格を使用しようとしても、サーバ側の RPC サブシステムは、資格がすでに失われていることを知って、資格を失ったクライアントからの要求に答えません。また資格の存在時間中に別のプログラムがその資格を再使用しようとしても拒否されます。サーバ側の RPC サブシステムが最近作成された資格を保存していて、重複して使用できないようにするためです。
authdes_seccreate() の第 3 引数は、クロックを同期させる timehost 名です。AUTH_DES タイプの認証を使用するには、サーバとクライアントの時間が一致していなければなりません。この例では、サーバに同期させています。(char *)NULL
と指定すると同期しません。この指定は、クライアントとサーバがすでに同期していることが確実な場合にだけ行なってください。
authdes_seccreate() の第 4 引数は、タイムスタンプとデータとを暗号化するための DES 暗号化キーへのポインタです。この例のように (char *)NULL
と指定した場合は、ランダムキーが選択されます。このキーは、認証ハンドルの ah_key フィールドに入っています。
サーバ側はクライアント側より簡単です。例 4-27のサーバを AUTH_DES タイプの認証を使用するように変更したものを、例 4-28に示します。
#include <rpc/rpc.h> ... ... nuser(rqstp, transp) struct svc_req *rqstp; SVCXPRT *transp; { struct authdes_cred *des_cred; uid_t uid; gid_t gid; int gidlen; gid_t gidlist[10]; /* NULLPROC の場合は認証データなし */ if (rqstp->rq_proc == NULLPROC) { /* 元のプログラムと同じ */ } /* ここで uid を取得 */ switch(rqstp->rq_cred.oa_flavor) { case AUTH_DES: des_cred = (struct authdes_cred *) rqstp->rq_clntcred; if (! netname2user( des_cred->adc_fullname.name, &uid, &gid, &gidlen, gidlist)) { fprintf(stderr, "unknown user: %s¥n", des_cred->adc_fullname.name); svcerr_systemerr(transp); return; } break; default: svcerr_weakauth(transp); return; } /* 以降は元のプログラムと同じ */
netname2user() ルーチンは、ネットワーク名 (またはユーザの netname) をローカルシステム ID に変換することに注意してください。このルーチンはグループ ID も返します (この例では使用していません)。
SunOS 5.x は、klogin 以外の Kerberos 4.0 の大部分のクライアント側機能をサポートします。AUTH_KERB は AUTH_DES と概念的に同じです。主要な違いは、DES がネットワーク名と暗号化された DES セッションキーを引き渡すのに対し、Kerberos は、暗号化されたサービスチケットを引き渡すことです。実装状態と相互運用性に影響を及ぼすその他の要因については、続けて説明します。
詳細は、kerberos(3N) マニュアルページと MIT Project Athena implementation of Kerberos の Steiner-Neuman-Shiller 報告書 [Steiner, Jennifer G., Neuman, Clifford, and Schiller, Jeffrey J. "Kerberos: An Authentication Service for Open Network Systems." USENIX Conference Proceedings, USENIX Association, カリフォルニア州, バークレー, June 1988.] を参照してください。MIT 文書には、athena-dist.mit.edu 上の FTP ディレクトリ、/pub/kerberos/doc または、ドキュメント URL、ftp://athena-dist.mit.edu/pub/kerberos/doc を使用して Mosaic でアクセスできます。
Kerberos はその資格が有効である時間ウィンドウの概念を使用します。クライアントまたはサーバのクロックを制限しません。クライアントは、サーバに指定されたウィンドウの時間を調整することによって、自身とサーバ間のずれを決定し、サーバに指定されたウィンドウ時間を調整することによって、この違いを補う必要があります。具体的には、window を authkerb_seccreate() に引き数として渡します。この場合、ウィンドウは変わりません。timehost が authkerb_seccreate() の引き数として指定されると、クライアント側は timehost から時刻を取得して、時刻の差異によってタイムスタンプを変更します。時刻を同期化するには、さまざまな方法が使用できます。詳細は、kerberos_rpc(3N) マニュアルページを参照してください。
Kerberos ユーザは、一次名、インスタンス、領域によって識別されます。RPC 認証コードは、領域とインスタンスを無視しますが、Kerberos ライブラリコードは無視しません。ユーザ名は、クライアントとサーバ間で同じであると仮定します。これによって、サーバは一次名をユーザ ID 情報に変換することができます。周知の名前として 2 つの書式が使用されます (領域は省略されます)。
root.host は、クライアント側 host の特権を与えられたユーザを表します。
user.ignored は、ユーザ名が user であるユーザを表します。インスタンは無視されます。
Kerberos は、完全資格名 (チケットとウィンドウを含むもの) の送信時に暗号文ブロックチェイン (CBC: Cipher Block Chaining) モード、それ以外の場合は、電子コードブック (ECB: Electronic Code Book) モードを使用します。CBC と ECB は、DES 暗号化のための 2 つの方法です。詳細は、des_crypt(3) マニュアルページを参照してください。セッションキーは、CBC モードに対する初期入力ベクトルとして使用されます。表記は次のようになります。
xdr_type(object)
これは、XDR が object を type
とみなして使用されることを示します。次のコードセクションの長さ (資格またはベリファイアのバイト数) を、4 バイト単位に丸めらたサイズで表されます。完全資格名およびベリファイアは、次のようになります。
xdr_long(timestamp.seconds) xdr_long(timestamp.useconds) xdr_long(window) xdr_long(window - 1)
セッションキーに等しい入力ベクトルを持つ CBC で暗号化を行うと、出力結果は次のような 2 つの DES 暗号化ブロックになります。
CB0 CB1.low CB1.high
xdr_long(AUTH_KERB) xdr_long(length) xdr_enum(AKN_FULLNAME) xdr_bytes(ticket) xdr_opaque(CB1.high)
xdr_long(AUTH_KERB) xdr_long(length) xdr_opaque(CB0) xdr_opaque(CB1.low)
xdr_long(timestamp.seconds) xdr_long(timestamp.useconds)
ニックネームは、ECB によって暗号化され、ECB0 と資格を得ます。
xdr_long(AUTH_KERB) xdr_long(length) xdr_enum(AKN_NICKNAME) xdr_opaque(akc_nickname)
xdr_long(AUTH_KERB) xdr_long(length) xdr_opaque(ECB0) xdr_opaque(0)
RPC サーバは、inetd や listen のようなポートモニタから起動できます。ポートモニタは、要求が来ているかどうか監視し、要求が来ればそれに応じてサーバを生成します。生成されたサーバプロセスには、要求を受信したファイル記述子 0 が渡されます。inetd の場合、サーバは処理を終えるとすぐに終了するか、次の要求がくる場合に備えて指定された時間だけ待ってから終了します。
listen の場合は常に新たなプロセスが生成されるため、サーバは応答を返したらすぐに終了しなければなりません。次に示す関数呼び出しでは、ポートモニタから起動されるサービスで使用する SVCXPRT ハンドルが作成されます。
transp = svc_tli_create(0, nconf, (struct t_bind *)NULL, 0, 0)
ここで、nconf は要求を受信したトランスポートの netconfig 構造体です。
サービスはポートモニタによりすでに rpcbind で登録されているので、登録する必要はありません。ただし、サービス手続きは次のように svc_reg() を呼び出して登録しなければなりません。
svc_reg(transp, PROGNUM, VERSNUM, dispatch,(struct netconfig *)NULL)
ここでは netconfig 構造体として NULL を渡し、svc_reg() が rpcbind を呼び出してサービスを登録しないようにしています。
rpcgen が生成したサーバ側スタブプログラムを調べて、これらのルーチンの呼び出し順序を確認してください。
接続型トランスポートの場合は、次のルーチンにより下位レベルインタフェースが提供されます。
transp = svc_fd_create(0, recvsize, sendsize);
最初の引数ではファイル記述子 0 を指定します。recvsize と sendsize には、適当なバッファサイズを指定できます。どちらの引数も 0 とすると、システムのデフォルト値が使用されます。自分で監視を行わないアプリケーションサーバの場合は、svc_fd_create() を使用します。
/etc/inet/inetd.conf のエントリ形式は、ソケットサービス、TLI サービス、RPC サービスによってそれぞれ異なります。RPC サービスの場合の inetd.conf のエントリ形式は次のようになります。
rpc_prog/vers endpoint_type rpc/proto flags user pathname args
各エントリの内容を次に示します。
表 4-7 RPC inetd サービス
rpc_prog/vers |
RPC プログラム名に / とバージョン番号 (またはバージョン番号の範囲) を付けたもの。 |
endpoint_type |
dgram (非接続型ソケット)、stream (接続型ソケット)、tli (TLI 端点) のどれか。 |
proto |
サポートされているトランスポートすべてを意味する *、nettype、netid のどれか。または、nettype と netid をコンマで区切ったリスト。 |
flags |
wait または nowait のどちらか。 |
user |
有効な passwd データベースに存在しているユーザ。 |
pathname |
サーバデーモンへのフルパス名。 |
args |
デーモンの呼び出し時に渡される引数。 |
エントリの例を次に示します。
rquotad/1 tli rpc/udp wait root /usr/lib/nfs/rquotad rquotad
inetd についての詳細は、inetd.conf(4) のマニュアルページを参照してください。
次に示すように pmadm を使用して RPC サービスを追加します。
pmadm -a -p pm_tag -s svctag -i id -v vers ¥ -m `nlsadmin -c command -D -R prog:vers`
引数の -a はサービスの追加を意味します。-p pm_tag ではサービスへのアクセスを提供するポートモニタに結合したタグを指定します。-s svctag はサーバの ID コードです。-i id はサービス svctag に割り当てられた /etc/passwd 内のユーザ名です。-v vers はポートモニタのデータベースファイルのバージョン番号です。-m ではサービスを呼び出す nlsadmin コマンドを指定します。nlsadmin コマンドには引数を渡すことができます。たとえば、rusersd という名前の遠隔プログラムサーバのバージョン 1 を追加する場合は、pmadm コマンドは次のようになります。
# pmadm -a -p tcp -s rusers -i root -v 4 ¥ -m `nlsadmin -c /usr/sbin/rpc.ruserd -D -R 100002:1`
このコマンドでは、root パーミッションが指定され、listener データベースファイルのバージョン 4 でインストールされ、TCP トランスポート上で使用可能になります。pmadm の引数やオプションは複雑であるため、RPC サービスはコマンドスクリプトでもメニューシステムでも追加できます。メニューシステムを使用するには、sysadm ports と入力して、port_services オプションを選択します。
サービスを追加した場合は、その後リスナを再初期化してサービスを利用可能にしなければなりません。そのためには、次のようにリスナを一度止めてから再起動します (このとき rpcbind が実行されていなければならないことに注意してください)。
# sacadm -k -p pmtag # sacadm -s -p pmtag
リスナプロセスの設定などについての詳細は、listen(1M)、pmadm(1M)、sacadm(1M)、sysadm(1M) のマニュアルページ、『TCP/IP とデータ通信』を参照してください。
一般に、プログラム PROG の最初のバージョンは PROGVERS_ORIG とし、最新バージョンは PROGVERS と命名します。プログラムのバージョン番号は続き番号で割り当てなければなりません。バージョン番号に飛ばされた番号があると、検索したときに定義済みのバージョン番号を探し出せないようなことが起こります。
プログラムのバージョン番号は、プログラムの所有者以外は決して変更してはいけません。自分が所有していないプログラムのバージョン番号を追加したりすると、そのプログラムの所有者がバージョン番号をインクリメントするときに重大な問題が起こります。バージョン番号の登録やご質問はご購入先へお問い合わせ下さい。
ruser プログラムの新バージョンが、long
ではなく unsigned
short
を返すように変更されたとします。新バージョンの名前を RUSERSVERS_SHORT とすると、新旧の 2 つのバージョンをサポートするサーバは二重登録することになります。次のように、どちらの登録でも同じサーバハンドルを使用します。
if (!svc_reg(transp, RUSERSPROG, RUSERSVERS_ORIG, nuser, nconf)) { fprintf(stderr, "can't register RUSER service¥n"); exit(1); } if (!svc_reg(transp, RUSERSPROG, RUSERSVERS_SHORT, nuser, nconf)) { fprintf(stderr, "can't register RUSER service¥n"); exit(1); }
次のように、1 つの手続きで両バージョンを実行できます。
void nuser(rqstp, transp) struct svc_req *rqstp; SVCXPRT *transp; { unsigned long nusers; unsigned short nusers2; switch(rqstp->rq_proc) { case NULLPROC: if (!svc_sendreply( transp, xdr_void, 0)) fprintf(stderr, "can't reply to RPC call¥n"); return; case RUSERSPROC_NUM: /* * ユーザ数を求めて変数 nusers に設定するコード */ switch(rqstp->rq_vers) { case RUSERSVERS_ORIG: if (! svc_sendreply( transp, xdr_u_long, &nusers)) fprintf(stderr, "can't reply to RPC call¥n"); break; case RUSERSVERS_SHORT: nusers2 = nusers; if (! svc_sendreply( transp, xdr_u_short, &nusers2)) fprintf(stderr, "can't reply to RPC call¥n"); break; } default: svcerr_noproc(transp); return; } return; }
異なるホストでは RPC サーバの異なるバージョンが実行されている可能性があるので、クライアントはさまざまなバージョンに対応できるようにしなければなりません。たとえば、あるサーバでは旧バージョン RUSERSPROG(RUSERSVERS_ORIG) が実行されており、別のサーバでは最新バージョン RUSERSPROG(RUSERSVERS_SHORT) が実行されているとします。
サーバのバージョンがクライアント作成ルーチン clnt_call() で指定したバージョン番号と一致しない場合は、clnt_call() から RPCPROGVERSMISMATCH というエラーが返されます。サーバがサポートしているバージョン番号を取り出して、正しいバージョン番号をもつクライアントハンドルを作成することもできます。そのためには、例 4-31 のルーチンを使用するか、clnt_create_vers() を使用します。詳細については、rpc(3N) のマニュアルページを参照してください。
main() { enum clnt_stat status; u_short num_s; u_int num_l; struct rpc_err rpcerr; int maxvers, minvers; CLIENT *clnt; clnt = clnt_create("remote", RUSERSPROG, RUSERSVERS_SHORT, "datagram_v"); if (clnt == (CLIENT *) NULL) { clnt_pcreateerror("unable to create client handle"); exit(1); } to.tv_sec = 10; /* タイムアウト値を設定 */ to.tv_usec = 0; status = clnt_call(clnt, RUSERSPROC_NUM, xdr_void, (caddr_t) NULL, xdr_u_short, (caddr_t)&num_s, to); if (status == RPC_SUCCESS) { /* 最新バージョン番号が見つかった場合 */ printf("num = %d¥n", num_s); exit(0); } if (status != RPC_PROGVERSMISMATCH) { /* その他のエラーr */ clnt_perror(clnt, "rusers"); exit(1); } /* 指定したバージョンがサポートされていない場合 */ clnt_geterr(clnt, &rpcerr); maxvers = rpcerr.re_vers.high; /* サポートされている最新バージョン */ minvers = rpcerr.re_vers.low; /* サポートされている最も古いバージョン */ if (RUSERSVERS_SHORT < minvers || RUSERSVERS_SHORT > maxvers) { /* サポート範囲内にない場合 */ clnt_perror(clnt, "version mismatch"); exit(1); } (void) clnt_control(clnt, CLSET_VERSION, RUSERSVERS_ORIG); status = clnt_call(clnt, RUSERSPROC_NUM, xdr_void, (caddr_t) NULL, xdr_u_long, (caddr_t)&num_l, to); if (status == RPC_SUCCESS) /* 識別できるバージョン番号が見つかった場合 */ printf("num = %d¥n", num_l); else { clnt_perror(clnt, "rusers"); exit(1); } }
場合によっては、動的に生成される RPC プログラム番号をアプリケーションが使用すると便利なことがあります。たとえば、コールバック手続きを実装する場合などです。コールバックでは、クライアントプログラムは通常、動的に生成される、つまり一時的な RPC プログラム番号を使用して RPC サービスを登録し、これを要求とともにサーバに渡します。次にサーバは一時的な RPC プログラム番号を使用してクライアントプログラムをコールバックし、結果を返します。クライアントの要求を処理するのにかなりの時間がかかり、クライアントが停止できない (シングルスレッドであると仮定して) 場合などには、この機構が必要になります。このような場合、サーバはクライアントの要求を認識し、あとで結果とともにコールバックを行います。コールバックを使用する別の例としては、サーバから定期的なレポートを生成する場合があります。クライアントは RPC 呼び出しを行い、報告を開始します。そしてサーバはクライアントプログラムが提供する一時的な RPC プログラム番号を使用して、定期的にレポートとともにクライアントをコールバックします。
動的に生成される一時的な RPC 番号は、0x40000000 から 0x5fffffff の範囲です。次に示すルーチンは指定されるトランスポートタイプ用に、一時的な RPC プログラムに基づいてサービスを作成します。サービスハンドルと一時的な rpc プログラム番号が返されます。呼び出し側はサービスディスパッチルーチン、バージョンタイプ、トランスポートタイプを提供します。
SVCXPRT * register_transient_prog(dispatch, program, version, netid) void (*dispatch)(); /* サービスディスパッチルーチン */ u_long *program; /* 一時的な RPC 番号が返される */ u_long version; /* プログラムバージョン */ char *netid; /* トランスポート id */ { SVCXPRT *transp; struct netconfig *nconf; u_long prognum; if ((nconf = getnetconfigent(netid)) == (struct netconfig *)NULL) return ((SVCXPRT *)NULL); if ((transp = svc_tli_create(RPC_ANYFD, nconf, (struct t_bind *)NULL, 0, 0)) == (SVCXPRT *)NULL) { freenetconfigent(nconf); return ((SVCXPRT *)NULL); } prognum = 0x40000000; while (prognum < 0x60000000 && svc_reg(transp, prognum, version, dispatch, nconf) == 0) { prognum++; } freenetconfigent(nconf); if (prognum >= 0x60000000) { svc_destroy(transp); return ((SVCXPRT *)NULL); } *program = prognum; return (transp); }
このマニュアルには、Solaris でのマルチスレッドプログラミングについては説明していません。次の項目については、『マルチスレッドのプログラミング』を参照してください。
スレッドの作成
スケジューリング
同期
シグナル
プロセスリソース
軽量プロセス (lwp)
並列性
データロックの技術
TI-RPC は、Solaris 2.4 以降のマルチスレッド RPC サーバをサポートします。マルチスレッドサーバと シングルスレッドのサーバの違いは、マルチスレッドサーバがスレッドの技術を使用して複数のクライアント要求を同時に処理することです。マルチスレッドサーバの方が、高度なパフォーマンスと可用性を備えています。
このリリースで新規に使用可能なインタフェースについては、まず、「マルチスレッドサーバの概要」から読んでください。
マルチスレッド対応のクライアントプログラムでは、RPC 要求が出されるたびにスレッドを 1 つ作成することができます。複数スレッドが同一のクライアントハンドルを共有する場合は、RPC要求発行できるのは一度に 1 つのスレッドだけです。その他のすべてのスレッドは、未処理の要求が終了するまで待たなければなりません。これに対して、複数スレッドがそれぞれ固有のクライアントハンドルを使用して RPC 要求を出す場合には、複数の要求が同時に処理されます。図 4-1は、異なるクライアントハンドルを使用するクライアント側の 2 つのスレッドから成るマルチスレッッド対応クライアント環境でのタイミングの例を示したものです。
例 4-33 は、クライアント側でマルチスレッド rstat プログラムを実行する場合を示します。クライアントプログラムは各ホストに対してスレッドを作成します。スレッドはそれぞれ、固有のクライアントハンドルを作成し、指定のホストにさまざまなRPC 呼び出しを行なっています。クライアント側の各スレッドは異なるハンドルを使用して RPC 呼び出しを行うため、RPC 呼び出しは同時に実行されます。
RPC マルチスレッド対応アプリケーションを作成する場合は常に、スレッドライブラリをリンクしなければなりません。コンパイルコマンドで -lthread を指定して、スレッドライブラリを最後にリンクするようにしなければなりません。
次のように入力して 例 4-33のプログラムを作成します。
$ cc rstat.c -lnsl -lthread
/* @(#)rstat.c 2.3 93/11/30 4.0 RPCSRC */ /* * w コマンドと同様の形式で、遠隔ホストのステータスを表示する簡単な * プログラム */ #include <thread.h> /* スレッドインターフェースの定義 */ #include <synch.h> /* 相互排他的ロックの定義 */ #include <stdio.h> #include <sys/param.h> #include <rpc/rpc.h> #include <rpcsvc/rstat.h> #include <errno.h> mutex_t tty; /* printf のための tty の制御 */ cond_t cv_finish; int count = 0; main(argc, argv) int argc; char **argv; { int i; thread_t tid; void *do_rstat(); if (argc < 2) { fprintf(stderr, "usage: %s ¥"host¥" [...]¥n", argv[0]); exit(1); } mutex_lock(&tty); for (i = 1; i < argc; i++) { if (thr_create(NULL, 0, do_rstat, argv[i], 0, &tid) < 0) { fprintf(stderr, "thr_create failed: %d¥n", i); exit(1); } else fprintf(stderr, "tid: %d¥n", tid); } while (count < argc-1) { printf("argc = %d, count = %d¥n", argc-1, count); cond_wait(&cv_finish, &tty); } exit(0); } bool_t rstatproc_stats(); void * do_rstat(host) char *host; { CLIENT *rstat_clnt; statstime host_stat; bool_t rval; struct tm *tmp_time; struct tm host_time; struct tm host_uptime; char days_buf[16]; char hours_buf[16]; mutex_lock(&tty); printf("%s: starting¥n", host); mutex_unlock(&tty); /* rstat クライアントハンドル */ rstat_clnt = clnt_create(host, RSTATPROG, RSTATVERS_TIME, "udp"); if (rstat_clnt == NULL) { mutex_lock(&tty); /* ty の制御権を取得 */ clnt_pcreateerror(host); count++; cond_signal(&cv_finish); mutex_unlock(&tty); /* tty の制御権を解放 */ thr_exit(0); } rval = rstatproc_stats(NULL, &host_stat, rstat_clnt); if (!rval) { mutex_lock(&tty); /* tty の制御権を取得 */ clnt_perror(rstat_clnt, host); count++; cond_signal(&cv_finish); mutex_unlock(&tty); /* tty の制御権を解放 */ thr_exit(0); } tmp_time = localtime_r(&host_stat.curtime.tv_sec, &host_time); host_stat.curtime.tv_sec = host_stat.boottime.tv_sec; tmp_time = gmtime_r(&host_stat.curtime.tv_sec, &host_uptime); if (host_uptime.tm_yday != 0) sprintf(days_buf, "%d day%s, ", host_uptime.tm_yday, (host_uptime.tm_yday > 1) ? "s" : ""); else days_buf[0] = '¥0'; if (host_uptime.tm_hour != 0) sprintf(hours_buf, "%2d:%02d,", host_uptime.tm_hour, host_uptime.tm_min); else if (host_uptime.tm_min != 0) sprintf(hours_buf, "%2d mins,", host_uptime.tm_min); else hours_buf[0] = '¥0'; mutex_lock(&tty); /* tty の制御権を取得 */ printf("%s: ", host); printf(" %2d:%02d%cm up %s%s load average: %.2f %.2f %.2f¥n", (host_time.tm_hour > 12) ? host_time.tm_hour - 12 : host_time.tm_hour, host_time.tm_min, (host_time.tm_hour >= 12) ? 'p' : 'a', days_buf, hours_buf, (double)host_stat.avenrun[0]/FSCALE, (double)host_stat.avenrun[1]/FSCALE, (double)host_stat.avenrun[2]/FSCALE); count++; cond_signal(&cv_finish); mutex_unlock(&tty); /* tty の制御権を解放 */ clnt_destroy(rstat_clnt); sleep(10); thr_exit(0); } /* クライアント側の MT rstat プログラムの実行 */ /* clnt_control() を使用してデフォルトのタイムアウトを変更可能 */ static struct timeval TIMEOUT = { 25, 0 }; bool_t rstatproc_stats(argp, clnt_resp, clnt) void *argp; statstime *clnt_resp; CLIENT *clnt; { memset((char *)clnt_resp, 0, sizeof (statstime)); if (clnt_call(clnt, RSTATPROC_STATS, (xdrproc_t) xdr_void, (caddr_t) argp, (xdrproc_t) xdr_statstime, (caddr_t) clnt_resp, TIMEOUT) != RPC_SUCCESS) { return (FALSE); } return (TRUE); }
Solaris 2.4 より前のバージョンでは、RPC サーバはシングルスレッドでした。つまり、クライアント側から要求が来るごとに処理していました。たとえば、2 つの要求を同時に受け取り、最初の処理に 30 秒、次の処理に 1 秒かかるとすると、2 つめの要求を出したクライアントは最初の処理が完了するまで待たなければなりません。これは、各 CPU が異なる要求を同時に処理するマルチプロセッササーバ環境を利用できず、他の要求がサーバによって処理することができるのに 1 つの要求の I/O の完了を待っている状態が生じ、望ましいものではありません。
Solaris 2.4 以降 の RPC ライブラリでは、サービス開発者がエンドユーザにより良いパフォーマンスを提供するマルチスレッドサーバを作成できる機能を追加しました。サーバのマルチスレッドの 2 つのモード、自動マルチスレッドモードとユーザ・マルチスレッド・モードは、TI-RPC でサポートされます。
自動モードでは、サーバは、クライアント要求を受信するごとに新規スレッドを自動的に作成します。このスレッドは要求を処理し、応答してから終了します。ユーザモードでは、サービス開発者が、入って来るクライアント要求を同時に処理するスレッドを作成、管理します。自動モードはユーザモードより使用はしやすいのですが、ユーザモードの方が特別な要件を必要とするサービス開発者に対して柔軟性があります。
RPC マルチスレッド対応アプリケーションを作成する場合は常に、スレッドライブラリをリンクしなければなりません。コンパイルコマンドで -lthread を指定して、スレッドライブラリを最後にリンクするようにしなければなりません。
サーバ側のマルチスレッドをサポートする呼び出しでは、rpc_control() と svc_done() がサポートされています。これらの呼び出しによってサーバ側でマルチスレッド処理が行なえるようになりました。rpc_control() 呼び出しがマルチスレッドモードを設定するために、自動モードとユーザモードの両方で使用されます。サーバが自動モードを使用する場合には、svc_done() を呼び出す必要はありません。ユーザモードの場合には、サーバが要求処理からのリソースを再要求できるようにするため、svc_done() は各クライアント要求が処理されてから呼び出されなければなりません。さらにマルチスレッド RPC サーバは、svc_run() をマルチスレッド対応で呼び出さなければなりません。svc_getreqpoll() と svc_getreqset() は、MT アプリケーションでは安全ではありません。
サーバプログラムが新規インタフェース呼び出しを行わない場合には、デフォルトのモードのシングルスレッドモードのままです。
サーバが使用しているモードに関係なく、RPC サーバ手続きはマルチスレッド対応にしなければなりません。通常これは、すべての静的変数とグロール変数が mutex ロックで保護される必要がある、ということです。相互排他と他の同期 API は、synch.h で定義されます。さまざまな同期インタフェースのリストは、condition(3T)、rwlock(3T)、mutex(3T) を参照してください。
図 4-2 は、マルチスレッドモードのどちらかで実行されるサーバの実行タイミングを示します。
サービス・トランスポート・ハンドル、SVCXPRT には、引き数を復号化するための領域と結果をコード化するための領域である 1 つのデータ領域があります。したがって、デフォルトでは、シングルスレッドモードであり、この構造は、これらの操作を行う関数を呼び出すスレッド間では自由に共有することはできません。ただし、サーバが、マルチスレッド自動モードまたはユーザモードにある場合には、この構造のコピーは、同時要求処理を可能にするために、サービスディスパッチ用のプログラムに引き渡されます。これらの状況では、ルーチンのマルチスレッド対応ではない一部のルーチンがマルチスレッド対応となります。特別に注意書きがない場合には、サーバインタフェースは通常、マルチスレッド対応です。サーバ側のインタフェースについての詳細は、rpc_svc_calls(3N) マニュアルページを参照してください。
マルチスレッド自動モードでは、RPC ライブラリはスレッドを作成し、管理することができます。サービス開発者が新規インタフェース呼び出し、rpc_control() を呼び出し、svc_run() を呼び出す前にサーバをマルチスレッド自動モードにします。このモードでは、プログラマはサービスプロシージャがマルチスレッド対応であることを確認するだけで十分です。
rpc_control() の使用によって、アプリケーションでグローバル RPC 属性を設定できます。現在はサービス側の操作しかサポートしていません。 表 4-8 は、自動モード用に定義された rpc_control() 操作を示します。追加の情報については、rpc control(3N) マニュアルページを参照してください。
表 4-8 rpc_control() ライブラリルーチン
マルチスレッドモードの設定 |
|
マルチスレッドの取得 |
|
最大スレッド数の設定 |
|
最大スレッド数の取得 |
|
現在アクティブなスレッドの合計数 |
|
RPC ライブラリ作成のスレッドの累積数 |
|
RPC ライブラリ内の thr_create エラー数 |
表 4-8 の get 演算は、RPC_SVC_MTMODE_GET() 以外はすべて、自動マルチスレッドモードにだけ適用されます。マルチスレッド・ユーザ・モードまたはデフォルトのシングル・スレッド・モードで使用する場合には、演算の結果は定義されません。
デフォルトでは、RPC ライブラリが一度に作成できるスレッドの最大数は 16 です。サーバが 16 以上のクライアント要求を同時に処理する必要がある場合には、スレッドの最大数を指定して設定する必要があります。このパラメータは、サーバによっていつでも設定することができ、これによってサーバ開発者はサーバによって使用されるスレッドリソースの上限を設定できます。例 4-34 は、マルチスレッド自動モードに作成された RPC プログラムの例です。この例では、スレッドの最大数は 20 に設定されています。
マルチスレッドのパフォーマンスは、関数 svc_getargs() が、NULLPROCS 以外のプロシージャによって呼び出されるごとに、引き数 (この場合には xdr_void())がない場合でも改善されていきます。これはマルチスレッド自動モードとマルチスレッドユーザモード両方の場合においでてす。詳細は、rpc_svc_calls(3N) マニュアルページを参照してください。
例 4-34 は、マルチスレッド自動モードでのサーバを示したものです。
RPC マルチスレッド対応アプリケーションを作成する場合は常に、スレッドライブラリ内でリンクしなければなりません。コンパイルコマンドで -lthread を指定して、スレッドライブラリを最後にリンクするようにしなければなりません。
次のように入力して例 4-34 のプログラムを作成します。
$ cc time_svc.c -lnsl -lthread
#include <stdio.h> #include <rpc/rpc.h> #include <synch.h> #include <thread.h> #include "time_prot.h" void time_prog(); main(argc, argv) int argc; char *argv[]; { int transpnum; char *nettype; int mode = RPC_SVC_MT_AUTO; int max = 20; /* スレッド最大数を 20 に設定 */ if (argc > 2) { fprintf(stderr, "usage: %s [nettype]¥n", argv[0]); exit(1); } if (argc == 2) nettype = argv[1]; else nettype = "netpath"; if (!rpc_control(RPC_SVC_MTMODE_SET, &mode)) { printf("RPC_SVC_MTMODE_SET: failed¥n"); exit(1); } if (!rpc_control(RPC_SVC_THRMAX_SET, &max)) { printf("RPC_SVC_THRMAX_SET: failed¥n"); exit(1); } 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(); } /* * サーバのディスパッチプログラムです。RPC サーバライブラリは、 * サーバのディスパッチャルーチン time_prog () を実行するスレッドを * 作成します。RPC ライブラリがスレッドを廃棄した後に行われます。 */ static void time_prog(rqstp, transp) struct svc_req *rqstp; SVCXPRT *transp; { switch (rqstp->rq_proc) { case NULLPROC: svc_sendreply(transp, xdr_void, NULL); return; case TIME_GET: dotime(transp); break; default: svcerr_noproc(transp); return; } } dotime(transp) SVCXPRT *transp; { struct timev rslt; time_t thetime; 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,(caddr_t) &rslt)) { svcerr_systemerr(transp); } }
例 4-35 は、サーバに対するtime_prot.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)
マルチスレッド・ユーザ・モードでは、RPC ライブラリはスレッドを作成しません。このモードは、基本的には、シングルスレッド、またはデフォルトのモードのように作動します。唯一の違いは、データ構造のコピー (サービス・ディスパッチ・ルーチンへのトランスポートサービスなど) をマルチスレッド対応に引き渡す点です。
RPC サーバ開発者は、スレッドライブラリ全体のスレッドの作成と管理に対する責任を持ちます。ディスパッチルーチンでは、サービス開発者は、プロシージャの実行を新規作成のまたは既存のスレッドに割り当てることができます。thr_create() API は、さまざまな属性を持つスレッドを作成するために使用されます。すべてのスレッドのライブラリインタフェースは、thread.h で定義されます。詳細は、pthread_create(3T) マニュアルページを参照してください。
このモードは、サーバ開発者に幅広い柔軟性を提供しています。スレッドは、サービス要件に応じたスタックサイズを持ちます。スレッドは限定されます。異なるプロシージャは、異なる特長を持つスレッドによって実行されます。サービス開発者は、サービスの一部をシングルスレッドで実行できます。また、特別なスレッド特定シングルプロセスを行うこともできます。
自動モードの場合と同じように、rpc_control() ライブラリは、ユーザモードに切り換える場合に使用されます。表 4-8 に示した rpc_control() 演算 (RPC_SVC_MTMODE_GET() 以外) は、マルチスレッド自動モードにだけ適用されます。マルチスレッド・ユーザ・モードまたはシングルスレッドのデフォルトモードで使用すると、演算の結果が定義できません。
マルチスレッド・ユーザ・モードでは、サービスプロシージャは、戻しの前に svc_done() を呼び出さなければなりません。svc_done() は、クライアント要求が指定のサービス・トランスポート・ハンドルに向けたサービスに割り当てたリソースを解放しなければなりません。この機能は、クライアント要求がサービスされた後、あるいは応答の送信を妨げたエラーまたは異常な状態の後に呼び出されます。svc_done() が呼び出された後に、サービス・トランスポート・ハンドルは、サービスプロシージャによって参照されるべきではありません。例 4-36は、マルチスレッド・ユーザ・モードでのサーバを示します。
svc_done() は、マルチスレッド・ユーザ・モード内でだけ呼び出すことができます。詳細は、rpc_svc_calls(3N) マニュアルページを参照してください。
#define SVC2_PROG 0x30000002 #define SVC2_VERS ((u_long) 1) #define SVC2_PROC_ADD ((u_long) 1) #define SVC2_PROC_MULT ((u_long) 2) struct intpair { u_short a; u_short b; }; typedef struct intpair intpair; struct svc2_add_args { long argument; SVCXPRT *transp; }; struct svc2_mult_args { intpair mult_argument; SVCXPRT *transp; }; extern bool_t xdr_intpair(); #define NTHREADS_CONST 500
例 4-37 は、マルチスレッド・ユーザ・モードに対するクライアントです。
#define _REENTRANT #include <stdio.h> #include <rpc/rpc.h> #include <sys/uio.h> #include <netconfig.h> #include <netdb.h> #include <rpc/nettype.h> #include <thread.h> #include "rpc_test.h" void *doclient(); int NTHREADS; struct thread_info { thread_t client_id; int client_status; }; struct thread_info save_thread[NTHREADS_CONST]; main(argc, argv) int argc; char *argv[]; { int index, ret; int thread_status; thread_t departedid, client_id; char *hosts; if (argc < 3) { printf("Usage: do_operation [n] host¥n"); printf("¥twhere n is the number of threads¥n"); exit(1); } else if (argc == 3) { NTHREADS = NTHREADS_CONST; hosts = argv[1]; /* live_host */ } else { NTHREADS = atoi(argv[1]); hosts = argv[2]; } for (index = 0; index < NTHREADS; index++){ if (ret = thr_create(NULL, NULL, doclient, (void *) hosts, THR_BOUND, &client_id)){ printf("thr_create failed: return value %d", ret); printf(" for %dth thread¥n", index); exit(1); } save_thread[index].client_id = client_id; } for (index = 0; index < NTHREADS; index++){ if (thr_join(save_thread[index].client_id, &departedid, (void *) &thread_status)){ printf("thr_join failed for thread %d¥n", save_thread[index].client_id); exit(1); } save_thread[index].client_status = thread_status; } } void *doclient(host) char *host; { struct timeval tout; enum clnt_stat test; long result = 0; u_short mult_result = 0; long add_arg; long EXP_RSLT; intpair pair; CLIENT *clnt; if ((clnt = clnt_create(host, SVC2_PROG, SVC2_VERS, "udp" ==NULL) { clnt_pcreateerror("clnt_create error: "); thr_exit((void *) -1); } tout.tv_sec = 25; tout.tv_usec = 0; memset((char *) &result, 0, sizeof (result)); memset((char *) &mult_result, 0, sizeof (mult_result)); if (thr_self() % 2){ EXP_RSLT = thr_self() + 1; add_arg = thr_self(); test = clnt_call(clnt, SVC2_PROC_ADD, (xdrproc_t) xdr_long, (caddr_t) &add_arg, (xdrproc_t) xdr_long, (caddr_t) &result, tout); } else { pair.a = (u_short) thr_self(); pair.b = (u_short) 1; EXP_RSLT = (long) pair.a * pair.b; test = clnt_call(clnt, SVC2_PROC_MULT, (xdrproc_t) xdr_intpair, (caddr_t) &pair, (xdrproc_t) xdr_u_short, (caddr_t) &mult_result, tout); result = (long) mult_result; } if (test != RPC_SUCCESS) { printf("THREAD: %d clnt_call hav thr_exit((void *) -1); }; thr_exit((void *) 0); }
例 4-38 は、マルチスレッド・ユーザ・モードのサーバ側を示します。マルチスレッドパフォーマンスは、関数 svc_getargs() が NULLPROC 以外の各プロシージャに呼び出される場合は、引き数 (この場合には xdr_void) がなくても改善されます。これは、マルチスレッド自動モードモードとマルチスレッドユーサモードにおいてです。詳細は、rpc_svc_calls(3N) マニュアルページを参照してください。
RPC マルチスレッド対応アプリケーションを作成する場合は常に、スレッドライブラリ内でリンクしなければなりません。コンパイルコマンドで -lthread を指定して、スレッドライブラリを最後にリンクするようにしなければなりません。
#define _REENTRANT #include <stdio.h> #include <rpc/rpc.h> #include <sys/types.h> #include <netinet/in.h> #include <sys/uio.h> #include <signal.h> #include <thread.h> #include "operations.h" SVCXPRT *xprt; void add_mult_prog(); void *svc2_add_worker(); void *svc2_mult_worker(); main(argc, argv) int argc; char **argv; { int transpnum; char *nettype; int mode = RPC_SVC_MT_USER; if(rpc_control(RPC_SVC_MTMODE_SET,&mode) == FALSE){ printf(" rpc_control is failed to set AUTO mode¥n"); exit(0); } 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(add_mult_prog, SVC2_PROG, SVC2_VERS, nettype); if (transpnum == 0) { fprintf(stderr, "%s: cannot create %s service.¥n", argv[0], nettype); exit(1); } svc_run(); } void add_mult_prog (rqstp, transp) struct svc_req *rqstp; SVCXPRT *transp; { long argument; u_short mult_arg(); intpair mult_argument; bool_t (*xdr_argument)(); struct svc2_mult_args *sw_mult_data; struct svc2_add_args *sw_add_data; int ret; thread_t worker_id; switch ((long) rqstp->rq_proc){ case NULLPROC: svc_sendreply(transp, xdr_void, (char *) 0); svc_done(transp); break; case SVC2_PROC_ADD: xdr_argument = xdr_long; (void) memset((char *) &argument, 0, sizeof (argument)); if (!svc_getargs(transp, xdr_argument, (char *) &argument)){ printf("problem with getargs¥n"); svcerr_decode(transp); exit(1); } sw_add_data = (struct svc2_add_args *) malloc(sizeof (struct svc2_add_args)); sw_add_data->transp = transp; sw_add_data->argument = argument; if (ret = thr_create(NULL, THR_MIN_STACK + 16 * 1024, svc2_add_worker, (void *) sw_add_data, THR_DETACHED, printf("SERVER: thr_create failed:"); printf(" return value %d", ret); printf(" for add thread¥n"); exit(1); } break; case SVC2_PROC_MULT: xdr_argument = xdr_intpair; (void) memset((char *) &mult_argument, 0, sizeof (mult_argument)); if (!svc_getargs(transp, xdr_argument, (char *) &mult_argument)){ printf("problem with getargs¥n"); svcerr_decode(transp); exit(1); } sw_mult_data = (struct svc2_mult_args *) malloc(sizeof (struct svc2_mult_args)); sw_mult_data->transp = transp; sw_mult_data->mult_argument.a = mult_argument.a; sw_mult_data->mult_argument.b = mult_argument.b; if (ret = thr_create(NULL, THR_MIN_STACK + 16 * 1024, svc2_mult_worker, (void *) sw_mult_data, THR_DETACHED, &worker_id)){ printf("SERVER: thr_create failed:"); printf("return value %d", ret); printf("for multiply thread¥n"); exit(1); break; default: svcerr_noproc(transp); svc_done(transp); break; } } u_short mult_arg(); long add_one(); void *svc2_add_worker(add_arg) struct svc2_add_args *add_arg; { long *result; bool_t (*xdr_result)(); xdr_result = xdr_long; result = (long *) malloc(sizeof (long)); *result = add_one(add_arg->argument); if (!svc_sendreply(add_arg->transp, xdr_result, (caddr_t) result)){ printf("sendreply failed¥n"); svcerr_systemerr(add_arg->transp); svc_done(add_arg->transp); thr_exit((void *) -1); } svc_done(add_arg->transp); thr_exit((void *) 0); } void *svc2_mult_worker(m_arg) struct svc2_mult_args *m_arg; { u_short *result; bool_t (*xdr_result)(); xdr_result = xdr_u_short; result = (u_short *) malloc(sizeof (u_short)); *result = mult_arg(&m_arg->mult_argument); if (!svc_sendreply(m_arg->transp, xdr_result, (caddr_t) result)){ printf("sendreply failed¥n"); svcerr_systemerr(m_arg->transp); svc_done(m_arg->transp); thr_exit((void *) -1); } svc_done(m_arg->transp); thr_exit((void *) 0); } u_short mult_arg(pair) intpair *pair; { u_short result; result = pair->a * pair->b; return (result);} long add_one(arg) long arg; { return (++arg); }
例 4-39 に示すサンプルプログラムは、あるホストのファイルを別のホストにコピーするプログラムです。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-40 と 例 4-41には、例 4-39 に示した 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); }
例 4-41 では、受信側のルーチンの定義します。サーバ側では、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 ポインタを渡された場合は何もしません。
これまでに説明したことをまとめると、次のようになります。
svc_getargs() は、XDR ルーチンを呼び出してデシリアライズを行います。
トランスポート独立の RPC ルーチン (TI-RPC ルーチン) を使用すると、アプリケーション開発者はトランスポート層へのアクセスレベルを自由に選択できます。最上位レベルのルーチンは、トランスポートが完全に抽象化されて、本当の意味でトランスポート独立になっています。下位レベルのルーチンを使用すると、旧バージョンと同じように個々のトランスポートに依存したアクセスレベルになります。
この節では、トランスポート特定 RPC (TS-RPC) アプリケーションを TI-RPC へ移行するための非公式ガイドになっています。表 4-9 では、いくつかのルーチンを選んで相違点を示します。ソケットとトランスポート層インタフェース (TLI) の移行の問題点についての詳細は、『Transport Interfaces Programming Guide』を参照してください。
TCP または UDP に基づくアプリケーションはバイナリ互換モードで実行できます。すべてのソースファイルをコンパイルし直したり、リンクし直したりできるのは、一部のアプリケーションだけです。RPC呼び出しだけを使用し、ソケット、TCP、UDP に固有の機能を使用していないアプリケーションがこれに当たります。
ソケットセマンティクスに依存していたり、TCP や UDP の固有の機能を使用しているアプリケーションでは、コードの変更や追加が必要な場合があります。ホストアドレス形式を使用したり、バークレイUNIX の特権ポートを使用するアプリケーションがこれに当たります。
ライブラリ内部や個々のソケット仕様に依存していたり、特定のトランスポートアドレスに依存するアプリケーションは、移行の手間も大きく、本質的な変更が必要な場合もあります。
アプリケーションがトランスポート独立になるため、より多くのトランスポート上で実行できます。
アプリケーションの効率を改善する新規インタフェースが使用できます。
バイナリレベルの互換性の影響は、ネイティブモードより少なくなります。
旧インタフェースは、将来のバージョンで使用できなくなる可能性があります。
ネットワーク関数は libc から外されました。コンパイル時には libnsl を明示的に指定して、ネットワーク・サービス・ルーチンをリンクする必要があります。
旧インタフェースの多くは libnsl ライブラリでもサポートされていますが、TCP と UDP でしか使用できません。それ以外の新たなトランスポートを利用するには、新インタフェースを使用する必要があります。
トランスポート独立にするには、アドレスを直接使用できません。すなわち、アプリケーションでアドレス変換を行う必要があります。
トランスポート独立型の RPC とトランスポート特定の RPC との主な相違点を 表 4-9に示します。TI-RPC と TS-RPC の比較については、「旧バージョンとの比較」のサンプルプログラムを参照してください。
表 4-9 TI-RPC と TS-RPC の相違点
この節では、RPC ライブラリ関数を機能別にグループ化して示します。各グループ内では、旧バージョンと同じ関数、機能が追加された関数、旧バージョンにはなかった新規関数、に分けて示します。
アスタリスクの付いた関数は、新バージョンへの移行期間はサポートされていますが、Solaris の将来のバージョンではなくなる可能性があります。
次の関数は、旧バージョンと同じで、現在の SunOS で使用できます。
clnt_destroy clnt_pcreateerror *clntraw_create clnt_spcreateerror *clnttcp_create *clntudp_bufcreate *clntudp_create clnt_control clnt_create clnt_create_timed clnt_create_vers clnt_dg_create clnt_raw_create clnt_tli_create clnt_tp_create clnt_tp_create_timed clnt_vc_create
次の関数は、旧バージョンと同じで、現在の SunOS で使用できます。
svc_destroysvcfd_create *svc_raw_create *svc_tp_create *svcudp_create *svc_udp_bufcreate svc_create svc_dg_create svc_fd_create svc_raw_create svc_tli_create svc_tp_create svc_vc_create
次の関数は、旧バージョンと同じで、現在の SunOS で使用できます。
*registerrpc *svc_register *svc_unregister xprt_register xprt_unregister rpc_reg svc_reg svc_unreg
次の関数は、旧バージョンと同じで、現在の SunOS で使用できます。
*callrpc clnt_call *svc_getcaller - IP に基づくトランスポートでのみ使用可。 rpc_call svc_getrpccaller
次の関数の機能は旧バージョンと同じです。旧バージョンとの互換性を保つためにだけサポートされています。
*clnt_broadcast
clnt_broadcast() は portmap サービスにだけブロードキャストできます。
portmap と rpcbind の両方にブロードキャストできる次の関数が現在の SunOS で使用できます。
rpc_broadcast
TI-RPC ライブラリ関数は、portmap と rpcbind の両方で使用できますが、それぞれサービスが異なるため、次のように 2 組の関数が提供されています。
次の関数は portmap と共に使用します。
pmap_set pmap_unset pmap_getport pmap_getmaps pmap_rmtcall
次の関数は rpcbind と共に使用します。
rpcb_set rpcb_unset rpcb_getaddr rpcb_getmaps rpcb_rmtcall
次の関数の機能は旧バージョンと同じです。旧バージョンとの互換性を保つためにだけサポートされています。
authdes_create authunix_create authunix_create_default authdes_seccreate authsys_create authsys_create_default
現バージョンの rpcbind ではタイムサービス (主として、安全な RPC のためにクライアント側とサーバ側の時間を同期させるときに使用) が提供されており、rpcb_gettime() 関数で利用できます。pmap_getport() と rpcb_getaddr() は、登録サービスのポート番号を取り出すときに使用します。サーバでバージョンが 2、3、4 の rcpbind が実行されている場合には、rpcb_getaddr() を使用します。pmap_getport() はバージョン 2 が実行されている場合しか使用できません。
例 4-42 と 例 4-43 では、クライアント作成部分が TS-RPC と TI-RPC とでどう違うかを示します。どちらのプログラムも次のことを実行します。
UDP 記述子を作成します。
遠隔ホストの RPC 結合プロセスと通信してサービスアドレスを得ます。
遠隔サービスのアドレスを記述子に結合します。
クライアントハンドルを作成してタイムアウト値を設定します。
struct hostent *h; struct sockaddr_in sin; int sock = RPC_ANYSOCK; u_short port; struct timeval wait; if ((h = gethostbyname( "host" )) == (struct hostent *) NULL) { syslog(LOG_ERR, "gethostbyname failed"); exit(1); } sin.sin_addr.s_addr = *(u_long *) hp->h_addr; if ((port = pmap_getport(&sin, PROGRAM, VERSION, "udp")) == 0) { syslog (LOG_ERR, "pmap_getport failed"); exit(1); } else sin.sin_port = htons(port); wait.tv_sec = 25; wait.tv_usec = 0; clntudp_create(&sin, PROGRAM, VERSION, wait, &sock);
TI-RPC では、UDP トランスポートは netid udp を持つものとみなします。netidはよく知られた名前でなくてもかまいません。
struct netconfig *nconf; struct netconfig *getnetconfigent(); struct t_bind *tbind; struct timeval wait; nconf = getnetconfigent("udp"); if (nconf == (struct netconfig *) NULL) { syslog(LOG_ERR, "getnetconfigent for udp failed"); exit(1); } fd = t_open(nconf->nc_device, O_RDWR, (struct t_info *)NULL); if (fd == -1) { syslog(LOG_ERR, "t_open failed"); exit(1); } tbind = (struct t_bind *) t_alloc(fd, T_BIND, T_ADDR); if (tbind == (struct t_bind *) NULL) { syslog(LOG_ERR, "t_bind failed"); exit(1); } if (rpcb_getaddr( PROGRAM, VERSION, nconf, &tbind->addr, "host") == FALSE) { syslog(LOG_ERR, "rpcb_getaddr failed"); exit(1); } cl = clnt_tli_create(fd, nconf, &tbind->addr, PROGRAM, VERSION, 0, 0); (void) t_free((char *) tbind, T_BIND); if (cl == (CLIENT *) NULL) { syslog(LOG_ERR, "clnt_tli_create failed"); exit(1); } wait.tv_sec = 25; wait.tv_usec = 0; clnt_control(cl, CLSET_TIMEOUT, (char *) &wait);
例 4-44 と 例 4-45では、ブロードキャスト部分が旧バージョンと SunOS 5.3 とでどう違うかを示します。SunOS 4.x の clnt_broadcast() は SunOS 5.3 の rpc_broadcast() とほぼ同じです。大きく異なるのは、collectnames() 関数で重複アドレスを削除し、ブロードキャストに応答したホスト名を表示する点です。
statstime sw; extern int collectnames(); clnt_broadcast(RSTATPROG, RSTATVERS_TIME, RSTATPROC_STATS, xdr_void, NULL, xdr_statstime, &sw, collectnames); ... collectnames(resultsp, raddrp) char *resultsp; struct sockaddr_in *raddrp; { u_long addr; struct entry *entryp, *lim; struct hostent *hp; extern int curentry; /* 重複アドレスはカット */ addr = raddrp->sin_addr.s_addr; lim = entry + curentry; for (entryp = entry; entryp < lim; entryp++) if (addr == entryp->addr) return (0); ... /* ホスト名がわかればホスト名、わからなければアドレスを表示 */ hp = gethostbyaddr(&raddrp->sin_addr.s_addr, sizeof(u_long), AF_INET); if( hp == (struct hostent *) NULL) printf("0x%x", addr); else printf("%s", hp->h_name); }
例 4-45 は、TI-RPC におけるブロードキャストを示します。
statstime sw; extern int collectnames(); rpc_broadcast(RSTATPROG, RSTATVERS_TIME, RSTATPROC_STATS, xdr_void, NULL, xdr_statstime, &sw, collectnames, (char *) 0); ... collectnames(resultsp, taddr, nconf) char *resultsp; struct t_bind *taddr; struct netconfig *nconf; { struct entry *entryp, *lim; struct nd_hostservlist *hs; extern int curentry; extern int netbufeq(); /* 重複アドレスはカット */ lim = entry + curentry; for (entryp = entry; entryp < lim; entryp++) if (netbufeq( &taddr->addr, entryp->addr)) return (0); ... /* ホスト名がわかればホスト名、わからなければアドレスを表示 */ if (netdir_getbyaddr( nconf, &hs, &taddr->addr ) == ND_OK) printf("%s", hs->h_hostservs->h_host); else { char *uaddr = taddr2uaddr(nconf, &taddr->addr); if (uaddr) { printf("%s¥n", uaddr); (void) free(uaddr); } else printf("unknown"); } } netbufeq(a, b) struct netbuf *a, *b; { return(a->len == b->len && !memcmp( a->buf, b->buf, a->len)); }