使用Python实现gRPC通信

2,143 阅读4分钟

前言

本文主要讲述如何使用Python实现gRPC通信,并通过三个例子,由浅入深地进行说明。

注:本文基于Python3

项目结构

本文的代码结构如下,源代码均存放在本人的Github上,需要的话点击这里进行查阅。

├── README.md
├── hello_client.py
├── hello_server.py
├── helloworld_pb2.py
├── helloworld_pb2_grpc.py
├── http_client.py
├── http_server.py
└── proto
    ├── __init__.py
    └── helloworld.proto

依赖安装

1.安装gRPC

pip install grpcio

2.安装gRPC tools

pip install grpcio-tools

注:gRPC tools包含了protobuf的编译器protoc,以及编译插件grpc_python_out(后面编译会用到)。

简单Demo

简单Demo使用的是官方例子,比较简单。

接口定义

// 文件位置:helloworld.proto

syntax = "proto3";

package helloworld;

service Greeter {
    // 基础Demo
    rpc SayHello (HelloRequest) returns (HelloResponse) {}
}

// 简单请求
message HelloRequest {
    string name = 1;
}

// 简单响应
message HelloResponse {
    string message = 1;
}

大体意思就是我们有一个Greeter服务,该服务提供一个SayHello接口,请求体是HelloRequest,包含一个name参数;响应体是一个HelloResponse,包含一个参数message

编译proto

$ cd python_grpc

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

编译后会生成helloworld_pb2.pyhelloworld_pb2_grpc.py两份存根文件(stub)。

服务端的实现

# hello_server.py

import grpc
import random
from concurrent import futures
import helloworld_pb2
import helloworld_pb2_grpc


# 实现定义的方法
class Greeter(helloworld_pb2_grpc.GreeterServicer):
    def SayHello(self, request, context):
        return helloworld_pb2.HelloResponse(message='Hello {msg}'.format(msg=request.name))


def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    # 绑定处理器
    helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)

    server.add_insecure_port('[::]:50054')
    server.start()
    print('gRPC 服务端已开启,端口为50054...')
    server.wait_for_termination()


if __name__ == '__main__':
    serve()

服务端的实现主要分为两步,第一步是实现我们定义的接口,即接收请求,返回响应;第二步就是创建一个gRPC服务器实例,绑定处理函数并启动,监听特定端口,等待请求到来。

客户端的实现

# hello_client.py

import grpc

import helloworld_pb2, helloworld_pb2_grpc


def run():
    # 本次不使用SSL,所以channel是不安全的
    channel = grpc.insecure_channel('localhost:50054')
    # 客户端实例
    stub = helloworld_pb2_grpc.GreeterStub(channel)
    # 调用服务端方法
    response = stub.SayHello(helloworld_pb2.HelloRequest(name='World'))
    print("Greeter client received: " + response.message)


if __name__ == '__main__':
    run()

进阶Demo

上述简单Demo只是方便讲述gRPC的通信流程,实际参考价值不太大,因为字段的定义过于简单,所以这里提供了一个比较复杂的例子。

接口定义

// 文件位置:helloworld.proto

syntax = "proto3";

package helloworld;

service Greeter {
    // 进阶Demo
    rpc GetDeptUser (GetDeptUserRequest) returns (GetDeptUserResponse) {}
}

// 复杂请求
message GetDeptUserRequest {
    uint32 dept_id = 1; // 部门
    string dept_name = 2; // 部门名称
    repeated uint32 uid_list = 3; // 用户id列表
    map<string, string> filter = 4; // 筛选条件
}

// 复杂响应
message GetDeptUserResponse {
    repeated BasicUser user_list = 1; // 用户列表
    map<uint32, BasicUser> user_map = 2; // 用户哈希表
}
// 用户基本信息
message BasicUser {
    uint32 id = 1;
    string name = 2;
}

注:接口编译命令与上述一致,因为定义在同一份文件中

服务端的实现

关于服务端的实现,只列举接口实现部分,因为创建gRPC服务器实例的过程与上述一致。

写法一:

class Greeter(helloworld_pb2_grpc.GreeterServicer):
    def GetDeptUser(self, request, context):
        # 字段使用点号获取
        dept_id = request.dept_id
        dept_name = request.dept_name
        uid_list = request.uid_list
        if dept_id <= 0 or dept_name == '' or len(uid_list) <= 0:
            return helloworld_pb2.GetDeptUserResponse()
        print('dept_id is {0}, dept_name is {1}'.format(dept_id, dept_name))
        user_list = []
        user_map = {}
        for id_ in uid_list:
            uid = id_ + random.randint(0, 1000)
            letters = 'qwertyuiopasdfghjklzxcvbnm'
            name = "".join(random.sample(letters, 10))
            user = helloworld_pb2.BasicUser()
            user.id = uid
            user.name = name
            user_list.append(user) # 与正常的添加操作差不多
            user_map[uid] = user
        return helloworld_pb2.GetDeptUserResponse(user_list=user_list, user_map=user_map)

