gRPC入门

1. 什么是gRPC

gRPC是谷歌公司基于protobuf开发的跨语言的开源RPC框架,基于http/2协议设计,对移动设备更加友好。

go语言gRPC技术栈最底层为TCP或者Unix套接字协议,在此之上是http/2协议的实现,然后在http/2协议之上有构建了针对go语言的gRPC核心库。应用程序通过gRPC插件生成的Stub代码和gRPC核心库通信,也可以直接和gRPC核心库通信。

 

2. gRPC入门

创建hello.proto文件,定义需要的HelloService接口:

syntax="proto3";
package pb;
option go_package="../pb";

message String{
  string value=1;
}

service HelloService{
  rpc Hello (String) returns (String);
}

 

使用gRPC生成指令

protoc --go_out=plugins=grpc:. hello.proto

 

我们可以在生成的代码文件中看到gRPC插件会为服务器和客户端生成不同的接口

gRPC通过context参数,为每个方法调用提供了上下文支持。客户端在调用方法的时候,可以通过可选的CallOption类型的参数提供额外的上下文信息。

我们可以构建一个gRPC的客户端:

type HelloServiceImpl struct {
}

func (p *HelloServiceImpl) Hello(ctx context.Context, args *pb.String) (*pb.String, error) {
	reply := &pb.String{Value: "hello: " + args.GetValue()}
	return reply, nil
}

func main() {
	// 构造一个grpc服务对象
	grpcServer := grpc.NewServer()
	// 注册grpc服务,和rpc很类似
	pb.RegisterHelloServiceServer(grpcServer, new(HelloServiceImpl))

	listener, err := net.Listen("tcp", ":1234")
	if err != nil {
		log.Fatal(err)
	}

	// 在指定端口提供grpc服务
	grpcServer.Serve(listener)
}

 

然后构建一个gRPC客户端接收gRPC服务:

func main() {
	// 和grpc服务建立连接
	conn, err := grpc.Dial("localhost:1234",
		grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	// 构造client对象,client可以调用服务端注册的grpc服务提供的方法
	client := pb.NewHelloServiceClient(conn)

	// 调用grpc服务
	reply, err := client.Hello(context.Background(), &pb.String{Value: "hello"})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(reply.GetValue())
}

 

现在就可以启用grpc服务了

 

gRPC和标准库RPC有一个区别,即gRPC生成的接口并不支持异步调用。不过我们可以在多个goroutine之间安全的共享gRPC底层的http/2链接,因此可以通过在另一个goroutine阻塞调用的方法模拟异步调用。

 

3. gRPC流

远程调用要求每次传输的数据量不能太大,否则响应时间带来的问题将损害rpc的高效性,所以传统rpc对上传下载数据量较大的场景并不合适,同时传统rpc也不适用于时间不确定的订阅和发布模式。

为此,gRPC框架针对服务端和客户端分别提供了流特性。

服务端或客户端的单项流是双向流的特例,我们在HelloService增加一个支持双向流的Channel()方法

关键字stream指定启用的流特性,参数部分是接受客户端的流,返回值是返回给客户端的流。

service HelloService{
  rpc Hello (String) returns (String);
  rpc Channel(stream String) returns (stream String);
}

 

重新生成代码,可以看到接口中新增的Channel()方法的定义

现在我们可以开始实现服务端流服务,基本的方法就是Recv() 和Send()

func (p *HelloServiceImpl) Channel(stream pb.HelloService_ChannelServer) error {

	// 循环接收客户端发来的数据
	for {
		args, err := stream.Recv()
		if err != nil {
			// 如果遇到EOF表示客户端流关闭
			if err == io.EOF {
				return nil
			}
			return err
		}

		// 生成数据
		reply := &pb.String{Value: "hello: " + args.GetValue()}

		// 通过流发送给客户端
		err = stream.Send(reply)
		if err != nil {
			return err
		}
	}
}

 

然后让客户端在两个不同的协程中处理接收和发送:

func main() {
	// 和grpc服务建立连接
	conn, err := grpc.Dial("localhost:1234",
		grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	// 构造client对象,client可以调用服务端注册的grpc服务提供的方法
	client := pb.NewHelloServiceClient(conn)

	stream, err := client.Channel(context.Background())
	if err != nil {
		log.Fatal(err)
	}

	// 将发送和接收数据放到两个独立的goroutine中
	// 发送数据
	go func() {
		for {
			if err := stream.Send(&pb.String{Value: "hi"}); err != nil {
				log.Fatal(err)
			}
			time.Sleep(time.Second)
		}
	}()

	// 接收数据
	for {
		reply, err := stream.Recv()
		if err != nil {
			if err == io.EOF {
				break
			}
			log.Fatal(err)
		}
		fmt.Println(reply.GetValue())
	}
}

 

posted @ 2022-03-20 21:01  aganippe  阅读(307)  评论(0编辑  收藏  举报