Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 16629] ファイルの概要

このコミットは、Go言語の標準ライブラリ net/rpc パッケージにおけるバグ修正に関するものです。具体的には、rpc.RegisterName 関数を使用してサービスを登録する際に、サービス名にドット(.)文字が含まれていると、そのサービス内のメソッド呼び出しが正しく解決されない問題を修正しています。

コミット

commit 3eaaed5030a531d11a228353b3f964e4b0227ba7
Author: ChaiShushan <chaishushan@gmail.com>
Date:   Mon Jun 24 13:23:02 2013 -0700

    net/rpc: fix RegisterName rejects "." character.
    
    Fixes #5617.
    
    R=r, rsc
    CC=gobot, golang-dev
    https://golang.org/cl/10370043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/3eaaed5030a531d11a228353b3f964e4b0227ba7

元コミット内容

net/rpc: fix RegisterName rejects "." character.

このコミットは、net/rpc パッケージにおいて、RegisterName 関数がサービス名にドット(.)文字を含むことを拒否する問題を修正します。これはIssue #5617を解決するものです。

変更の背景

Goの net/rpc パッケージは、リモートプロシージャコール(RPC)を実装するためのメカニズムを提供します。クライアントはサービス名とメソッド名をドットで区切った文字列(例: Service.Method)として指定してRPCを呼び出します。

このコミットが修正する問題は、rpc.RegisterName を使ってサービスを登録する際に、サービス名自体にドットが含まれている場合に発生していました。例えば、"net.rpc.Arith" のようなサービス名を登録しようとすると、内部のメソッド解決ロジックがこのドットをサービス名とメソッド名の区切りと誤解し、正しいサービスやメソッドを見つけられなくなるというバグがありました。

元の実装では、req.ServiceMethod(クライアントから送られてくる Service.Method 形式の文字列)を単純に strings.Split(req.ServiceMethod, ".") で分割していました。これにより、サービス名自体にドットが含まれていると、分割結果の配列の要素数が2ではなくなり、エラーとなっていました。

この問題は、特に既存の命名規則や他のシステムとの連携において、サービス名にドットを使用したい場合に制約となっていました。Issue #5617で報告され、このコミットによって解決されました。

前提知識の解説

Go言語の net/rpc パッケージ

net/rpc パッケージは、Goプログラム間でRPCを実装するための標準ライブラリです。これは、ネットワーク越しに別のGoプログラムの関数を呼び出すことを可能にします。

  • サービス (Service): RPCで公開される一連のメソッドを持つGoの構造体(またはポインタ)。
  • メソッド (Method): サービス内で公開される関数。特定のシグネチャ(func (t *T) Method(argType T1, replyType *T2) error)を持つ必要があります。
  • rpc.Register(rcvr interface{}): 指定されたレシーバ(構造体のインスタンス)を、その型名(例: Arith)をサービス名としてRPCシステムに登録します。
  • rpc.RegisterName(name string, rcvr interface{}): 指定されたレシーバを、name で指定された文字列をサービス名としてRPCシステムに登録します。この関数が、サービス名にドットを含む場合に問題を引き起こしていました。
  • クライアントからの呼び出し: クライアントは client.Call("Service.Method", args, reply) の形式でRPCを呼び出します。ここで "Service.Method" は、サービス名とメソッド名をドットで区切った文字列です。

サービスとメソッドの解決

net/rpc サーバーは、クライアントから送られてきた Service.Method 形式の文字列を受け取ると、それを解析して適切なサービスとメソッドを特定します。

  1. サービス名の抽出: Service.Method 文字列からサービス名を抽出します。
  2. メソッド名の抽出: Service.Method 文字列からメソッド名を抽出します。
  3. サービスマップの検索: サーバーが内部で保持している登録済みサービスのマッピング(server.serviceMap)から、抽出したサービス名に対応するサービスを探します。
  4. メソッドマップの検索: 見つかったサービス内のメソッドのマッピングから、抽出したメソッド名に対応するメソッドを探します。

この一連の解決プロセスにおいて、サービス名とメソッド名の区切り文字としてドットが使用されるため、サービス名自体にドットが含まれると、この解析ロジックが誤動作する可能性がありました。

