异步客户端¶
AsyncP4RuntimeClient 使用 grpc.aio,适合并发配置多台交换机,或在事件循环中接收 PacketIn。它与同步客户端 P4RuntimeClient 使用相同的表项编码和异常体系,但调用方式、部分方法和返回值不同。该接口自 1.7.0 起属于 1.x 的稳定 API。
完整示例¶
下面的程序启动一台交换机和两台主机,写入一条 IPv4 路由并读回表项。await net.astart() 在工作线程中完成编译和流水线下发,异步客户端直接使用同步客户端的 P4Info 索引。示例使用较大的选举 ID,让异步客户端获得主控权。
在仓库根目录将代码保存为 async_demo.py:
import asyncio
from pathlib import Path
from p4net import Network
from p4net.control import AsyncP4RuntimeClient
from p4net.topo import Topology
topology = Topology()
h1 = topology.add_host("h1", ip="10.0.1.1/24")
h2 = topology.add_host("h2", ip="10.0.2.1/24")
s1 = topology.add_switch("s1", p4_src=Path("examples/async_concurrent/concurrent.p4"))
topology.add_link(h1, s1, port_b=1)
topology.add_link(h2, s1, port_b=2)
async def main() -> None:
net = Network(topology)
await net.astart()
try:
sw = net.switch("s1")
client = AsyncP4RuntimeClient(
grpc_address=("127.0.0.1", sw.bmv2.grpc_port),
device_id=sw.client.device_id,
info_index=sw.client.index,
thrift_address=("127.0.0.1", sw.bmv2.thrift_port),
election_id=(sw.client.election_id[0] + 1, 0),
)
async with client:
await client.insert_table_entry(
"MyIngress.ipv4_lpm",
{"hdr.ipv4.dstAddr": "10.0.2.0/24"},
"MyIngress.set_egress_port",
{"port": 2},
)
async for entry in client.list_table_entries("MyIngress.ipv4_lpm"):
print(entry)
finally:
await net.astop()
asyncio.run(main())
使用已安装 p4net 的 Python,以 root 身份运行:
输出中应有 MyIngress.ipv4_lpm 表项,动作参数为 {'port': b'\x02'}。完整拓扑转发配置可参见异步并发示例。
连接与主控权¶
net.switch("s1").async_client 会延迟创建并缓存一个尚未连接的客户端,创建时带入当前同步客户端的 P4Info 索引。调用 await client.connect() 后才能使用。同步和异步客户端各有自己的通道与选举 ID,连接其中一个不会关闭另一个。
未传入 election_id 时,异步客户端在首次连接时用当前毫秒时间戳生成选举 ID。Network 创建的同步客户端也使用时间戳,因此不要依赖创建顺序决定主控权。需要指定主控时,按完整示例自行构造客户端并传入选举 ID。选举 ID 按 (high, low) 比较。
若只需作为备用客户端读取,构造时设置 election_id=(0, 0)。该值允许客户端以备用身份完成连接。其他选举 ID 在初次仲裁未获得主控权时,connect() 会抛出 NotPrimaryError 并关闭连接。
client.is_primary 反映最近一次仲裁结果。连接后失去主控权仍可读表,P4Runtime 写请求会抛出 NotPrimaryError。client.is_connected 是布尔属性,使用时不加括号。
与同步接口的区别¶
| 操作 | 同步客户端 | 异步客户端 |
|---|---|---|
| 连接与断开 | connect()、disconnect() |
await connect()、await disconnect() |
| 表项写入 | 直接调用 | 同名方法,使用 await |
| 表项读取 | list_table_entries() 返回列表 |
返回异步迭代器,使用 async for,不先 await |
| P4Info 索引 | client.index |
client.info_index |
| 流水线下发 | set_pipeline_config() 接收文件路径 |
await push_pipeline() 接收 P4Info 文本和 BMv2 JSON 的字节内容 |
| 单个计数器单元 | CounterData,含包数和字节数 |
int,仅返回包数 |
| 全部计数器单元 | {索引: CounterData} |
{索引: 包数} |
| PacketIn 处理器 | 普通函数,在接收线程中调用 | 异步函数,在事件循环中等待执行 |
异步客户端目前没有 get_pipeline_config()、clear_table()、reset_counter() 或组播组操作。完整签名见控制平面 API。
自行下发流水线时,push_pipeline() 的第一个参数需要文本格式的 P4Info,不是二进制 protobuf。成功响应后才会替换客户端的 P4Info 索引,失败或取消时保留原索引。寄存器读写通过 simple_switch_CLI 执行,需要安装该工具,并在构造客户端时传入 thrift_address。
并发操作与 PacketIn¶
对已连接且拥有主控权的多个客户端,可并发下发表项:
async def program_switches(clients: list[AsyncP4RuntimeClient]) -> None:
await asyncio.gather(
*(
client.insert_table_entry(
"MyIngress.ipv4_lpm",
{"hdr.ipv4.dstAddr": "10.0.2.0/24"},
"MyIngress.set_egress_port",
{"port": 2},
)
for client in clients
)
)
这样可以让多个交换机的 RPC 同时等待响应,实际耗时取决于请求量和交换机处理速度。
on_packet_in() 和它返回的注销函数都需要 await。下面的函数注册处理器,等到一个 PacketIn 后注销:
async def watch_packet(client: AsyncP4RuntimeClient) -> None:
async def handle_packet(payload: bytes, metadata: dict[str, int]) -> None:
print("PacketIn:", len(payload), metadata)
unsubscribe = await client.on_packet_in(handle_packet)
try:
await client.expect_packet_in(timeout=5.0)
finally:
await unsubscribe()
P4 程序需要将数据包送到 CPU 端口,具体配置见 CPU punt 示例。处理器由消息接收任务依次等待执行,耗时工作可交给队列或其他任务。
错误、超时与取消¶
RPC 错误会转换成 p4net 异常,原始 grpc.aio.AioRpcError 保留在 __cause__ 中:
| RPC 状态 | 异常 |
|---|---|
PERMISSION_DENIED |
NotPrimaryError |
UNAVAILABLE |
ConnectionError |
ALREADY_EXISTS |
DuplicateEntryError |
NOT_FOUND |
EntryNotFoundError |
CANCELLED |
AsyncOperationCancelledError |
| 其他错误状态 | P4RuntimeError |
push_pipeline() 另将 INVALID_ARGUMENT 和 FAILED_PRECONDITION 转为 PipelineError。表项或计数器请求的 FAILED_PRECONDITION 则是普通 P4RuntimeError,例如交换机尚未配置流水线时。
外层状态为 UNKNOWN 且携带逐条 P4Runtime 错误时,客户端使用第一个可识别的非成功状态码选择异常。无法解析出这样的状态码时,保留为 P4RuntimeError。
取消和超时的结果取决于具体操作。下表的超时指方法自身的 timeout= 参数:
| 操作 | 调用任务被取消 | 超时 |
|---|---|---|
connect() |
清理连接后传播 asyncio.CancelledError |
仲裁等待超时为 ConnectionError |
push_pipeline()、表项写入 |
AsyncOperationCancelledError |
asyncio.TimeoutError |
list_table_entries() 的迭代 |
asyncio.CancelledError |
P4RuntimeError,原始 RPC 状态为 DEADLINE_EXCEEDED |
read_counter() |
AsyncOperationCancelledError |
P4RuntimeError,原始 RPC 状态为 DEADLINE_EXCEEDED |
寄存器操作、expect_packet_in() |
AsyncOperationCancelledError |
P4RuntimeError |
外层 asyncio.wait_for() 会取消正在等待的操作,其最终异常也受该操作的取消处理影响。
关闭网络¶
在协程中使用 await net.astart() 启动网络。它先在调用线程安装清理处理器、注册网络,再由工作线程执行启动。需要自动处理 SIGINT/SIGTERM 时,让事件循环运行在主线程。Python 只允许在主线程注册信号处理器。同步接口 net.start() 仍可使用。
astart() 被取消时,会等待启动线程结束并关闭网络,再传播 CancelledError。启动失败时会回收已创建的资源,并传播原始异常。在启动期间调用 stop() 或 astop(),清理会等待启动线程结束。如果启动尚未完成,启动调用会抛出 NetworkError。
在同一事件循环中连接、使用和关闭异步客户端。gRPC AsyncIO 文档也要求异步对象在创建它们的线程中使用。
await net.astop() 会关闭网络缓存的异步客户端,再释放网络资源。自行构造的客户端需要用 async with 或 await client.disconnect() 关闭,完整示例使用了前一种方式。保持客户端所属的事件循环运行,直到关闭完成。astop() 被取消时会先完成清理,再传播 CancelledError。
net.stop() 在事件循环之外会等待清理完成,在事件循环内则启动后台清理并返回。需要等待关闭时,使用 await net.astop()。
事件循环运行时,SIGINT/SIGTERM 处理器会先等待网络清理,再恢复原处理器并重新投递信号。事件循环之外使用同步清理。
StreamChannel 结束或报错后,客户端会清除连接和主控状态并关闭通道。可在同一实例上调用 connect() 重连。初次仲裁期间断流会直接结束连接等待,清理进行中调用 connect() 或 disconnect() 会等待清理完成。
grpc.aio 仍使用后台线程。需要创建工作进程时,应先创建进程,再在各自进程中建立 gRPC 连接。参见 gRPC 的 fork 说明。