python asyncio grpc
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 msg_pb2*.py
4. 服务端程序msg_server.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': 0}
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()
Thanks to https://blog.sneawo.com/blog/2022/01/23/how-to-use-asyncio-grpc-in-aiohttp-microservices/
New version with anyio worked now:
#!/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 os
import signal
import multiprocessing
from contextlib import asynccontextmanager
from datetime import datetime
from typing import AsyncGenerator
import anyio
import grpc
import psutil
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 _stop() -> None:
me = Path(__file__).name
ps = [p for p in psutil.process_iter() if (args := p.cmdline()) and args[-1] == me]
print(f"Found {len(ps)} grpc server processes.")
if ps:
print("Terminating...")
for p in ps:
p.terminate()
print(f"{p} killed.")
def term_proc(signals, frame):
print("====current pid is % s, group id is % s" % (os.getpid(), os.getpgrp()))
# os.getpid() 获取当前进程号
# os.getpgid(params[pid]) 获取当前进程号的所在的组的组进程号
# 杀死某个进程:os.kill(params[pid], params[signal.SIGKILL])
# 杀死某个进程组下的所有进程:os.killpg(params[gpid], params[signal.SIGKILL])
# os.kill(os.getpid(), signal.SIGKILL)
os.killpg(os.getpgid(os.getpid()), signal.SIGKILL)
print("-------") # 这行代码永远也不会被执行
def signal_handler(signals, frame):
print("You pressed Ctrl+C,进程号{}".format(os.getpid()))
# 优雅地退出(让 control + c 不抛出异常信息)
sys.exit(0)
def main() -> None:
if sys.argv[1:]:
if (a1 := sys.argv[1]) == "stop":
_stop()
return
elif a1 == "--single":
serve()
return
# 监听信号,注册回调函数(主进程或者子进程都会监听)
signal.signal(signal.SIGINT, signal_handler) # control + c 会发送该信号
signal.signal(signal.SIGTERM, term_proc)
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()
5. 启动服务
python msg_server.py
# Starting server on [::]:50051
6. 客户端代码msg_client.py
import asyncio
import os
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: ")
print(response)
asyncio.run(run())
if __name__ == "__main__":
main()
7. 运行客户端
python msg_client.py
结果如下:
Client received:
id: 1
role {
name: "you"
}
8.复杂一点的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;
bool is_superuser = 15; // 布尔类型
}
// 角色(嵌套字段)
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}])
- 注:默认消息体长度不超过4M,如果报消息too large,可以在server和client都加上options:
max_length = 30 * 1024 * 1024 # 30M
options = [
('grpc.max_send_message_length', max_length),
('grpc.max_receive_message_length', max_length),
]
Supervisor配置
- 解决poetry命令找不到的问题:
sudo ln -s $HOME/.local/bin/poetry /usr/bin/poetry
- command项使用:
poetry run python main.py
- 其他的配置参照后端API通用配置就行