Golang - RPC与gRPC入门(下)

159 阅读4分钟

上一篇主要记录了RPC与gRPC的一些概念和环境搭建,这一篇主要记录Protocol Buffers的详细使用及gRPC四种数据流模式

Protocol Buffers

Protocol buffers提供了一种与语言无关、平台中立的可扩展机制,用于序列化数据结构,跟JSON类似,不过它比JSON体积更小、速度更快

1. Protocol Buffers类型

Protocol Buffers的数据类型和其它语言类似,有double、float、int类型等,一个message的结构如下。

syntax="proto3"
package tutorial;

import "google/protobuf/timestamp.proto";
option go_package = "github.com/protocolbuffers/protobuf/examples/go/tutorialpb";


message Person {
  string name = 1;
  int32 id = 2;  // Unique ID number for this person.
  string email = 3;

// 枚举类型
  enum PhoneType { 
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }
  
// 嵌套另外一个message
  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }

// 数组
  repeated PhoneNumber phones = 4;
  
// map类型
  map<string, string> lang = 6;

// 引用第三方proto文件的message类型: import "google/protobuf/timestamp.proto";
  google.protobuf.Timestamp last_updated = 5;
}

// Our address book file is just one of these.
message AddressBook {
  repeated Person people = 1;
}

Protocol Buffers与其它语言类型的映射关系 protoType.png 默认值 Protocol Buffers在解析message结构的时候,如果编码中不赋值的话,则将该message对象中的相应字段设置为该字段的默认值。

  • string的默认值为"",
  • bytes的默认值为空字节
  • bool的默认值为false
  • number的默认值为0
  • enum的默认值为第一个定义的枚举值,该值必须为0
  • message的默认值取决于编译的语言类型

2.option go_package

go_package有两部分组成,一部分是proto生成后的文件需要放在哪个目录下, 另一部分是包名

例如:将proto生成的pb.go文件放在相对于hello.proto文件的./hello/proto目录下

// hello_grpc/proto/hello.proto
syntax="proto3";

package hello;

option go_package="hello/proto;hello";

message Req {
  string name = 1;
}

message Resp {
  string message = 1;
}

service Greeter {
  rpc SayHello (Req) returns(Resp);
}

执行protoc -I . hello.proto --go-grpc_out=. --go_out=.后,生成的目录如下

image.png

如果想放根目录下的common目录,go_package="../../common/hello/proto;hello" 生成的目录就变成了

image.png

package的详细说明可以看官网的package说明

3. 引用其它proto文件

  • 引入同一个包下的不同文件的proto
// grpcdemo/hello_grpc/proto/hello.proto

syntax="proto3";

package hello;

option go_package=".;hello";

// 引入ping.proto文件, 这里import不能使用相对路径,例如: "./ping.proto"
import "ping.proto"; 

message Req {
  string name = 1;
}

message Resp {
  string message = 1;
}

service Greeter {
  rpc SayHello (Req) returns(Resp);
}

