gRPC Python 教程(二):ProtoBuf语法与gRPC四种通信方式的实现

ProtoBuf 协议

Protobuf 是一种序列化数据的方法,独立于编程语言与平台,它采用了压缩方式存储数据,传输相同的数据,可以用更少的字节。因此主要用于定义网络接口参数及序列化数据。

gRPC使用protobuf 做接口定义规范,这就强制要求开发人员按Protobuf语法显式地定义API接口及参数类型,独立于开发语言,所有人都可以清晰地理解接口,给接口调试带来了很大便利。

1. ProtoBuf 使用分3步

在这里插入图片描述
对 gRPC来说, 这个流程是:
1) gRPC的message 与 service 均须在protobuf 文件中定义好。
2) 编译 protobuf文件,生成两个python文件
3) 在Server 与 Client 代码中导入protobuf message 与接口,客户端接口编程要做的工作是:按message 定义准备接口参数,调用 rpc 接口函数, 输入参数时,注意要用 argument_1=value, argument_2=value,… 这种方式,序列化工作由gRPC自动完成,其次就是解析收到的响应消息。 而服务端则正好相反,先接收请求消息,解析请求参数,处理完以后再发送响应。

2. Protobuf 文件语法

对接口的描述是放在1个 .proto文件中。

该文件主要分为3部分:
i. 全局参数定义
ii. message定义
iii. 服务接口定义

Protobuf 示例 :


// proto版本3
syntax = "proto3";
//import与python用法相同,导入其它文件
import "generated/person_info.proto";
//定义包名称,等同于1个包命名空间
package demo; 

//定义服务接口, rcp请求名称对应服务器侧的接口函数名称 。
//每个接口可以包含多个rpc 请求
service HelloService {
  rpc SayHello (HelloRequest) returns (HelloResponse);
}

//Message 就是1个数据结构,类似于C++的struct 结构体, 
//这个数据结构可做为接口函数的参数类型与返回值类型. 
message HelloRequest {
  // Message的每个元素为1个field,
  string greeting = 1;
}

message HelloResponse {
  string reply = 1;
}

说明:

  • Service : 就是定义1个 gRPC service, 在service代码块内,定义rpc 方法,指定request, response类型。 gRPC支持4种rpc方法。
service interface_name {
rpc api_name( request ) returns ( response );
}
  • Message: 相当于接口函数的参数类型与返回值类型 ,需要分开定义

详细 protobuf 使用教程,请参考国外的菜鸟教程tutorialspoint 的教程 https://www.tutorialspoint.com/protobuf/index.htm, (还是国外教程讲得清楚)

gRPC 4种通信模式介绍

1. 单向RPC

gRPC的术语为unary RPC,这种方式与函数调用类似, client 发送1条请求,server回1条响应。
rpc SayHello(HelloRequest) returns (HelloResponse);

2. 服务器流式处理 RPC

客户端向服务器发送1条请求,服务器以回应多条响应消息,客户机从返回的流中读取数据,直至没有更多消息。 这时要在响应类型前加1个 stream关键字修饰。
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);

3. 客户端流式处理 RPC,

由客户端写入一系列消息并将其发送到服务。 客户端完成消息写入后,它将等待服务器读取消息并返回其响应. 这种模式,要在request的类型前加 stream 修饰。
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);

4. 双向流式处理 RPC

其中双方使用读写流发送一系列消息。这两个流独立运行,因此客户端和服务器可以按照它们喜欢的任何顺序进行读取和写入:例如,服务器可以等待接收所有客户端消息,然后再写入响应,或者它可以交替读取消息然后写入消息,或者读取和写入的某种其他组合。将保留每个流中消息的顺序。此模式下, request 与 response类型均需要用stream修饰。
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse);

说明

  • 流式处理中,服务器A向客户机B发送数据,必须是在protobuf 中 rpc方法中Response message包含的数据类型。而不是任意发送数据,程序会报错。不限制的是响应中可以回1条或多条消息。
  • 如果需要A向B发送请求,则需要定义新的protobuf 文件,并编写新的server, client python代码,这时,两台机器的角色是倒过来,B为server, A为client。仍然符合gRPC的原则。
  • 如果一定需要在1条连接中,双方互发请求,socket 模块的低阶API接口函数编程可以满足要求,但必须注意,管理socket双向通信必须小心翼翼,否则会造成混乱,