技術的詳細

このコミットの技術的な核心は、クライアントから送られてくる req.ServiceMethod 文字列の解析方法の変更にあります。

元の実装では、strings.Split(req.ServiceMethod, ".") を使用して、文字列をドットで分割していました。例えば、"Arith.Add"["Arith", "Add"] となります。しかし、"net.rpc.Arith.Add" のようなケースでは、["net", "rpc", "Arith", "Add"] となり、配列の長さが2ではなくなってしまうため、サービス名とメソッド名の区別が不可能になり、エラーとなっていました。

修正後のコードでは、strings.LastIndex(req.ServiceMethod, ".") を使用しています。これは、文字列内で最後のドットの位置を探す関数です。

  • dot := strings.LastIndex(req.ServiceMethod, ".")
    • これにより、"net.rpc.Arith.Add" の場合、dot は最後のドット(ArithAdd の間のドット)のインデックスを指します。
  • serviceName := req.ServiceMethod[:dot]
    • 文字列の先頭から最後のドットの直前までをサービス名とします。
    • 例: "net.rpc.Arith.Add" の場合、serviceName"net.rpc.Arith" となります。
  • methodName := req.ServiceMethod[dot+1:]
    • 最後のドットの直後から文字列の末尾までをメソッド名とします。
    • 例: "net.rpc.Arith.Add" の場合、methodName"Add" となります。

この変更により、サービス名自体にドットが含まれていても、最後のドットがサービス名とメソッド名の区切りとして正しく機能するようになり、サービスとメソッドの解決が正常に行われるようになりました。

この修正は、net/rpc の柔軟性を高め、より複雑なサービス命名規則を許容するようになります。

コアとなるコードの変更箇所

変更は src/pkg/net/rpc/server.gosrc/pkg/net/rpc/server_test.go の2つのファイルにわたります。

src/pkg/net/rpc/server.go

