|
VLink
2.0.0
A high-performance communication middleware
|
观测一个运行中的分布式系统,需回答两个递进的问题:系统里**存在哪些端点**,以及这些端点上**正在流动什么数据**。VLink 用两层机制分别覆盖:服务发现(Discovery)自动感知全网拓扑,回答"有什么";代理监控(Proxy)旁路聚合话题载荷与统计,回答"在传什么",并能跨越网段边界搬运数据或反向注入。两者共享同一发现数据——Proxy 内部即运行一个发现视图来枚举待订阅话题,CLI 工具 vlink-list / vlink-monitor 亦建立在同一发现层之上。发现是基础,代理是其在跨网段、需要载荷数据时的延伸。

二者的能力边界如下,按需取用:
| 维度 | 🛰️ 服务发现 Discovery | 🔭 代理监控 Proxy |
|---|---|---|
| 回答的问题 | 系统中存在哪些端点(拓扑) | 端点上流动的数据与统计 |
| 数据内容 | URL、传输类型、角色、进程 | 话题统计 + 原始消息载荷 |
| 网段范围 | 同一发现网络(组播可达) | 可跨网段(单播 / 路由可达) |
| 方向 | 只读感知 | 观测 + 录制 + 注入 |
| 部署 | 内嵌库调用,零额外进程 | 需服务端(独立进程或嵌入业务进程) |
| 开销 | 周期组播,极低 | 订阅全部话题并转发,较高 |
服务发现使任一进程自动感知系统中全部活跃的 VLink 端点:URL(话题地址)、传输类型、所属进程,以及实时的上线与下线。机制基于 UDP 组播——节点运行时自动注册,断线由超时自动剔除,全程无需中心注册表或人工配置。发现层是 vlink-list、vlink-monitor 与代理监控的统一数据来源。
进程内若存在 VLink 节点,运行时即周期性地以 UDP 组播将本进程的端点拓扑广播至发现网络。监听者解码这些报文并按 URL 聚合,便得到一张全局拓扑视图,回答四个问题:
dds://、shm://、intra:// 等)。
两个边界条件需明确:
dds:// 配合 CDR 序列化的节点,默认不参与上报,不出现在发现视图中。整条发现链路(节点上报 → 组播 → 监听者聚合 → 回调通知)的时序如下:

