高效性跨平台分布式软件开发技术——gRPC

相信不少人在做项目开发的时候都会遇到这样的问题:一套系统中不同部分的代码会部署在多个不同的设备平台上,多个平台间需要保证数据相连。对于分布式部署的系统来说,高效的通信直接决定了整个系统的性能。此外,由于平台的多样性,不同平台上所适合的编程语言也可能有所差异(如:c++、python、objective-c等)。为此,一门支持“跨平台”、“跨语言”的技术对于一个分布式部署的系统来说显得尤为重要。

1. gRPC(google Remote Procedure Call) 技术 —— 高效性地跨平台、跨语言开发

为了解决上述问题,google设计了一种远程系统调用技术,其将多个不同设备平台视为一个整体,保证了在一个设备上的代码能够对另一个设备上代码中的函数进行访问和调用,实现了多个平台之间的数据交互,且各设备间不需要约定使用同一种编程语言,这就是gRPC技术。例如,在服务器机器上使用c++实现好的函数,在客户端终端设备上可以直接使用Ruby调用,在另外一台客户端机器上也可以直接使用Android调用,很好的保证了“跨语言、跨平台”的开发流程。

要想实现跨平台的代码调用就必然涉及到平台间的数据通信问题,数据通信效率成为了系统性能的一个重要指标。gRPC基于protobuf设计了数据互通协议,保证了数据传输时的高效性和快速性。Protocol Buffers是一种数据格式,与XML、JSON类似,但比JSON和XML更加的高效和快速,在一些高性能且对响应速度有要求的数据传输场景非常适用。在使用JSON进行数据编译时,为了保证数据的易读性JSON是以文本的形式存储和传输的,这样一来导致进行数据交换时设备需要耗费大量的CPU在I/O动作上,从而影响整个传输速率。而protobuf不同于JSON,它会将字符串都进行序列化后再进行传输,即传输的是二进制数据,此外,protobuf是基于数字tag来标识各内容,而JSON则是以字符串来标识各内容的,因此protobuf在传输速率方面有着天然的优势。

// protobuf 基于数字来确定内容段
message Person {
  
  required string name = 1;
  
  required int32 age = 2;
  
  required string gender = 3; 
}

// Json 基于字符串来确定内容段
{
    "name" : "Wang DaChui",

    "age"  : "18",

    "gender" : "Male"
}
2. 基于 python 实现 gRPC 框架

如上一节中的图片中所示,我们可以利用gRPC框架实现不同平台上的代码之间相互调用,这一节中,我们通过分别编写“服务器”和“客户端”这两个不同平台的代码来实现在客户端代码中调用服务器代码中的函数。为了编写方便,我们将服务器和客户端代码存放在同一个机器上的不同位置,等最后部署的时候再将代码分别部署到对应的设备平台上。

2.1 一个 gPRC 项目必须包含的几大部分

下图是一个最简单的 gRPC 项目需要包含的文件,我们先来看看这些文件分别有什么用:

  • helloworld.proto:该文件的作用是“定义服务器函数” + “定义函数参数的数据类型”。
  • helloworld_pb2.pyhelloworld_pb2_grpc.py: 这两个文件是由python的grpc_tools根据helloworld.proto文件自动生成的,这两个文件保证了客户端能够调用服务器端的函数,不用修改。
  • server.py:服务器(设备平台1)上的代码。
  • client.py:客户端(设备平台2)上的代码。

上述4个部分是一个最简单的grpc项目所包含的文件,接下来我们依次实现每一个部分中代码,搭建一个简单的grpc项目,使得我们能够在client.py代码中调用server.py中定义的函数。这里可能有人会有疑问,如果要让client.py能调用server.py中的函数,那直接把server.py写成一个类,再在client.py中import server不就好了吗?需要注意的是,在最终部署的时候server.py和client.py是会部署在不同的机器上的,那时候client.py的机器上是找不到server.py代码文件的,这里只是为了方便开发和讲解才把这两个文件放在同一个机器上。

2.2 helloworld.proto 文件——定义全局可调用函数及其参数数据结构

.proto文件中需要定义服务器中需要实现的全局可调用函数,以及这个函数的参数数据结构,由于我们想在服务器上实现一个客户端可以直接调用的加法器,那么在.proto文件中我们就需要明确定义这个“加法函数”,以及“加法函数”所接收的参数类型和返回值类型,helloworld.proto文件内容如下:

syntax = "proto3";

