技术分享——gRPC

569 阅读7分钟

gRPC技术分享

1、简介

gRPC 一开始由 google 开发,是一款语言中立、平台中立、开源的远程过程调用(RPC)系统。

gRPC 是一个高性能、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计。目前提供 C、Java 和 Go 语言版本,分别是:grpc, grpc-java, grpc-go. 其中 C 版本支持 C, C++, Node.js, Python, Ruby, Objective-C, PHP 和 C# 支持.

gRPC 基于 HTTP/2 标准设计,带来诸如双向流、流控、头部压缩、单 TCP 连接上的多复用请求等特。这些特性使得其在移动设备上表现更好,更省电和节省空间占用。

在 gRPC 里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,使得能够更容易地创建分布式应用和服务。与许多 RPC 系统类似,gRPC 也是基于以下理念:定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。在服务端实现这个接口,并运行一个 gRPC 服务器来处理客户端调用。在客户端拥有一个存根能够像服务端一样的方法。

2、gRPC特性

2.1 基于HTTP/2

HTTP/2 提供了连接多路复用、双向流、服务器推送、请求优先级、首部压缩等机制。可以节省带宽、降低TCP链接次数、节省CPU,帮助移动设备延长电池寿命等。gRPC 的协议设计上使用了HTTP2 现有的语义,请求和响应的数据使用HTTP Body 发送,其他的控制信息则用Header 表示。

2.2 IDL(Interface Definition Language)使用ProtoBuf(protocol buffers)

protobuf官网developers.google.cn/protocol-bu…

gRPC使用ProtoBuf来定义服务,ProtoBuf是由Google开发的一种数据序列化协议(类似于XML、JSON、hessian)。ProtoBuf能够将数据进行序列化,并广泛应用在数据存储、通信协议等方面。压缩和传输效率高,语法简单,表达力强。

2.3 多语言支持