service PingPong {
  rpc Ping(Empty) returns(Pong);
// grpcdemo/hello_grpc/proto/ping.proto

syntax="proto3";

package hello;

option go_package=".;hello";

message Empty{}

message Pong {
  string name = 1;
}

grpcdemo/hellogrpc/proto目录下, 先后执行protoc -I ping.proto --go_out=. --go-grpc_out=.protoc -I hello.proto --go_out=. --go-grpc_out=.即可

  • 引入不同文件的不同包的proto
// grpcdemo/hello_grpc/proto/hello/hello.proto

syntax="proto3";


package hello;

option go_package="./;hello";

// 这里导入的是从项目根目录开始的绝对路径
import "grpcdemo/hello_grpc/proto/ping/ping.proto";

message Req {
  string name = 1;
}

message Resp {
  string message = 1;
}

service Greeter {
  rpc SayHello (Req) returns(Resp);
}

//service PingPong {
//  rpc Ping(ping.Empty) returns(ping.Pong);
//}

message PingPong{
  optional ping.Pong pingtest = 1;
}
// grpcdemo/hello_grpc/proto/ping/ping.proto
syntax="proto3";

package ping;

option go_package=".;ping";

message Empty{}

message Pong {
  string name = 1;
}

先在grpcdemo/hello_grpc/proto/ping目录下执行protoc -I . ping.proto --go_out=. --go-grpc_out=.生成ping.pb.goping_grpc.pb.go文件,然后在grpcdemo/hello_grpc/proto/hello目录下执行protoc -I ../../../../ -I ./hello --go_out=. --go-grpc_out=. *.proto

需要注意的是-I表示从项目根目录开始,一层一层找到项目的根目录执行

image.png

gRPC四种数据流模式

  1. 简单模式(Simple RPC)

  2. 服务端数据流模式(Server-side streaming RPC): 这种模式是客户端发起一次请求,服务端返回一段连续的数据流。典型的例子是股票交易

  3. 客户端数据流模式(Client-side stream RPC): 与服务端数据流模式相反,客户端源源不断的向服务端发送数据流,发送结束后,服务端返回一个相应。典型的例子是物联网终端向服务器报送数据

  4. 双向数据流模式(Bidirectional stream RPC): 客户端和服务端都可以向对方发送数据流,这个时候双方的数据可以同时互相发送,也就是实现实时交互。典型的例子就是聊天机器人

实践

  1. 新建stream.proto文件
//stream_grpc/proto/stream.proto 

syntax = "proto3";

package stream;

option go_package = "./;stream";

message StreamReqData {
  string data = 1;
}

message StreamResData {
  string data = 1;
}


service Greeter {
  rpc GetStream(StreamReqData) returns(stream StreamResData); // 服务端流模式
  rpc PostStream(stream StreamReqData) returns(StreamResData); // 客户端流模式
  rpc AllStream(stream StreamReqData) returns(stream StreamResData); // 双向数据流模式
}
  1. 创建服务端
// stream_grpc/server/server.go

package main

import (
	"fmt"
	"google.golang.org/grpc"
	stream "grpcdemo/stream_grpc/proto"
	"net"
	"sync"
	"time"
)

const PORT = ":8888"

type server struct {
	stream.UnimplementedGreeterServer
}

func (s *server) GetStream(req *stream.StreamReqData, res stream.Greeter_GetStreamServer) error {
	i := 0
	for {
		i++
		res.Send(&stream.StreamResData{
			Data: fmt.Sprintf("%v", time.Now().Unix()),
		})
		time.Sleep(time.Second)
		if i > 10 {
			break
		}

	}
	return nil
}

func (s *server) PostStream(cliStr stream.Greeter_PostStreamServer) error {
	for {
		if a, err := cliStr.Recv(); err != nil {
			fmt.Println(err)
			break
		} else {
			fmt.Println(a.Data)
		}
	}
	return nil
}

func (s *server) AllStream(allStr stream.Greeter_AllStreamServer) error {
	wg := sync.WaitGroup{}
	wg.Add(2)
	go func() {
		defer wg.Done()
		for {
			data, _ := allStr.Recv()
			fmt.Println("收到客户端消息: " + data.Data)
		}
	}()

	go func() {
		defer wg.Done()
		for {
			allStr.Send(&stream.StreamResData{Data: "我是服务器"})
			time.Sleep(time.Second)
		}
	}()

	wg.Wait()
	return nil
}

func main() {
	s := grpc.NewServer()
	stream.RegisterGreeterServer(s, &server{})
	listener, err := net.Listen("tcp", PORT)
	if err != nil {
		panic(err)
	}
	s.Serve(listener)
}

  1. 创建客户端
// stream_grpc/client/client.go

package main

import (
	"context"
	"fmt"
	"google.golang.org/grpc"
	stream "grpcdemo/stream_grpc/proto"
	"sync"
	"time"
)

func main() {
	conn, err := grpc.Dial("127.0.0.1:8888", grpc.WithInsecure())
	if err != nil {
		return
	}
	client := stream.NewGreeterClient(conn)

	//服务端数据流
	res, _ := client.GetStream(context.Background(), &stream.StreamReqData{Data: "choi"})
	for {
		a, err := res.Recv()
		if err != nil {
			fmt.Println(err)
			break
		}
		fmt.Println(a.Data)
	}

	// 客户端数据流
	putS, _ := client.PostStream(context.Background())
	i := 0
	for {
		i++
		err := putS.Send(&stream.StreamReqData{
			Data: fmt.Sprintf("choi %d", i),
		})
		if err != nil {
			return
		}
		time.Sleep(time.Second)
		if i > 10 {
			break
		}
	}

	// 双向模式
	allStr, _ := client.AllStream(context.Background())
	wg := sync.WaitGroup{}
	wg.Add(2)
	go func() {
		defer wg.Done()
		for {
			data, _ := allStr.Recv()
			fmt.Println("收到服务器消息: " + data.Data)
		}
	}()

	go func() {
		defer wg.Done()
		for {
			allStr.Send(&stream.StreamReqData{Data: "我是客户端"})
			time.Sleep(time.Second)
		}
	}()

	wg.Wait()
}

分别启动server.go和client.go程序,可以看到如下

// client.go
...
1663997151
1663997152
EOF
收到服务器消息: 我是服务器
收到服务器消息: 我是服务器
收到服务器消息: 我是服务器
...



// server.go
...
choi 10
choi 11
收到客户端消息: 我是客户端
收到客户端消息: 我是客户端
收到客户端消息: 我是客户端
...

参考链接

Protocol Buffers