<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Protorpc on chai2010 的博客</title>
    <link>https://chai2010.cn/tags/protorpc/</link>
    <description>Recent content in Protorpc on chai2010 的博客</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>zh-CN</language>
    <lastBuildDate>Wed, 08 Jan 2014 00:00:00 +0000</lastBuildDate>
    
        <atom:link href="https://chai2010.cn/tags/protorpc/index.xml" rel="self" type="application/rss+xml" />
    
    
    <item>
      <title>Go语言的RPC介绍(含Protobuf-RPC)</title>
      <link>https://chai2010.cn/post/golang/go-protorpc-2014/</link>
      <pubDate>Wed, 08 Jan 2014 00:00:00 +0000</pubDate>
      
      <guid>https://chai2010.cn/post/golang/go-protorpc-2014/</guid>
      
        <description>

&lt;p&gt;本文在 &lt;a href=&#34;http://blog.go-china.org/&#34;&gt;Golang中国博客&lt;/a&gt; 的地址: &lt;a href=&#34;http://blog.go-china.org/09-protorpc&#34;&gt;http://blog.go-china.org/09-protorpc&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;标准库的rpc&#34;&gt;标准库的RPC&lt;/h2&gt;

&lt;p&gt;RPC是远程调用的简称, 简单的说就是要像调用本地函数一样调用服务器的函数.&lt;/p&gt;

&lt;p&gt;Go语言的标准库已经提供了RPC框架和不同的RPC实现.&lt;/p&gt;

&lt;p&gt;下面是一个服务器的例子:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;type Echo int

func (t *Echo) Hi(args string, reply *string) error {
    *reply = &amp;quot;echo:&amp;quot; + args
    return nil
}

