Go Engineering - Foundation - API - RPC

RPC

984yy094616b9b24193b22a1f2f2271d

  1. Client 通过本地调用,调用 Client Stub
  2. Client Stub 将参数打包(Marshalling)成一个消息,然后发送这个消息
  3. Client 所在的 OS 将消息发送给 Server
  4. Server 接收到消息后,将消息传递给 Server Stub(或者 Server Skeleton
  5. Server Stub 将消息解包(Unmarshalling)后得到消息
  6. Server Stub 调用服务端的子程序,处理完成后,将最终结果按照相反的步骤返回给 Client

image-20210331181307179

gRPC

概述

  1. gRPC:google Remote Procedure Call
  2. gRPC 是由 Google 开发的高性能、开源、跨语言通用 RPC 框架,基于 HTTP 2.0,默认使用 Protocol Buffers 序列化
  3. gRPC 的特点
    • 支持多语言
    • 基于 IDL(Interface Definition Language)文件定义服务
      • 通过 proto3 生成指定语言的数据结构服务端接口客户端 Stub
    • 通信协议基于标准的 HTTP/2,支持特性:双向流消息头压缩单 TCP 的多路复用服务端推送
    • 支持的序列化方式:ProtobufJSON
      • Protobuf 是语言无关高性能序列化框架,可以减少网络传输流量、提高通信效率

01ac424c7c1d64f678e1218827bc0109

Protocol Buffers

  1. Protocol Buffers 是由 Google 开发的序列化方法,可用作数据通信协议和数据存储格式,非常灵活高效
  2. Protocol Buffers 的传输性能非常优秀,常用于对数据传输性能要求比较高的系统中,作为数据传输格式
  3. Protocol Buffers 的特点
    • 更快的传输速度:二进制序列化,与 JSON 和 XML 等文本序列化方式相比,可以节省大量 IO
    • 跨平台多语言:protoc 基于 protobuf 定义文件,编译出不同语言的客户端或者服务端
    • 非常好的扩展性和兼容性:可以更新已有的数据结构,而不会破坏和影响原有的程序
    • 基于 IDL 文件定义服务:通过 proto3 生成指定语言的数据结构、服务端和客户端接口
  4. Protocol Buffers 在 gRPC 框架中发挥的作用
    • 定义数据结构
    • 定义服务接口
    • 通过 protobuf 进行序列化反序列化,提升传输效率

示例

定义服务

gRPC 支持定义 4 种类型的服务方法

  1. Simple RPC
    • 客户端发起一次请求,服务端响应一个数据
    • rpc SayHello (HelloRequest) returns (HelloReply) {}
  2. Server-side streaming RPC
    • 客户端发送一个请求,服务端返回数据流响应,客户端从流中读取数据直到为空
    • rpc SayHello (HelloRequest) returns (stream HelloReply) {}
  3. Client-side streaming RPC
    • 客户端将消息以流的方式发送给服务器,服务器全部处理完成后返回一次响应
    • rpc SayHello (stream HelloRequest) returns (HelloReply) {}
  4. Bidirectional streaming RPC
    • 客户端和服务端都可以向对方发送数据流,双方的数据可以同时互相发送
    • rpc SayHello (stream HelloRequest) returns (stream HelloReply) {}
helloworld.proto
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
syntax = "proto3";

// go_package 是必须的,定义包导入的路径
option go_package = "github.com/zhongmingmao/grpc/helloworld";

// package 指定生成的 .pb.go 文件所在的包名
package helloworld;

message HelloRequest {
string name = 1;
}

message HelloReply {
string message = 1;
}

service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

生成代码

1
2
3
4
5
6
7
❯ ls
helloworld.proto

❯ protoc -I. --go_out=plugins=grpc:$GOPATH/src helloworld.proto

❯ ls
helloworld.pb.go helloworld.proto

image-20220417230421181

helloworld.pb.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
type HelloRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields

Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
}

type HelloReply struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields

Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
}

// GreeterServer is the server API for Greeter service.
type GreeterServer interface {
SayHello(context.Context, *HelloRequest) (*HelloReply, error)
}

// GreeterClient is the client API for Greeter service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type GreeterClient interface {
SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
}

实现 Server

server.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import (
"context"
pb "github.com/zhongmingmao/grpc/helloworld"
"google.golang.org/grpc"
"log"
"net"
)

const (
port = ":50505"
)

// server is used to implement helloworld.GreeterServer.
type server struct {
pb.UnimplementedGreeterServer
}

// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("Received: %v", in.GetName())
return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}

func main() {
listen, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}

// 创建一个 gRPC Server 实例
s := grpc.NewServer()
// 注册服务到 gRPC 框架中
pb.RegisterGreeterServer(s, &server{})
// 启动 gRPC 服务
if err := s.Serve(listen); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
1
❯ go run server.go

实现 Client

屏蔽了底层的网络通信细节(调用方便) + 入参和出参都是 Go 结构体(不需要打包和解包)

client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
"context"
pb "github.com/zhongmingmao/grpc/helloworld"
"google.golang.org/grpc"
"log"
"os"
"time"
)

const (
address = "localhost:50505"
defaultName = "grpc"
)

func main() {
// Set up a connection to the server.
conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewGreeterClient(conn)

name := defaultName
if len(os.Args) > 1 {
name = os.Args[1]
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.Message)
}
1
2
3
4
5
❯ go run client.go
2022/04/16 15:32:15 Greeting: Hello grpc

❯ go run server.go
2022/04/16 15:32:15 Received: grpc

optional + nil

user.proto
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
syntax = "proto3";

package proto;
option go_package = "github.com/zhongmingmao/grpc/user";

message GetUserRequest {
string class = 1;
optional string username = 2; // 可选字段
optional string user_id = 3;
}

message GetUserResponse {
string class = 1;
string user_id = 2;
string username = 3;
string address = 4;
string sex = 5;
string phone = 6;
}

service User {
rpc GetUser (GetUserRequest) returns (GetUserResponse) {}
}

experimental_allow_proto3_optional:将 optional 字段编译成指针类型

1
❯ protoc --experimental_allow_proto3_optional --go_out=plugins=grpc:$GOPATH/src user.proto
1
2
3
4
5
6
7
8
9
type GetUserRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields

Class string `protobuf:"bytes,1,opt,name=class,proto3" json:"class,omitempty"`
Username *string `protobuf:"bytes,2,opt,name=username,proto3,oneof" json:"username,omitempty"` // 可选字段
UserId *string `protobuf:"bytes,3,opt,name=user_id,json=userId,proto3,oneof" json:"user_id,omitempty"`
}
user.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package user

import "context"

type User struct {
}

func (receiver *User) GetUser(ctx context.Context, in *GetUserRequest) (*GetUserResponse, error) {
// Username 的类型为 *string,可以先判断是否为 nil,而 string 的零值是空字符串,并非 nil
if in.Username != nil {
return nil, nil
}
return nil, nil
}

RESTful vs gRPC

e6ae61fc4b0fc821f94d257239f332ab

RESTful API 和 gRPC API 是合作关系,对内业务使用 gRPC API,对外业务使用 RESTful API

471ac923d2eaeca8fe13cb74731c1318