gRPC支持多种语言(C, C++, Python, PHP, Nodejs, C#, Objective-C、Golang、Java),并能够基于语言自动生成客户端和服务端功能库。目前已提供了C版本grpc、Java版本grpc-java 和 Go版本grpc-go,其它语言的版本正在积极开发中,其中,grpc支持C、C++、Node.js、Python、Ruby、Objective-C、PHP和C#等语言,grpc-java已经支持Android开发。

3、gRPC通信流程

1.gRPC通信的第一步是定义IDL,即我们的接口文档(后缀为.proto)

2.第二步是编译proto文件,得到存根(stub)文件,即上图深绿色部分。

3.第三步是服务端(gRPC Server)实现第一步定义的接口并启动,这些接口的定义在存根文件里面

4.最后一步是客户端借助存根文件调用服务端的函数,虽然客户端调用的函数是由服务端实现的,但是调用起来就像是本地函数一样。

以上就是gRPC的基本流程,从图中还可以看出,由于我们的proto文件的编译支持多种语言(Go、Java、Python等),所以gRPC也是跨语言的。

image-20220727173508118.png

4、gRPC通信方式

4.1 简单 RPC ( Simple RPC )

这就是一般的rpc调用,一个请求对象对应一个返回对象。

// proto语法
rpc simpleHello(Person) returns (Result) {}
4.2 服务端流式 RPC ( Server-side streaming RPC )

一个请求对象,服务端可以传回多个结果对象

//proto语法
rpc serverStreamHello(Person) returns (stream Result) {}
4.3 客户端流式rpc ( Client-side streaming RPC )

客户端传入多个请求对象,服务端返回一个响应结果

// proto语法
rpc clientStreamHello(stream Person) returns (Result) {}
4.4 双向流式rpc ( Bidirectional streaming RPC )

结合客户端流式rpc和服务端流式rpc,可以传入多个对象,返回多个响应对象

// proto语法
rpc biStreamHello(stream Person) returns (stream Result) {}

5、gRPC优缺点

5.1 优点

protobuf二进制消息,性能好/效率高(空间和时间效率都很不错)

proto文件生成目标代码,简单易用

序列化反序列化直接对应程序中的数据类,不需要解析后在进行映射(XML,JSON都是这种方式)

支持向前兼容(新加字段采用默认值)和向后兼容(忽略新加字段),简化升级

支持多种语言(可以把proto文件看做IDL文件)

Netty等一些框架集成

5.2 缺点

GRPC尚未提供连接池,需要自行实现

尚未提供“服务发现”、“负载均衡”机制

因为基于HTTP2,绝大部多数HTTP Server、Nginx都尚不支持,即Nginx不能将GRPC请求作为HTTP请求来负载均衡,而是作为普通的TCP请求。(nginx1.9版本已支持)

Protobuf二进制可读性差(貌似提供了Text_Fromat功能) 默认不具备动态特性(可以通过动态定义生成消息类型或者动态编译支持)

6、gRPC使用

1、编写protobuf定义接口

在指定目录下创建hello.proto,内容如下

//定义protobuf的协议类型,其中protobuf协议有两个版本,proto2和proto3
syntax = "proto3";
​
​
​
//生成配置文件的路径
option java_package = "com.mine.learn.hello";
//protobuf协议接口包装类 如果不配置的话使用驼峰命名自动生成本例为Hello
option java_outer_classname = "HelloProto";
//该项配置可以使每个对象生成单独的.java文件分别为service request response
option java_multiple_files = true;
option objc_class_prefix = "HL";
//定义包名
package proto;
​
// 服务接口.定义请求参数和相应结果 
service Hello {
  rpc SayHello (HelloRequest) returns (HelloResponse) {}
}
​
// 定义请求体
message HelloRequest {
//数据类型 数据名称 字段编号(用于二进制格式数据中标识字段位置
  string name = 1;
}
​
// 定义响应内容
message HelloResponse {
  string message = 1;
}

2、配置maven依赖和插件

 <!-- 需要 Java 9+ -->
<properties>
        <java.version>11</java.version>
        <grpc.version>1.29.0</grpc.version>
        <protobuf.version>3.11.0</protobuf.version>
    </properties>
    <dependencies>
​
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-netty-shaded</artifactId>
            <version>${grpc.version}</version>
        </dependency>
​
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-protobuf</artifactId>
            <version>${grpc.version}</version>
        </dependency>
​
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-stub</artifactId>
            <version>${grpc.version}</version>
        </dependency>
​
        <dependency> <!-- necessary for Java 9+ -->
            <groupId>org.apache.tomcat</groupId>
            <artifactId>annotations-api</artifactId>
            <version>6.0.53</version>
            <scope>provided</scope>
        </dependency>
​
    </dependencies><!--插件-->
    <build>
​
        <extensions>
            <extension>
                <groupId>kr.motd.maven</groupId>
                <artifactId>os-maven-plugin</artifactId>
                <version>1.5.0.Final</version>
            </extension>
        </extensions>
​
        <plugins>
            <plugin>
                <groupId>org.xolstice.maven.plugins</groupId>
                <artifactId>protobuf-maven-plugin</artifactId>
                <version>0.5.1</version>
​
                <configuration>
                    <protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
                    <pluginId>grpc-java</pluginId>
                    <pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
                    <!--指定包路径-->
                    <protoSourceRoot>./proto</protoSourceRoot>
                </configuration>
​
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>compile-custom</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

3、执行生成文件命令

mvn protobuf:compile

mvn protobuf:compile-custom

4、编写server端

package com.mine.learn.server;
​
import com.mine.learn.hello.HelloGrpc;
import com.mine.learn.hello.HelloRequest;
import com.mine.learn.hello.HelloResponse;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;
​
import java.io.IOException;
import java.util.concurrent.TimeUnit;
​
public class HelloServer {
    private Server server;
    private void start() throws IOException {
        /* The port on which the server should run */
        int port = 50051;
        server = ServerBuilder.forPort(port)
                .addService(new HelloIml())  //这里可以添加多个模块
                .build()
                .start();
        System.out.println("Server started, listening on " + port);
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                // Use stderr here since the logger may have been reset by its JVM shutdown hook.
                System.err.println("*** shutting down gRPC server since JVM is shutting down");
                try {
                    HelloServer.this.stop();
                } catch (InterruptedException e) {
                    e.printStackTrace(System.err);
                }
                System.err.println("*** server shut down");
            }
        });
    }
​
    private void stop() throws InterruptedException {
        if (server != null) {
            server.shutdown().awaitTermination(30, TimeUnit.SECONDS);
        }
    }
