EternalWindows
RPC / バインディング
対応するクライアントはこちら

今回は、RPCサーバーの開発を行います。 前節で説明したように、サーバーはクライアントが呼び出すための関数を実装することになりますが、 こうした関数を実装しただけでは、クライアントが関数を呼び出せるようにはなりません。 RPCではネットワーク通信の部分がスタブによって隠蔽されているわけですが、 ネットワーク通信を行うための準備はアプリケーションが行っておく必要があります。 ここで述べている準備とは、Winsockで言うところのプロトコルの選択や、 クライアントからの要求を受け取るためのリッスンのことです。

RPCが定義しているのは、アプリケーションからスタブ、 及びスタブからRPCランタイムライブラリへのデータの受け渡し方法であって、 通信の部分は既存のネットワークAPIを使用しています。 つまり、Winsockを使用して通信を行うこともできますし、 名前付きパイプを使用して通信を行うこともできます。 アプリケーションはRpcServerUseProtseqEpを呼び出すことで、 使用するAPIなどを設定することができます。

RPC_STATUS RPC_ENTRY RpcServerUseProtseqEp(
    unsigned char *Protseq,
    unsigned int MaxCalls,
    unsigned char *Endpoint,
    void *SecurityDescriptor
);

Protseqは、プロトコルシーケンスを表す文字列を指定します。 MaxCallsは、キューに格納できる接続要求の最大数を指定します。 この引数はProtseqがncacn_ip_tcpの場合に意味を持ち、 RPC_C_PROTSEQ_MAX_REQS_DEFAULTを指定することもできます。 Endpointは、エンドポイントを表す文字列を指定します。 SecurityDescriptorは、セキュリティ記述子を指定します。 この引数はProtseqがncacn_npまたはncalrpcの場合に意味を持ちます。

プロトコルシーケンスは、使用するネットワークプロトコル、 またはネットワークAPIを表す文字列です。 次に、定義されているプロトコルシーケンスの一部を示します。

プロトコルシーケンス 説明
ncacn_ip_tcp TCP/IP通信を行う。 エンドポイントには、ポート番号を指定する。
ncadg_ip_udp UDP/IP通信を行う。 エンドポイントには、ポート番号を指定する。
ncacn_np 名前付きパイプによる通信を行う。 エンドポイントには、\\pipe\\paienameを指定する(paienameは任意の文字列)。
ncalrpc LPC通信を行う。 エンドポイントには、任意の文字列を指定する。

使用するプロトコルシーケンスによって、エンドポイントの形式も変化する点に注意してください。 たとえば、ncacn_ip_tcpの場合はポート番号を指定しますが、ncacn_npの場合はパイプの名前を指定します。

クライアントがサーバーの関数を呼び出すためには、 IDLファイルに記述されたインターフェースをネットワーク上で識別できなければなりません。 このため、サーバーはRpcServerRegisterIfを呼び出すことで、 RPCランタイムライブラリにインターフェースを登録する必要があります。

RPC_STATUS RPC_ENTRY RpcServerRegisterIf(
    RPC_IF_HANDLE IfSpec,
    UUID *MgrTypeUuid,
    RPC_MGR_EPV *MgrEpv
);

IfSpecは、MIDLによって作成されたインターフェースハンドルを指定します。 MgrTypeUuidは、MgrEpvに関連付けるUUIDを指定します。 MgrEpvは、EPV(Entry Point Vector)を指定します。 通常は、MIDLによって作成されたEPVを使用するため、NULLを指定します。

インターフェースの登録を終えたサーバーは、 RpcServerListenを呼び出してリッスン状態に入ります。 これにより、クライアントはサーバーの関数を呼び出せることになります。

RPC_STATUS RPC_ENTRY RpcServerListen(
    unsigned int MinimumCallThreads,
    unsigned int MaxCalls,
    unsigned int DontWait
);

