gRPC + Protobuf + gRPC-gateway的组合拳✨

612 阅读7分钟

🎈前言

最近老大要求调研一下gRPC框架,计划在项目组中推广使用,所以做了一些调研及验证,踩了不少坑,这里做下记录。希望大家都少走弯路😁

代码仓库(github):github.com/zzusp/grpc-…

❓介绍

为什么要使用gRPC呢?主要出于以下几个方面考虑:

  • gRPC接口及参数可以通过proto文件统一管理,不再需要额外维护接口文档
  • gRPC支持多种主流语言,如:Java、Python、Go、Nodejs等,结合代码生成工具,可快速生成参数、响应体及方法,开发人员只需要关注业务处理逻辑
  • Protobuf将传输的数据序列化为紧凑的二进制格式,数据更小,速度更快,数据序列化后更加安全

gRPC

gRPC(Google Remote Procedure Call)是由Google开发的一个高性能广泛应用于分布式系统、微服务架构以及需要高性能通信的场景。开源的远程过程调用(RPC)框架。它允许客户端和服务器之间通过定义好的服务接口进行通信。

grpc.jpg

以下是gRPC的一些关键特点:

  1. 跨语言支持:gRPC支持多种编程语言,包括Java、C++、Python、Go等,使得不同语言的客户端和服务器可以互操作。
  2. 基于HTTP/2:gRPC使用HTTP/2协议进行传输,提供了多路复用、流控制、头部压缩等特性,提高了传输效率和性能。
  3. 接口定义:gRPC使用Protocol Buffers(protobuf)作为接口定义语言(IDL),通过.proto文件定义服务和消息格式,然后生成相应的代码。
  4. 双向流:除了传统的请求-响应模式,gRPC还支持双向流,允许客户端和服务器之间进行流式通信。
  5. 负载均衡和认证:gRPC内置支持负载均衡、命名解析、认证和超时控制等特性。

Protobuf

Protocol Buffers(简称Protobuf)是Google开发的一种语言中立、平台无关的可扩展机制,用于序列化结构化数据。它类似于XML或JSON,但更小、更快、更简单。

protobuf.jpg

以下是Protobuf的一些关键特点:

  1. 数据序列化:gRPC使用Protobuf作为默认的序列化协议。Protobuf负责将数据结构(消息)序列化为紧凑的二进制格式,从而在客户端和服务器之间传输。Protobuf的高效序列化和反序列化能力是gRPC高性能通信的基础。

  2. 接口定义:在gRPC中,服务和方法的接口定义通过Protobuf的.proto文件来描述。在.proto文件中,不仅定义了消息结构(数据格式),还定义了gRPC服务及其方法。例如:

    protobuf
    复制代码
    syntax = "proto3";
    
    service MyService {
        rpc MyMethod (MyRequest) returns (MyResponse);
    }
    
    message MyRequest {
        string name = 1;
    }
    
    message MyResponse {
        string message = 1;
    }
    

    在这个例子中,.proto文件定义了一个MyService服务和一个MyMethod方法,以及请求和响应消息格式。

  3. 自动生成代码:通过Protobuf编译器,.proto文件会生成相应的客户端和服务器端代码。这些生成的代码包含了gRPC客户端和服务器的逻辑以及消息的序列化和反序列化方法,使得开发者可以专注于业务逻辑而不是通信细节。

  4. 语言中立:Protobuf和gRPC都支持多种编程语言,这使得不同语言的客户端和服务器可以通过定义好的.proto文件进行互操作。Protobuf的语言中立性和跨平台特性进一步增强了gRPC的灵活性和可移植性。

总之,Protobuf在gRPC中扮演着关键角色,负责定义数据格式和服务接口,并提供高效的序列化机制,而gRPC利用这些特性实现了高性能、跨语言的远程过程调用。

gRPC-gateway

如果使用gRPC,则必须要考虑前端框架(如:vue的axios)发起请求的情况,因gRPC是基于HTTP/2的,而前端框架发起的请求都是HTTP/1.x,参数大多都是json格式,直接调用时会报错。

所以这里需要引入gRPC-gateway这个插件,它用于将 gRPC 服务转换为 JSON over HTTP/1.1 的 RESTful API。允许开发人员在保持 gRPC 的高性能和高效通信的同时,也能够提供传统的 RESTful API 接口。

grpc-gateway.jpg

使用gRPC-gateway时,proto文件也要做些相应修改,以下是一个添加了HTTP映射的proto文件:

syntax = "proto3";
package example;

import "google/api/annotations.proto";
import "google/rpc/code.proto";

service HelloService {
    rpc SayHello (HelloRequest) returns (HelloResponse) {
        option (google.api.http) = {
            post: "/hello"
            body: "*"
        };
    }
}

message HelloRequest {
    string name = 1;
}

message HelloResponse {
    string message = 1;
}

☝️Protoc工具(官方代码生成工具)

# 代码生成工具及下载路径
protoc.exe(需要配置环境变量)
 - https://github.com/protocolbuffers/protobuf/releases

# 以下工具需要放在protoc.exe的同级目录

protoc-gen-go.exe
 - https://github.com/protocolbuffers/protobuf-go/releases

protoc-gen-go-grpc.exe
 - https://github.com/grpc/grpc-go/releases

protoc-gen-grpc-gateway.exe, protoc-gen-openapiv2.exe
 - https://github.com/grpc-ecosystem/grpc-gateway/releases/

protoc-gen-js.exe
 - https://github.com/protocolbuffers/protobuf-javascript/releases

protoc-gen-grpc-web.exe
 - https://github.com/grpc/grpc-web/releases

⚡代码生成

