Python版grpc使用asyncio

1. 准备环境

python3.11 -m venv venv
source venv/*/activate
pip install grpcio-tools #包含了grpcio和protobuf
pip install types-protobuf grpc-stubs # 可选安装,用于mypy静态检查

2. 编写msg.proto

syntax = "proto3";

// 这是注释,同时也是类文档
service MsgService {
 rpc handler (MsgRequest) returns (MsgResponse){}
}

// 这也是注释
message MsgRequest {
  // 1,2,3...是字段编号,正整数就行,可以不连续
  string name = 1;  // 姓名
  optional uint32 age = 2;  // 年龄
  optional float high = 3;  // 身高
  optional bytes avatar = 4;  // 头像
}


message MsgResponse {  // 注释也可以在行尾
  uint64 id = 1; // ID
  Role role = 2;  // 角色
  optional uint64 last_login = 10;  // 上一次登陆的时间戳
}

// 角色(嵌套字段)
message Role {
  string name = 1;
  int32 level = 2;
}

3. 把proto编译成python文件

python -m grpc_tools.protoc -I . --python_out=. --grpc_python_out=. msg.proto
ls -lh msg_pb2*.py

4. 服务端程序msg_server.py

参考了官方示例:https://github.com/grpc/grpc/blob/master/examples/python/helloworld/async_greeter_server_with_graceful_shutdown.py

#!/usr/bin/env python3
import asyncio

import grpc

import msg_pb2
import msg_pb2_grpc

try:
    from rich import print
except ImportError:
    ...


class MsgServicer(msg_pb2_grpc.MsgServiceServicer):
    def handler(self, request: "msg_pb2.MsgRequest", context) -> "msg_pb2.MsgResponse":
        print("Received name: %s" % request.name)
        # 响应的处理逻辑写在这里
        # ...
        role = {'name': request.name, 'level': 2}
        return msg_pb2.MsgResponse(role=role, id=1)


def serve() -> None:
    _cleanup_coroutines = []

    async def run() -> None:
        server = grpc.aio.server()
        msg_pb2_grpc.add_MsgServiceServicer_to_server(MsgServicer(), server)
        listen_addr = "[::]:50051"
        server.add_insecure_port(listen_addr)
        print(f"Starting server on {listen_addr}")
        await server.start()

        async def server_graceful_shutdown():
            print("Starting graceful shutdown...")
            # Shuts down the server with 5 seconds of grace period. During the
            # grace period, the server won't accept new connections and allow
            # existing RPCs to continue within the grace period.
            await server.stop(5)
            print(f"{server} was graceful shutdown~")

        _cleanup_coroutines.append(server_graceful_shutdown())
        await server.wait_for_termination()

    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(run())
    finally:
        loop.run_until_complete(*_cleanup_coroutines)
        loop.close()


if __name__ == "__main__":
    serve()

5. 启动服务

python msg_server.py
# Starting server on [::]:50051

6. 客户端代码msg_client.py

import asyncio
import os
from datetime import datetime

import grpc

import msg_pb2
import msg_pb2_grpc

try:
    from rich import print
except ImportError:
    ...


def main():
    async def run() -> None:
        host = os.getenv("RPC_HOST", "localhost")
        async with grpc.aio.insecure_channel(f"{host}:50051") as channel:
            stub = msg_pb2_grpc.MsgServiceStub(channel)

            response = await stub.handler(msg_pb2.MsgRequest(name="you"))
            print("Client received at", datetime.now())
            uid: int = response.id
            role: msg_pb2.Role = response.role
            role_name: str = role.name
            level: int = role.level
            print("id:", uid)
            print("role:")
            print(role)
            print("role name:", role_name)
            print("level:", level)

    asyncio.run(run())


if __name__ == "__main__":
    main()

7. 运行客户端

python msg_client.py

结果如下:

Client received at 2024-08-11 12:41:41.355122
id: 1
role:
name: "you"
level: 2

role name: you
level: 2

8. 部署

Docker方式

  • Dockerfile
FROM python:3.11-slim

RUN mkdir /service
WORKDIR /service
COPY msg.proto .
COPY msg_server.py .
RUN python -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple/ --upgrade pip grpcio-tools
RUN python -m grpc_tools.protoc -I . --python_out=. --grpc_python_out=. msg.proto

EXPOSE 50051
ENTRYPOINT [ "python", "msg_server.py"]
  • 创建镜像
docker build -t grpc-msg-server .
  • 启动容器
docker run -p 50051:50051 --name msg_server grpc-msg-server
  • 验证效果可以执行:RPC_HOST=localhost python msg_client.py

Supervisor方式

  • msg_server.conf
[program:msg_server]
environment =
  ENV1=1,
  ENV2=foo,
  ENV3="(1ja8h3a1^n+xdddx*d)*jv4)00asd   ddasdf4+k*s7#1w3d#l2rg@5lajy4"
directory=/path/to/workdir
command=/path/to/workdir/venv/bin/python msg_server.py

numprocs=1
autostart=true
startsecs=10
autorestart=true
startretried=3
user=wenping
redirect_stderr=true
stdout_logfile=/path/to/workdir/supervisor.log
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=20
  • 启用服务
sudo supervisorctl update
sudo supervisorctl status

附:msg_server.py也可以采用更加优雅的anyio版

#!/usr/bin/env python3
from contextlib import asynccontextmanager
from datetime import datetime
from typing import AsyncGenerator

import anyio
import grpc

import msg_pb2
import msg_pb2_grpc

try:
    from rich import print
except ImportError:
    ...


class MsgServicer(msg_pb2_grpc.MsgServiceServicer):
    def handler(self, request: "msg_pb2.MsgRequest", context) -> "msg_pb2.MsgResponse":
        print(f"Received msg@{datetime.now()}", request)
        # 响应的处理逻辑写在这里
        # ...
        role = {"name": request.name, "level": 2}
        return msg_pb2.MsgResponse(role=role, id=1)


async def _start_grpc_server(server: grpc.aio.Server) -> None:
    await server.start()
    await server.wait_for_termination()


@asynccontextmanager
async def grpc_server_ctx(port=50051) -> AsyncGenerator:
    listen_addr = f"[::]:{port}"
    server = grpc.aio.server()
    msg_pb2_grpc.add_MsgServiceServicer_to_server(MsgServicer(), server)
    server.add_insecure_port(listen_addr)
    async with anyio.create_task_group() as tg:
        tg.start_soon(_start_grpc_server, server)
        print(f"action=init_grpc_server, address={listen_addr}")
        try:
            yield
        finally:
            print("Starting graceful shutdown...")
            # Shuts down the server with 5 seconds of grace period. During the
            # grace period, the server won't accept new connections and allow
            # existing RPCs to continue within the grace period.
            await server.stop(5)
            print(f"{server} was graceful stopped~")
            tg.cancel_scope.cancel()


def serve() -> None:
    async def run():
        async with grpc_server_ctx():
            await anyio.sleep_forever()

    anyio.run(run)


if __name__ == "__main__":
    serve()
  • 多进程版
#!/usr/bin/env python3
import sys
import multiprocessing
from contextlib import asynccontextmanager
from datetime import datetime
from typing import AsyncGenerator

import anyio
import grpc

import msg_pb2
import msg_pb2_grpc

try:
    from rich import print
except ImportError:
    ...


class MsgServicer(msg_pb2_grpc.MsgServiceServicer):
    def handler(self, request: "msg_pb2.MsgRequest", context) -> "msg_pb2.MsgResponse":
        print(f"Received msg@{datetime.now()}", request)
        # 响应的处理逻辑写在这里
        # ...
        role = {"name": request.name, "level": 2}
        return msg_pb2.MsgResponse(role=role, id=1)


async def _start_grpc_server(server: grpc.aio.Server) -> None:
    await server.start()
    await server.wait_for_termination()


@asynccontextmanager
async def grpc_server_ctx(port=50051) -> AsyncGenerator:
    listen_addr = f"[::]:{port}"
    server = grpc.aio.server(options=(("grpc.so_reuseport", 1),))
    msg_pb2_grpc.add_MsgServiceServicer_to_server(MsgServicer(), server)
    server.add_insecure_port(listen_addr)
    async with anyio.create_task_group() as tg:
        tg.start_soon(_start_grpc_server, server)
        print(f"action=init_grpc_server, address={listen_addr}")
        try:
            yield
        finally:
            print("Starting graceful shutdown...")
            # Shuts down the server with 5 seconds of grace period. During the
            # grace period, the server won't accept new connections and allow
            # existing RPCs to continue within the grace period.
            await server.stop(5)
            print(f"{server} was graceful stopped~")
            tg.cancel_scope.cancel()


def serve() -> None:
    async def run():
        async with grpc_server_ctx():
            await anyio.sleep_forever()

    anyio.run(run)


def main() -> None:
    if '--single' in sys.argv:
    	serve()
    	return
    try:
    	import torch
    except ImportError:
    	pass
    else:
    	if torch.cuda.is_available():
    		torch.multiprocessing.set_start_method("spawn")
    workers = []
    for _ in range(multiprocessing.cpu_count()):
        # NOTE: It is imperative that the worker subprocesses be forked before
        # any gRPC servers start up. See
        # https://github.com/grpc/grpc/issues/16001 for more details.
        worker = multiprocessing.Process(target=serve, args=())
        worker.start()
        workers.append(worker)
    for worker in workers:
        worker.join()


if __name__ == "__main__":
    main()

补充:复杂一点的proto文件

syntax = "proto3";

// 这是注释,同时也是类文档
service MsgService {
 rpc handler (MsgRequest) returns (MsgResponse){}
}

// 接收的消息格式
message MsgRequest {
  // 1,2,3...是字段编号,正整数就行,可以不连续
  string name = 1;  // 姓名
  optional uint32 age = 2;  // 年龄
  optional float high = 3;  // 身高
  optional bytes avatar = 4;  // 头像
  repeated float vision = 5 [packed=true];  // 视力
}


message MsgResponse {  // 注释也可以在行尾
  uint64 id = 1; // ID
  Role role = 2;  // 角色
  optional uint64 last_login = 10;  // 上一次登陆的时间戳
  repeated Permission permissions = 13;
}

// 角色(嵌套字段)
message Role {
  string name = 1;
  int32 level = 2;
}

// 权限列表
message Permission {
  uint64 id = 1;
  string name = 2;
}

对应的Python片段:

# client
            req = msg_pb2.MsgRequest(
                name="you",
                age=18,
                high=1.8,
                avatar=b'jpg',
                vision=[5.1, 5.2]
            )
            response = await stub.handler(req)
# server
        role = {"name": request.name, "level": 2}
        return msg_pb2.MsgResponse(role=role, id=1,permissions=[{'name':'p1','id':1},{'name':'p2', 'id':2}])
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值