gRPC的使用 | 青训营

142 阅读6分钟

gRPC介绍

gRPC(gRPC Remote Procedure Call)是一种高性能、开源的远程过程调用(RPC)框架,最初由Google开发并于2015年开源发布。

1. RPC: gRPC是一种远程过程调用框架,允许应用程序在不同的计算机或设备之间像调用本地函数一样进行通信。这意味着您可以远程调用另一台计算机上的函数,而无需手动编写复杂的网络通信代码。

2. 基于HTTP/2: gRPC使用HTTP/2协议进行通信,这使得它更高效和更快速。HTTP/2支持多路复用、头部压缩和流控制等功能,这些特性有助于减少网络延迟,提高性能。

3. 跨语言支持: gRPC支持多种编程语言,包括C++, Java, Python, Go, Ruby, Node.js, C#,和其他语言。这意味着您可以在不同的编程语言中使用gRPC编写客户端和服务器,并且它们可以相互通信。

4. 自动代码生成: gRPC使用Protocol Buffers(ProtoBuf)作为其接口定义语言(IDL),并且可以自动生成客户端和服务器端的代码。这简化了开发过程,减少了潜在的错误,并提高了代码的一致性。

总的来说,gRPC是一种强大的远程通信框架,适用于构建分布式系统、微服务架构和跨语言应用程序。它提供了高性能、跨平台、自动代码生成等一系列优点,使得开发者能够更轻松地构建可扩展和高效的分布式应用。

实现流程

想要实现基于 gRPC 的通信,首先需要定义好proto文件,并生成对应的 Go 代码和 gRPC 代码。

定义protobuf

ProtoBuf的主要目的是用于在不同应用程序之间以二进制形式进行数据交换,通常用于数据存储、数据交换协议、RPC(远程过程调用)等领域。以下是关于如何定义ProtoBuf的基本概念:

1. ProtoBuf 文件: ProtoBuf的定义通常包含在一个或多个.proto文件中。这些文件包含了数据结构的定义、消息格式以及与数据交换相关的元数据。

2. 消息类型(Message Types): 在ProtoBuf中,您定义了一组消息类型,每个消息类型代表一种数据结构或对象。消息类型由字段组成,每个字段有一个唯一的数字标识符和一个字段类型。常见的字段类型包括整数、浮点数、字符串、枚举、子消息等。

3. 枚举类型(Enum Types): 枚举类型定义了一个有限的一组命名值,通常用于表示某些特定的选项或状态。每个值都有一个关联的整数值,可以在消息中使用。

4. 服务定义(Service Definitions): 除了消息类型,您还可以定义服务接口,用于描述可以通过RPC进行调用的方法。服务定义包括方法名称、输入消息类型和输出消息类型。

下面是一个简单的ProtoBuf示例,展示了如何定义一个消息类型:

syntax = "proto3";

package pb;

option go_package="addsrv/pb";


service Add {
  // Sum 对两个数字求和
  rpc Sum (SumRequest) returns (SumResponse) {}

  // Concat 方法拼接两个字符串
  rpc Concat (ConcatRequest) returns (ConcatResponse) {}
}


// Sum方法的请求参数
message SumRequest {
  int64 a = 1;
  int64 b = 2;
}

// Sum方法的响应
message SumResponse {
  int64 v = 1;
  string err = 2;
}

// Concat方法的请求参数
message ConcatRequest {
  string a = 1;
  string b = 2;
}

// Concat方法的响应
message ConcatResponse {
  string v = 1;
  string err = 2;
}

将上面的文件保存至项目目录下的pb/addsrv.proto文件中。

执行下面的命令根据上述proto文件编译生成go代码(需事先安装好protocprotoc-gen-go-grpc)。

protoc -I=pb \
   --go_out=pb --go_opt=paths=source_relative \
   --go-grpc_out=pb --go-grpc_opt=paths=source_relative \
   pb/addsrv.proto

上述命令的解释如下:

  • --go_out=.:指定生成的Go代码的输出目录为当前目录。
  • --go-grpc_out=.:指定生成的Go gRPC代码的输出目录为当前目录。
  • pb/addsrv.proto:指定要编译的ProtoBuf文件的路径。

执行上述命令后,将在当前目录中生成与addsrv.proto文件相关的Go文件,其中包括用于消息和gRPC服务的定义的Go代码。

此时项目目录如下:

├── go.mod
├── go.sum
├── main.go
└── pb
    ├── addsrv.pb.go
    ├── addsrv.proto
    └── addsrv_grpc.pb.go

grpcServer

main.go中定义好grpcServer结构体,其内部包含sumconcat两个grpctransport.Handler