# go语言 windows环境
# 生成代码需要安装:protoc.exe,protoc-gen-go.exe,protoc-gen-go-grpc.exe,protoc-gen-grpc-gateway.exe,protoc-gen-openapiv2.exe
protoc -I . --go_out ../生成代码目录 --go_opt paths=source_relative --go-grpc_out ../生成代码目录 --go-grpc_opt paths=source_relative --grpc-gateway_out ../生成代码目录 --grpc-gateway_opt logtostderr=true,paths=source_relative ./proto文件目录/*.proto

# java语言 windows环境
# java的maven项目可以使用protobuf-maven-plugin插件,具体配置的可以参考提供的代码仓库
mvn protobuf:compile
mvn protobuf:compile-custom

# python语言 windows环境
# 安装
python -m pip install --upgrade pip
python -m pip install grpcio
python -m pip install grpcio-tools
python -m pip install googleapis-common-protos
# 生成
python -m grpc_tools.protoc -I ./ --python_out=../生成代码目录 --grpc_python_out=../生成代码目录 ./proto文件目录(该目录会作为package)/*.proto

♨️业务代码示例

代码生成后,这里列一下核心业务代码,具体代码逻辑可以查看代码仓库

gRPC-gateway中转

package main

import (
    "context"
    "flag"
    "net/http"

    "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
    "google.golang.org/grpc/grpclog"

    gw "grpc-gateway/example/grpc/proto" // Update
)

var (
    // command-line options:
    // gRPC server endpoint
    grpcServerEndpoint = flag.String("grpc-server-endpoint", "localhost:9090", "gRPC server endpoint")
)

func run() error {
    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    // Register gRPC server endpoint
    // Note: Make sure the gRPC server is running properly and accessible
    mux := runtime.NewServeMux()
    opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
    // 这里注册接口
    err := gw.RegisterHelloServiceHandlerFromEndpoint(ctx, mux, *grpcServerEndpoint, opts)
    if err != nil {
       return err
    }

    // Start HTTP server (and proxy calls to gRPC server endpoint)
    return http.ListenAndServe(":8081", mux)
}

func main() {
    flag.Parse()

    if err := run(); err != nil {
       grpclog.Fatal(err)
    }
}

Java服务端

package org.example.grpc.impl;

import com.google.rpc.Code;
import io.grpc.stub.StreamObserver;
import org.example.grpc.proto.HelloProto;
import org.example.grpc.proto.HelloServiceGrpc;

public class HelloServiceGrpcImpl extends HelloServiceGrpc.HelloServiceImplBase {

    @Override
    public void sayHello(HelloProto.HelloRequest request, StreamObserver<HelloProto.HelloResponse> responseObserver) {

        HelloProto.HelloResponse response = HelloProto.HelloResponse.newBuilder().setCode(Code.OK)
                .setMessage("hello: " + request.getYourName() + ", this is java grpc server").build();
        System.out.println("request success.");
        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }
}

Java客户端

package org.example.grpc.client;

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import org.example.grpc.proto.HelloProto;
import org.example.grpc.proto.HelloServiceGrpc;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloServiceClient {

    private ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 40000).usePlaintext().build();

    @GetMapping("/sendToPython")
    public String sendToPython(@RequestParam String name) {
        HelloProto.HelloRequest.newBuilder().setYourName(name).build();
        HelloProto.HelloResponse response = HelloServiceGrpc.newBlockingStub(channel).sayHello(HelloProto.HelloRequest.newBuilder().setYourName(name).build());
        System.out.println("code: " + response.getCode() + ", message: " + response.getMessage());
        return response.getMessage();
    }

}

Python服务端

from concurrent import futures

import grpc

from example.grpc.proto import hello_pb2_grpc, hello_pb2


class HelloServer(hello_pb2_grpc.HelloServiceServicer):

    # 继承
    def SayHello(self, request, context):
        name = request.your_name
        print("name " + name)
        return hello_pb2.HelloResponse(code=0, message="hello: {}, this is python grpc server".format(name))


def serve():
    port = "40000"
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    hello_pb2_grpc.add_HelloServiceServicer_to_server(HelloServer(), server)
    server.add_insecure_port('[::]:' + port)
    server.start()
    print("Server started, listening on " + port)
    server.wait_for_termination()


if __name__ == '__main__':
    serve()

Python客户端

import grpc

from example.grpc.proto import hello_pb2_grpc, hello_pb2

def client():
    port = "9090"
    channel = grpc.insecure_channel('localhost:' + port)

    stub = hello_pb2_grpc.HelloServiceStub(channel)
    print(stub.SayHello(hello_pb2.HelloRequest(your_name="python client")))


if __name__ == '__main__':
    client()

❗问题记录

  • proto文件中,java和go语言都有对应的package配置,但python没有,python的package在生成目录中指定
  • proto文件不支持Object这类未知类型的对象,虽然有any类型,但使用时还是需要指定每个字段的具体类型,比较复杂。所以部分复杂的参数需要转为字符串来传输
  • proto生成的代码如果报错,排查问题困难
  • proto生成的参数、结果对象,不能用json工具直接转换,需要额外声明一个对象
  • proto传参是序列化后的数据,调试比较困难
  • 每次proto接口文件有新增或更新,gRPC-gateway都要重新生成代码,并在代码中注册需要中转的方法
  • 尝试过使用traefik作为HTTP/1.x请求转HTTP/2请求的尝试,但HTTP/1.x请求的参数需要是protobuf序列化后的数据,不能直接传json

💌最后

有问题或者想法的话,欢迎大家在评论区或者issue沟通📞

希望大家能点个star支持一下,灰常感谢💪