拥抱云原生,Java与Python基于gRPC通信

时间:2022-10-24 13:52:19

拥抱云原生,Java与Python基于gRPC通信

????你好,我是小航,一个正在变秃、变强的文艺倾年。
????本文讲解实战gRPC通信,欢迎大家多多关注!
????每天进步一点点,一起卷起来叭!

需求描述:

前端-java后台-python算法分析-java处理分析结果返回

大概就是这么一个情况,公司产品需要一个上万人排班(而且可能是多个上万人进行排班)的功能,但系统是基于java做的后台,公司的算法那工程师使用的是python进行算法实现,故需要进行跨系统支持,毕竟算法运算非常非常吃资源

gRPC简介:

gRPC is a modern open source high performance Remote Procedure Call (RPC) framework that can run in any environment. It can efficiently connect services in and across data centers with pluggable support for load balancing, tracing, health checking and authentication. It is also applicable in last mile of distributed computing to connect devices, mobile applications and browsers to backend services.

gRPC是一个现代的开源高性能远程过程调用(RPC)框架,可以在任何环境中运行。它可以有效地连接数据中心内和跨数据中心的服务,支持负载均衡、跟踪、健康检查和身份验证。它也适用于分布式计算,将设备、移动应用程序和浏览器连接到后端服务
拥抱云原生,Java与Python基于gRPC通信
好处可概括为:1.简单 2.快 3.跨语言跨平台 4.双向+安全

gRPC 是Google公司开发的一个高性能、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计。

  • gRpc官网地址:https://www.grpc.io
  • gRpc中文文档地址:http://doc.oschina.net/grpc

具体内容查看文档即可

gRPC机制:

拥抱云原生,Java与Python基于gRPC通信
当客户端发送请求的网络消息到达服务器时,服务器上的网络服务将其传递给服务器存根(server-stub)。服务器存根与客户端存根一一对应,是远程方法在服务端的体现,用来将网络请求传递来的数据转换为本地过程调用。服务器存根一般处于阻塞状态,等待消息输入。

当服务器存根收到网络消息后,服务器将方法参数从网络消息中提取出来,然后以常规方式调用服务器上对应的实现过程。从实现过程角度看,就好像是由客户端直接调用一样,参数和返回地址都位于调用堆栈中,一切都很正常。实现过程执行完相应的操作,随后用得到的结果设置到堆栈中的返回值,并根据返回地址执行方法结束操作。以 read 为例,实现过程读取本地文件数据后,将其填充到 read 函数返回值所指向的缓冲区。

read 过程调用完后,实现过程将控制权转移给服务器存根,它将结果(缓冲区的数据)打包为网络消息,最后通过网络响应将结果返回给客户端。网络响应发送结束后,服务器存根会再次进入阻塞状态,等待下一个输入的请求。

客户端接收到网络消息后,客户操作系统会将该消息转发给对应的客户端存根,随后解除对客户进程的阻塞。客户端存根从阻塞状态恢复过来,将接收到的网络消息转换为调用结果,并将结果复制到客户端调用堆栈的返回结果中。当调用者在远程方法调用 read 执行完毕后重新获得控制权时,它唯一知道的是 read 返回值已经包含了所需的数据,但并不知道该 read 操作到底是在本地操作系统读取的文件数据,还是通过远程过程调用远端服务读取文件数据。

总结下RPC执行步骤:

  1. 调用客户端句柄,执行传递参数。

  2. 调用本地系统内核发送网络消息。

  3. 消息传递到远程主机,就是被调用的服务端。

  4. 服务端句柄得到消息并解析消息。

  5. 服务端执行被调用方法,并将执行完毕的结果返回给服务器句柄。

  6. 服务器句柄返回结果,并调用远程系统内核。

  7. 消息经过网络传递给客户端。

  8. 客户端接受数据。

拥抱云原生,Java与Python基于gRPC通信

RPC与HTTP2:

通俗解释:HTTP VS RPC (普通话 VS 方言)

HTTP 与 RPC 的关系就好比普通话与方言的关系。要进行跨企业服务调用时,往往都是通过 HTTP API,也就是普通话,虽然效率不高,但是通用,没有太多沟通的学习成本。但是在企业内部还是 RPC 更加高效,同一个企业公用一套方言进行高效率的交流,要比通用的 HTTP 协议来交流更加节省资源。整个中国有非常多的方言,正如有很多的企业内部服务各有自己的一套交互协议一样。虽然国家一直在提倡使用普通话交流,但是这么多年过去了,你回一趟家乡探个亲什么的就会发现身边的人还是流行说方言。