流程处理实现过程

1. 用protobuf 定义接口

下面以实例说明: users.proto ,

syntax = "proto3";
package users;

message User {
  string username = 1;
  uint32 user_id = 2;
}

message CreateUserRequest {
  string username = 1;
  string password = 2;
  string email = 3;
}

message CreateUserResult {
  User user = 1;
}

message GetUsersRequest {
  repeated User user = 1;
}

service Users {
  rpc CreateUser (users.CreateUserRequest) returns (users.CreateUserResult);
  //  response为流式通信,1个response将返回多条消息
  rpc GetUsers (users.GetUsersRequest) returns (stream users.GetUsersResult);
}

message GetUsersResult {
  User user = 1;
}

2、根据.protobuf文件生成客户方与服务方代码

首先要安装 grpcio-tools package:
pip install grpcio-tools

进入proto文件所在目录,执行如下命令
python -m grpc_tools.protoc
\ --proto_path=.
\ --python_out=.
\ --grpc_python_out=.
\ proto文件名

参数说明

  • —proto_path=proto文件路径,
  • –python_out=编译生成的文件的路径 ,
  • –grpc_python_out=编译生成的接口文件路径
  • ./route_guide.proto 是要编译的协议文件

本例 :
python -m grpc_tools.protoc --proto_path=. --python_out=. --grpc_python_out=. users.proto

生成的文件有两个: Users_pb2.py 与 Users_pb2_grpc.py,

3. 服务器端代码

from concurrent import futures
import time

import grpc

import users_pb2_grpc as users_service
import users_pb2 as users_messages

_ONE_DAY_IN_SECONDS = 60 * 60 * 24


class UsersService(users_service.UsersServicer):

    def CreateUser(self, request, context):
        metadata = dict(context.invocation_metadata())
        print(metadata)
        user = users_messages.User(username=request.username, user_id=1)
        return users_messages.CreateUserResult(user=user)

    def GetUsers(self, request, context):
        for user in request.user:
            user = users_messages.User(
                username=user.username, user_id=user.user_id
            )
            yield users_messages.GetUsersResult(user=user)


def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    users_service.add_UsersServicer_to_server(UsersService(), server)
    server.add_insecure_port('0.0.0.0:50051')
    server.start()
    try:
        while True:
            time.sleep(_ONE_DAY_IN_SECONDS)
    except KeyboardInterrupt:
        server.stop(0)


if __name__ == '__main__':
    serve()

4. 客户端侧代码

import sys

import grpc

import users_pb2_grpc as users_service
import users_pb2 as users_messages


def run():
    channel = grpc.insecure_channel('localhost:50051')
    try:
        grpc.channel_ready_future(channel).result(timeout=10)
    except grpc.FutureTimeoutError:
        sys.exit('Error connecting to server')
    else:
        stub = users_service.UsersStub(channel)
        metadata = [('ip', '127.0.0.1')]
        response = stub.CreateUser(
            users_messages.CreateUserRequest(username='tom'),
            metadata=metadata,
        )
        if response:
            print("User created:", response.user.username)
        request = users_messages.GetUsersRequest(
            user=[users_messages.User(username="alexa", user_id=1),
                  users_messages.User(username="christie", user_id=1)]
        )
        response = stub.GetUsers(request)
        for resp in response:
            print(resp)


if __name__ == '__main__':
    run()

5. 测试代码

打开两个终端窗口,分别运行grpc_server.py, grpc_client.py

可以看到client.py 窗口显示

(enva) D:\workplace\python\enva\test1>py grpc_client.py
User created: tom
user {
  username: "alexa"
  user_id: 1
}

user {
  username: "christie"
  user_id: 1
}

服务器窗口同时显示

(enva) D:\workplace\python\enva\test1>py grpc_server.py
{'user-agent': 'grpc-python/1.50.0 grpc-c/28.0.0 (windows; chttp2)', 'ip': '127.0.0.1'}

6. 双向流式通信的实现

1) 定义protobuf 服务接口

rpc 接口函数的请求参数与response参数前都加 stream 表示 client侧与 server侧都会发送多条messages,

syntax = "proto3";
package greet;

message Person {
    string name = 1; 
}

message GreetRequest {
    string greet_word = 1;
    repeated Person user = 2; 
}

message GreetResponse {
    string response = 1; 
}