DiscoveryViewer 是发现机制的用户入口,继承自 MessageLoop。使用分为四步:构造时选定过滤模式、注册回调、async_run() 启动后台接收、按需读取快照。async_run() 是接收数据的前提,未启动则回调不会触发、快照为空。
回调在 viewer 的 MessageLoop 线程上执行,入参列表仅在回调内有效;需在回调外使用时先复制一份。多处需要同一张拓扑视图时,在应用层共享同一个 DiscoveryViewer 实例(或经 std::shared_ptr 持有)即可,无需逐处构造。
下表列出 DiscoveryViewer 的高频接口。除回调外,亦可在任意时刻经 get_info_list() 主动拉取一份当前快照,二者基于同一份内部状态。
| 方法 | 语义 |
|---|---|
DiscoveryViewer(FilterType type = kFilterNone) | 构造,指定过滤模式(见 12.4) |
void register_callback(Callback&&) | 注册 void(const std::vector<Info>&) 回调,拓扑变化时触发;重复注册仅最后一次生效 |
bool async_run() | 启动后台接收(继承自 MessageLoop),接收数据的前提 |
std::vector<Info> get_info_list() | 拉取当前拓扑快照 |
std::string get_ser_type(const std::string& url) const | 查询某 URL 当前的序列化类型名 |
SchemaType get_schema_type(const std::string& url) const | 查询某 URL 的 schema 家族 |
静态工具方法用于将角色位掩码格式化为可读标签,以及查询发现网络地址:
| 静态方法 | 语义 |
|---|---|
convert_type_to_view(uint32_t type) | 将角色位掩码格式化为可读标签 |
convert_type_to_view(uint32_t type, const std::vector<Process>&) | 同上,并附带进程列表的完整标签 |
convert_type(std::string_view str) | 将角色字符串("Pub"、"Sub" 等)转回 ImplType 位 |
get_listen_address() | 返回发现子系统使用的 UDP 组播/广播地址(默认 239.255.0.100) |
构造时唯一需要决策的参数是过滤模式,它决定哪些广播被纳入快照。默认 kFilterNone。
| FilterType | 行为 | 适用场景 |
|---|---|---|
kFilterNone | 保留全部发现到的端点 | 查看全网拓扑 |
kFilterAvailable | 屏蔽来自其它主机的本地传输(intra://、shm://、shm2://)端点,本机端点全部保留 | 监控工具的常用选择 |
kFilterNative | 仅保留与 viewer 同一主机名的端点 | 仅关心本机 |
判据:本地传输(intra://、shm://、shm2://)的端点仅在本机内可达,远端主机上报的此类端点对本机无实际意义。kFilterAvailable 据此剔除跨主机的本地传输噪声、同时保留本机全部端点,最适合通用监控;kFilterNative 则进一步排除一切非本机端点。
回调入参与 get_info_list() 返回的均为 Info 列表。每个 Info 描述一个 URL 的聚合信息,并内含承载它的进程列表 Process。
Info 字段 | 含义 |
|---|---|
url | 完整 VLink URL,如 "dds://camera_image" |
type | 该 URL 上全部角色的位掩码,经 convert_type_to_view 转为可读标签 |
ser_type | 序列化类型名,如 "demo.proto.PointCloud"、"standard" |
schema_type | 粗粒度 schema 家族:kProtobuf / kFlatbuffers / kRaw / kZeroCopy / kUnknown |
process_list | 承载此 URL 的进程列表 |
Process 字段 | 含义 |
|---|---|
type | 该进程在此 URL 上承担角色的位掩码(Publisher / Subscriber / Server 等) |
host | 主机名 |
pid | 进程 ID |
name | 进程名 |
ip | 主机 IP 地址 |
profiler | CPU 利用率百分比,-1 表示未启用(需 VLINK_PROFILER_ENABLE=1) |
type 是位掩码而非单一角色:同一 URL 可在同进程或跨进程上被多种角色同时使用(例如既有发布者又有订阅者),展示时统一经 convert_type_to_view 转换。
下例实时打印每个 URL 的角色与序列化类型,并逐进程输出 PID / 主机 / IP,在 Profiler 启用时附带 CPU 占用:
发现层回答的是"系统中存在哪些端点"。若需进一步掌握单个端点底层传输的运行状态——匹配对端的出现与消失、截止时间错过、liveliness 变化——则使用节点的 register_status_handler()。
该机制仅 DDS 系列后端生效(dds://、ddsc://、ddsr://、ddst://);其它后端调用将打印警告并被忽略。回调接收 Status::BasePtr,在回调内经 status->get_type() 分发、经 status->as<...>() 安全下转以读取详情字段。
写方(Publisher / Server / Setter)与读方(Subscriber / Client / Getter)的状态类型如下:
| 状态类型 | 侧 | 触发条件 | 关键字段 |
|---|---|---|---|
kPublicationMatched | 写方 | 匹配的订阅者出现或消失 | current_count、total_count |
kSubscriptionMatched | 读方 | 匹配的发布者出现或消失 | current_count、total_count |
kOfferedDeadlineMissed / kRequestedDeadlineMissed | 写 / 读 | 未在截止时间内发出 / 收到数据 | total_count、last_instance_handle |
kLivelinessLost | 写方 | liveliness 时限内未声明 | total_count |
kLivelinessChanged | 读方 | 对端存活状态变化 | alive_count、not_alive_count |
kOfferedIncompatibleQos / kRequestedIncompatibleQos | 写 / 读 | 发现 QoS 不兼容的对端 | last_policy_id |
kSampleRejected / kSampleLost | 读方 | 样本因资源限制被拒 / 送达前丢失 | total_count(kSampleRejected 另有 last_reason) |
Deadline、Liveliness 等语义由 QoS 策略定义,配置方式见 QoS 配置。代表性回调如下:
发现机制是若干现成工具的底层数据源,多数场景无需自行编写监控程序。
| 入口 | 形态 | 用途 |
|---|---|---|
vlink-list | 一次性查询 | 打印当前全网拓扑表格后退出 |
vlink-monitor | 持续监控(类 top) | 实时刷新,支持按 URL 子串过滤(-i) |
| 应用内嵌 | 库调用 | 在应用内构造 DiscoveryViewer,无需单独进程 |
| 代理聚合 | 代理层 | 代理内部运行 viewer 聚合多进程拓扑,供远程监控 |
参数与输出示例见 CLI 工具;代理聚合见 12.8 起。
发现行为经环境变量调节(完整说明见 集成与扩展):
| 环境变量 | 作用 |
|---|---|
VLINK_DISCOVER_DISABLE=1 | 禁用本进程的发现上报 |
VLINK_DISCOVER_NATIVE=1 | 仅限本机发现(组播绑定 loopback) |
VLINK_PROFILER_ENABLE=1 | 启用 CPU Profiler,使 Process::profiler 有效 |
若需让单个节点不出现在发现视图,可在 init() 之前关闭其上报。这要求节点以延迟初始化方式构造:
Proxy 是 VLink 的旁路监控、录制与注入中间层。业务节点照常使用 dds://、shm:// 等原生传输通信,Proxy 在不改动业务代码的前提下聚合全部话题的载荷与统计,并转发给监控、可视化、录制与回放工具。

VLink 原生传输要求通信双方在同一网段或 DDS 域内能直接发现彼此。当存在路由、防火墙或 VLAN 隔离,或需要在远端集中观测时,原生发现无法跨越边界。Proxy 在业务侧引入一个服务端,由它在本地订阅全部话题(待订阅话题来自 12.1 的发现视图),再通过一条受控链路把数据与统计转发到远端客户端。
体系由三个组件构成,按部署形态选用:
| 组件 | 形态 | 职责 |
|---|---|---|
vlink-proxy | 独立守护进程 | 一条命令拉起服务端,部署最简,同机仅一个实例 |
ProxyServer | C++ 服务端库 | 将服务端嵌入业务进程,省去独立进程 |
ProxyAPI | C++ 客户端库 | 监控、可视化、录制、注入工具连接服务端的入口 |
链路划分为两个平面,职责与安全级别不同:
| 平面 | 承载内容 | 特性 |
|---|---|---|
| 控制面 | 握手、心跳、控制指令、话题统计 | 经 DDS 安全扩展加密;自动握手、协商会话凭据、断线自愈 |
| 数据面 | 转发与注入的原始消息载荷 | 可选 reliable / TCP / SHM 直连,客户端与服务端配置须一致 |
数据面搬运的是原始序列化字节(Bytes,回调内以浅拷贝视图交付);当载荷本身为零拷贝容器序列化结果时,其布局细节见 零拷贝,普通使用无需关心。
监控端的最小流程为四步:填写 Config、注册回调、async_run() 启动事件循环、send_control() 选择工作模式。
服务端在目标机上启动一行命令:
连接建立后,客户端每秒收到一次话题统计(InfoCallback);选择观察模式后还会收到原始数据(DataCallback)。链路中断约 5 秒触发 ConnectCallback(false),框架在后台自动重连并重发上一次控制指令,无需用户干预。
完整的连接、心跳、控制与数据流转时序如下:

ProxyAPI 为客户端类。构造后调用 async_run()(后台线程)或 run()(当前线程)启动,quit() / wait_for_quit() 退出。Config 一经构造不可更改。
角色在构造时由 Config::role 固定,决定客户端能否驱动服务端:
| 角色 | 权限 |
|---|---|
kController | 可调用 send_control() / send_data(),驱动服务端 |
kListener | 只读观察;send_control() / send_data() 立即返回 false |
工作模式由控制端通过 Control::mode 切换,决定服务端订阅哪些话题、是否转发数据、是否接受注入:
| 模式 | 用途 |
|---|---|
kObserveAll | 观察域内全部话题:监控面板、Foxglove/Rerun 全量桥接 |
kObserveOne | 仅观察 url_meta_list 指定的单个话题 |
kRecord | 录制指定话题,配合落盘工具 |
kPlay | 回放注入:经 send_data() 灌入数据,服务端转发到真实话题 |
kEdit | 编辑注入:服务端转发控制端注入的数据 |
kAuto / kAutoAndObserveAll | 观察 url_meta_list 指定话题并接受控制端注入;后者改为观察全部话题 |
kOffline | 离线:服务端释放全部订阅 |
五个回调各注册一次,框架在对应事件触发时调用。回调入参(尤其 Data::raw)仅在回调内有效,需外带时先复制。
| 方法 | 触发时机 |
|---|---|
register_connect_callback(void(bool connected)) | 连接状态变化(首个心跳建立 / 断线约 5 秒) |
register_error_callback(void(Error error)) | 错误状态翻转时(见 12.10.5) |
register_time_callback(void(uint64_t sys_time, uint64_t boot_time)) | 每秒心跳,携带服务端系统时间与启动时长(微秒) |
register_info_callback(void(const std::vector<Info>&)) | 每秒一次的话题统计列表 |
register_data_callback(void(const Data&)) | 转发模式下每条数据 |
| 方法 | 语义 |
|---|---|
bool send_control(const Control& ctrl, bool async = true) | 设置模式与关注话题,控制端专用。async=true(默认)投递到事件循环后即返回;false 同步发布并返回真实结果 |
bool send_data(const Data& data) | 向数据通道注入一条消息(kPlay / kEdit 用),控制端专用 |
send_control() 被内部缓存,断线重连后自动重发。send_data() 返回 false 不一定是链路故障,也可能是对端暂无订阅者,或注入数据未填全 ser 与 schema 路由元数据。
连接与统计可随时主动查询:
| 方法 | 返回 |
|---|---|
bool is_connected() | 是否已连接服务端 |
Error get_current_error() | 当前错误码(kNoError 为正常) |
Mode get_current_mode() | 最近一次请求的工作模式 |
int64_t get_latency() | 数据通道延迟(纳秒;direct/SHM 模式返回 0) |
SampleLostInfo get_lost() | 数据通道丢包统计 |
std::string get_proxy_version() | 服务端上报的 VLink 版本字符串 |
std::string get_current_hostname() | 已连服务端的主机名 |
double get_current_cpu_usage() / get_current_memory_usage() | 服务端 CPU / 内存占用(0~100) |
static bool is_support_shm() | 当前构建是否支持 SHM(用于决定 direct) |
两个静态辅助函数把心跳时间戳格式化为可读串:
错误仅在状态翻转时通过 ErrorCallback 上报。除配置类错误外,多数可自愈:
| 错误码 | 触发条件 | 自动恢复 |
|---|---|---|
kNoError | 连接正常 | — |
kControlError | 控制标识与服务端不匹配 | — |
kReliableCompError / kTcpCompError / kDirectCompError | 客户端与服务端的 reliable / enable_tcp / direct 设置不一致 | 否,需统一配置后重连 |
kMultiProxyError | 同一域内检测到多个 ProxyServer,或服务端身份突变 | 部分,真实多服务端需移除多余实例 |
kVersionCompError | 客户端与服务端 VLink 版本不一致 | 是,版本恢复后自动清错 |
kTokenError | 控制面握手未通过 | 是,后台自动重握手直至成功 |
Info(每条话题统计)常用字段:
| 字段 | 类型 | 说明 |
|---|---|---|
url | std::string | 话题 URL,如 "dds://sensor/lidar" |
ser | std::string | 序列化类型名,如 "demo.proto.PointCloud" |
schema | SchemaType | 粗粒度 schema 家族(kProtobuf / kFlatbuffers 等) |
status | Status | kActive / kInActive / kPending / kInvalid |
freq / rate | float / uint64_t | 频率(条/秒)/ 吞吐(字节/秒),滑动平均 |
loss / latency | float / float | 丢包率 [0,1] / 延迟(毫秒;-1 无样本,-2 无效) |
process_list | std::vector<Process> | 收发该话题的进程列表(type 角色位掩码 / host / pid / name / ip) |
Data(每条转发或注入数据)字段:url、ser、schema、raw(Bytes 原始载荷)、timestamp(微秒)、seq(序号)。注入时 ser 与 schema 必须显式填全。
构造 ProxyAPI 时传入。控制面行为由前几项确定,数据面三项必须与服务端逐一对齐:
| 字段 | 默认 | 说明 |
|---|---|---|
role | kController | 客户端角色(见 12.10.1) |
domain_id | 0 | DDS 域 ID,须与服务端一致 |
security_key | "" | 控制面对称密钥;空串使用内置默认槽位,显式设置时须与服务端一致 |
reliable / enable_tcp / direct | false | 数据通道选项,三者均须与服务端完全一致 |
match_version | true | 是否校验 VLink 版本字符串一致 |
跨网段单播发现使用 allow_ip(本地绑定 IP)与 peer_ip(对端 IP),留空时走组播(部署示例见 12.16)。其余高级字段(dds_impl 等)保持默认即可。
vlink-proxy 是 ProxyServer 的独立可执行版本,在终端直接启动,同机仅一个实例。命令行参数与 ProxyServer::Config 一一对应:
| 选项 | 说明 |
|---|---|
-d, --domain_id INT | DDS 域 ID(0~255,默认 0),须与客户端一致 |
-k, --key STRING | 安全密钥(默认空),须与客户端一致 |
-r, --reliable | 可靠 QoS(须与客户端一致) |
-t, --tcp | TCP 传输(须与客户端一致) |
-g, --direct | SHM 直连数据通道(须与客户端一致) |
-a, --async | 在 MessageLoop 线程异步转发数据(默认内联转发) |
-x, --max_packet_size FLOAT | 单条消息最大转发大小(MiB,默认 4.0;设 0 会丢弃全部非空消息) |
-b, --bind_ip / -p, --peer_ip | 本地绑定 IP / 单播对端 IP(跨子网用) |
-s, --buf_size INT / -e, --mtu_size INT | DDS 收发缓冲区 / MTU 字节数(默认 0 = 内置默认) |
-n, --native | 限制 DDS 流量到 127.0.0.1(本机测试) |
-c, --iox_config PATH | 指定 Iceoryx TOML 配置;提供此项即拉起内嵌 RouDi(direct/SHM 所需) |
-l, --iox_strategy INT | Iceoryx 内存策略(1 低 / 2 中 / 3 高,默认 2) |
-m, --iox_monitoring on\|off | Iceoryx 监控开关(默认 on) |
--dds_impl STRING | DDS 实现选择(dds / ddsc) |
--runnable [NAME...] | 加载 RunablePluginInterface 插件 |
SHM 直连(
-g)需 Iceoryx RouDi 在运行:可由-c让代理内嵌拉起,或外部预先启动iox-roudi。
无需独立进程时,用 ProxyServer 库将服务端嵌入业务进程。每个进程仅一个实例,构造后调用 async_run() / run() 启动。Config 字段与 vlink-proxy 参数对应:domain_id、security_key、reliable、enable_tcp、direct、native_mode、max_packet_size、bind_ip / peer_ip。
ProxyServer 析构同步阻塞,会等待全部 DDS 句柄清理后返回,须确保进程退出前调用。
控制面经 DDS 安全扩展加密,由 security_key 对称密钥保护,两端必须一致:
security_key 留空时两端使用同一内置默认槽位,可正常通信;生产环境应显式设置自定义密钥。控制面自动完成安全握手与会话凭据协商,对业务代码透明,无需介入。
match_version(默认 true)在首次心跳时校验服务端与本端的 VLink 版本一致,不符触发 kVersionCompError。需跨版本连接(不推荐)时设 match_version = false。
Control 支持在服务端按 URL 关键词或进程名过滤,减少订阅与转发量:
filter_type 取值:0 全部;1/2/3 分别仅显示成对的 Pub+Sub / Server+Client / Setter+Getter 话题;4/5/6 分别显示 Event / Method / Field 类话题;7~12 分别仅显示单一角色。可用 ProxyAPI::is_enable_filter() 查询当前构建是否支持过滤。
典型场景:车载计算单元(EdgePC)运行业务节点与代理服务端,开发机(DevPC)远程监控与注入。两端以 bind_ip / peer_ip 做 DDS 单播发现,双方 IP 须可路由可达(非 NAT 穿透;穿透场景改用 zenoh://)。
EdgePC 在 12.13 的嵌入式服务端基础上补充绑定 IP:
DevPC 复用 12.9 的最小客户端,补上 IP:
**录制**:选 kRecord 模式,在 DataCallback 中将数据写入 bag(落盘容器用法见 录制与回放)。Data::raw 仅在回调内有效,异步写盘前需以 shallow_copy 保留载荷视图后再交由落盘线程持有。
**回放**:选 kPlay 模式,url_meta_list 中 type 设为 vlink::kPublisher(服务端充当发布者),再以 send_data() 逐条注入:
vlink-proxy 可执行文件通常直接使用系统安装版本,无需手动链接。
错误码语义见 12.10.5;下表给出常见现象与处置:
| 现象 | 原因与处置 |
|---|---|
连接被拒,报 kReliableCompError 等 | reliable / enable_tcp / direct 客户端与服务端不一致,须逐项对齐后重连 |
报 kMultiProxyError | 同一 DDS 域内运行了多个 ProxyServer,须每域一个实例或为各实例分配独立 domain_id |
Data::raw 外带后失效 | raw 仅在回调内有效,外带前须先复制(Bytes::shallow_copy 保留视图) |
direct 模式无数据 | SHM 直连需 Iceoryx RouDi 在运行,须先启动 iox-roudi 或由代理内嵌拉起 |
| 大包被丢弃 | max_packet_size = 0 表示丢弃全部非空消息,须显式设为足够大的 MiB 值 |
| 进程退出残留句柄 | ProxyServer 析构同步阻塞清理 DDS 句柄,须在进程退出前显式调用 |