写法二:先定义对象,再赋值

class Greeter(helloworld_pb2_grpc.GreeterServicer):
    def GetDeptUser(self, request, context):
        rsp = helloworld_pb2.GetDeptUserResponse()
        dept_id = request.dept_id
        dept_name = request.dept_name
        uid_list = request.uid_list
        if dept_id <= 0 or dept_name == '' or len(uid_list) <= 0:
            return rsp
        print('dept_id is {0}, dept_name is {1}'.format(dept_id, dept_name))

        user_list = []
        for id_ in uid_list:
            uid = id_ + random.randint(0, 1000)
            letters = 'qwertyuiopasdfghjklzxcvbnm'
            name = "".join(random.sample(letters, 10))
            user = helloworld_pb2.BasicUser()
            user.id = uid
            user.name = name
            user_list.append(user)
            # 注意map的写法:rsp.user_map[uid] = user 的写法会报错
            rsp.user_map[uid].id = uid
            rsp.user_map[uid].name = name
        # 注意map的写法:rsp.user_map = user_map,或者 rsp.user_map.update(user_map) 都会报错
        rsp.user_list.extend(user_list)
        return rsp

客户端的实现

客户端也有两种写法,与服务端类似。

写法一:

response = stub.GetDeptUser(helloworld_pb2.GetDeptUserRequest(dept_id=1, dept_name='dd', uid_list=[1, 2, 3]))
print(response.user_list)
print(response.user_map)

写法二:

user_req = helloworld_pb2.GetDeptUserRequest()
user_req.dept_id = 110
user_req.dept_name = 'police'
user_req.uid_list.append(1)
user_req.uid_list.append(2)
user_req.uid_list.append(3)
# 可用extend函数替换
# user_req.uid_list.extend([1, 2, 3])
response = stub.GetDeptUser(user_req)
print(response.user_list)
print(response.user_map)

使用HTTP作为中介

有时候设计上我们并不会让client和server直接进行RPC通信,而是通过HTTP做了一层转换,常见的做法是有一个HTTP网关,client的数据序列化后传输到server,server解析后发送响应数据,client收到后也进行解析。

这里也有一个例子加以说明(接口定义与上述一致):

服务端的实现

import random
import flask
from flask import request
import helloworld_pb2, helloworld_pb2_grpc

app = flask.Flask(__name__)


@app.route('/get_dept_user', methods=['POST'])
def get_dept_user():
    # 接收client数据并进行反序列化
    req_data = request.data
    user_req = helloworld_pb2.GetDeptUserRequest()
    user_req.ParseFromString(req_data)
    
    # 拼接响应信息
    dept_id = user_req.dept_id
    dept_name = user_req.dept_name
    print('dept_id is {0}, dept_name is {1}'.format(dept_id, dept_name))
    uid_list = user_req.uid_list
    user_list = []
    for id_ in uid_list:
        uid = id_ + random.randint(0, 1000)
        letters = 'qwertyuiopasdfghjklzxcvbnm'
        name = "".join(random.sample(letters, 10))
        user = helloworld_pb2.BasicUser()
        user.id = uid
        user.name = name
        user_list.append(user)
    # 将响应信息序列化
    rsp = helloworld_pb2.GetDeptUserResponse()
    rsp.user_list.extend(user_list)
    rsp_data = rsp.SerializeToString()
    return rsp_data


if __name__ == '__main__':
    app.run('0.0.0.0', port=5001)

服务端这里使用Flask构建了一个简单的HTTP服务器,client和server序列化数据使用的是SerializeToString函数,反序列化使用的是ParseFromString函数

客户端的实现

import requests

import helloworld_pb2, helloworld_pb2_grpc

if __name__ == '__main__':
    user_req = helloworld_pb2.GetDeptUserRequest()
    user_req.dept_id = 110
    user_req.dept_name = 'police'
    user_req.uid_list.append(1)
    user_req.uid_list.append(2)
    user_req.uid_list.append(3)

    # 如果不是直接用gRPC,而是先经过HTTP,就得进行序列化
    data = user_req.SerializeToString()
    req_url = 'http://127.0.0.1:5001/get_dept_user'

    try:
        response = requests.post(req_url, data=data)
        if response.status_code != 200:
            print('request failed, code: ', response.status_code)

        # 反序列化数据
        rsp = helloworld_pb2.GetDeptUserResponse()
        rsp.ParseFromString(response.content)
        print('rsp: ', rsp.user_list)

    except Exception as e:
        print('request failed, err: ', e)

客户端这里使用request框架发送请求,同样也有序列化和反序列化的过程。

写在最后

写到这里,相信你对Python如何使用gRPC有了一定了解,如果你觉得有用的话,麻烦给我点一个赞!

我是言淦,欢迎关注我的公众号言淦Coding,你们的点赞与关注是我创作的最大动力!!

注:再提醒下,源代码最上面有地址哦!

参考

Python gRPC Quick Start
python基础--protobuf的使用(一)