​
    private void blockUntilShutdown() throws InterruptedException {
        if (server != null) {
            server.awaitTermination();
        }
    }
​
    public static void main(String[] args) throws IOException, InterruptedException {
        final HelloServer server = new HelloServer();
        server.start();
        server.blockUntilShutdown();
    }
​
    private static class HelloIml extends HelloGrpc.HelloImplBase{
        @Override
        public void sayHello(HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
            // super.sayHello(request, responseObserver);
            HelloResponse helloResponse=HelloResponse.newBuilder().setMessage("Hello "+request.getName()+", I'm Java grpc Server").build();
            responseObserver.onNext(helloResponse);
            responseObserver.onCompleted();
        }
    }
}
​

5、编写client端

package com.mine.learn.client;
​
import com.mine.learn.hello.HelloGrpc;
import com.mine.learn.hello.HelloRequest;
import com.mine.learn.hello.HelloResponse;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
​
import java.util.concurrent.TimeUnit;
​
public class HelloClient {
    //远程连接管理器,管理连接的生命周期
    private final ManagedChannel channel;
    private final HelloGrpc.HelloBlockingStub blockingStub;
​
    public HelloClient(String host, int port) {
        //初始化连接
        channel = ManagedChannelBuilder.forAddress(host, port)
                .usePlaintext()
                .build();
        //初始化远程服务Stub
        blockingStub = HelloGrpc.newBlockingStub(channel);
    }
​
​
    public void shutdown() throws InterruptedException {
        //关闭连接
        channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
    }
​
    public String sayHello(String name) {
        //构造服务调用参数对象
        HelloRequest request = HelloRequest.newBuilder().setName(name).build();
        //调用远程服务方法
        HelloResponse response = blockingStub.sayHello(request);
        //返回值
        return response.getMessage();
    }
​
​
    public static void main(String[] args) throws InterruptedException {
        HelloClient client = new HelloClient("127.0.0.1", 50052);
        //服务调用
        String content = client.sayHello("Java");
        //打印调用结果
        System.out.println(content);
        //关闭连接
        client.shutdown();
    }
​
}
​

7、python环境client调试

1、安装

gRPC由两个部分构成,grpcio 和 gRPC 工具, 后者是编译 protocol buffer 以及提供生成代码的插件。

pip install grpcio
pip install grpcio-tools googleapis-common-protos

2、生成接口代码

python -m grpc_tools.protoc -I . --python_out=. --grpc_python_out=. hello.proto

这里会生成两个文件, hello_pb2.pyhello_pb2_grpc.py 。这两个文件是为后续的服务端和客户端所用。前者是定义了一些变量,例如 _HELLOREQUEST 中就包含了请求函数的名字,可接受的变量,实际上还是 hello.proto 里定义的东西。

3、创建客户端

import grpc
import hello_pb2
import hello_pb2_grpc
​
def run():
  # NOTE(gRPC Python Team): .close() is possible on a channel and should be
  # used in circumstances in which the with statement does not fit the needs
  # of the code.
  with grpc.insecure_channel('localhost:50051') as channel:
    stub = hello_pb2_grpc.HelloStub(channel)
    response = stub.SayHello(hello_pb2.HelloRequest(name='hello'))
  print("Client received: " + response.message)
 
 
if __name__ == '__main__':
  run()

4、创建服务端

import grpc
import hello_pb2
import hello_pb2_grpc
 
from concurrent import futures
import time
 
_ONE_DAY_IN_SECONDS = 60 * 60 * 24
 
 
class HellloServicer(hello_pb2_grpc.HelloServicer):
 
  def SayHello(self, request, context):
    print("Received name: %s" % request.name)
    return hello_pb2.HelloResponse(message='Hello, %s!' % request.name)
 
 
def serve():
  server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
  hello_pb2_grpc.add_HelloServicer_to_server(HellloServicer(), server)
  server.add_insecure_port('[::]:50052')
  server.start()
  try:
    while True:
      time.sleep(_ONE_DAY_IN_SECONDS)
  except KeyboardInterrupt:
    server.stop(0)
 
if __name__ == '__main__':
  serve()

5、测试java-server,python-client

python—client image-20220727185737117.png java-server image-20220727185850795.png

6、测试python-server,java-client

python-server

image-20220727190002016.png

java-client

image-20220727190116078.png