🎈前言
最近老大要求调研一下gRPC框架,计划在项目组中推广使用,所以做了一些调研及验证,踩了不少坑,这里做下记录。希望大家都少走弯路😁
代码仓库(github):github.com/zzusp/grpc-…
❓介绍
为什么要使用gRPC呢?主要出于以下几个方面考虑:
- gRPC接口及参数可以通过proto文件统一管理,不再需要额外维护接口文档
- gRPC支持多种主流语言,如:Java、Python、Go、Nodejs等,结合代码生成工具,可快速生成参数、响应体及方法,开发人员只需要关注业务处理逻辑
- Protobuf将传输的数据序列化为紧凑的二进制格式,数据更小,速度更快,数据序列化后更加安全
gRPC
gRPC(Google Remote Procedure Call)是由Google开发的一个高性能广泛应用于分布式系统、微服务架构以及需要高性能通信的场景。开源的远程过程调用(RPC)框架。它允许客户端和服务器之间通过定义好的服务接口进行通信。
以下是gRPC的一些关键特点:
- 跨语言支持:gRPC支持多种编程语言,包括Java、C++、Python、Go等,使得不同语言的客户端和服务器可以互操作。
- 基于HTTP/2:gRPC使用HTTP/2协议进行传输,提供了多路复用、流控制、头部压缩等特性,提高了传输效率和性能。
- 接口定义:gRPC使用Protocol Buffers(protobuf)作为接口定义语言(IDL),通过.proto文件定义服务和消息格式,然后生成相应的代码。
- 双向流:除了传统的请求-响应模式,gRPC还支持双向流,允许客户端和服务器之间进行流式通信。
- 负载均衡和认证:gRPC内置支持负载均衡、命名解析、认证和超时控制等特性。
Protobuf
Protocol Buffers(简称Protobuf)是Google开发的一种语言中立、平台无关的可扩展机制,用于序列化结构化数据。它类似于XML或JSON,但更小、更快、更简单。
以下是Protobuf的一些关键特点:
-
数据序列化:gRPC使用Protobuf作为默认的序列化协议。Protobuf负责将数据结构(消息)序列化为紧凑的二进制格式,从而在客户端和服务器之间传输。Protobuf的高效序列化和反序列化能力是gRPC高性能通信的基础。
-
接口定义:在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方法,以及请求和响应消息格式。 -
自动生成代码:通过Protobuf编译器,.proto文件会生成相应的客户端和服务器端代码。这些生成的代码包含了gRPC客户端和服务器的逻辑以及消息的序列化和反序列化方法,使得开发者可以专注于业务逻辑而不是通信细节。
-
语言中立: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时,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支持一下,灰常感谢💪