Go语言_RPC_Go语言的RPC

一 标准库的RPC

RPC(Remote Procedure Call,远程过程调用)是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络细节的应用程序通信协议。简单的说就是要像调用本地函数一样调用服务器的函数。

RPC协议构建于TCP或UDP,或者是 HTTP之上,允许开发者直接调用另一台计算机上的程序,而开发者无需额外地为这个调用过程编写网络通信相关代码,使得开发包括网络分布式程序在内的应用程序更加容易.

Go语言的标准库已经提供了RPC框架和不同的RPC实现.

下面是一个服务器的例子:

type Echo int

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

func main() {
    rpc.Register(new(Echo))
    rpc.HandleHTTP()
    l, e := net.Listen("tcp", ":1234")
    if e != nil {
        log.Fatal("listen error:", e)
    }
    http.Serve(l, nil)
}

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

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

func (t *T) MethodName(argType T1, replyType *T2) error

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

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

客户端可以这样调用Echo.Hi接口:

func main() {
    client, err := rpc.DialHTTP("tcp", "127.0.0.1:1234")
    if err != nil {
        log.Fatal("dialing:", err)
    }

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

客户端先用rpc.DialHTTP和RPC服务器进行一个链接(协议必须匹配).

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

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

另外一个例子:

服务器端代码:

[plain] view plain copy

  1. package main
  2. import (
  3. "errors"
  4. "fmt"
  5. "net/http"
  6. "net/rpc"
  7. )
  8. const (
  9. //URL = "10.200.7.244:3545"
  10. URL = "127.0.0.1:3545"
  11. )
  12. type Args struct {
  13. A, B int
  14. }
  15. type Quotient struct {
  16. Quo, Rem int
  17. }
  18. type Arith int
  19. func (t *Arith) Multiply(args *Args, reply *int) error {
  20. *reply = args.A * args.B
  21. return nil
  22. }
  23. func (t *Arith) Divide(args *Args, quo *Quotient) error {
  24. if args.B == 0 {
  25. return errors.New("divide by zero!")
  26. }
  27. quo.Quo = args.A / args.B
  28. quo.Rem = args.A % args.B
  29. return nil
  30. }
  31. func main() {
  32. arith := new(Arith)
  33. rpc.Register(arith)
  34. rpc.HandleHTTP()
  35. err := http.ListenAndServe(URL, nil)
  36. if err != nil {
  37. fmt.Println(err.Error())
  38. }
  39. }

客户端代码:

[plain] view plain copy

  1. package main
  2. import (
  3. "fmt"
  4. "net/rpc"
  5. )
  6. const (
  7. //URL = "10.200.7.234:3545"
  8. URL = "127.0.0.1:3545"
  9. )
  10. type Args struct {
  11. A, B int
  12. }
  13. func main() {
  14. client, err := rpc.DialHTTP("tcp", URL)
  15. if err != nil {
  16. fmt.Println(err.Error())
  17. }
  18. args := Args{4, 4}
  19. var reply int
  20. err = client.Call("Arith.Multiply", &args, &reply)
  21. if err != nil {
  22. fmt.Println(err.Error())
  23. } else {
  24. fmt.Println(reply)
  25. }
  26. }

[plain] view plain copy

  1. client.Call("Arith.Multiply", &args, &reply)

以上的方式为同步调用

异步调用的代码:

[plain] view plain copy

  1. quotient := new(Quotient)
  2. divCall := client.Go("Arith.Divide", args, "ient, nil)
  3. replyCall := <-divCall.Done

测试截图:

二  基于 JSON 的 RPC 调用

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

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

以前有个Issue2738是针对这个问题. 我曾提交的 CL10704046 补丁用于修复这个问题. 不过因为涉及到增加rpc的接口, 官方没有接受(因为自己重写一个DialHTTP会更简单).

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

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

Go的标准库还提供了一个"net/rpc/jsonrpc"包, 用于提供基于JSON编码的RPC支持.

服务器部分只需要用rpc.ServeCodec指定json编码协议就可以了:

func main() {
    lis, err := net.Listen("tcp", ":1234")
    if err != nil {
        return err
    }
    defer lis.Close()

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

    for {
        conn, err := lis.Accept()
        if err != nil {
            log.Fatalf("lis.Accept(): %v\n", err)
        }
        go srv.ServeCodec(jsonrpc.NewServerCodec(conn))
    }
}

客户端部分值需要用 jsonrpc.Dial 代替 rpc.Dial 就可以了:

func main() {
    client, err := jsonrpc.DialHTTP("tcp", "127.0.0.1:1234")
    if err != nil {
        log.Fatal("dialing:", err)
    }
    ...
}

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

关于jsonrpc的实现细节这里就不展开讲了, 感兴趣的话可以参考这篇文章: JSON-RPC: a tale of interfaces.

三 基于 Protobuf 的 RPC 调用

Protobuf 是 Google 公司开发的编码协议. 它的优势是编码后的数据体积比较小(并不是压缩算法), 比较适合用于命令的传输编码.

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

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

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

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

要使用 protorpc, 需要先在proto文件定义接口(arith.pb/arith.proto):

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

protorpc使用cc_generic_services选择控制是否输出RPC代码. 因此, 需要设置cc_generic_servicestrue.

然后下载 protoc-2.5.0-win32.zip, 解压后可以得到一个 protoc.exe 的编译命令.

然后使用下面的命令获取 protorpc 和对应的 protoc-gen-go 插件.

go get code.google.com/p/protorpc
go get code.google.com/p/protorpc/protoc-gen-go

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

cd arith.pb && protoc --go_out=. arith.proto

新生成的文件为arith.pb/arith.pb.go.

下面是基于 Protobuf-RPC 的服务器:

package main

import (
    "errors"

    "code.google.com/p/goprotobuf/proto"

    "./arith.pb"
)

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("divide by zero")
    }
    reply.Quo = proto.Int32(args.GetA() / args.GetB())
    reply.Rem = proto.Int32(args.GetA() % args.GetB())
    return nil
}