service Greeting {
    rpc SayGeeting (greet.GreetRequest) returns (greet.GreetResponse);
    rpc MultiGreeting (stream GreetRequest)  returns (GreetResponse );
    rpc MultiGreetingResp (GreetRequest) returns (stream GreetResponse);
}

2) 服务器侧代码
注意服务器收到的 streaming request 的参数名,在这里改为request_iterator

import grpc
from concurrent import futures
import greet_pb2_grpc as greet_service
import greet_pb2 as greet_messages
import time

_ONE_DAY_IN_SECONDS = 60 * 60 * 24

class GreetingService(greet_service.GreetingServicer):
    def SayGeeting(self,request, context):
        metadata = dict(context.invocation_metadata())
        print(metadata)      
        g_word = request.greet_word
        users = request.user
        namelist = ", ".join([user.name for user in users ])
        print(f"receved: \n {g_word} from {namelist}")
        return greet_messages.GreetResponse(response=f"Received yours message {g_word} from { namelist }")
    
    def MultiGreeting(self, request_iterator, context):
        namelist = ""
        for req in request_iterator:
            users = req.user 
            namelist = namelist + ", ".join([user.name for user in users ])+","
        print(f"receved greetings from {namelist}")
        return greet_messages.GreetResponse(response=f"receved greetings from {namelist}" )

    
    def MultiGreetingResp(self,request,context):
        g_word = request.greet_word
        users = request.user
        namelist = ", ".join([user.name for user in users ])
        print(f"receved: \n {g_word} from {namelist}")        
        for user in users:
            yield greet_messages.GreetResponse(response=f"Hello, {user.name}, thank you for greeting" )
            
        


def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    
    greet_service.add_GreetingServicer_to_server(GreetingService(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    #server.wait_for_termination()
    try:
        while True:
            time.sleep(_ONE_DAY_IN_SECONDS)
    except KeyboardInterrupt:
        server.stop(0)


if __name__=="__main__":
    serve()

3) 客户端侧代码

客户侧的代码,发送 streaming request 时,要用 yield 生成器来生成消息。


import grpc
import  greet_pb2_grpc 
import greet_pb2

# 第1个接口 SayGreeting 
def say_greeting(stub):
    res = stub.SayGeeting(
        greet_pb2.GreetRequest(
            greet_word='Good Morning', 
            user=[greet_pb2.Person(name='Jack'), greet_pb2.Person(name="Smith") ]
            )
        )
    if res:
        print(res.response)
    return None

# 第2个接口 MultiGreeting 
def make_greeting(greet,name1,name2):
    return greet_pb2.GreetRequest(
            greet_word=greet, 
            user=[greet_pb2.Person(name=name1), greet_pb2.Person(name=name2) ]
        )

def generate_greetings():
    greetings = [
        make_greeting("Good morning","Storn","abcd"),
        make_greeting("How are you","Adfdsf","adfidd"),
        make_greeting('Are your fine', "Wang","Zhang" ),
    ]
    for greet in greetings:
        print(f"greet_word: {greet.greet_word} , users: {greet.user} ")
        yield greet

def multiple_greeting(stub):
    resp = stub.MultiGreeting(generate_greetings())
    print(resp)

def multiple_greeting_response(stub):
    responses  = stub.MultiGreetingResp(
        greet_pb2.GreetRequest(
            greet_word='Good Morning', 
            user=[greet_pb2.Person(name='Zhang'), greet_pb2.Person(name="Smith") ]
            )
        )
    for res in responses:
        print(res.response)
    return None        

def run():
    with grpc.insecure_channel('localhost:50051') as channel:
        stub = greet_pb2_grpc.GreetingStub(channel)
        print("-------------- SayGreeting --------------")
        say_greeting(stub)
        print("-------------- MultiGreeting --------------")
        multiple_greeting(stub)        
        print("-------------- MultiGreetingResponse --------------")
        multiple_greeting_response(stub)       


if __name__=="__main__":
    run()

学习小记

流式处理编程,其实比较简单,只是流式处理一方要构建多条mesage,接口方法会自动逐条发送,接收侧也只须遍历读取即可。流式处理用来发送大文件,如图片,视频之类,比REST有明显优势,而且有规范接口,也便于团队合作。

在这里插入图片描述

再坚持一下,再写点代码,这就是锤炼
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值