gRPC协议架构:

拥抱云原生,Java与Python基于gRPC通信
gRPC是一种用于实现RPC API的技术。由于gRPC是开源框架,通信双方都基于该框架进行二次开发,从而使得通信双方聚焦在业务,无需关注由gRPC软件框架实现的底层通信。


gRPC引入了三个新概念:Channel、RPC和Message。三者之间的关系很简单:每个Channel可能有许多RPC,而每个RPC可能有许多Message。

拥抱云原生,Java与Python基于gRPC通信
那么gRPC如何关联HTTP/2呢?

拥抱云原生,Java与Python基于gRPC通信
HTTP/2中的流在一个连接上允许多个并发会话,而gRPC的通过支持多个并发连接上的多个流扩展了这个概念。

Channel: 表示和终端的一个虚拟链接

Channel 背后实际上可能有多个HTTP/2 连接。从上面关系图来看,一个RPC和一个HTTP/2连接相关联,rpc实际上是纯HTTP/2流。Message与rpc关联,并以HTTP/2数据帧的形式发送。

拥抱云原生,Java与Python基于gRPC通信

消息是在数据帧之上分层的。一个数据帧可能有许多gRPC消息,或者如果一个gRPC消息非常大,它可能跨越多个数据帧。

服务分类:

一元RPC

客户端发送一个单独的请求到服务器,然后服务器在返回一个单独响应给客户端
拥抱云原生,Java与Python基于gRPC通信
protobuf定义方式:

rpc SayHello(HelloRequest) returns (HelloResponse);

服务器流式RPC

客户端向服务器发送请求,并获得一个流来读取一系列消息。客户端从返回的流中读取,直到没有更多的消息。gRPC保证单个RPC调用中的消息顺序
拥抱云原生,Java与Python基于gRPC通信
客户端请求一次,但是服务器通过流返回多个返回消息。

定义方式:

rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);

在返回的消息的前面使用 stream 关键字

客户端流式RPC

其中客户端写入一系列消息并将它们发送到服务器,再次使用提供的流。一旦客户端完成了消息的写入,它就会等待服务器读取消息并返回响应。
拥抱云原生,Java与Python基于gRPC通信
定义方式:

rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);

双向流RPC

双方使用读写流发送一系列消息。两个流独立运作,因此客户端和服务器可以读和写在他们喜欢的任何顺序:例如,服务器可以等待收到所有客户端消息之前写的反应,也可以交替阅读一条消息然后写一个消息,或其他一些读写的结合。每个流中的消息顺序被保留。
拥抱云原生,Java与Python基于gRPC通信
定义方式:

rpc BidiHello(stream HelloRequest) returns (stream HelloResponse);

通信实战:

根据需求,我们这里以Python(服务端)与Java(客户端)为例:

安装插件:

Protobuf - IntelliJ IDEs Plugin | Marketplace (jetbrains.com)

拥抱云原生,Java与Python基于gRPC通信
设计 protocol buffer:

Protocol Buffers是什么?
Protocol Buffers官网:https://developers.google.com/protocol-buffers

定义一个msg.proto文件:

// @1 使用proto3语法
syntax = "proto3";
// @2 生成多个类(一个类方便管理)
option java_multiple_files = false;
// @3 生成java类所在包
option java_package = "com.example.spbclient.proto";
// @4 生成外层类类名
option java_outer_classname = "MsgProto";

// @6 .proto包名(逻辑包名)
package msg;

// @7 定义服务,用于描述要生成的API接口,类似于Java的业务逻辑接口类
service MsgService {
    // imgIdentify 方法名 ImgRequest 传入参数  ImgResponse 返回响应
    //注意:这里是returns 不是return
    rpc GetMsg (MsgRequest) returns (MsgResponse) {}
}
//定义请求数据结构
// string 数据类型
// calName 参数名称
// 1 序号、索引值(表示第一个参数,防止传参顺序错乱),一旦开始就不能改变
// int32 int
// int64 long
// 不可以使用 19000-19999 保留字
// TODO 大小写问题
message MsgRequest {
    string name = 1;
}

message MsgResponse {
    string msg = 1;
}

proto语法:

.proto Type Notes C++ Type Java Type Python Type[2] Go Type Ruby Type C# Type PHP Type
double double double float float64 Float double float
float float float float float32 Float float float
int32 使用变长编码,对于负值的效率很低,如果你的域有可能有负值,请使用sint64替代 int32 int int int32 Fixnum 或者 Bignum(根据需要) int integer
uint32 使用变长编码 uint32 int int/long uint32 Fixnum 或者 Bignum(根据需要) uint integer
uint64 使用变长编码 uint64 long int/long uint64 Bignum ulong integer/string
sint32 使用变长编码,这些编码在负值时比int32高效的多 int32 int int int32 Fixnum 或者 Bignum(根据需要) int integer
sint64 使用变长编码,有符号的整型值。编码时比通常的int64高效。 int64 long int/long int64 Bignum long integer/string
fixed32 总是4个字节,如果数值总是比总是比228大的话,这个类型会比uint32高效。 uint32 int int uint32 Fixnum 或者 Bignum(根据需要) uint integer
fixed64 总是8个字节,如果数值总是比总是比256大的话,这个类型会比uint64高效。 uint64 long int/long uint64 Bignum ulong integer/string
sfixed32 总是4个字节 int32 int int int32 Fixnum 或者 Bignum(根据需要) int integer
sfixed64 总是8个字节 int64 long int/long int64 Bignum long integer/string
bool bool boolean bool bool TrueClass/FalseClass bool boolean
string 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。 string String str/unicode string String (UTF-8) string string
bytes 可能包含任意顺序的字节数据。 string ByteString str []byte String (ASCII-8BIT) ByteString string

Python端:

使用 pip安装:

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

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

生成接口代码:(记得进入msg.proto所在文件夹下)

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

新建msg_server.py

// RPC 必备的包,以及刚刚生成的俩文件
import grpc
import msg_pb2
import msg_pb2_grpc

// 并发
from concurrent import futures
import time

_ONE_DAY_IN_SECONDS = 60 * 60 * 24

// service 实现GetMsg方法
class MsgServicer(msg_pb2_grpc.MsgServiceServicer):

    def GetMsg(self, request, context):
        print("Received name: %s" % request.name)
        return msg_pb2.MsgResponse(msg='Hello, %s!' % request.name)


def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    msg_pb2_grpc.add_MsgServiceServicer_to_server(MsgServicer(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    try:
        while True:
            time.sleep(_ONE_DAY_IN_SECONDS)
    except KeyboardInterrupt:
        server.stop(0)


if __name__ == '__main__':
    serve()

启动服务端:

python msg_server.py

Java端:

将msg.proto文件放到与Java同级的proto文件夹下:

拥抱云原生,Java与Python基于gRPC通信

导入依赖:

<!-- https://mvnrepository.com/artifact/net.devh/grpc-client-spring-boot-starter -->
<dependency>
            <groupId>net.devh</groupId>
            <artifactId>grpc-client-spring-boot-starter</artifactId>
            <version>2.13.1.RELEASE</version>
</dependency>

增加代码生成插件:

<build>
        <extensions>
            <extension>
                <groupId>kr.motd.maven</groupId>
                <artifactId>os-maven-plugin</artifactId>
                <version>1.4.1.Final</version>
            </extension>
        </extensions>
        <plugins>
            <plugin>
                <groupId>org.xolstice.maven.plugins</groupId>
                <artifactId>protobuf-maven-plugin</artifactId>
                <version>0.5.0</version>
                <configuration>
                    <protocArtifact>com.google.protobuf:protoc:3.0.0:exe:${os.detected.classifier}</protocArtifact>
                    <pluginId>grpc-java</pluginId>
                    <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.0.0:exe:${os.detected.classifier}</pluginArtifact>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>compile-custom</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

application.yml配置grpc

grpc:
  client:
    grpc-server:
      address: 'static://127.0.0.1:50051'
      negotiationType: plaintext
spring:
  application:
    name: spb-client
server:
  port: 8080

生成对应代码:
拥抱云原生,Java与Python基于gRPC通信
拥抱云原生,Java与Python基于gRPC通信

编写Controller层

package com.example.spbclient.controller;

import com.example.spbclient.proto.MsgProto;
import com.example.spbclient.proto.MsgServiceGrpc;
import net.devh.boot.grpc.client.inject.GrpcClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author xh
 * @Date 2022/10/14
 */
@RestController
public class ApiController {
    @GrpcClient("grpc-server")
    private MsgServiceGrpc.MsgServiceBlockingStub msgStub;

    @GetMapping("/test")
    public String testImgCal() {
        MsgProto.MsgRequest msgRequest = MsgProto.MsgRequest.newBuilder()
                .setName("图片信息")
                .build();
        MsgProto.MsgResponse msgResponse = msgStub.getMsg(msgRequest);
        return msgResponse.getMsg();
    }

}

客户端测试:

Java端:
拥抱云原生,Java与Python基于gRPC通信
Python端:
拥抱云原生,Java与Python基于gRPC通信