import grpctransport "github.com/go-kit/kit/transport/grpc"


type grpcServer struct {
	pb.UnimplementedAddServer
	sum    grpctransport.Handler
	concat grpctransport.Handler
}

grpctransport.Handler本质上是一个接口类型。

// Handler 应该从服务实现的gRPC绑定调用。
// 传入的请求参数和返回的响应参数都是gRPC类型,而不是用户域类型。
type Handler interface {
	ServeGRPC(ctx context.Context, request interface{}) (context.Context, interface{}, error)
}

我们先定义好处理请求和响应数据的编解码函数。

// decodeGRPCSumRequest 将Sum方法的gRPC请求参数转为内部的SumRequest
func decodeGRPCSumRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
	req := grpcReq.(*pb.SumRequest)
	return SumRequest{A: int(req.A), B: int(req.B)}, nil
}

// decodeGRPCConcatRequest 将Concat方法的gRPC请求参数转为内部的ConcatRequest
func decodeGRPCConcatRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
	req := grpcReq.(*pb.ConcatRequest)
	return ConcatRequest{A: req.A, B: req.B}, nil
}

// encodeGRPCSumResponse 封装Sum的gRPC响应 
func encodeGRPCSumResponse(_ context.Context, response interface{}) (interface{}, error) {
	resp := response.(SumResponse)
	return &pb.SumResponse{V: int64(resp.V), Err: resp.Err}, nil
}

// encodeGRPCConcatResponse 封装Concat的gRPC响应
func encodeGRPCConcatResponse(_ context.Context, response interface{}) (interface{}, error) {
	resp := response.(ConcatResponse)
	return &pb.ConcatResponse{V: resp.V, Err: resp.Err}, nil
}

有了编解码的处理函数后,便可以通过grpctransport.NewServer得到grpctransport.Handler

// NewGRPCServer grpcServer构造函数
func NewGRPCServer(svc AddService) pb.AddServer {
	return &grpcServer{
		sum: grpctransport.NewServer(
			makeSumEndpoint(svc),
			decodeGRPCSumRequest,
			encodeGRPCSumResponse,
		),
		concat: grpctransport.NewServer(
			makeConcatEndpoint(svc),
			decodeGRPCConcatRequest,
			encodeGRPCConcatResponse,
		),
	}
}

最后再为我们的grpcServer实现服务。

func (s *grpcServer) Sum(ctx context.Context, req *pb.SumRequest) (*pb.SumResponse, error) {
	_, rep, err := s.sum.ServeGRPC(ctx, req)
	if err != nil {
		return nil, err
	}
	return rep.(*pb.SumResponse), nil
}

func (s *grpcServer) Concat(ctx context.Context, req *pb.ConcatRequest) (*pb.ConcatResponse, error) {
	_, rep, err := s.concat.ServeGRPC(ctx, req)
	if err != nil {
		return nil, err
	}
	return rep.(*pb.ConcatResponse), nil
}

启动gRPC服务

  1. svc := addService{}:这一行创建了一个addService类型的服务实例。您需要实现这个服务,以便gRPC服务器可以调用其中的方法。
  2. gs := NewGRPCServer(svc):这里创建了一个gRPC服务器,NewGRPCServer是一个用于创建服务器的函数,它接受一个服务实例svc作为参数。
  3. listener, err := net.Listen("tcp", ":8972"):这一行创建了一个TCP监听器,用于监听指定的地址和端口号(在这种情况下是:8972)。如果出现错误,它会将错误信息打印出来并返回。
  4. s := grpc.NewServer():这一行创建了一个gRPC服务器实例s,用于托管gRPC服务。
  5. pb.RegisterAddServer(s, gs):这一行通过pb包中的RegisterAddServer函数在gRPC服务器上注册了一个服务。这将服务的方法绑定到gRPC服务器上,使其可以被客户端调用。
  6. err = s.Serve(listener):这一行启动了gRPC服务器,并开始监听来自客户端的连接请求。如果有错误发生,它会将错误信息打印出来并返回。

这段代码的作用是创建一个gRPC服务器,注册一个服务(addService类型的服务),并启动服务器以接受来自客户端的请求。

svc := addService{}

gs := NewGRPCServer(svc)

listener, err := net.Listen("tcp", ":8972")
if err != nil {
	fmt.Printf("failed to listen: %v", err)
	return
}
s := grpc.NewServer()       // 创建gRPC服务器
pb.RegisterAddServer(s, gs) // 在gRPC服务端注册服务
// 启动服务
err = s.Serve(listener)
if err != nil {
	fmt.Printf("failed to serve: %v", err)
	return
}