8.4 RPC
前の節でどのようにSocketとHTTPに基づいてネットワークアプリケーションを書くかご紹介しました。SocketとHTTPが採用しているのは"情報交換"パターン、すなわちクライアントがサーバに情報を一つ送信し、その後(一般的に)サーバが一定の情報を返すことでレスポンスとする、ようなものであると理解しました。双方は互いが発生させた情報を解析できるように、クライアントとサーバ間で情報をやり取りする形式が取り決められています。しかし多くの独立したアプリケーションは特にこのようなモデルを採用していません。その代わり通常の関数をコールするのに似た方法で必要となる機能を実現しています。
RPCは関数をコールするモデルをネットワーク化したものです。クライアントはローカルの関数をコールするのと同じように、引数をひっくるめてネットワークを通じてサーバに送信します。サーバでは処理の中でそれを展開し実行します。そして、実行結果をクライアントにフィードバックします。
RPC(Remote Procedure Call Protocol) 、このリモートプロセスのコールプロトコルは、ネットワークを通してリモートコンピュータのプログラムにおいてリクエストするサービスです。低レイヤのネットワーク技術におけるプロトコルを理解する必要はありません。これは何らかの転送プロトコルの存在を仮定します。例えばTCPまたはUDPです。通信を行うプログラム間で情報データを簡単にやりとりすることができます。これを使って関数をコールするモデルをネットワーク化することができます。OSIネットワーク通信モデルで、RPCはデータリンク層とアプリケーション層を飛び越えます。RPCはネットワーク分散型の複数プログラムを含めてアプリケーションプログラムの開発を容易にします。
RPCの動作原理
図8.8 RPCの動作プロセス図
実行時、クライアントマシンがサーバのRPCに対してコールを行うと、そこにはだいたい以下のような10ステップの操作があります:
- 1.クライアントハンドルをコールする。転送引数を実行する。
- 2.ローカルシステムのカーネルがネットワーク情報を送信する。
- 3.情報がリモートホストに送信される。
- 4.サーバハンドル情報を受け取り、引数を取り出す。
- 5.リモートプロセスを実行する。
- 6.実行されたプロセスは結果をサーバハンドルに返す。
- 7.サーバハンドルは結果を返し、リモートシステムのカーネルをコールする。
- 8.情報がローカルホストに送信される。
- 9.クライアントハンドルがカーネルから情報を取得する。
- 10.クライアントはハンドルが返すデータを受け取る。
Go RPC
Go標準パッケージではすでにRPCに対するサポートがされています。また、3つのレベルとなるRPC、HTTP、JSONRPCをサポートしています。しかしGoのRPCパッケージは唯一無二のRPCであり、伝統的なRPCシステムとは異なります。これはGoが開発したサーバとクライアント間のやりとりのみをサポートします。なぜなら内部ではGoを採用してエンコードされているからです。
Go RPCの関数は以下の条件に合致した時のみリモートアクセスされます。そうでないものは無視されます。細かい条件は以下の通り:
- 関数はエクスポートされていなければなりません。(頭文字が大文字)
- 2つのエクスポートされた型の引数が必要です。
- はじめの引数は受け取る引数、2つ目の引数はクライアントに返す引数です。2つ目の引数はポインタ型でなければなりません。
- 関数はさらに戻り値errorを持っています。
例を挙げましょう。正しいRPC関数では以下のような形式になります:
func (t *T) MethodName(argType T1, replyType *T2) error
T、T1とT2型はかならずencoding/gob
パッケージによってエンコード/デコードできなければなりません。
いかなるRPCもネットワークを通じてデータを転送します。Go RPCはHTTPとTCPによってデータを転送することができます。HTTPを利用するメリットは直接net/http
の中のいくつかの関数を再利用することができるということです。詳細な例は以下をご覧ください。
HTTP RPC
httpのサーバコードは以下の通り:
package main
import (
"errors"
"fmt"
"net/http"
"net/rpc"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
type Arith int
func (t *Arith) Multiply(args *Args, reply *int) error {
*reply = args.A * args.B
return nil
}
func (t *Arith) Divide(args *Args, quo *Quotient) error {
if args.B == 0 {
return errors.New("divide by zero")
}
quo.Quo = args.A / args.B
quo.Rem = args.A % args.B
return nil
}
func main() {
arith := new(Arith)
rpc.Register(arith)
rpc.HandleHTTP()
err := http.ListenAndServe(":1234", nil)
if err != nil {
fmt.Println(err.Error())
}
}
上の例を見ると、ArithのRPCサーバを登録しており、rpc.HandleHTTP
関数を使ってこのサービスをHTTPプロトコルに登録し、httpのメソッドを使ってデータを転送することができるとわかります。
下のクライアントのコードをご覧ください:
package main
import (
"fmt"
"log"
"net/rpc"
"os"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "server")
os.Exit(1)
}
serverAddress := os.Args[1]
client, err := rpc.DialHTTP("tcp", serverAddress+":1234")
if err != nil {
log.Fatal("dialing:", err)
}
// Synchronous call
args := Args{17, 8}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
var quot Quotient
err = client.Call("Arith.Divide", args, ")
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
}
我々は上のサーバとクライアントのコードを別々にコンパイルして、先にサーバ側を起動し、その後クライアントを起動して、コードを入力し、以下のような情報が出力されました:
$ ./http_c localhost
Arith: 17*8=136
Arith: 17/8=2 remainder 1
上のコールにおいて引数と戻り値は我々が定義したstruct型であると確認できます。サーバではこれらをコールする関数の引数の型とみなしています。クライアントではclient.Call
の第二、第三のふたつの引数の型となります。クライアントで最も重要なのはこのCall関数です。これは3つの引数を持っています。はじめの引数はコールする関数の名前、2つ目は転送する引数、3つ目は戻り値の参照(これはポインタ型です)となります。上のコードを通して、GoでRPCを実装するのは非常に簡単で便利であるとお分かりいただけたかと思います。
TCP RPC
上ではHTTPプロトコルに基づくRPCを実現しました。以降ではTCPプロトコルに基づいたRPCを実現します。サーバで実装されるコードは以下の通り:
package main
import (
"errors"
"fmt"
"net"
"net/rpc"
"os"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
type Arith int
func (t *Arith) Multiply(args *Args, reply *int) error {
*reply = args.A * args.B
return nil
}
func (t *Arith) Divide(args *Args, quo *Quotient) error {
if args.B == 0 {
return errors.New("divide by zero")
}
quo.Quo = args.A / args.B
quo.Rem = args.A % args.B
return nil
}
func main() {
arith := new(Arith)
rpc.Register(arith)
tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")
checkError(err)
listener, err := net.ListenTCP("tcp", tcpAddr)
checkError(err)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
rpc.ServeConn(conn)
}
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
上のコードはhttpのサーバと比べ、以下の点が異なっています:ここではTCPプロトコルを採用しています。自分で接続をコントロールする必要があり、クライアントが接続した場合に、この接続をrpcに渡して処理する必要があります。
これはブロッキング型の単一のユーザのプロセスだとお気づきかもしれません。もしマルチスレッドを実装したい場合、goroutineを使って実現することができます。前のsocket節ですでにどのようにgoroutineを処理するかご紹介しました。 以下ではTCPで実現するRPCクライアントをお見せします:
package main
import (
"fmt"
"log"
"net/rpc"
"os"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "server:port")
os.Exit(1)
}
service := os.Args[1]
client, err := rpc.Dial("tcp", service)
if err != nil {
log.Fatal("dialing:", err)
}
// Synchronous call
args := Args{17, 8}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
var quot Quotient
err = client.Call("Arith.Divide", args, ")
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
}
このクライアントコードとhttpのクライアントコードを比較した場合、唯一の違いはDialHTTPです。もう片方はDial(tcp)で、その他の処理は全く同じです。
JSON RPC
JSON RPCはデータエンコードにJSONを採用しております。gobエンコードではありません。その他は上でご紹介したRPCの概念と全く同じです。以下ではどのようにGoが提供するjson-rpc標準パッケージを使うかご説明します。サーバのコードの実装をご覧ください:
package main
import (
"errors"
"fmt"
"net"
"net/rpc"
"net/rpc/jsonrpc"
"os"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
type Arith int
func (t *Arith) Multiply(args *Args, reply *int) error {
*reply = args.A * args.B
return nil
}
func (t *Arith) Divide(args *Args, quo *Quotient) error {
if args.B == 0 {
return errors.New("divide by zero")
}
quo.Quo = args.A / args.B
quo.Rem = args.A % args.B
return nil
}
func main() {
arith := new(Arith)
rpc.Register(arith)
tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")
checkError(err)
listener, err := net.ListenTCP("tcp", tcpAddr)
checkError(err)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
jsonrpc.ServeConn(conn)
}
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error ", err.Error())
os.Exit(1)
}
}
json-rpcはTCPプロトコルにもとづいて実装されていることがお分かりいただけるかと思います。現在はまだHTTPメソッドをサポートしていません。
クライアントのコードをご覧ください:
package main
import (
"fmt"
"log"
"net/rpc/jsonrpc"
"os"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "server:port")
log.Fatal(1)
}
service := os.Args[1]
client, err := jsonrpc.Dial("tcp", service)
if err != nil {
log.Fatal("dialing:", err)
}
// Synchronous call
args := Args{17, 8}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
var quot Quotient
err = client.Call("Arith.Divide", args, ")
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
}
まとめ
GoはすでにRPCに対して良いサポートを提供しています。上のHTTP、TCP、JSON RPCの実装を通して、多くの分散型のWebアプリケーションの開発を簡単に行うことができます。読者であるあなたはすでにここまでマスターしたものと思われます。ただ残念なことに現在GoはまだSOAP RPCのサポートを提供していません。幸い現在すでにサードパーティのオープンソースで実現されています。