func main() {
    arith.ListenAndServeArithService("tcp", ":1984", new(Arith))
}

其中导入的 "./arith.pb" 的名字为 arith, 在 arith.pb/arith.proto 文件中定义(这2个可能不同名, 导入时要小心).

arith.ArithRequestarith.ArithResponse是RPC接口的输入和输出参数, 也是在在arith.pb/arith.proto 文件中定义的.

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

客户端的使用也很简单, 只要一个 arith.DialArithService 就可以链接了:

stub, client, err := arith.DialArithService("tcp", "127.0.0.1:1984")
if err != nil {
    log.Fatal(`arith.DialArithService("tcp", "127.0.0.1:1984"):`, err)
}
defer client.Close()

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

var args ArithRequest
var reply ArithResponse

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

相比标准的RPC的库, protorpc 由以下几个优点:

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

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

四 C++ 调用 Go 提供的 Protobuf-RPC 服务

protorpc 同时也提供了 C++ 语言的实现.

C++版本的安装如下:

  1. hg clone https://code.google.com/p/protorpc.cxx/
  2. cd protorpc.cxx
  3. build with cmake

C++ 版本 的 protorpc 对 protoc.exe 扩展了一个 
--cxx_out 选项, 用于生成RPC的代码:

${protorpc_root}/protobuf/bin/protoc --cxx_out=. arith.proto

注:--cxx_out 选项生成的代码除了RPC支持外, 还有xml的序列化和反序列化支持.

下面是 C++ 的客户端链接 Go 语言版本的 服务器:

#include "arith.pb.h"

#include <google/protobuf/rpc/rpc_server.h>
#include <google/protobuf/rpc/rpc_client.h>

