- 前端
- 跨平台
- 桌面应用
- 移动开发
【免费下载链接】flet
Build realtime web, mobile and desktop apps in Python only. No frontend experience required.
导读
DataChannel是 Flet 中用于在单个自定义控件(widget)的 Dart 侧与 Python 侧之间传输大批量二进制数据的双向字节通道。它独立于常规的 Flet 控件协议(MsgPack),专门承载图像帧、音频缓冲、机器学习张量等“走常规协议通道会付出 MsgPack 编解码开销”的负载。读完本文,你将掌握 DataChannel 的定位、两种底层传输实现(嵌入式原生 PythonBridge 端口 / 协议复用帧)、Python 侧与 Dart 侧完整的对接流程,以及如何在自定义控件中安全地使用它。
本文以 website/docs/types/datachannel.md 对应的
flet.DataChannel类文档为骨架(该页面通过<ClassAll name="flet.DataChannel" />组件动态渲染类源码的 docstring),并辅以仓库内源码验证。
DataChannel 是什么:为什么需要一条“专用字节通道”
Flet 的常规控件通信走Flet 控制协议:Python 与 Dart 之间的消息以 MsgPack 编码(见 connection.py 中send_message的msgpack.packb调用)。MsgPack 对结构化对象很友好,但大批量二进制数据(图像帧、音频缓冲、ML 张量)经过 MsgPack 编码/解码会产生不可忽略的额外开销。
DataChannel正是为这类场景设计的旁路通道(bypass channel):
- 它是面向 widget 的字节通道 API("Widget-facing byte-channel API",见 data_channel.py 模块 docstring);
- 每个
DataChannel绑定单个 widget,在其 Dart 侧与其 Python 对应类之间建立专属双向字节管道; - 原始字节直接流动,不经过 MsgPack 编码,从而规避常规协议通道的编解码开销。
从源码结构看,通道的分配(allocation)发生在 Dart 侧:widget 的 Dart 代码在initState中调用FletBackend.of(context).openDataChannel(),然后触发携带{channel_name, channel_id}的data_channel_open控件事件;Python 侧声明on_data_channel_open处理器,在处理器内调用self.get_data_channel(e.channel_id)完成挂接。
传输实现:两种底层模式
DataChannel 的实际传输由当前激活的Connection子类决定(data_channel.py 模块 docstring 明确列出两种模式):
模式一:嵌入式原生模式(FletDartBridgeServer)
当应用通过dart_bridge以嵌入式原生方式运行时,每个通道独占一个专属的 PythonBridge 原生端口:
channel_id就是 Dart 侧铸造(mint)的 Dart 原生端口号;- Python 侧对应实现为
_DartBridgeDataChannel(data_channel.py),直接调用内置的dart_bridge模块:set_enqueue_handler_func(port, handler)注册字节回调、send_bytes(port, payload)发送字节; - 由于是独立端口直连,此类模式吞吐能力极高(模块 docstring 提到在 M2 Pro 上可达 4–7 GiB/s 量级,具体数据以实际环境测为准)。
模式二:协议复用模式(FletSocketServer/flet_web.fastapi.FletApp)
在开发模式(dev mode)或 Web(Python 服务端)场景下,字节被复用到现有 Flet 协议传输之上:
- 帧格式为1 字节类型判别符(
0x01)+ 4 字节 channel id的帧头,后跟原始 payload; - Python 侧对应实现为
_ProtocolMuxedDataChannel(data_channel.py),发送时调用self._conn.send_data_channel_frame(self._id, payload); - 由于需要与其他协议帧共享同一条传输,此类模式的吞吐上限取决于底层传输本身(Socket/WebSocket)。
两种模式的分工总结
| 维度 | 嵌入式原生模式 | 协议复用模式 |
|---|---|---|
| 底层传输 | 每通道专属 PythonBridge 端口 | 复用 Flet 协议传输(Socket / WebSocket) |
| channel_id 含义 | Dart 原生端口号 | 单调递增的 u32 |
| 帧格式 | 无(端口直连) | [0x01][channel_id:u32 LE][payload] |
| Python 实现类 | _DartBridgeDataChannel | _ProtocolMuxedDataChannel |
传输层钩子:Connection 的 DataChannel 支持
Python 侧Connection基类(connection.py)为支持 DataChannel 的传输定义了三个钩子,由各传输子类实现:
data_channel_for(channel_id):返回指定 id 对应的DataChannel,幂等——同一会话内对同一 id 的重复调用返回同一实例;由Control.get_data_channel(id)在data_channel_open事件到达后调用;send_data_channel_frame(channel_id, payload):发送原始 DataChannel 帧,仅协议复用实现使用——dart_bridge 通道走各自的dart_bridge.send_bytes(port, ...);unregister_data_channel(channel_id):尽力而为(best-effort)地从复用注册表中移除 id,默认空操作,不维护注册表的子类无需覆写。
Socket 服务端的帧解析
FletSocketServer的接收循环(flet_socket_server.py)展示了一套非常清晰的流式帧格式:
[length:u32 LE][type:u8][payload]type=0x00:MsgPack 编码的 Flet 协议帧;type=0x01:原始 DataChannel 帧,其 payload 内部结构为[channel_id:u32 LE][bytes](因此整帧长度 = 4 字节长度前缀 + 1 字节类型 + 4 字节 channel id + payload)。
收到0x01帧后,__on_data_channel_frame(flet_socket_server.py)按 channel id 在_data_channels注册表中查找并调用channel._deliver(payload);未知 id 的帧被静默丢弃——这是为了处理控件卸载(unmount)时的竞态。
发送方向(flet_socket_server.py)采用单次拷贝组装:b"".join一次拼接长度前缀、0x01、channel id 与 payload,避免对可能达数 MB 的负载进行两次顺序拼接复制。Web 模式下flet_web.fastapi.FletApp使用同样的[0x01][channel_id:u32 LE][bytes]帧格式在 WebSocket 上复用(flet_app.py),同样以_ProtocolMuxedDataChannel为通道实现。
核心 API:事件、抽象方法与生命周期
DataChannelOpenEvent
Dart 打开通道后触发的控件事件(data_channel.py):
channel_name: str——用户自定义名称,供打开多个通道的 widget 分发使用。注意字段名是channel_name而非name,因为Event.name已承载事件自身的名称("data_channel_open");channel_id: int——嵌入式模式下是 Dart 原生端口号,复用回退模式下是单调递增的 u32。
DataChannel抽象基类的三个方法
Python 侧DataChannel是抽象基类(ABC),对 widget 暴露三个核心方法(data_channel.py):
| 方法 | 作用 | 注意事项 |
|---|---|---|
on_bytes(handler) | 注册“从 Dart 推送的字节”处理器 | 传None清除;处理器在传输投递线程上同步执行,重活应推到队列/工作线程 |
send(payload) | 从 Python 向 Dart 发送字节 | fire-and-forget(即发即忘) |
close() | 释放通道 | 幂等,可安全重复调用 |
两个具体实现都围绕这三个抽象方法展开:_DartBridgeDataChannel在close()时清除dart_bridge上的 enqueue handler;_ProtocolMuxedDataChannel的_deliver()会捕获处理器抛出的异常并记录日志(避免一个通道的异常击穿整个连接),close()则尽力调用unregister_data_channel并清空 handler。
完整对接流程:Dart 侧到 Python 侧
第一步:Dart 侧打开通道
在扩展 widget 的 Dart 代码中(见 data_channel.dart 与真实用例 raw_image.dart):
- 在
initState中调用FletBackend.of(context).openDataChannel(); - 触发携带
{channel_name, channel_id: id}的data_channel_open控件事件,把通道“公告”给 Python。
Dart 侧DataChannel接口包含:id(稳定标识符,用于事件负载中让 Python 挂接同一通道)、messages(Python 推送字节的Stream<Uint8List>,属热路径,监听器中应避免同步重活)、send(bytes)(Dart → Python,仅在嵌入式模式 Python 尚未挂接的短暂启动窗口内返回false,widget 代码应视其为瞬态并重试/排队)、close()(必须在 widget 的dispose()中调用,幂等)。
第二步:Python 侧声明事件处理器并挂接
from flet.controls.base_control import BaseControl from flet.data_channel import DataChannel, DataChannelOpenEvent from flet.event_handler import EventHandler from typing import Optional class MyBulkControl(BaseControl): # 声明事件处理器字段 on_data_channel_open: Optional[EventHandler[DataChannelOpenEvent]] = None def init(self): self.on_data_channel_open = self._on_open def _on_open(self, e: DataChannelOpenEvent): # 通过事件携带的 channel_id 挂接通道 self._channel: DataChannel = self.get_data_channel(e.channel_id) def _on_bytes_from_dart(self, payload: bytes): # 处理 Dart 推送来的字节(应投递到工作线程处理重活) ... def send_to_dart(self, payload: bytes): self._channel.send(payload)关键点:get_data_channel(channel_id)(base_control.py)最终调用self.page.session.connection.data_channel_for(channel_id)。它是幂等的——底层 Connection 按 id 缓存通道,重复调用返回同一实例;且无错误路径——id 总是来自框架触发的事件,因此执行时通道在两侧都已存在。
第三步:关闭与资源释放
Python 侧调用close()(幂等);Dart 侧则必须在 widget 的dispose()中调用close()释放通道。
仓库中的实际使用范例:RawImage 控件
仓库中最直接的实战参考是RawImage控件——它正是“大块二进制数据需要高效传输”的典型:
- Dart 侧:raw_image.dart 中
_channel = FletBackend.of(context).openDataChannel();打开通道; - Python 侧:raw_image.py 声明
on_data_channel_open,若用户未自定义该处理器,则默认将其绑定到_capture_channel,通过self.get_data_channel(e.channel_id)捕获通道。
Isolate 作用域注意事项(嵌入式模式)
Dart 侧文档(data_channel.dart)特别提醒Isolate 作用域:FletBackend.openDataChannel运行在主 Isolate(因为它经由FletBackend.of(BuildContext)),返回的通道以及嵌入式模式下的底层PythonBridge都存活于主 Isolate。若需在 worker Isolate 中使用按 Isolate 隔离的 bridge,应直接从package:serious_python构造PythonBridge,并通过SendPort把端口传回主 Isolate,再触发data_channel_open。
使用建议与最佳实践
综合 Python 与 Dart 两侧的文档与实现,可以总结出以下实践准则:
- 面向大块二进制数据:图像帧、音频缓冲、ML 张量等场景收益最大;小体积结构化消息走常规协议即可,不必引入通道。
- 重活不要堵热路径:
on_bytes处理器与 Dartmessages流监听器都在投递线程/热路径上同步执行,应尽快把数据投递到队列或工作线程。 - 发送是 fire-and-forget:
send不提供确认机制,需要可靠投递的语义请自行在业务层实现。 - 务必释放通道:Python 侧
close()幂等,Dart 侧必须从dispose()调用close()。 - 利用
channel_name分发:一个 widget 可打开多个通道,用用户自定义的channel_name在同一个on_data_channel_open处理器内区分它们。 - 幂等挂接:
get_data_channel幂等,重复调用无副作用,可在多处安全使用。
延伸阅读
- 传输钩子定义:connection.py
- Socket 服务端帧解析与发送:flet_socket_server.py、flet_socket_server.py
- Web 模式复用实现:flet_app.py
- Dart 侧接口与工厂:data_channel.dart
- 实战范例(RawImage):raw_image.py、raw_image.dart
- 前端
- 跨平台
- 桌面应用
- 移动开发
【免费下载链接】flet
Build realtime web, mobile and desktop apps in Python only. No frontend experience required.
相关推荐
Flet 0.86.0 协议升级详解:DataChannel 字节通道与新版 wire format 迁移指南
Flet 0.86.0 协议升级详解:DataChannel 字节通道与新版 wire format 迁移指南 本篇指南以 Flet 0.86.0 引入的 Da
前端跨平台桌面应用移动开发Flet 0.86 发布详解:dart-bridge 进程内通信、全新 Android 打包与 Python 3.14 支持
Flet 0.86 发布详解:dart bridge 进程内通信、全新 Android 打包与 Python 3.14 支持 Flet 0.86.0 是通往 1
前端跨平台桌面应用移动开发Flet FilterQuality 详解:图像采样质量与性能的平衡之道
Flet FilterQuality 详解:图像采样质量与性能的平衡之道 导读 flet.FilterQuality 是 Flet 中用于控制图像( Image
前端跨平台桌面应用移动开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考