在 tRPC-Python 中,如果希望访问 Redis、MySQL 等存储时,能够复用 RPC 客户端的名字服务路由(Selector)、超时控制以及客户端拦截器(如监控上报、调用链追踪),可以通过自定义 protocol 将第三方数据库 SDK 接入 Client.invoke 调用链路。tRPC-Python 提供了 Redis、MySQL、Kafka 插件,可以参考生态仓库 python-database。
整体思路如下:
- 命令传递:数据库 SDK 通常接收结构化的命令或参数,而非序列化后的字节流。调用方将请求体设为空字节
b""(配合SerializationType.Noop),把真正的命令对象放进Options().set_req_head(command),通过消息头msg.client_req_head传给传输层。 - 协议占槽(
CLIENT_CODEC):注册一个直通(pass-through)的客户端编解码器,对字节流原样透传。 - 执行调用(
CLIENT_TRANSPORT):在客户端过滤器链末端的round_trip中,从options.address拿到路由解析出的(host, port),从msg.client_req_head拿到命令对象,调用第三方数据库 SDK 完成读写,并将结果写回命令对象。
需要注意区分两个配置项的职责:
client.service.protocol:决定使用哪套CLIENT_CODEC和CLIENT_TRANSPORT(即选中上述数据库调用链路)。client.service.target:其中的 scheme(如ip://、polar://)用于选择NAMING_SELECTOR插件,仅负责将目标解析为具体的节点地址(host, port)。
下面以通用的协议名 store 为例说明完整实现(不绑定具体数据库)。
在 trpc_python.yaml 或调用参数中指定 protocol: store 时,Options.set_protocol("store") 会按以下顺序查找并挂载插件:
- 查找
CLIENT_CODEC:调用get_client_codec(protocol)。如果未找到同名 codec,set_protocol会直接返回,不会继续替换后续的 checker 和 transport。此时 options 仍保留默认的trpctransport,且opts.codec为None,后续调用Client.invoke会在进入传输层前抛出ClientEncodeError("client codec empty")。 - 查找
CLIENT_CHECKER:找到 codec 后,继续调用get_client_parser(protocol)设置分包器。数据库调用由第三方 SDK 自行管理连接与协议解析,不需要框架做 TCP 切帧,因此无需注册同名 checker(set_parser(None)不会影响自定义round_trip)。 - 查找
CLIENT_TRANSPORT:调用get_client_transport(protocol)。若存在同名实例,则用它替换_create_client_options默认设置的trpctransport。
因此,自定义数据库协议时,必须以相同的协议名同时注册 CLIENT_CODEC 和 CLIENT_TRANSPORT;只注册 transport 会导致 protocol: store 配置不生效。
由于命令对象放在 msg.client_req_head 中传递,Codec 不需要做任何组包或解包,只需将字节流原样返回即可。
encode_data 和 decode_data 的第一个参数均为调用上下文 context。注意这两个方法必须是普通同步函数(不能是 async def),因为客户端 _invoke_backend 会直接同步调用它们,不会 await。
from trpc_sdk.abc import BaseCodec
from trpc_sdk.abc import PluginType
from trpc_sdk.plugin import register_plugin
@register_plugin(PluginType.CLIENT_CODEC, "store")
class StoreCodec(BaseCodec):
"""Pass the body through without framing."""
def encode_data(self, context, body: bytes) -> bytes:
return body or b""
def decode_data(self, context, buffer: bytes) -> bytes:
return buffer or b""继承 BaseClientTransport 并实现异步方法 round_trip(ctx, req_data, options)。该方法位于客户端 filter 链的最内层,所有配置的客户端拦截器都会包裹在这次 round_trip 外围执行。
在 round_trip 中可以获取以下信息来调用第三方数据库 SDK:
- 目标地址:
options.address,格式为(host, port)元组,由Selector根据target解析出节点后通过load_node_config填入。 - 鉴权口令:如果在 YAML 的 service 配置下写了
password,框架会将其写入options.password。 - 请求命令:从
ctx.get_message().client_req_head中取出业务侧传入的命令对象(此时req_data为空字节b"")。
from trpc_sdk.abc import BaseClientTransport
from trpc_sdk.abc import PluginType
from trpc_sdk.plugin import register_plugin
@register_plugin(PluginType.CLIENT_TRANSPORT, "store")
class StoreTransport(BaseClientTransport):
"""Run one already-installed SDK call for a store command."""
async def round_trip(self, ctx, req_data: bytes, options):
message = ctx.get_message()
command = message.client_req_head
host, port = options.address
# 在此处调用第三方数据库 SDK,可直接使用 host、port、options.password 和 command
command.result = None
return b""执行完毕后直接返回空字节 b"" 即可。_invoke_backend 在拿到返回值后会调用 codec.decode_data,当解码出的响应体长度为 0 时,框架会自动跳过反序列化步骤,不会去填充响应对象。
在 client.service 下配置下游存储服务:
name:被调服务名(callee)。未显式配置callee字段时,parse_client_config会以name作为键存入客户端配置表。protocol:填写上面注册的协议名store,用于选中StoreCodec和StoreTransport。target:目标地址。例如ip://127.0.0.1:6379使用内置的ipselector,直接解析 IP 和端口而不走 DNS。password(可选):数据库访问口令,可在round_trip中通过options.password读取。
client:
service:
- name: trpc.app.server.store
network: tcp
protocol: store
target: ip://127.0.0.1:6379
timeout: 1000业务代码通过 Client().invoke() 发起调用时,有三个细节需要注意:
- 先在
message上设置被调服务名:get_options会先用调用时刻msg.callee_service_name的值去匹配 YAML 配置(load_client_config),随后才执行本次传入的Options覆盖项。因此必须在invoke之前调用msg.set_callee_service_name("trpc.app.server.store")。如果只在Options().set_service_name(...)中设置名字,加载 YAML 的阶段已经结束,配置里的protocol和target将无法生效。 - 使用
SerializationType.Noop与空字节请求体:NoopSerializer.serialize仅接受bytes或Body实例,不能直接把自定义的StoreCommand当作请求体req传入。正确做法是请求体传b""、响应类型传Body,并将command放到Options().set_req_head(command)中(update_message会将其复制到msg.client_req_head)。 - 确保插件模块已被导入:服务启动时必须先
import定义了StoreCodec和StoreTransport的模块,触发@register_plugin完成注册。
from trpc_sdk.abc import clone_client_message
from trpc_sdk.client import Client
from trpc_sdk.client import Options
from trpc_sdk.serialization import Body
from trpc_sdk.serialization import SerializationType
class StoreCommand:
def __init__(self, statement: str):
self.statement = statement
self.result = None
async def execute(ctx, statement: str):
ctx, _ = clone_client_message(ctx)
ctx.get_message().set_callee_service_name("trpc.app.server.store")
command = StoreCommand(statement)
await Client().invoke(
ctx,
b"",
Body,
[
Options()
.set_req_head(command)
.set_serialization_type(SerializationType.Noop),
],
)
return command.result如果不使用 YAML 配置文件,也可以在传入的 Options 上链式调用 .set_protocol("store").set_target("ip://127.0.0.1:6379") 来直接指定协议和目标地址。