int main() {
  ::google::protobuf::rpc::Client client("127.0.0.1", 1234);

  service::ArithService::Stub arithStub(&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(&arithArgs, &arithReply);
  if(!err.IsNil()) {
    fprintf(stderr, "arithStub.multiply: %s\n", err.String().c_str());
    return -1;
  }
  if(arithReply.c() != 12) {
    fprintf(stderr, "arithStub.multiply: expected = %d, got = %d\n", 12, arithReply.c());
    return -1;
  }

  printf("Done.\n");
  return 0;
}

详细的使用说明请参考: README.md . 
更多的例子请参考: rpcserver.cc 
和 rpcclient.cc

五 总结

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

不过在开发 protorpc 的过程中也发现了net/rpc包的一些不足之处:

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

因为Go1需要保证API的兼容性, 因此上述的问题只能希望在未来的Go2能得到改善.

时间: 2024-09-30 07:02:44

Go语言_RPC_Go语言的RPC的相关文章

嵌入式 Linux C语言——C语言基础

嵌入式 Linux C语言--C语言基础 一.数据类型 1.基本数据类型 数据类型是创建变量的模型.变量名是连续存储空间的别名,程序中使用变量命名存储空间,通过变量可以使用存储空间.变量所占的内存大小取决于创建变量的数据类型. 2.有符号和无符号 有符号数中数据类型的最高位用于标识数据的符号,最高位为1表示为负数,最高位为0表示为正数. 计算机中有符号数通常使用补码表示,正数的补码为正数本身,负数的补码为负数的绝对值的各位取反后加1. 计算机中无符号数通常使用原码表示,无符号数默认为正数,没有符

Atitit.编程语言的主要的种类and趋势 逻辑式语言..函数式语言...命令式语言

Atitit.编程语言的主要的种类and趋势 逻辑式语言..函数式语言...命令式语言 1. 编程语言的主要的种类 逻辑式语言..函数式语言...命令式语言 1 2. 逻辑式语言,,不必考虑实现过程而只需考虑定义和结果 1 3. 第五代语言 1 1. 编程语言的主要的种类 逻辑式语言..函数式语言...命令式语言 在FAQ3里说过,世界上有C和LISP两种编程语言,你现在学了C,以后学了C++.Java..NET 等等,也仍然只认识了半个世界.LISP和 Haskell构成了另外半个世 2. 逻

ios学习笔记图片+图片解释(c语言 oc语言 ios控件 ios小项目 ios小功能 swift都有而且笔记完整喔)

下面是目录其中ios文件夹包括了大部分ios控件的介绍和演示,swift的时完整版,可以学习完swift(这个看的是swift刚出来一周的视频截图,可能有点赶,但是完整),c语言和oc语言的也可以完整的学习完所需知识,,其他文件夹的内容如其名说描述一样 没张图片都有文字说明,可以需要该功能的时候搜索一下然后打开图片就可以学习到 网盘下载地址:需要的话给留言我再传上去 http://www.cnblogs.com/langtianya原创 ios学习笔记图片+图片解释(c语言 oc语言 ios控件

编程语言 标记语言 脚本语言分别有哪些? 区别是什么?

著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处.作者:蔡超凡链接:http://www.zhihu.com/question/22443881/answer/48223449来源:知乎 一.各自的定义标记语言 标记语言,是一种将文本(Text)以及文本相关的其他信息结合起来,展现出关于文档结构和数据处理细节的电脑文字编码.与文本相关的其他信息(包括例如文本的结构和表示信息等)与原来的文本结合在一起,但是使用标记(markup)进行标识. 如:HTML.XML 脚本语言 脚本语

编程语言 标记语言 脚本语言区别是什么?

一.各自的定义标记语言 标记语言,是一种将文本(Text)以及文本相关的其他信息结合起来,展现出关于文档结构和数据处理细节的电脑文字编码.与文本相关的其他信息(包括例如文本的结构和表示信息等)与原来的文本结合在一起,但是使用标记(markup)进行标识. 如:HTML.XML 脚本语言 脚本语言是为了缩短传统的编写-编译-链接-运行(edit-compile-link-run)过程而创建的计算机编程语言.它的命名起源于一个脚本"screenplay",每次运行都会使对话框逐字重复.早期

标记语言 脚本语言 编译型语言

1.标记语言 标记语言,是一种将文本(Text)以及文本相关的其他信息结合起来,展现出关于文档结构和数据处理细节的电脑文字编码.与文本相关的其他信息(包括例如文本的结构和表示信息等)与原来的文本结合在一起,但是使用标记(markup)进行标识.如:HTML.XML 2.脚本语言 脚本语言是为了缩短传统的编写-编译-链接-运行(edit-compile-link-run)过程而创建的计算机编程语言.它的命名起源于一个脚本“screenplay”,每次运行都会使对话框逐字重复.早期的脚本语言经常被称

每天一点Go语言——Go语言语法基础及基本数据类型

每天一点Go语言--Go语言语法基础及基本数据类型 一.Go语言基础语法 ? 上篇Go语言讲解中谈到了有关Go语言的基本组成结构,该小节就来讲述有关go语言的基础语法.Go语言程序由多个标记组成,如:关键字.标识符.常量.符号.字符串. ? 行分割符--一行表示一个语句结束,无需写":".当然,你可以将多条语句写在一行,并且使用":"号区分,但是不推荐这样写开发代码. ? 注释在本文就不多说了. 标识符 ? 如果说变量是shell入门的第一道关卡,那么可以说,标识符

OC语言-04-OC语言-核心语法

一.点语法 1> 基本使用 点语法本质上是set方法/get方法的调用 2> 使用注意 若出现在赋值操作符的右边,在执行时会转换成get方法 若出现在赋值操作符的左边,在执行时会转换成set方法 不能在set.get方法中用self使用点语法,会造成死循环 二.property和synthesize关键字 1> @property 作用 ① 自动生成某个成员变量的set方法和get方法 使用注意 ① 只能出现在@interface中 ② 可以使用逗号运算符为多个同类型的成员变量生成set

李洪强-C语言7-C语言运算符

C语言运算符 一.算术运算 C语言一共有34种运算符,包括常见的加减乘除运算. ①. 加法:+ 还可以表示正号 ②. 减法:- 还可以表示负号 ③. 乘法:* 非数学意义上的X ④. 除法:/  注意1/2的值为0而非0.5 ⑤. 取余(模运算):两个整数相除之后的余数(注意两边都必须是整数,带符号只和左值有关) 注意点: ①. Int a=10.8;//数据精度丢失警告.结果为10——自动类型转换 ②. Int a=(int)10.8;//无警告.把10.8强制转换为整型——强制类型转换 ③.