Go语言gRPC通信框架

453 阅读6分钟

RPC

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

go语言标准库提供了简单RPC的实现,包路径为net/rpc

RPC-hello world

构造一个HelloService类型,其中的Hello方法用于实现打印功能

type HelloService struct {}
​
func (p *HelloService) Hello(request string, reply *string) error {
    *reply = "hello:" + request
    return nil
}

Hello方法必须满足Go语言的RPC规则

RPC规则

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

然后就可以将HelloService类型的对象注册为一个RPC服务

func main() {
    rpc.RegisterName("HelloService", new(HelloService))
​
    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(conn)
}

客户端请求HelloService服务

func main() {
    client, err := rpc.Dial("tcp", "localhost:1234")
    if err != nil {
        log.Fatal("dialing:", err)
    }
​
    var reply string
    err = client.Call("HelloService.Hello", "hello", &reply)
    if err != nil {
        log.Fatal(err)
    }
​
    fmt.Println(reply)
}

RPC规范的设计

在涉及RPC的应用中,开发人员一般分为三种角色

  • 服务端实现RPC方法的开发人员
  • 客户端调用RPC方法的人员
  • 制定服务端和客户端RPC接口规范的设计人员

接下来重构HelloService服务

第1步,明确服务名字和接口

将RPC服务的接口规范分为三个部分

  • 服务的名字
  • 服务要实现的详细方法列表
  • 注册该类型服务的函数
const HelloServiceName = "path/to/pkg.HelloService"type HelloServiceInterface interface {
    Hello(request string, reply *string) error
}
​
func RegisterHelloService(svc HelloServiceInterface) error {
    return rpc.RegisterName(HelloServiceName, svc)
}

第2步,客户端根据规范编写RPC调用的代码

type HelloServiceClient struct {
    *rpc.Client
}
​
var _ HelloServiceInterface = (*HelloServiceClient)(nil)
​
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)
}
​
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)
    }
}

第3步,基于RPC接口规范编写真实的服务端代码

type HelloService struct {}
​
func (p *HelloService) Hello(request string, reply *string) error {
    *reply = "hello:" + request
    return nil
}
​
func main() {
    RegisterHelloService(new(HelloService))
​
    listener, err := net.Listen("tcp", ":1234")
    if err != nil {
        log.Fatal("ListenTCP error:", err)
    }
​
    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Fatal("Accept error:", err)
        }
​
        go rpc.ServeConn(conn)
    }
}

跨语言的RPC

通信对端不一定都使用go作为rpc通信的语言,所以go支持将通信数据转成json格式的数据,发送时转成json格式数据,对端接收时从json数据提取

Protobuf

可以生成代码,并实现将结构化数据序列化的功能

Protobuf中最基本的数据单元是message,是类似Go语言中结构体的存在。

在message中可以嵌套message或其它的基础数据类型的成员。

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

proto语法

  • message:用于定义消息类型

  • 字段规则

    • required:消息中必填字段,不设置会导致编码异常;在protobuf23中被删掉
    • optional:可选字段,protobuf3没有了required和optional等关键字,默认为optional
    • repeate:消息中可重复字段,重复的值的顺序会被保存成切片
  • 消息号:在消息体的定义中,每个字段都必须有一个唯一标识符,是一个整数

  • 嵌套消息:可以在其他消息类型中定义,就是message里套message

  • 服务定义:如果想要将消息类型用在rpc中,需要在proto文件中定义rpc服务接口

gRPC

gRPC是Google公司开发的跨语言RPC框架,基于HTTP/2协议设计,使用protobuf(protocol buffers)作为其接口定义语言,同时底层消息交换格式也使用protobuf

gRPC示例

protobuf安装

1.下载protocol buffers编译器

下载连接https://github.com/protocolbuffers/protobuf/releases

这里下载21.9版本,下载后将bin目录添加到环境变量,命令行protoc测试是否配置成功

2.安装gRPC核心库

在go项目的IDE的terminal里执行即可

go get google.golang.org/grpc

3.除了编译器还需要安装转换成go的工具,github的protoc-gen-go是旧版本,新版本是google的protoc-gen-go

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

下载完后在GOPATH目录下会出现相应的工具protoc-gen-go-grpc和protoc-gen-go,到此环境准备完毕

proto文件编写

编写server的hello.proto

这个.proto其实就是一个接口文档,用来定义约束

//bproto版本
syntax = "proto3";
//规定生成的go文件存放位置(.代表当前目录; service代表生成的文件包名是service)
option go_package = ".;service";
//定义一个服务service名为SayHello,接收HelloRequest,返回HelloResponse
service SayHello{
  rpc SayHello(HelloRequest) returns (HelloResponse){}
}
message HelloRequest{
  string requestName=1;
}
message HelloResponse{
  string responseMsg=1;
}

在proto目录下执行命令,就会在同目录下生成两个go文件,这三个文件都拷贝到client项目的相同位置

protoc --go_out=. hello.proto   # 生成request和response等消息格式文件 
protoc --go-grpc_out=. hello.proto  # 生成服务端和客户端可调用的包文件

编写服务端步骤

  • 创建grpc server对象,可以理解为server端的抽象对象
  • 将server(其中包含需要被调用的服务端接口)注册到grpc server的内部注册中心
  • 创建listen,监听tcp端口
  • grpc server开始listen。Accept,直到Stop

编写客户端步骤

  • 创建和给定目标服务器的连接交互
  • 创建server的客户端对象
  • 发送rpc请求,等待同步响应,得到回调后返回响应结果
  • 输出响应结果

编写服务端

编辑server项目main.go

package main
​
import (
    "fmt"
    "golang.org/x/net/context"
    "google.golang.org/grpc"
    "google.golang.org/grpc/reflection"
    pb "grpcProject/hello-server/proto"
    "net"
)
​
type server struct {
    pb.UnimplementedSayHelloServer
}
​
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
    return &pb.HelloResponse{ResponseMsg: "Hello " + in.RequestName}, nil
}
​
func main() {
    // 监听本地的8972端口
    listen, err := net.Listen("tcp", ":9090")
    if err != nil {
        fmt.Printf("failed to listen: %v", err)
        return
    }
    grpcServer := grpc.NewServer()                   // 创建gRPC服务器
    pb.RegisterSayHelloServer(grpcServer, &server{}) // 在gRPC服务端注册服务
​
    reflection.Register(grpcServer) //在给定的gRPC服务器上注册服务器反射服务
    // Serve方法在lis上接受传入连接,为每个连接创建一个ServerTransport和server的goroutine。
    // 该goroutine读取gRPC请求,然后调用已注册的处理程序来响应它们。
    err = grpcServer.Serve(listen)
    if err != nil {
        fmt.Printf("failed to serve: %v", err)
        return
    }
}

编写客户端

编写client项目的main.go

package main
​
import (
    "context"
    "fmt"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
    pb "grpcProject/hello-server/proto"
    "log"
)
​
func main() {
    //连接server端,不使用安全传输
    conn, err := grpc.Dial("127.0.0.1:9090", grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        log.Fatalf("connect fail: %v", err)
    }
    defer conn.Close()
    //建立连接
    client := pb.NewSayHelloClient(conn)
    //执行rpc调用,这个方法在服务器端实现并返回结果
    resp, _ := client.SayHello(context.Background(), &pb.HelloRequest{RequestName: "ahahahahah"})
    fmt.Println(resp.GetResponseMsg())
}