RPC 入门

243 阅读5分钟

RPC入门

远程过程调用,是分布式系统中不同节点间流行的通信方式。

Go语言RPC

Go 标准库提供了一个简单的 RPC 实现, 包的路径为 net/rpc ,也就是放在了 net包 目录下面。因此我们可以猜测该 RPC包 是建立在 net包 基础之上的。

因此是之前这两种知识的集合:

  • Socket编程
  • 序列化(json/xml/...)

"Hello, World"

由上图可知一个rpc服务由2个部分组成:

  • server
  • client

我们分别建2个项目

image.png

RPC Server

常用方法

  • func Register(rcvr interface{}) error
    
    • RPC Server 中注册接口,并公布 rcvr 的方法集。

    • 公布的方法 需满足以下条件:

      • 方法是导出
      • 方法有两个参数,都是导出类型内建类型
      • 方法的第二个参数是 指针
      • 方法只有一个 error 接口类型的返回值
    • 如果某个要求没有达到,Register 会返回错误, 并使用 log 包 将错误写入日志。

  • func RegisterName(name string, rcvr interface{}) error
    
    • 类似 Register,但使用提供的 name 代替 rcvr 的具体类型名作为服务名。

Server 搭建

下面是 HelloService 结构体, 提供了一个 Hello 方法

type HelloService struct {}

// Hello的逻辑:将对方发送的消息前面添加一个Hello 然后返还给对方
func (p *HelloService) Hello(request string, reply *string) error {
	*reply = "Hello:" + request
	return nil
}

如何把这个 Hello 方法, 变成一个 RPC ,直接供客户端调用喃?

Restful接口 , 我们已经很熟悉了, 这也是一种 RPC : JSON + HTTP

我们使用 net/rpc 包, 来实现一个 rpc server:

func main() {
	rpc.RegisterName("HelloService", new(HelloService))

	// 建立TCP链接,
	listener, err := net.Listen("tcp", ":1234")
	if err != nil {
		log.Fatal("ListenTCP error:", err)
	}

	// 通过rpc.ServeConn函数在该TCP链接上为对方提供RPC服务。
	// 每Accept一个请求,就创建一个goroutie进行处理
	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Fatal("Accept error:", err)
		}

		// 前面都是tcp的知识, 到这个RPC就接管了
		go rpc.ServeConn(conn)
	}
}

RPC Client

常用方法

  • func NewClient(conn io.ReadWriteCloser) *Client
    
    返回一个新的 RPC 客户端
  • func Dial(network, address string) (*Client, error)
    
    • 在指定的网络和地址与 RPC 服务端连接
    • 就是对 net.Dial 取得 conn 并将其传递给 NewClient 函数的封装
  • func Call(serviceMethod string, args interface{}, reply interface{}) error
    
    • 参数详情
      1. 用点号链接的RPC服务名字和方法名字,如 HelloService.Hello
      2. 请求参数
      3. 请求响应, 必须是一个指针, 有底层 rpc服务帮你赋值

客户端如何调用我们 serverHello 函数喃?

func main() {
	// 首先是通过rpc.Dial拨号RPC服务, 建立连接
	client, err := rpc.Dial("tcp", "localhost:1234")
	if err != nil {
		log.Fatal("dialing:", err)
	}

	var reply string
	err = client.Call("HelloService.Hello", "World", &reply)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(reply)
}

若将 reply 声明为 *string 调用时传递 reply 可以吗?

不行, 声明后为空指针,会报错

RPC 服务最多的优点就是 我们可以像使用本地函数一样使用 远程服务上的函数。

测试

  1. 启动服务端
go run rpc/server/main.go
  1. 客户端请求服务端
$ go run rpc/client/main.go
HelloWorld

一个基础的rpc服务就是这么简单,动手试试吧!

基于接口的RPC服务

问题

func (client *Client) Call(serviceMethod string, args interface{}, reply interface{}) error {
	call := <-client.Go(serviceMethod, args, reply, make(chan *Call, 1)).Done
	return call.Error
}

上面是 Client.Call 方法, 里面 3 个参数里有 2 个 interface{}, 你在使用的时候 可能真不知道要传入什么, 这就好像你写了一个 HTTP的服务 , 没有接口文档, 容易调用错误

如何避免这种情况喃? 我们可以对客户端进行一次封装, 使用接口当我们的文档, 明确参数类型

实现

image.png

  • 定义 hello service 的接口

    package service
    
    const HelloServiceName = "HelloService"
    
    type HelloService interface {
            Hello(request string, reply *string) error
    }
    
  • 服务端对象:

    type HelloServiceServer struct{}
    
    func (s *HelloServiceServer)Hello(request string, resp *string)error{
            *resp = fmt.Sprintf("hello, %s", request)
            return nil
    }
    
  • 约束服务端:

    // 通过接口约束HelloService服务
    var a1 service.HelloService = &HelloServiceServer{}
    
    • 但我们发现这个 a1 只是用来约束接口实现的,并没有实际作用
    // 用 下划线 忽略变量
    var _ service.HelloService = &HelloServiceServer{}
    
    • 我们发现 &HelloServiceServer{} 这个值也没有用到
    // 我们可以用 nil 强制类型转换成 *HelloServiceServer
    var _ service.HelloService = (*HelloServiceServer)(nil)
    
  • 封装客户端, 让其满足 HelloService接口 约束

    type HelloServiceClient struct{
            client *rpc.Client
        }
    
    func(c *HelloServiceClient)Hello(request string, response *string) error{
            return c.client.Call("HelloService.Hello", request, response)
        }
    
    // 约束客户端
    var _ service.HelloService = (*HelloServiceClient)(nil)
    // 新建客户端
    func NewHelloServiceClient(network , address string)*HelloServiceClient{
            conn, err := net.Dial(network, address)
            if err != nil {
                    panic(err)
            }
    
            client := rpc.NewClient(conn)
            return &HelloServiceClient{
                    client: client,
            }
        }
    
  • 基于接口约束后的客户端使用就要容易很多了:

    func main(){
            client := NewHelloServiceClient("tcp", "1234")
            var resp string
            err := client.Hello(",World", &resp)
            if err != nil{
                    panic(err)
            }
    
            fmt.Print(resp)
    }
    