func main() {
    rpc.Register(new(Echo))
    rpc.HandleHTTP()
    l, e := net.Listen(&amp;quot;tcp&amp;quot;, &amp;quot;:1234&amp;quot;)
    if e != nil {
        log.Fatal(&amp;quot;listen error:&amp;quot;, e)
    }
    http.Serve(l, nil)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;其中 &lt;code&gt;rpc.Register&lt;/code&gt; 用于注册RPC服务, 默认的名字是对象的类型名字(这里是&lt;code&gt;Echo&lt;/code&gt;). 如果需要指定特殊的名字, 可以用 &lt;code&gt;rpc.RegisterName&lt;/code&gt; 进行注册.&lt;/p&gt;

&lt;p&gt;被注册对象的类型所有满足以下规则的方法会被导出到RPC服务接口:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;func (t *T) MethodName(argType T1, replyType *T2) error
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;被注册对应至少要有一个方法满足这个特征, 否则可能会注册失败.&lt;/p&gt;

&lt;p&gt;然后 &lt;code&gt;rpc.HandleHTTP&lt;/code&gt; 用于指定 RPC 的传输协议, 这里是采用 http 协议作为RPC调用的载体. 用户也可以用&lt;code&gt;rpc.ServeConn&lt;/code&gt;接口, 定制自己的传输协议.&lt;/p&gt;

&lt;p&gt;客户端可以这样调用&lt;code&gt;Echo.Hi&lt;/code&gt;接口:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;func main() {
    client, err := rpc.DialHTTP(&amp;quot;tcp&amp;quot;, &amp;quot;127.0.0.1:1234&amp;quot;)
    if err != nil {
        log.Fatal(&amp;quot;dialing:&amp;quot;, err)
    }

    var args = &amp;quot;hello rpc&amp;quot;
    var reply string
    err = client.Call(&amp;quot;Echo.Hi&amp;quot;, args, &amp;amp;reply)
    if err != nil {
        log.Fatal(&amp;quot;arith error:&amp;quot;, err)
    }
    fmt.Printf(&amp;quot;Arith: %d*%d=%d\n&amp;quot;, args.A, args.B, reply)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;客户端先用&lt;code&gt;rpc.DialHTTP&lt;/code&gt;和RPC服务器进行一个链接(协议必须匹配).&lt;/p&gt;

&lt;p&gt;然后通过返回的&lt;code&gt;client&lt;/code&gt;对象进行远程函数调用. 函数的名字是由&lt;code&gt;client.Call&lt;/code&gt; 第一个参数指定(是一个字符串).&lt;/p&gt;

&lt;p&gt;基于HTTP的RPC调用一般是在调试时使用, 默认可以通过浏览&lt;code&gt;&amp;quot;127.0.0.1:1234/debug/rpc&amp;quot;&lt;/code&gt;页面查看RPC的统计信息.&lt;/p&gt;

&lt;h2 id=&#34;基于-json-的-rpc-调用&#34;&gt;基于 JSON 的 RPC 调用&lt;/h2&gt;

&lt;p&gt;在上面的RPC例子中, 我们采用了默认的HTTP协议作为RPC调用的传输载体.&lt;/p&gt;

&lt;p&gt;因为内置&lt;code&gt;net/rpc&lt;/code&gt;包接口设计的缺陷, 我们无法使用&lt;code&gt;jsonrpc&lt;/code&gt;等定制的编码作为&lt;code&gt;rpc.DialHTTP&lt;/code&gt;的底层协议. 如果需要让&lt;code&gt;jsonrpc&lt;/code&gt;支持&lt;code&gt;rpc.DialHTTP&lt;/code&gt;函数, 需要调整rpc的接口.&lt;/p&gt;

&lt;p&gt;以前有个&lt;a href=&#34;https://code.google.com/p/go/issues/detail?id=2738&#34;&gt;Issue2738&lt;/a&gt;是针对这个问题. 我曾提交的 &lt;a href=&#34;https://codereview.appspot.com/10704046/&#34;&gt;CL10704046&lt;/a&gt; 补丁用于修复这个问题. 不过因为涉及到增加rpc的接口, 官方没有接受(因为自己重写一个&lt;code&gt;DialHTTP&lt;/code&gt;会更简单).&lt;/p&gt;

&lt;p&gt;除了传输协议, 还有可以指定一个RPC编码协议, 用于编码/节目RPC调用的函数参数和返回值. RPC调用不指定编码协议时, 默认采用Go语言特有的&lt;code&gt;gob&lt;/code&gt;编码协议.&lt;/p&gt;

&lt;p&gt;因为, 其他语言一般都不支持Go语言的&lt;code&gt;gob&lt;/code&gt;协议, 因此如果需要跨语言RPC调用就需要
采用通用的编码协议.&lt;/p&gt;

&lt;p&gt;Go的标准库还提供了一个&lt;code&gt;&amp;quot;net/rpc/jsonrpc&amp;quot;&lt;/code&gt;包, 用于提供基于JSON编码的RPC支持.&lt;/p&gt;

&lt;p&gt;服务器部分只需要用&lt;code&gt;rpc.ServeCodec&lt;/code&gt;指定json编码协议就可以了:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;func main() {
    lis, err := net.Listen(&amp;quot;tcp&amp;quot;, &amp;quot;:1234&amp;quot;)
    if err != nil {
        return err
    }
    defer lis.Close()

    srv := rpc.NewServer()
    if err := srv.RegisterName(&amp;quot;Echo&amp;quot;, new(Echo)); err != nil {
        return err
    }

    for {
        conn, err := lis.Accept()
        if err != nil {
            log.Fatalf(&amp;quot;lis.Accept(): %v\n&amp;quot;, err)
        }
        go srv.ServeCodec(jsonrpc.NewServerCodec(conn))
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;客户端部分值需要用 &lt;code&gt;jsonrpc.Dial&lt;/code&gt; 代替 &lt;code&gt;rpc.Dial&lt;/code&gt; 就可以了:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;func main() {
    client, err := jsonrpc.DialHTTP(&amp;quot;tcp&amp;quot;, &amp;quot;127.0.0.1:1234&amp;quot;)
    if err != nil {
        log.Fatal(&amp;quot;dialing:&amp;quot;, err)
    }
    ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果需要在其他语言中使用&lt;code&gt;jsonrpc&lt;/code&gt;和Go语言进行通讯, 需要封装一个和&lt;code&gt;jsonrpc&lt;/code&gt;
匹配的库.&lt;/p&gt;

&lt;p&gt;关于&lt;code&gt;jsonrpc&lt;/code&gt;的实现细节这里就不展开讲了, 感兴趣的话可以参考这篇文章: &lt;a href=&#34;http://blog.golang.org/json-rpc-tale-of-interfaces&#34;&gt;JSON-RPC: a tale of interfaces&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&#34;基于-protobuf-的-rpc-调用&#34;&gt;基于 Protobuf 的 RPC 调用&lt;/h2&gt;

&lt;p&gt;&lt;a href=&#34;http://code.google.com/p/protobuf/;&#34;&gt;Protobuf&lt;/a&gt; 是 Google 公司开发的编码协议. 它的优势是编码后的数据体积比较小(并不是压缩算法), 比较适合用于命令的传输编码.&lt;/p&gt;

&lt;p&gt;Protobuf 官方团队提供 Java/C++/Python 几个语言的支持, Go语言的版本由Go团队提供支持, 其他语言由第三方支持.&lt;/p&gt;

&lt;p&gt;Protobuf 的语言规范中可以定义RPC接口. 但是在Go语言和C++版本的Protobuf中都没有生成RPC的实现.&lt;/p&gt;

&lt;p&gt;不过作者在 Go语言版本的Protobuf基础上开发了 RPC 的实现 &lt;a href=&#34;https://code.google.com/p/protorpc/&#34;&gt;protorpc&lt;/a&gt;, 同时提供的 &lt;code&gt;protoc-gen-go&lt;/code&gt;命令可以生成相应的RPC代码. 项目地址: &lt;a href=&#34;https://code.google.com/p/protorpc/&#34;&gt;https://code.google.com/p/protorpc/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;该实现支持Go语言和C++语言, 在Protobuf官方wiki的第三方RPC实现列表中有介绍: &lt;a href=&#34;https://code.google.com/p/protobuf/wiki/ThirdPartyAddOns#RPC_Implementations&#34;&gt;https://code.google.com/p/protobuf/wiki/ThirdPartyAddOns#RPC_Implementations&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;要使用 &lt;a href=&#34;https://code.google.com/p/protorpc/&#34;&gt;protorpc&lt;/a&gt;, 需要先在proto文件定义接口(&lt;code&gt;arith.pb/arith.proto&lt;/code&gt;):&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;package arith;

// go use cc_generic_services option
option cc_generic_services = true;

message ArithRequest {
    optional int32 a = 1;
    optional int32 b = 2;
}

message ArithResponse {
    optional int32 val = 1;
    optional int32 quo = 2;
    optional int32 rem = 3;
}

service ArithService {
    rpc multiply (ArithRequest) returns (ArithResponse);
    rpc divide (ArithRequest) returns (ArithResponse);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;a href=&#34;https://code.google.com/p/protorpc/&#34;&gt;protorpc&lt;/a&gt;使用&lt;code&gt;cc_generic_services&lt;/code&gt;选择控制是否输出RPC代码. 因此, 需要设置&lt;code&gt;cc_generic_services&lt;/code&gt;为&lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;然后下载 &lt;a href=&#34;https://code.google.com/p/protobuf/downloads/list&#34;&gt;protoc-2.5.0-win32.zip&lt;/a&gt;, 解压后可以得到一个 &lt;code&gt;protoc.exe&lt;/code&gt; 的编译命令.&lt;/p&gt;

&lt;p&gt;然后使用下面的命令获取 &lt;a href=&#34;https://code.google.com/p/protorpc/&#34;&gt;protorpc&lt;/a&gt; 和对应的 &lt;code&gt;protoc-gen-go&lt;/code&gt; 插件.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;go get code.google.com/p/protorpc
go get code.google.com/p/protorpc/protoc-gen-go
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;需要确保 &lt;code&gt;protoc.exe&lt;/code&gt; 和 &lt;code&gt;protoc-gen-go.exe&lt;/code&gt; 都在 &lt;code&gt;$PATH&lt;/code&gt; 中. 然后运行以下命令将前面的接口文件转换为Go代码:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;cd arith.pb &amp;amp;&amp;amp; protoc --go_out=. arith.proto
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;新生成的文件为&lt;code&gt;arith.pb/arith.pb.go&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;下面是基于 Protobuf-RPC 的服务器:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;package main

import (
    &amp;quot;errors&amp;quot;

    &amp;quot;code.google.com/p/goprotobuf/proto&amp;quot;

    &amp;quot;./arith.pb&amp;quot;
)

type Arith int

func (t *Arith) Multiply(args *arith.ArithRequest, reply *arith.ArithResponse) error {
    reply.Val = proto.Int32(args.GetA() * args.GetB())
    return nil
}

func (t *Arith) Divide(args *arith.ArithRequest, reply *arith.ArithResponse) error {
    if args.GetB() == 0 {
        return errors.New(&amp;quot;divide by zero&amp;quot;)
    }
    reply.Quo = proto.Int32(args.GetA() / args.GetB())
    reply.Rem = proto.Int32(args.GetA() % args.GetB())
    return nil
}

func main() {
    arith.ListenAndServeArithService(&amp;quot;tcp&amp;quot;, &amp;quot;:1984&amp;quot;, new(Arith))
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;其中导入的 &lt;code&gt;&amp;quot;./arith.pb&amp;quot;&lt;/code&gt; 的名字为 &lt;code&gt;arith&lt;/code&gt;, 在 &lt;code&gt;arith.pb/arith.proto&lt;/code&gt; 文件中定义(这2个可能不同名, 导入时要小心).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;arith.ArithRequest&lt;/code&gt;和&lt;code&gt;arith.ArithResponse&lt;/code&gt;是RPC接口的输入和输出参数, 也是在在 &lt;code&gt;arith.pb/arith.proto&lt;/code&gt; 文件中定义的.&lt;/p&gt;

&lt;p&gt;同时生成的还有一个&lt;code&gt;arith.ListenAndServeArithService&lt;/code&gt;函数, 用于启动RPC服务. 该函数的第三个参数是RPC的服务对象, 必须要满足 &lt;code&gt;arith.EchoService&lt;/code&gt; 接口的定义.&lt;/p&gt;

&lt;p&gt;客户端的使用也很简单, 只要一个 &lt;code&gt;arith.DialArithService&lt;/code&gt; 就可以链接了:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;stub, client, err := arith.DialArithService(&amp;quot;tcp&amp;quot;, &amp;quot;127.0.0.1:1984&amp;quot;)
if err != nil {
    log.Fatal(`arith.DialArithService(&amp;quot;tcp&amp;quot;, &amp;quot;127.0.0.1:1984&amp;quot;):`, err)
}
defer client.Close()
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;arith.DialArithService&lt;/code&gt; 返回了一个 &lt;code&gt;stub&lt;/code&gt; 对象, 该对象已经绑定了RPC的各种方法, 可以直接调用(不需要用字符串指定方法名字):&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;var args ArithRequest
var reply ArithResponse

args.A = proto.Int32(7)
args.B = proto.Int32(8)
if err = stub.Multiply(&amp;amp;args, &amp;amp;reply); err != nil {
    log.Fatal(&amp;quot;arith error:&amp;quot;, err)
}
fmt.Printf(&amp;quot;Arith: %d*%d=%d&amp;quot;, args.GetA(), args.GetB(), reply.GetVal())
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;相比标准的RPC的库, &lt;a href=&#34;https://code.google.com/p/protorpc/&#34;&gt;protorpc&lt;/a&gt; 由以下几个优点:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;采用标准的Protobuf协议, 便于和其他语言交互&lt;/li&gt;
&lt;li&gt;自带的 &lt;code&gt;protoc-gen-go&lt;/code&gt; 插件可以生成RPC的代码, 简化使用&lt;/li&gt;
&lt;li&gt;服务器注册和调用客户端都是具体类型而不是字符串和&lt;code&gt;interface{}&lt;/code&gt;, 这样可以由编译器保证安全&lt;/li&gt;
&lt;li&gt;底层采用了&lt;code&gt;snappy&lt;/code&gt;压缩传输的数据, 提高效率&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;不足之处是使用流程比标准RPC要繁复(需要将proto转换为Go代码).&lt;/p&gt;

&lt;h2 id=&#34;c-调用-go-提供的-protobuf-rpc-服务&#34;&gt;C++ 调用 Go 提供的 Protobuf-RPC 服务&lt;/h2&gt;

&lt;p&gt;&lt;a href=&#34;https://code.google.com/p/protorpc/&#34;&gt;protorpc&lt;/a&gt; 同时也提供了 C++ 语言的实现.&lt;/p&gt;

&lt;p&gt;C++版本的安装如下:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;hg clone https://code.google.com/p/protorpc.cxx/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cd protorpc.cxx&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;build with cmake&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;C++ 版本 的 &lt;a href=&#34;https://code.google.com/p/protorpc/&#34;&gt;protorpc&lt;/a&gt; 对 &lt;code&gt;protoc.exe&lt;/code&gt; 扩展了一个
&lt;code&gt;--cxx_out&lt;/code&gt; 选项, 用于生成RPC的代码:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;${protorpc_root}/protobuf/bin/protoc --cxx_out=. arith.proto
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;em&gt;注:&lt;code&gt;--cxx_out&lt;/code&gt; 选项生成的代码除了RPC支持外, 还有xml的序列化和反序列化支持.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;下面是 C++ 的客户端链接 Go 语言版本的 服务器:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;#include &amp;quot;arith.pb.h&amp;quot;

#include &amp;lt;google/protobuf/rpc/rpc_server.h&amp;gt;
#include &amp;lt;google/protobuf/rpc/rpc_client.h&amp;gt;

int main() {
  ::google::protobuf::rpc::Client client(&amp;quot;127.0.0.1&amp;quot;, 1234);

  service::ArithService::Stub arithStub(&amp;amp;client);

  ::service::ArithRequest arithArgs;
  ::service::ArithResponse arithReply;
  ::google::protobuf::rpc::Error err;

  // EchoService.mul
  arithArgs.set_a(3);
  arithArgs.set_b(4);
  err = arithStub.multiply(&amp;amp;arithArgs, &amp;amp;arithReply);
  if(!err.IsNil()) {
    fprintf(stderr, &amp;quot;arithStub.multiply: %s\n&amp;quot;, err.String().c_str());
    return -1;
  }
  if(arithReply.c() != 12) {
    fprintf(stderr, &amp;quot;arithStub.multiply: expected = %d, got = %d\n&amp;quot;, 12, arithReply.c());
    return -1;
  }

  printf(&amp;quot;Done.\n&amp;quot;);
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;详细的使用说明请参考: &lt;a href=&#34;https://code.google.com/p/protorpc/source/browse/README.md?repo=cxx&#34;&gt;README.md&lt;/a&gt; .
更多的例子请参考: &lt;a href=&#34;http://code.google.com/p/protorpc/source/browse/tests/rpctest/rpcserver.cc?repo=cxx&#34;&gt;rpcserver.cc&lt;/a&gt;
和 &lt;a href=&#34;http://code.google.com/p/protorpc/source/browse/tests/rpctest/rpcclient.cc?repo=cxx&#34;&gt;rpcclient.cc&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;总结&#34;&gt;总结&lt;/h2&gt;

&lt;p&gt;Go语言的RPC客户端是一个使用简单, 而且功能强大的RPC库. 基于标准的RPC库我们可以方便的定制自己的RPC实现(传输协议和串行化协议都可以定制).&lt;/p&gt;

&lt;p&gt;不过在开发 &lt;a href=&#34;https://code.google.com/p/protorpc/&#34;&gt;protorpc&lt;/a&gt; 的过程中也发现了&lt;code&gt;net/rpc&lt;/code&gt;包的一些不足之处:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;内置的&lt;code&gt;HTTP&lt;/code&gt;协议的RPC的串行化协议和传输协议耦合过于紧密, 用户扩展的协议无法支持内置的&lt;code&gt;HTTP&lt;/code&gt;传输协议(因为&lt;code&gt;rpc.Server&lt;/code&gt;和&lt;code&gt;rpc.Client&lt;/code&gt;接口缺陷导致的问题)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rpc.Server&lt;/code&gt; 只能注册 &lt;code&gt;rpc.ServerCodec&lt;/code&gt;, 而不能注册工厂函数. 而&lt;code&gt;jsonrpc.NewServerCodec&lt;/code&gt;需要依赖先建立链接(&lt;code&gt;conn&lt;/code&gt;参数), 这样导致了&lt;code&gt;HTTP&lt;/code&gt;协议只能支持内置的&lt;code&gt;gob&lt;/code&gt;协议&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rpc.Client&lt;/code&gt; 的问题和 &lt;code&gt;rpc.Server&lt;/code&gt; 类似&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;因为Go1需要保证API的兼容性, 因此上述的问题只能希望在未来的Go2能得到改善.&lt;/p&gt;
</description>
      
    </item>
    
  </channel>
</rss>