Go语言高级编程-RPC和Protobuf
2022-12-20
| 2023-2-19
0  |  0 分钟
password
Created
Feb 19, 2023 04:48 PM
type
Post
status
Published
date
Dec 20, 2022
slug
summary
Go语言高级编程-RPC和Protobuf
tags
Go语言高级编程
Go
category
Go
icon

RPC入门

Go语言的RPC规则: 方法只能有两个可序列化的参数, 其中第二个参数是指针类型, 并且返回一个error类型, 同时必须是公开的方法
 

创建一个简单的RPC服务

一个RPC服务需要定义一个struct, 并将该struct注册为rpc函数, 并将该函数放在一个命名空间下, 然后建立起一个唯一的tcp链接, 最后使用rpc.ServeConn来在tcp链接上为对方提供rpc服务
// 注册一个struct type HelloService struct {} // 根据RPC的规则, 我们注册一个符合规则的RPC函数 // 方法只能有两个可序列化参数, 其中第二个参数是指针类型, 并返回一个error类型 // 且是一个公开的方法 func (p *HelloService) Hello(request string, reply *string) error { *reply = "Hello:" + request return nil } func main() { // 将struct对象注册为一个RPC服务 rpc.RegisterName("HelloService", new(HelloService)) // 建立一个TCP链接 listener, err := net.Listen("tcp", ":1234") if err != nil { log.Fatal("ListenTCP error:", err) } conn, err := listener.Accept() if err != nil { log.Fatal("Accept error:", err) } // 通过rpc.ServeConn函数在改TCP连接上为对方提供RPC服务 rpc.ServerConn(conn) }
客户端请求对应的RPC服务
func main() { // 使用rpc.Dial拨号RPC服务 client, err := rpc.Dial("tcp", "localhost:1234") if err != nil { log.Fatal("dialog:", err) } var reply string // 通过client.Call调用具体的RPC方法 // 第一个参数是用点号连接的RPC服务名和方法名 // 第二个参数是我们定义RPC方法的第一个参数, 也就是Hello函数的request入参 // 第三个参数是对应RPC方法的第二个参数, 也就是Hello函数的reply指针入参 err = client.Call("HelloService.Hello", "hello", &reply) if err != nil { log.Fatal(err) } fmt.Println(reply) }
 

RPC服务的封装

一般来讲, 我们不会这样直接的去调用一个具名的rpc服务的名称, 而是会带上一个路径前缀, 例如, 我们一般不会注册名称为HelloService这样的名字, 而是会注册为类似path/to/HelloService这样的名字以防止出现命名冲突等问题
其次, 我们不会每次都去手写Dial拨号方法, 以及调用具名的rpc函数, 一般会对其进行一次封装以实现重复的调用
type HelloServiceClient struct { *rpc.Client } func DialHelloService(network, address string) (*HelloServiceClient, error) { c, err := rpc.Dial(network, address) if err != nil { return nil, err } return &HelloServiceClient{Client: c}, nil } func (p *HelloServiceClient) Hello(request string, reply *string) error { return p.Client.Call(HelloServiceName+".Hello", request, reply) }
使用的时候, 我们就可以实现请求远端RPC代码的简化了
func main() { client, err := DialHelloService("tcp", "localhost:1234") if err != nil { log.Fatal("dialing:", err) } var reply string err = client.Hello("hello", &reply) if err != nil { log.Fatal(err) } }
 

跨语言的RPC

Go 语言的 RPC 框架有两个比较有特色的设计:
一个是 RPC 数据打包时可以通过插件实现自定义的编码和解码;
另一个是 RPC 建立在抽象的 io.ReadWriteCloser 接口之上的,我们可以将 RPC 架设在不同的通讯协议之上。
 
 

Http上的RPC

Go 语言内在的 RPC 框架已经支持在 Http 协议上提供 RPC 服务。但是框架的 http 服务同样采用了内置的 gob 协议,并且没有提供采用其它协议的接口,因此从其它语言依然无法访问的。
新的 RPC 服务其实是一个类似 REST 规范的接口,接收请求并采用相应处理流程:
func main() { rpc.RegisterName("HelloService", new(HelloService)) http.HandleFunc("/jsonrpc", func(w http.ResponseWriter, r *http.Request) { var conn io.ReadWriteCloser = struct { io.Writer io.ReadCloser }{ ReadCloser: r.Body, Writer: w, } rpc.ServeRequest(jsonrpc.NewServerCodec(conn)) }) http.ListenAndServe(":1234", nil) }
 

Protobuf

在 XML 或 JSON 等数据描述语言中,一般通过成员的名字来绑定对应的数据。但是 Protobuf 编码却是通过成员的唯一编号来绑定对应的数据,因此 Protobuf 编码后数据的体积会比较小,但是也非常不便于人类查阅。
 

安装

 
// 1. 安装protobuf及其对应的go语言支持 brew install protobuf protoc-gen-go protoc-gen-go-grpc // 2. 如果有需要可以设置一下go proxy 在.bash_profile中设置, 有多个proxy可选 // 完事儿要source .bash_profile一下 export GOPROXY=https://mirrors.tencent.com/go // 3. proto文件中指定输出的pb文件的路径和名称, 例如 option go_package="./;main"; // 4. 运行一下命令 生成go代码 protoc --go_out=. hello.proto
例如, 有下面这样的proto文件
syntax = "proto3"; package main; option go_package="./;main"; message String { string value = 1; }
生成的hello.pb.go文件基本如下
type String struct { Value string `protobuf:"bytes,1,opt,name=value" json:"value,omitempty"` } func (m *String) Reset() { *m = String{} } func (m *String) String() string { return proto.CompactTextString(m) } func (*String) ProtoMessage() {} func (*String) Descriptor() ([]byte, []int) { return fileDescriptor_hello_069698f99dd8f029, []int{0} } // 为每个成员生成的Get方法 func (m *String) GetValue() string { if m != nil { return m.Value } return "" }
name基于新的String类型, 我们就可以重新实现HelloService服务
type HelloService struct{} // 注意, 这里的String是生成的pb文件中的String struct // request也使用到了指针的方式, reply使用了.Value的方式来进行赋值 func (p *HelloService) Hello(request *String, reply *String) error { reply.Value = "hello:" + request.GetValue() return nil }
用 Protobuf 定义语言无关的 RPC 服务接口才是它真正的价值所在
更新 hello.proto 文件,通过 Protobuf 来定义 HelloService 服务:
service HelloService { rpc Hello (String) returns (String); }
然后, 运行命令, 让protoc针对grpc生成代码
protoc --go-grpc_out=. hello.proto
在生成的代码中多了一些类似 HelloServiceServer、HelloServiceClient 的新类型。这些类型是为 gRPC 服务的,并不符合我们的 RPC 要求。
 

定制代码生成插件

Protobuf 的 protoc 编译器是通过插件机制实现对不同语言的支持。比如 protoc 命令出现 --xxx_out 格式的参数,那么 protoc 将首先查询是否有内置的 xxx 插件
如果没有内置的 xxx 插件那么将继续查询当前系统中是否存在 protoc-gen-xxx 命名的可执行程序,最终通过查询到的插件生成代码
对于 Go 语言的 protoc-gen-go 插件来说,里面又实现了一层静态插件系统。比如 protoc-gen-go 内置了一个 gRPC 插件,用户可以通过 --go_out=plugins=grpc参数来生成 gRPC 相关代码,否则只会针对 message 生成相关代码。
 
 
Go
  • Go语言高级编程
  • Go
  • 一些可能你不知道的Http HeadersGo语言实战读书笔记
    目录