3分で「わかった気」になる RPC【改訂２版】

# RPCとは

RPC(Remote Procedure Call) はプロセス間通信の一種です。ここでは 1990 年代に広く普及した ONC RPC（いわゆる SunRPC）の技術を解説します。対象読者としてイーサネットの基本（IPアドレスとTCP通信）とプログラムの基本（関数、変数）がわかる 18 歳の新人技術者を想定しています。

# RPC の背景と目的

RPC は遠隔（リモート）にあるコンピュータの関数（プロシージャ）を実行（コール）するためのソフトウェア技術です。1970年代に次の目標を達成するために考案されました。

- **遠隔地**にあるコンピュータや方式の異なるマシン間で透過的なデータのやり取りを可能にする
- 様々なサービスに対応できる**一貫性のある環境**を提供する
- 高いレベルの機能を実現し**プログラマの負担を減らす**

![021_RemoteProcedureCall.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3160433/96d12805-673c-5fff-dfa0-a392d23a5bc1.png)

# RPC の歴史

1988 年にサンマイクロシステムズが**ONC RPC** を開発しました。その後、**多くのプログラム言語で遠隔呼び出しを実現するためのRPC プロトコル**が開発されました。主な RPC プロトコルを以下に示します。

<!--
|1976 年|スタンフォード研究所|[RFC 707 High-Level Framework for Network-Based Resource Sharing](https://www.ietf.org/rfc/rfc707.txt)|なし|RPCのコンセプトの提案|
|1981 年|ゼロックス|[Courier](http://www.bitsavers.org/pdf/xerox/parc/techReports/CSL-83-7_Implementing_Remote_Procedure_Calls.pdf)|なし|初期の RPC プロトコルの１つ| -->
<!-- |1995 年|サン・マイクロシステムズ|[RPC 1831 Remote Procedure Call Protocol Specification Version 2](https://www.ietf.org/rfc/rfc1831.txt)|XDR|RFC 1057 の仕様を洗練し、**ONC RPC** として再発行| -->
|年|開発者|プロトコル名|データ形式|概要|
|----|----|----|----|----|
|1988 年|サン・マイクロシステムズ|[RFC 1057: RPC: Remote Procedure Call Protocol Specification Version 2](https://www.ietf.org/rfc/rfc1057.txt)|XDR|オープンソースの Unix 向け RPC プロトコル|
|1995 年|マイクロソフト|[MS-RPC](https://learn.microsoft.com/ja-jp/windows/win32/rpc/overviews)|DCOM|Windows 向け RPC プロトコル|
|2003 年|サン・マイクロシステムズ|[**Java Remote Method Invocation(RMI)**](https://docs.oracle.com/javase/jp/9/docs/specs/rmi/index.html)|Javaオブジェクト直列化仕様|Java 言語向け RPC プロトコル|
|2015 年|グーグル|[**gRPC**](https://atmarkit.itmedia.co.jp/ait/articles/2211/04/news002.html)|Protocol Buffers|HTTP/2 を使った RPC プロトコル|

これらの RPC 実装はそれぞれ「通信処理の手順」「データ形式」が違うため互換性はありません。

一方で、以下の特徴は共通しています。

- **クライアント・サーバモデル**
- **関数・引数・戻り値**の概念があり、通常の関数呼び出しと同様に実行できる
- **インタフェース記述言語**（IDL）を使ってソースコードの自動生成ができる

# 一般的な関数呼び出しの手順

一般的なプログラムでの関数呼び出しは

1. 親プログラムが**関数名と引数**を指定して、特定の関数を呼び出す
2. その**関数内の処理が実行される**
3. 結果の**戻り値**が、親プログラムに渡される

という順序で行われます。

![352_RPC_LocalFunctionCall_Sequence.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3160433/4f11c034-2465-8146-fbb7-4add77640ccd.png)

# 一般的な RPC 通信の手順

RPC はネットワーク環境のクライアントとサーバ間の通信を、一般的な関数呼び出しのように見せることを目的としています。

関数呼び出しを通信に変換する手順は以下の通りです。

1. **クライアント側の親プログラム**は、その処理を実行する関数が自分のプログラム内に存在するかのように、特定の関数を呼び出します
2. 呼び出した関数名、引数などは「**スタブ** (Stub)」と呼ばれるプログラムによって、RPC のプロトコルで定められた **RPC メッセージ**の形式に組み立てられます
3. TCP や UDP といったトランスポートプロトコルによって、このメッセージがサーバ側に**転送**されます
4. リモートシステム上では、サーバ側スタブプログラムが要求の到着を待っています
5. **サーバ側のスタブ・プログラム**は呼び出された関数、引数を取り出して、**該当するプログラム**を実際に呼び出します
6. サーバ側で要求された**関数**が実行されます
7. 処理を終えた関数は、結果をサーバ側スタブに戻します
8. サーバ側のスタブ・プログラムは、結果を返すための **RPC メッセージ**を組み立てます
9. そのRPCメッセージがクライアント側に**転送**されます
10. **クライアント側のスタブ・プログラム**が結果を取り出します
11. 必要があれば戻り値の型を変換し、処理を依頼した**親プログラム**に戻します

基本的なプログラム構成を以下に示します。

![342.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3160433/1b774c27-23b7-7c05-389f-cb0fdc706d9f.png)

シーケンス図で表現した処理の流れを図示します。

![353_RPC_RemoteProcedureCall_Sequence.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3160433/e51d1da4-1da5-bd2d-d11a-1e611e18daa9.png)

# RPC の基本データ型

RPC では方式の異なるコンピュータ間で相互通信できるようデータの構造を **XDR** として規定しています。XDR の詳細は [RFC 1832 XDR: External Data Representation Standard](https://www.ietf.org/rfc/rfc1832.txt) として公開されています。

![031_RPC_Layer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3160433/0a1494e7-df9a-669e-2217-12f66c8ea7c7.png)

XDR で規定されている主なデータ型を以下に挙げます。

|XDR のデータ型|C 言語による代替表現|説明|サイズ|
|--------|----|----|----|
|int|int|符号付き整数型|4 バイト|
|unsigned int|u_int|符号なし整数型|4 バイト|
|float|float|浮動小数点型|4 バイト|
|double|double|倍精度浮動小数点型|8 バイト|
|opaque var[n];|char var[n];|固定長の配列型|各要素が 1 番目から n 番目まで並びます|
|opaque var<m>;|struct {<BR>  u_int var_len;<BR>  char * var_val;<BR>} var;|可変長の配列型|長さを表現する4バイトのシーケンスが先頭に付きます。各要素が 1 番目から n 番目まで並びます|
|string var<m>;|struct {<BR>  u_int var_len;<BR>  char * var_val;<BR>} var;|文字列|長さを表現する4バイトのシーケンスが先頭に付きます。全体のバイト数が 4 の倍数でない場合、末尾に追加の 0 が付きます|

整数型は以下のメモリ配置になります。

![551_RFC1014_3.1_Integer_Format.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3160433/29a18c00-08d9-1f4a-499d-118248004b87.png)

文字列型は下のメモリ配置になります。

![552_RFC1014_3.10_String_Format.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3160433/17c3111f-e241-f19a-a75c-d89052057ab9.png)

# RPC の電文形式
RPC の通信仕様は [**RFC 1057 Remote Procedure Call Version 2**](https://www.ietf.org/rfc/rfc1057.txt) で公開されています。

## 呼び出しメッセージの形式
RPC でリモートの関数を呼び出す時のメッセージ形式は以下のようになります。

![362_RFC1057_Call_HeaderFormat.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3160433/376d43c0-99e1-72ec-a9fb-de0a99463d69.png)

+ **LAST_FLAG**：トランスポートプロトコルが TCP の場合のみ付与されます。呼び出しデータが複数のメッセージに分割されているかを示します。
+ **flag_header**：トランスポートプロトコルが  TCP の場合のみ付きます。flag_header を除いたメッセージ全体のバイト数を示します。
+ **xid**：１つの関数の呼び出しと応答で同一の値をとります。トランザクション識別子とも呼ばれます。
+ **msg_type**：呼び出しメッセージか応答メッセージの区別を示します。

|値|定数名|意味|
|--|--|--|
|**0**|**CALL**|**関数の呼び出し**|
|1|REPLY|関数の応答|

+ rpcvers： RPC プロトコルのバージョンを示します。通常は 2 です。
+ **prog, vers, proc**：prog（プログラム番号）, vers（バージョン番号）, proc（プロシージャ番号） の組は実行する関数を示します
+ cred.flavor, cred.body, verf.flavor, verf.body：これらはクライアント認証に関するパラメータです。認証を使わない場合、すべて 0 になります。
+ **データ**：関数の引数を示します。

## 応答メッセージの形式

関数の実行結果を返す時のメッセージ形式は以下のようになります。

![364_RFC1057_Reply_HeaderFormat.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3160433/cc2000d6-cc04-4120-1a8f-f508e5a0dc76.png)

+ **LAST_FLAG**：トランスポートプロトコルが TCP の場合のみつきます。応答データが複数のメッセージに分割されているかを示します。
+ **flag_header**：トランスポートプロトコルが TCP の場合のみつきます。flag_header を除いたメッセージ全体のバイト数を示します。
+ **xid**：呼び出しメッセージのトランザクション識別子を示します。
+ **msg_type**：呼び出しメッセージか応答メッセージの区別を示します。

|値|定数名|意味|
|--|--|--|
|0|CALL|関数の呼び出し|
|**1**|**REPLY**|**関数の応答**|

+ **reply_stat**：RPCメッセーを受け付けたかどうかを示します。RPCバージョンが2以外の場合や認証に失敗した場合にエラーが返ります。

|値|定数名|意味|
|--|--|--|
|**0**|**MSG_ACCEPTED**|**正常に実行された**|
|1|MSG_DENIED|エラーが発生した|

+ **accept_stat**：RPC の関数を実行したかどうかを示します。以下の値をとります。

|値|定数名|意味|
|--|--|--|
|**0**|**SUCCESS**|**RPCが正常に実行された**|
|1|PROG_UNAVAIL|プログラム番号が不在|
|2|PROG_MISMATCH|バージョン番号が不在|
|3|PROC_UNAVAIL|プロシージャ番号が不在|
|4|GARBAGE_ARGS|引数のデコード不能|
|5|SYSTEM_ERR|メモリ割り当ての失敗など|

+ verf.flavor, verf.body：クライアント認証の結果に関するパラメータです。認証を使わない場合、すべて 0 になります。
+ **データ**：関数の戻り値を示します。

# インタフェース記述言語（IDL）

RPC では [rpcgen](https://docs.oracle.com/cd/E86824_01/html/E54763/rpcgen-1.html) という自動生成ツールが用意されています。例えばインタフェース記述言語（IDL）を使って以下のようにサービスを定義します。

```ping.x
program PING_PROG {
	/* Latest version */
	version PING_VERS_PINGBACK {
		void
		PINGPROC_NULL(void) = 0;
		/*
		 * Ping the client, return the round-trip time
		 * (in microseconds). Returns -1 if the operation
		 * timed out.
		 */
		int
		PINGPROC_PINGBACK(void) = 1;
	} = 2;
	/* Original version */
	version PING_VERS_ORIG {
		void
		PINGPROC_NULL(void) = 0;
	} = 1;
} = 1;

const PING_VERS = 2;      /* latest version */
```

**rpcgen コマンド**を使うと、4 つの C 言語のソースファイルが生成されます。

- **ping.h**：そのプログラムで使う定数、データ構造、スタブ関数のインタフェース。
- **ping_clnt.c**：クライアント側のスタブ処理。
- **ping_xdr.c**：ping.x で定義したデータ構造について、整列化（マーシャリング）と非整列化（アンマーシャリング）を行なう手続き。
- **ping_svc.c**：サーバ側の main 関数とディスパッチ手続き。

自動生成されたファイルを利用する事でクライアント側とサーバ側の処理をより短時間で実装できます。

## RPC のソースファイルの構成例

IDL を使う場合、プログラムのソースファイルは以下のような構成になります。

![314.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3160433/72aa909a-ec0f-5494-8ae3-c67762f8d942.png)

# RPC の利用例：ポートマッパ

RPC の具体的な事例としてポートマッパを紹介します。

**ポートマッパ**はプログラム番号に対応する TCP または UDP の**ポート番号を返すサービス**です。[RFC 1833](https://www.ietf.org/rfc/rfc1833.txt) で規定されてます。ポートマッパ自身は TCP または UDP の 111 番ポートを使います。

ポートマッパの通信仕様は [**RFC 1057**](https://www.ietf.org/rfc/rfc1057.txt) で公開されています。

![402_Portmap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3160433/21498a16-d772-494a-2f06-24fe1f84d981.png)

## ポートマッパの関数

ポートマッパの関数とプログラム番号、バージョン番号、プロシージャ番号の対応表を以下に示します。

|関数名|プログラム番号|バージョン番号|プロシージャ番号|引数|戻り値|説明|
|----|----|----|----|----|----|----|
|PMAPPROC_NULL|10000|2|0|void|void|この関数はテスト用です。何も実行しません。|
|PMAPPROC_SET|10000|2|1|mapping 構造体|bool|ポートマッパにポート番号を登録します。|
|PMAPPROC_UNSET|10000|2|2|mapping 構造体|bool|ポートマッパに登録されているポート番号を削除します。|
|**PMAPPROC_GETPORT**|**10000**|**2**|**3**|mapping 構造体|unsigned int|対象プログラムのポート番号を返します。|
|PMAPPROC_DUMP|10000|2|4|void|pmaplist 構造体の配列|ポートマッパの登録プログラム一覧を返します。|
|PMAPPROC_CALLIT|10000|2|5|call_args 構造体|call_result 構造体|別の関数を呼び出します。|

## PMAPPROC_GETPORT のデータ構造

ポートマッパの PMAPPROC_GETPORT 関数の呼び出しメッセージを以下に示します。

![451_RFC1057_A1_PMAPPROC_GETPORT_args.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3160433/2a710c19-0650-ce8e-3c74-5cbad4142428.png)

各項目の説明は以下の通りです。

+ **xid**：一意の識別子を指定します。
+ **prog, vers**：ポート番号を取得したいプログラム番号とバージョン番号を指定します。
+ **prot**：プロトコル種別を指定します。

|値|定数名|意味|
|----|----|----|
|6|IPPROTO_TCP|TCP プロトコル|
|17|IPPROTO_UDP|UDP プロトコル|

ポートマッパの PMAPPROC_GETPORT 関数の応答メッセージを以下に示します。

![453_RFC1057_A1_PMAPPROC_GETPORT_result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3160433/6ebddf99-3a27-03e6-8307-160c51ee9b94.png)

各項目の説明は以下の通りです。

- **xid**：関数呼び出し時の識別子を示します。
- **port**：プログラム番号に対応する TCP または UDP のポート番号を示します。

## ソースコードの例

IDLを使わずに C# で書いた以下のファイルをご覧ください。
- [クライアント側処理](https://github.com/MitakaMakers/Ivi.Visa/blob/main/src/ClientPortmap.cs)
- [サーバ側処理](https://github.com/MitakaMakers/Ivi.Visa/blob/main/src/ServerPortmap.cs)

## ポートマッパの通信フロー

ポートマッパのパケットキャプチャの例を示します。

![610_wireshark.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3160433/abd0f37c-1838-a0f0-7f10-84b01d8cddee.png)

# まとめ
RPC 実装の元祖である ONC RPC について、目的、通信手順、データ構造、プログラム作成手順、実例を説明しました。

# 参考文献
本ページの作成にあたり参照した資料を挙げます。
- [SunRPCとNFS](https://www.coins.tsukuba.ac.jp/~yas/coins/dsys-2006/2007-01-30/)
- [4.3BSD-Reno/lib/librpc/](https://github.com/dank101/4.3BSD-Reno/tree/master/lib/librpc)