EternalWindows
RPC / 非同期RPC
対応するクライアントはこちら

RPCの1つの魅力は、実行したい処理をサーバー上で行えるという点にあります。 これにより、クライアントは他の処理を行う余裕ができるわけですが、 関数を同期的に呼び出した場合は、その余裕を上手く活用できるとはいえません。 たとえば、ServerFunctionの処理が非常に時間の掛かるものであった場合、 当然関数が制御を返す時間も長くなりますから、 クライアントは処理が終わるまで待たされることになり、 次の処理に入ることができません。 よって、このように時間の掛かる関数を呼び出す場合は、非同期RPCの仕組みを利用するべきといえます。 関数を非同期に呼び出した場合、その関数からは直ちに制御が返ることになり、 処理が完了したかを確認したくなった段階で待機処理を行うことができるため、 柔軟な実装が可能になります。

非同期呼び出しに対応する関数を実装する場合は、 その関数の処理が終了する段階でRpcAsyncCompleteCallを呼び出すことになります。 単純に関数から抜けるだけでは、クライアントに処理の終了が通知されないため、 この関数は必ず呼び出す必要があります。

RPC_STATUS RPC_ENTRY RpcAsyncCompleteCall(
    PRPC_ASYNC_STATE pAsync,
    PVOID Reply
);

pAsyncは、RPC_ASYNC_STATE構造体のアドレスを指定します。 Replyは、戻り値を格納したバッファを指定します。 戻り値が不要な場合はNULLを指定します。 この関数は、クライアントが戻り値を取得する目的で呼び出すこともあります。

サーバーが時間の掛かる処理を行っている場合、 クライアントは処理を途中でキャンセルしたい場合があるかもしれません。 こうした要求が発生しているかどうかを確認するには、 RpcServerTestCancelを呼び出します。

RPC_STATUS RPC_ENTRY RpcServerTestCancel(
    RPC_BINDING_HANDLE BindingHandle
);

BindingHandleは、RpcAsyncGetCallHandleの戻り値を指定します。 RpcAsyncGetCallHandleは、RPC_SYNC_STATE構造体のRuntimeInfoメンバを返すだけです。 戻り値は、クライアントからのキャンセルが発生している場合にRPC_S_CALL_CANCELLEDが返り、 発生していない場合はRPC_S_CALL_IN_PROGRESSが返ります。

次に、今回のIDLファイルを指定します。

[
	uuid (71746326-fc7b-4a92-b41e-24ec42069090),
	version (1.0)
]
interface sample
{
	int ServerFunction(void);
	void Shutdown(void);
}

次に、今回のACFファイルを指定します。

[
	implicit_handle(handle_t hBinding)
]
interface sample
{
	[async] ServerFunction();
}

[async]というキーワードを指定した場合、 その関数の第1引数にはRPC_ASYNC_STATE構造体へのアドレスが自動的に設定されます。 また、こうした関数内では戻り値をreturn文で返すことができませんが、 IDL内での定義では戻り値の型を指定するようにしてください。 たとえば、int型を指定すれば、RpcAsyncCompleteCallでint型の値を返すことができます。 次に、今回のプログラムを示します。

#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;
}

void ServerFunction(IN PRPC_ASYNC_STATE pAsync)
{
	int nReply;

	Sleep(1000);

	if (RpcServerTestCancel(RpcAsyncGetCallHandle(pAsync)) == RPC_S_CALL_CANCELLED) {
		nReply = 0;
		RpcAsyncCompleteCall(pAsync, &nReply);
		return;
	}
	
	Sleep(1000);
	
	nReply = 1;
	RpcAsyncCompleteCall(pAsync, &nReply);
}

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);
}

今回のServerFunctionでは、時間の掛かる処理を行うものとします。 まず、約1秒間ほど掛かる処理(Sleepに相当)を行い、 次の処理に入る前にRpcServerTestCancelを呼び出して、 クライアントからのキャンセル要求を確認します。 ここで、RPC_S_CALL_CANCELLEDを返る場合は、 クライアントがRpcAsyncCancelCallを呼び出したことを意味するため、 関数を終了することになります。 RpcAsyncCompleteCallを呼び出しているのは、この関数が終了したことをクライアントに示すためであり、 これを呼び出さないとクライアントが半永久的に待機してしまう可能性があります。 nReplyに指定する値はServerFunctionの戻り値であり、その値は特に問いません。 ここでは、キャンセルによって終了した場合は0を指定し、 全ての処理を終えた場合は1を指定するようにしています。 IDL内で指定したintという型は、RpcAsyncCompleteCallの第2引数の型を意味しており、 ServerFunctionのプロトタイプ上における戻り値は常にvoidであることに注意してください。

RpcServerSubscribeForNotificationについて

サーバーの関数内で時間の掛かる処理を行うような場合、 一定間隔でクライアントからのキャンセルを確認するのは煩わしいことです。 Windows Vistaから登場したRpcServerSubscribeForNotificationを呼び出せば、 クライアントがキャンセルや切断を行ったタイミングでアプリケーションに通知が入るため、 これを利用する方法を次に示します。

void RPC_ENTRY RpcNotificationRountine(struct _RPC_ASYNC_STATE *pAsync, void *Context, RPC_ASYNC_EVENT Event)
{
	// クライアントがキャンセルした場合に実行される。
}

void ServerFunction(IN PRPC_ASYNC_STATE pAsync)
{
	RPC_ASYNC_NOTIFICATION_INFO notificationInfo;
	unsigned long               lQueued;
	
	notificationInfo.NotificationRoutine = RpcNotificationRountine;
	RpcServerSubscribeForNotification(NULL, RpcNotificationCallCancel, RpcNotificationTypeCallback, ¬ificationInfo);

	// 時間の掛かる処理を行う

	RpcServerUnsubscribeForNotification(NULL, RpcNotificationCallCancel, &lQueued);
}

RpcServerSubscribeForNotificationの第1引数は、NULLで問題ありません。 第2引数は、どのような要求を検出するかどうかであり、 RpcNotificationCallCancelを指定するとクライアントからのキャンセルを検出できるようになります。 第3引数は通知タイプであり、コールバック関数で通知を受け取る場合はRpcNotificationTypeCallbackを指定します。 この場合は、RPC_ASYNC_NOTIFICATION_INFO構造体のNotificationRoutineにコールバック関数を指定しておく必要があります。 通知タイプにはこの他に、イベントオブジェクトを利用する方法などがあります。



戻る