MinimumCallThreadsは、RPCランタイムライブラリが内部で作成するスレッド数の最小値を指定します。 サーバーが実装した関数のコードを実行するのは、このような内部的に作成されたスレッドになります。 MaxCallsは、サーバーの関数を並列して実行できる最大数を指定します。 RPC_C_LISTEN_MAX_CALLS_DEFAULTを指定することもできます。 DontWaitは、この関数の呼び出しでブロッキングを行うことがどうかを指定します。 0でない値を指定した場合は直ちに制御が返りますが、 0を指定した場合はRpcMgmtStopServerListeningが呼ばれるまで、 制御が返ることはありません。

次に、今回使用するIDLファイルを示します。

[
	uuid (3b5c8791-ada4-4f39-9644-9edd05e199ac),
	version (1.0)
]
interface sample
{
	int ServerFunction(void);
	void Shutdown(void);
}

ヘッダには、uuidとversionを指定します。 uuidの中身は、IDLファイル毎に異なっている必要があります。 ボディには、サーバーが公開する関数を指定します。 次に、今回のACFファイルを指定します。

[
	implicit_handle(handle_t hBinding)
]
interface sample
{
}

implicit_handleは、暗黙的バインディングと呼ばれ、 クライアントが明示的にバインディングを行うことを意味しています。 handle_tは、バインディングハンドルを示す型であり、 hBindingがバインディングハンドルを示す変数です。 バインディングについては、クライアントの説明時に取り上げます。 次に、今回のプログラムを指定します。

#include <windows.h>
#include <rpc.h>
#include "sample_h.h"