service Adder{
	// 将全局可调用函数AddNumber在这里声明,第一个括号内是传入参数,第二个括号是返回值参数
    rpc AddNumber(AddRequest) returns (AddResponse) {}
}

// 对全局函数AddNumber的输入参数AddRequest进行数据格式定义
message AddRequest{
    int32 Number1 = 1;
    int32 Number2 = 2;
}

// 对全局函数AddNumber的返回值参数AddResponse进行数据格式定义
message AddResponse{
    string ResultString = 1;
}

编写完成后保存文件,在Terminal中运行:

python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. ./helloworld.proto

运行成功后会在当前目录生成两个文件:helloworld_pb2.pyhelloworld_pb2_grpc.py

  • –python_out:该参数指定了helloworld_pb2.py文件的生成路径。
  • –grpc_python_out:该参数指定了helloworld_pb2_grpc.py文件的生成路径。
  • -I:该参数指定了.proto文件的存放路径。
  • ./helloworld.proto:.proto文件的文件名。
2.3 server.py 文件——实现服务器端的全局可调用函数

server.py文件用于实现在.proto文件中声明的那些全局可调用函数,例如我们在helloworld.proto文件中声明了一个AddNumber()函数,那么我们就需要在server.py文件中去亲自实现这个函数,server.py文件内容如下:

from concurrent import futures
import logging

import grpc

import helloworld_pb2
import helloworld_pb2_grpc

"""对应.proto文件中的service Adder类"""
class Adder(helloworld_pb2_grpc.AdderServicer):
	
    """实现.proto文件中的AdderNumber函数,这里request参数类型和.proto文件中AddRequest类型一样"""
    def AddNumber(self, request, context):
        
        """保证该函数的返回值和.proto文件中定义的AddResponse返回值类型一样"""
        return helloworld_pb2.AddResponse(ResultString="Result: %d" % (request.Number1 + request.Number2))

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    helloworld_pb2_grpc.add_AdderServicer_to_server(Adder(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    server.wait_for_termination()

if __name__ == "__main__":
    logging.basicConfig()
    serve() 

在这个文件中最重要的部分就是Adder这个类,这个类是对.proto文件中的service Adder的实现,因此在.proto中service部分里定义的函数在这个Adder class中都必须要实现。AddNumber(self, request, context)函数就是其对应的实现,第二个参数request的类型和.proto中定义的AddRequest类型是一样的(这个request参数是在客户端调用这个函数时需要传入的),因此可以直接通过request.Number1 和 request.Number2来直接访问这两个整型数据。最后在函数返回的时候必须要保证值也是.proto中定义的返回值类型,最好的办法就是直接返回.proto文件中的这个对象,只是修改这个对象中的值ResultString即可。

2.4 client.py 文件——调用在服务器代码中实现的全局函数

当我们实现好了server.py中的函数后,我们就可以在客户端代码中调用这个全局函数了,在这里我们在client.py代码中去调用server.py代码中实现的Adder()函数,client.py的内容如下:

import logging

import grpc

import helloworld_pb2
import helloworld_pb2_grpc

def run():
    
    """建立通信管道"""
    with grpc.insecure_channel('localhost:50051') as channel:
        
        """建立一个stub对象,通过这个对象我们就可以调用服务器端的函数了"""
        stub = helloworld_pb2_grpc.AdderStub(channel)
        
        """调用stub.AddNumber()函数,要保证传入参数是.proto文件中的AddRequest对象"""
        response = stub.AddNumber(helloworld_pb2.AddRequest(Number1=1, Number2=2))
    
    print(response)

if __name__ == "__main__":
    logging.basicConfig
    run()

在客户端代码中首先要建立一个stub对象,该对象就是远程服务器对象,我们通过stub对象就可以访问远程服务器上实现的全部函数了。要注意的是,在传入参数里必须要和.proto文件中的传入对象完全保持一致,最好的办法还是传入一个.proto文件中所声明的对象,只是修改其中的值便可。

2.5 运行结果

依次在两个不同的Terminal中先后运行server.py和client.py,在client端的运行环境中看到成功调用了server端中的AddNumber()函数:

3. 基于gRPC技术搭建一个简单的Agent学习系统

使用gRPC技术可以方便的设计一个Agent学习系统,即仿真端负责仿真环境的数据计算,算法端负责Agent行为决策。我使用gRPC技术搭建了一个简单的机器人找钻石的学习系统,github地址:这里

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值