--- a/src/pkg/net/rpc/server.go
+++ b/src/pkg/net/rpc/server.go
@@ -560,20 +560,23 @@ func (server *Server) readRequestHeader(codec ServerCodec) (service *service, mt
 	// we can still recover and move on to the next request.
 	keepReading = true
 
-	serviceMethod := strings.Split(req.ServiceMethod, ".")
-	if len(serviceMethod) != 2 {
+	dot := strings.LastIndex(req.ServiceMethod, ".")
+	if dot < 0 {
 		err = errors.New("rpc: service/method request ill-formed: " + req.ServiceMethod)
 		return
 	}
+	serviceName := req.ServiceMethod[:dot]
+	methodName := req.ServiceMethod[dot+1:]
+
 	// Look up the request.
 	server.mu.RLock()
-	service = server.serviceMap[serviceMethod[0]]
+	service = server.serviceMap[serviceName]
 	server.mu.RUnlock()
 	if service == nil {
 		err = errors.New("rpc: can't find service " + req.ServiceMethod)
 		return
 	}
-	mtype = service.method[serviceMethod[1]]
+	mtype = service.method[methodName]
 	if mtype == nil {
 		err = errors.New("rpc: can't find method " + req.ServiceMethod)
 	}

src/pkg/net/rpc/server_test.go

--- a/src/pkg/net/rpc/server_test.go
+++ b/src/pkg/net/rpc/server_test.go
@@ -84,6 +84,7 @@ func listenTCP() (net.Listener, string) {
 
 func startServer() {
 	Register(new(Arith))
+	RegisterName("net.rpc.Arith", new(Arith))
 
 	var l net.Listener
 	l, serverAddr = listenTCP()
@@ -97,6 +98,7 @@ func startServer() {
 func startNewServer() {
 	newServer = NewServer()
 	newServer.Register(new(Arith))
+	newServer.RegisterName("net.rpc.Arith", new(Arith))
 
 	var l net.Listener
 	l, newServerAddr = listenTCP()
@@ -234,6 +236,17 @@ func testRPC(t *testing.T, addr string) {
 	if reply.C != args.A*args.B {
 		t.Errorf("Mul: expected %d got %d", reply.C, args.A*args.B)
 	}\n
+\n	// ServiceName contain "." character
+\targs = &Args{7, 8}
+\treply = new(Reply)
+\terr = client.Call("net.rpc.Arith.Add", args, reply)
+\tif err != nil {
+\t\tt.Errorf("Add: expected no error but got string %q", err.Error())
+\t}
+\tif reply.C != args.A+args.B {
+\t\tt.Errorf("Add: expected %d got %d", reply.C, args.A+args.B)
+\t}
 }\n
 func TestHTTP(t *testing.T) {

コアとなるコードの解説

src/pkg/net/rpc/server.go の変更

このファイルは net/rpc サーバーの主要なロジックを含んでいます。変更は readRequestHeader 関数内で行われています。この関数は、クライアントからのRPCリクエストヘッダーを読み込み、サービスとメソッドを解決する役割を担っています。

  • 変更前:

    serviceMethod := strings.Split(req.ServiceMethod, ".")
    if len(serviceMethod) != 2 {
        err = errors.New("rpc: service/method request ill-formed: " + req.ServiceMethod)
        return
    }
    // ...
    service = server.serviceMap[serviceMethod[0]]
    // ...
    mtype = service.method[serviceMethod[1]]
    

    ここでは、req.ServiceMethod をドットで分割し、その結果が必ず2つの要素を持つことを期待していました。サービス名にドットが含まれるとこの条件を満たさなくなり、エラーが発生していました。

  • 変更後:

    dot := strings.LastIndex(req.ServiceMethod, ".")
    if dot < 0 {
        err = errors.New("rpc: service/method request ill-formed: " + req.ServiceMethod)
        return
    }
    serviceName := req.ServiceMethod[:dot]
    methodName := req.ServiceMethod[dot+1:]
    // ...
    service = server.serviceMap[serviceName]
    // ...
    mtype = service.method[methodName]
    

    strings.Split の代わりに strings.LastIndex を使用することで、req.ServiceMethod 文字列の最後のドットをサービス名とメソッド名の区切りとして扱います。

    • req.ServiceMethod[:dot] は、文字列の先頭から最後のドットの直前までを抽出し、これを serviceName とします。
    • req.ServiceMethod[dot+1:] は、最後のドットの直後から文字列の末尾までを抽出し、これを methodName とします。 このロジックにより、サービス名自体にドットが含まれていても、正しくサービス名とメソッド名を分離できるようになりました。

src/pkg/net/rpc/server_test.go の変更

このファイルは net/rpc パッケージのテストコードを含んでいます。変更は、新しいテストケースの追加と、既存のテストセットアップの修正です。

  • startServer() および startNewServer() 関数への追加:

    RegisterName("net.rpc.Arith", new(Arith))
    

    RegisterName を使用して、サービス名にドットを含む "net.rpc.Arith" という名前で Arith サービスを登録しています。これは、修正されたロジックが正しく機能するかを検証するための準備です。

  • testRPC() 関数への新しいテストケースの追加:

    // ServiceName contain "." character
    args = &Args{7, 8}
    reply = new(Reply)
    err = client.Call("net.rpc.Arith.Add", args, reply)
    if err != nil {
        t.Errorf("Add: expected no error but got string %q", err.Error())
    }
    if reply.C != args.A+args.B {
        t.Errorf("Add: expected %d got %d", reply.C, args.A+args.B)
    }
    

    この新しいテストケースでは、サービス名にドットを含む "net.rpc.Arith.Add" を使用して Add メソッドを呼び出しています。このテストがエラーなく成功することで、server.go で行われた修正が意図通りに機能し、サービス名にドットが含まれていてもRPC呼び出しが正しく解決されることが検証されます。

これらの変更により、net/rpc パッケージは、より柔軟なサービス命名規則に対応できるようになり、特定の命名パターンに依存する既存システムとの連携が容易になりました。

関連リンク

参考にした情報源リンク

  • Go言語の net/rpc パッケージのドキュメント
  • Go言語の strings パッケージのドキュメント
  • GitHub上のGoリポジトリのコミット履歴
  • Go Issue #5617 の議論内容