gob编码

标准库的 RPC 默认采用 Go语言 特有的 gob编码, 标准库encoding/gob 是 Golang 提供的“私有”的编解码方式,它的效率会比 json,xml 等更高,特别适合在Go语言程序间传递数据

func (server *Server) ServeConn(conn io.ReadWriteCloser) {
	buf := bufio.NewWriter(conn)
	srv := &gobServerCodec{
		rwc:    conn,
		dec:    gob.NewDecoder(conn),
		enc:    gob.NewEncoder(buf),
		encBuf: buf,
	}
	server.ServeCodec(srv)
}

gob 的使用很简单, 和之前使用 base64 编码理念一样, 有 EncoderDecoder

func GobEncode(val interface{}) ([]byte, error) {
	buf := bytes.NewBuffer([]byte{})
	encoder := gob.NewEncoder(buf)
	if err := encoder.Encode(val); err != nil {
		return []byte{}, err
	}
	return buf.Bytes(), nil
}

func GobDecode(data []byte, value interface{}) error {
	reader := bytes.NewReader(data)
	decoder := gob.NewDecoder(reader)
	return decoder.Decode(value)
}

写个测试用例试试

func TestGobCode(t *testing.T) {
	t1 := &TestStruct{"name", "value"}
	resp, err := service.GobEncode(t1)
	fmt.Println(resp, err)

	t2 := &TestStruct{}
	service.GobDecode(resp, t2)
	fmt.Println(t2, err)
}

Json ON TCP

gob 是 Golang 提供的“私有”的编解码方式,因此从其它语言调用 Go语言 实现的 RPC服务 将比较困难

因此我们可以选用所有语言都支持的比较好的一些编码:

  • MessagePack: 高效的二进制序列化格式。它允许你在多种语言之间交换数据。但它更快更小
  • JSON: 文本编码
  • XML:文本编码
  • Protobuf 二进制编码

Go语言的RPC框架有两个比较有特色的设计:

  • RPC数据打包时可以通过插件实现自定义的编码和解码
  • RPC建立在抽象的 io.ReadWriteCloser 接口之上的,我们可以将RPC架设在不同的通讯协议之上

这里我们将尝试通过官方自带的 net/rpc/jsonrpc 扩展实现一个跨语言的RPC。与上一步实现的 rpc_interface 大体相同

服务端:

func main() {
    ...
    for {
        ...
	// 代码中唯一的变化是用rpc.ServeCodec函数替代了rpc.ServeConn函数,
	// 传入的参数是针对服务端的json编解码器
        go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
    }
}

客户端:

func NewHelloServiceClient(network, address string) *HelloServiceClient {
	...

	// 本来使用 NewClient()
	// client := rpc.NewClient(conn)

	// 客户端使用Json编解码器
	client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))

	return &HelloServiceClient{
		client: client,
	}
}

Json ON HTTP

Go语言内在的 RPC框架 已经支持在 HTTP协议 上提供 RPC服务 , 为了支持跨语言,编码我们依然使用 Json

首先我们依然要解决 JSON编解码 的问题, 我们需要将 HTTP接口Handler参数 传递给 jsonrpc , 因此需要满足 NewServerCodec函数 , 即

func jsonrpc.NewServerCodec(conn io.ReadWriteCloser) rpc.ServerCodec
  • 实现 io.ReadWriteCloser 需要

    • 实现 reader 接口,需要函数: Read(p []byte) (n int, err error)
    • 实现 writer 接口,需要函数: Write(p []byte) (n int, err error)
    • 实现 closer 接口,需要函数: Close() error
  • Handler参数 中的 http.ResponseWriter 包含 Write 方法,实现了 writer 接口

  • Handler参数 中的 http.Requestio.ReadCloser 即实现了 readercloser 接口 image.png

因此我们可以提前构建所需参数 conn io.ReadWriteCloser

func NewRPCReadWriteCloserFromHTTP(w http.ResponseWriter, r *http.Request) *RPCReadWriteCloser {
	return &RPCReadWriteCloser{w, r.Body}
}

type RPCReadWriteCloser struct {
	io.Writer
	io.ReadCloser
}

服务端:

func main() {
	rpc.RegisterName("HelloService", new(HelloService))

	// RPC的服务架设在“/jsonrpc”路径,
	// 在处理函数中基于http.ResponseWriter和http.Request类型的参数构造一个io.ReadWriteCloser类型的conn通道。
	// 然后基于conn构建针对服务端的json编码解码器。
	// 最后通过rpc.ServeRequest函数为每次请求处理一次RPC方法调用
	http.HandleFunc("/jsonrpc", func(w http.ResponseWriter, r *http.Request) {
		conn := NewRPCReadWriteCloserFromHTTP(w, r)
		rpc.ServeRequest(jsonrpc.NewServerCodec(conn))
	})

	http.ListenAndServe(":1234", nil)
}

image.png 这种用法常见于你的 RPC服务 需要暴露多种协议的时候, 其他时候还是老老实实写 Restful API