#pragma comment (lib, "rpcrt4.lib")

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	if (RpcServerUseProtseqEpW((USHORT *)L"ncacn_np", 0, (USHORT *)L"\\pipe\\sample", NULL) != RPC_S_OK) {
		MessageBox(NULL, TEXT("プロトコルシーケンスの指定に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}
	
	RpcServerRegisterIf(sample_v1_0_s_ifspec, NULL, NULL);
	
	if (RpcServerListen(1, RPC_C_LISTEN_MAX_CALLS_DEFAULT, FALSE) != RPC_S_OK) {
		MessageBox(NULL, TEXT("リッスンに失敗しました。"), NULL, MB_ICONWARNING);
		RpcServerUnregisterIf(NULL, NULL, FALSE);
		return 0;
	}

	MessageBox(NULL, TEXT("終了します。"), TEXT("OK"), MB_OK);

	RpcServerUnregisterIf(NULL, NULL, FALSE);

	return 0;
}

int ServerFunction(void)
{
	return 1;
}

void Shutdown(void)
{
	RpcMgmtStopServerListening(NULL);
}

void __RPC_FAR * __RPC_API midl_user_allocate(size_t len)
{
	return(malloc(len));
}

void __RPC_API midl_user_free(void __RPC_FAR * ptr)
{
	free(ptr);
}

まず、RpcServerUseProtseqEpを呼び出して、プロトコルシーケンスとエンドポイントの設定を行います。 第1引数にncacn_npを指定しているため、使用するプロトコルシーケンスは名前付きパイプであり、 第3引数に指定するエンドポイントは名前付きパイプの形式に設定します。 第2引数は、ncacn_npの場合は0で問題ありません。 RpcServerUseProtseqEpのような文字列を受け取る関数は、ANSI版とUNICODE版の2種類が存在しますが、 今回のコードでは明示的にUNICODE版の関数を呼び出しています。 TEXTマクロを使用すればよいように思えますが、 RPC関数ではANSI文字列をUCHAR型のポインタ、UNICODE文字列をUSHORT型のポインタで表すため、 UNICODE文字列をwchar_tと解釈するTEXTマクロを使用することはできません。 ちなみに、RPC_CSTR(UCHAR *)やRPC_WSTR(USHORT *)という型を使用することもできますが、 この型は古いSDKで定義されていないため注意してください。 続いて、RpcServerRegisterIfでインターフェースを登録します。

RpcServerRegisterIf(sample_v1_0_s_ifspec, NULL, NULL);

第1引数に指定しているsample_v1_0_s_ifspecは、 sample_h.hにRPC_IF_HANDLE型として定義されています。 これは、インターフェースハンドルであるため、 RpcServerRegisterIfに指定することができます。 この変数の名前は、IDLファイルに記述したインターフェースの名前とバージョンによって決定されます。 RpcServerListenの呼び出しは、次のようになっています。

if (RpcServerListen(1, RPC_C_LISTEN_MAX_CALLS_DEFAULT, FALSE) != RPC_S_OK) {
	MessageBox(NULL, TEXT("RPC要求のリッスンに失敗しました。"), NULL, MB_ICONWARNING);
	RpcServerUnregisterIf(NULL, NULL, FALSE);
	return 0;
}

MessageBox(NULL, TEXT("終了します。"), TEXT("OK"), MB_OK);

第3引数にFALSEを指定していることから、RpcServerListenは内部で処理を待機することになります。 これにより、以降に存在するコードは、RpcMgmtStopServerListeningを呼び出さない限り、 実行されることはありません。 逆に、処理を待機したくない場合は、第3引数にTRUEを指定することになるでしょう。 TRUEを指定した場合でも、RpcMgmtWaitServerListenを呼び出せばいつでも待機できるようになります。 RpcServerListenの後にMessageBoxを呼び出しているのは、 RpcServerListenから制御が返ったことをユーザーに知らせるためです。 クライアントが終了したのにも関わらず、サーバーがメッセージボックスを表示していない場合は、 サーバーをタスクマネージャなどの強制終了してください。

今回、サーバーがクライアントに公開している関数は、ServerFunctionとShutdownの2つです。 ServerFunctionはサンプルとして用意している関数であり、 実際の開発ではクライアントにとって意味のある実装を行うことになります。 一方、Shutdownという関数は、クライアントがサーバーのリッスン状態を停止するための関数です。 クライアントは、これ以上サーバーの関数を呼び出す必要がなくなった段階でShutdownを呼び出します。

int ServerFunction(void)
{
	return 1;
}

void Shutdown(void)
{
	RpcMgmtStopServerListening(NULL);
}

Shutdownが呼び出されると、サーバーは自分自身でRpcMgmtStopServerListeningを呼び出せることになります。 これにより、RpcServerListenを呼び出したスレッドは、関数から制御が返ることになります。 Shutdownのような関数を公開せず、クライアントが直接RpcMgmtStopServerListeningを呼び出すことも可能ではありますが、 この場合はサーバーがRpcMgmtSetAuthorizationFnを予め呼び出しておく必要があり、少し処理が複雑になります。 クライアントがサーバーの関数を呼び出さないことを明確にするためにも、 できるだけShutdownのような関数を呼び出す設計にしておいたほうがよいでしょう。 ちなみに、RpcMgmtStopServerListeningを呼び出す関数の名前はShutdownにすることが多いため、 これに倣うようにしています。

endpointキーワードの利用

今回は、RpcServerUseProtseqEpでプロトコルシーケンスとエンドポイントの設定を行いましたが、 このような情報はIDLファイルのendpointキーワードで指定することもできます。

[
	uuid (12345678-4321-1234-4321-987654321CBA),
	version (1.0),
	endpoint ("ncacn_np:[\\pipe\\sample]")
]
interface sample
{
	int ServerFunction(void);
	void Shutdown(void);
}

このようにした場合、サーバーはRpcServerUseAllProtseqsIfを呼び出すことによって、 IDLファイルのプロトコルシーケンスとエンドポイントを参照します。

RpcServerUseAllProtseqsIf(1, sample_v1_0_s_ifspec, NULL);

第1引数は、RpcServerUseProtseqEpの第2引数と同じ意味を持ちます。 第2引数は、インターフェースハンドルを指定します。 第3引数は、RpcServerUseProtseqEpの第4引数と同じ意味を持ちます。



戻る