Posted in

【工业级BLE网关架构】:单机万连接Go蓝牙服务设计(含连接复用、心跳熔断、OTA升级)

第一章:工业级BLE网关的架构演进与Go语言选型依据

工业级BLE网关已从早期单任务嵌入式固件,逐步演进为支持多协议接入、边缘计算、OTA协同与高并发设备管理的分布式边缘节点。其架构变迁可划分为三个典型阶段:

  • 单片机直驱阶段:基于nRF52系列MCU运行裸机BLE广播扫描+简单串口透传,资源受限,无法扩展安全策略与远程运维;
  • Linux容器化阶段:采用ARM Cortex-A平台(如Raspberry Pi 4),以Python/Node.js构建服务,依赖systemd管理进程,但GC抖动与内存泄漏在7×24运行中频发;
  • 云边协同阶段:网关作为Kubernetes边缘单元(K3s节点),承担BLE数据预处理、规则引擎执行与MQTT/CoAP协议桥接,要求低延迟、强稳定性与热插拔能力。

Go语言成为当前主流工业网关后端实现的核心依据,在以下维度形成不可替代优势:

并发模型与资源确定性

Go的goroutine轻量级线程(初始栈仅2KB)与非阻塞I/O调度器,天然适配BLE设备海量连接(单网关常需处理500+ BLE传感器并发扫描+连接)。对比Python GIL或Java线程模型,Go在4核ARM64平台下实测可稳定维持1200+ goroutines,CPU占用率波动小于±3%。

静态编译与部署一致性

# 构建无依赖二进制,直接部署至Yocto定制Linux系统
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o ble-gateway .

该命令生成的单一二进制文件不含动态链接库依赖,规避glibc版本兼容问题,满足工业现场离线升级需求。

生态工具链对工业场景的深度支持

能力 Go生态方案 工业价值
设备固件OTA签名验证 crypto/ecdsa + go.mozilla.org/pkcs7 确保固件来源可信,防中间人篡改
BLE协议栈封装 github.com/tinygo-org/bluetooth(TinyGo)或 github.com/go-ble/ble(Linux BlueZ) 统一抽象HCI层,屏蔽底层差异
指标采集与上报 prometheus/client_golang + OpenTelemetry SDK 对接企业级监控平台(如Grafana+VictoriaMetrics)

现代工业网关不再仅是“蓝牙转WiFi”的透明桥接器,而是具备协议解析、本地决策、安全审计能力的边缘智能体——Go语言凭借其确定性性能、零依赖分发与成熟可观测性生态,已成为构建此类系统的事实标准。

第二章:高并发BLE连接管理核心设计

2.1 基于Linux BlueZ D-Bus协议栈的Go绑定与异步事件驱动模型

BlueZ 通过 D-Bus 暴露完整的 BLE 协议栈能力,Go 生态中 github.com/alexbrainman/bluez 提供了轻量级绑定,其核心是基于 dbus 库构建的异步事件循环。

核心设计原则

  • 所有 BlueZ 接口调用非阻塞,返回 *dbus.Call 并支持 Call.Async()
  • 设备发现、GATT 特征读写、属性变更均通过 dbus.Object.Signal() 订阅
  • 连接状态机由 PropertyChange 信号驱动,避免轮询

示例:监听适配器状态变化

// 订阅 org.bluez.Adapter1 接口的 PropertiesChanged 信号
sig := dbus.WithMatchSignal(
    dbus.WithMatchObjectPath("/org/bluez/hci0"),
    dbus.WithMatchInterface("org.freedesktop.DBus.Properties"),
)
conn.BusObject().AddMatchSignal(sig)

// 信号处理(需在 goroutine 中持续接收)
for signal := range conn.Signal {
    if signal.Name == "org.freedesktop.DBus.Properties.PropertiesChanged" {
        // 参数: [interfaceName, changedProps, invalidatedProps]
        var iface string
        var props map[string]dbus.Variant
        signal.Args(&iface, &props, &[]string{})
        if iface == "org.bluez.Adapter1" && props["Powered"] != nil {
            powered := props["Powered"].Value().(bool)
            log.Printf("Adapter powered: %t", powered)
        }
    }
}

逻辑分析AddMatchSignal 注册内核级 D-Bus 匹配规则,避免用户态过滤开销;signal.Args() 自动反序列化变体(dbus.Variant)为 Go 原生类型,Powered 字段值为布尔型,直接解包即可使用。

关键接口映射表

BlueZ D-Bus 接口 Go 绑定方法示例 触发方式
org.bluez.Adapter1 SetProperty("Powered", true) 同步方法调用
org.bluez.Device1 Signal("PropertiesChanged") 异步事件订阅
org.bluez.GattCharacteristic1 ReadValue([]string{}) 回调式响应
graph TD
    A[Go App] -->|dbus.Call.Async| B[BlueZ D-Bus Daemon]
    B -->|PropertiesChanged| C[Signal Channel]
    C --> D[goroutine 处理]
    D --> E[状态机更新]
    E --> F[GATT 操作调度]

2.2 连接复用机制:设备ID绑定+连接池抽象+GATT会话生命周期管理

蓝牙低功耗(BLE)场景中频繁建连导致高延迟与资源浪费,需构建三层协同的复用体系。

设备ID绑定:唯一性锚点

将物理设备 MAC 地址哈希为稳定 deviceId,作为连接池键值:

String deviceId = DigestUtils.md5Hex(bleDevice.getAddress()); // 防止地址格式差异(冒号/空格)

逻辑分析:md5Hex 提供确定性映射;避免直接使用原始地址可规避 Android 8+ 随机化 MAC 导致的键漂移。

连接池抽象

池状态 最大连接数 驱逐策略
空闲 3 LRU + 60s 空闲超时
活跃 1/设备 绑定 GATT 会话引用计数

GATT 会话生命周期管理

graph TD
    A[APP 请求 GATT 访问] --> B{deviceId 是否存在活跃连接?}
    B -->|是| C[引用计数+1,复用会话]
    B -->|否| D[创建新连接 → 绑定 deviceId → 初始化 GATT]
    C & D --> E[APP 使用完毕 → 引用计数-1]
    E --> F{计数=0?}
    F -->|是| G[延迟30s后关闭底层连接]

2.3 单机万级连接的资源隔离策略:goroutine调度优化与内存对象复用实践

为支撑单机万级长连接,需从调度开销与内存分配两方面实施资源隔离。

goroutine 轻量级隔离

避免每个连接启动独立 goroutine(易触发调度器争抢),改用工作窃取池 + 连接绑定轮询器

type ConnPoller struct {
    ch   chan *Conn // 每个 Poller 独占 channel,避免跨 M 竞争
    proc func(*Conn)
}
// 启动固定 N 个 poller goroutine(N ≈ GOMAXPROCS)

ch 容量设为 1024,配合 runtime.Gosched() 防止单连接长期占用 M;proc 封装协议解析逻辑,实现业务逻辑与调度解耦。

内存对象复用关键路径

组件 复用方式 减少 GC 压力
TCP buffer sync.Pool + 预分配 ↓ 68%
Protocol msg 对象池 + Reset() ↓ 92%
TLS session 连接生命周期绑定 ↓ 41%

调度拓扑优化

graph TD
    A[Client Conn] --> B{Router}
    B --> C[Worker Pool-1]
    B --> D[Worker Pool-2]
    C --> E[Sync.Pool Buffer]
    D --> F[Sync.Pool Message]

2.4 BLE连接状态机建模:从discovery到bonding的全链路状态收敛实现

BLE连接并非线性流程,而是由事件驱动、需严格状态守卫的闭环系统。核心挑战在于异步GAP/GATT操作与安全策略(如MITM保护)的协同收敛。

状态跃迁约束

  • DISCOVERINGCONNECTING 需校验RSSI ≥ −70 dBm且设备支持LE Secure Connections
  • PAIRINGBONDED 必须完成LTK分发+IRK绑定+CSRK存储三重确认

状态收敛判定逻辑

bool is_state_converged(ble_sm_state_t current, uint8_t security_level) {
    // security_level: 1=unauthenticated, 2=authenticated, 3=LE SC required
    return (current == BONDED) && 
           (sm_get_auth_status() >= security_level) && 
           (sm_has_valid_ltk() && sm_has_valid_irk()); // LTK/IRK双重存在性验证
}

该函数在每次SM事件回调中触发,确保bonding完成不仅依赖状态位,更强制校验密钥材料完整性与认证等级匹配性。

典型状态迁移路径

起始状态 触发事件 目标状态 收敛条件
DISCOVERING 扫描响应匹配 CONNECTING 连接参数协商成功
PAIRING STK/LTK交换完成 BONDING IRK写入NV存储并返回ACK
BONDING CSRK同步确认 BONDED 所有密钥组通过is_state_converged()
graph TD
    A[DISCOVERING] -->|Scan Response| B[CONNECTING]
    B -->|Connection Complete| C[PAIRING]
    C -->|LTK/IRK/CSRK Exchange| D[BONDING]
    D -->|NV Commit + ACK| E[BONDED]
    E -->|Link Loss| A

2.5 连接上下文元数据治理:标签化设备画像与动态路由索引构建

设备画像不再依赖静态配置,而是通过实时采集的运行时元数据(如固件版本、网络延迟、资源负载、地理位置、接入协议)生成多维语义标签。

标签化建模示例

# 基于设备实时指标生成语义标签
def generate_device_tags(device_metrics):
    tags = []
    if device_metrics["cpu_usage"] > 80: tags.append("high-load")
    if device_metrics["rtt_ms"] < 15: tags.append("low-latency-edge")
    if "BLE" in device_metrics["supported_protocols"]: tags.append("ble-capable")
    return list(set(tags))  # 去重保障标签唯一性

逻辑分析:该函数将原始监控指标映射为可检索、可聚合的语义标签;rtt_ms阈值设定依据边缘SLA要求,ble-capable支持协议级路由决策。

动态路由索引结构

标签组合 路由策略 生效时效
["low-latency-edge", "high-load"] 本地卸载+就近分流 30s
["ble-capable"] 启用蓝牙Mesh网关 持久

元数据同步机制

graph TD
    A[设备Agent] -->|MQTT/JSON Schema| B(元数据清洗服务)
    B --> C{标签引擎}
    C --> D[动态索引更新]
    D --> E[API网关路由表]

第三章:可靠性保障体系构建

3.1 心跳熔断双模机制:L2CAP层保活探测与应用层业务心跳协同设计

蓝牙低功耗通信中,单一心跳易导致误判:L2CAP链路空闲超时(如Linux内核l2cap_conn->idle_timeout默认30s)可能中断合法长周期业务,而纯应用层心跳又无法感知底层链路瞬断。

协同触发逻辑

  • L2CAP层启用HCI_OP_WRITE_LINK_SUPERVISION_TIMEOUT配置监督超时(建议设为8–12s)
  • 应用层心跳周期设为15s,携带业务上下文标识(如session_id, seq_no
  • 双通道任一失败即触发熔断,但恢复需双通道均确认

熔断状态机(Mermaid)

graph TD
    A[正常] -->|L2CAP超时| B[链路疑似中断]
    A -->|应用心跳超时| C[业务不可达]
    B & C --> D[熔断激活]
    D -->|L2CAP重连成功+心跳响应| E[双模恢复]

示例心跳报文结构

字段 长度 说明
magic 2B 0x424D(BM),校验协议版本
session_id 4B TLS握手生成的会话唯一标识
timestamp 8B Unix纳秒级时间戳

L2CAP保活探测代码片段

// 启用L2CAP保活:发送空ACL包触发监督定时器重置
int l2cap_send_keepalive(int sk, uint16_t cid) {
    struct sockaddr_l2 addr = {.l2_cid = htobs(cid)};
    uint8_t keepalive_pkt[1] = {0};
    return sendto(sk, keepalive_pkt, 0, MSG_NOSIGNAL, 
                   (struct sockaddr*)&addr, sizeof(addr));
}

该函数不携带有效载荷,仅利用ACL包传输本身重置链路监督定时器;cid为信道ID,需与已建立的L2CAP连接匹配;MSG_NOSIGNAL避免因对端关闭触发SIGPIPE。

3.2 网络抖动下的连接自愈:基于指数退避重连与拓扑感知重定向实践

在边缘计算与多活数据中心场景中,网络抖动常导致短时连接中断。单纯依赖 TCP 重传无法应对跨地域链路的间歇性劣化。

指数退避重连策略

采用 base_delay × 2^retry_count 动态退避,最大上限 30s,避免雪崩式重连:

import time
import random

def exponential_backoff(retry_count: int) -> float:
    base = 0.5  # 初始延迟(秒)
    cap = 30.0
    delay = min(base * (2 ** retry_count), cap)
    return delay + random.uniform(0, 0.2)  # 加入抖动防同步

# 示例:第3次重试 → delay ≈ 4.0 ~ 4.2s

逻辑分析:base=0.5 平衡响应速度与负载压力;random.uniform(0, 0.2) 引入微小随机偏移,消除重连风暴风险;cap=30.0 防止无限等待。

拓扑感知重定向机制

客户端依据实时延迟与区域亲和度,从健康节点池中优选目标:

节点ID RTT(ms) 同AZ 权重
node-a 12 95
node-b 47 68
node-c 82 32

自愈流程协同

graph TD
    A[连接断开] --> B{抖动检测}
    B -->|是| C[启动指数退避]
    B -->|否| D[触发拓扑重定向]
    C --> E[重连前查健康节点列表]
    E --> F[按权重选取新 endpoint]
    F --> G[建立新连接]

3.3 设备离线归因分析:从HCI错误码解析到网关侧故障根因定位

设备离线并非原子事件,而是多层协议栈异常的外在表征。需逆向追踪:从终端上报的HCI错误码出发,逐级映射至蓝牙基带、BLE连接管理、网关协议栈及MQTT会话层。

HCI错误码语义映射

常见离线诱因对应关键错误码:

  • 0x3E(Connection Failed to be Established)→ 扫描响应超时或白名单不匹配
  • 0x08(Connection Timeout)→ 链路层握手失败,常因信号衰减或干扰

网关侧根因判定逻辑

def classify_offline_cause(hci_code: int, rssi: int, conn_interval_ms: int) -> str:
    if hci_code == 0x08 and rssi < -85:
        return "PHY_LAYER_LOSS"  # 物理层信号劣化
    elif hci_code == 0x3E and conn_interval_ms > 200:
        return "GATEWAY_CONN_POLICY_VIOLATION"  # 网关连接参数协商失败
    return "UNKNOWN"

该函数将HCI原始码与上下文指标(RSSI、连接间隔)联合判别,避免单点误判。rssi < -85 表明接收功率低于BLE可靠通信阈值;conn_interval_ms > 200 违反网关默认策略(标准为7.5–40 ms),触发主动断链。

故障传播路径

graph TD
    A[终端HCI Error 0x08] --> B[Link Layer Timeout]
    B --> C[网关L2CAP重传超限]
    C --> D[MQTT Will Message触发]
    D --> E[平台标记“离线”]

第四章:固件全生命周期OTA升级引擎

4.1 安全OTA协议栈设计:基于BLE Secure Connection + AES-GCM分片加密传输

为兼顾低功耗与端到端完整性,本方案将BLE链路层安全连接(LE Secure Connections,配对采用SC模式+OOB或Passkey)与应用层AES-GCM分片加密协同设计。

分片加密流程

  • 每个OTA数据包(最大MTU=247字节)预留16字节GCM认证标签空间;
  • 有效载荷≤231字节,使用唯一nonce(packet_id || session_counter)驱动AES-GCM-128;
  • 接收端验证tag后才提交固件镜像缓冲区。
// AES-GCM加密伪代码(mbed TLS)
int encrypt_chunk(uint8_t *out, const uint8_t *in, size_t len,
                   const uint8_t *nonce, const uint8_t *aad) {
    mbedtls_gcm_context ctx;
    mbedtls_gcm_init(&ctx);
    mbedtls_gcm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key, 128);
    mbedtls_gcm_crypt_and_tag(&ctx, MBEDTLS_GCM_ENCRYPT,
        len, nonce, 12, aad, 8, in, out, 16, out+len); // tag appended
}

nonce长度固定12字节,避免重放;aad含包序号与固件版本,确保上下文绑定;输出为密文+16B标签,总长len+16

安全参数对照表

参数 安全意义
密钥长度 128 bit 抵御暴力破解(≥2⁸⁰枚举难度)
GCM Tag长度 128 bit 防篡改/重放攻击
Nonce结构 packet_id(4)+cnt(8) 全局唯一,防跨会话重用
graph TD
    A[OTA Init] --> B[BLE SC配对]
    B --> C[生成会话密钥K_session]
    C --> D[分片+AES-GCM加密]
    D --> E[BLE ATT Write Request]
    E --> F[接收端GCM验证+写Flash]

4.2 断点续传与原子升级:Flash分区校验、CRC32块签名与双Bank切换实践

双Bank切换核心流程

// Bank切换原子操作(基于STM32L4+HAL)
if (is_bank_a_active()) {
    HAL_FLASHEx_OBProgram(&OBInit, OB_USER_CONFIG, 0x00000001); // 切Bank B
} else {
    HAL_FLASHEx_OBProgram(&OBInit, OB_USER_CONFIG, 0x00000000); // 切Bank A
}
HAL_FLASHEx_OBLaunch(); // 硬复位生效,确保无中间态

该代码通过Option Bytes修改启动Bank标识后强制复位,避免运行时跳转导致的指令乱序。OB_USER_CONFIG位控制BOOT0引脚行为,0x00000001表示从Bank B启动。

校验与签名协同机制

阶段 操作 触发条件
升级中 每写入4KB块计算CRC32 防止传输截断
写入完成 全镜像SHA256 + 签名验证 抵御恶意篡改
启动前 Bank头部CRC32校验 快速拒绝损坏固件

数据同步机制

  • 断点信息持久化至独立EEPROM页(非Flash),含:当前Bank、已接收字节数、块索引、时间戳
  • 每次写块后更新,支持毫秒级恢复
graph TD
    A[OTA下载] --> B{断电?}
    B -->|是| C[重启读EEPROM]
    B -->|否| D[继续写入]
    C --> E[定位最后完整块]
    E --> F[从该偏移续传]

4.3 OTA任务编排引擎:多设备并发升级队列、优先级抢占与带宽动态限速

OTA任务编排引擎是大规模设备固件升级的核心调度中枢,需在资源约束下保障升级可靠性与时效性。

多级优先级队列设计

采用双队列结构:

  • 高优队列:紧急安全补丁(priority=100)、VIP设备;
  • 常规队列:按设备分组哈希轮询调度,避免单组阻塞。

带宽动态限速机制

def adjust_bandwidth(current_load: float, target_devices: int) -> int:
    # 基于实时网络负载与并发数动态计算单设备限速(KB/s)
    base = 512  # 基准带宽
    load_factor = max(0.3, 1.0 - current_load * 0.7)  # 负载越高,分配越保守
    concurrency_factor = min(1.0, 16 / max(1, target_devices))  # 设备越多,单设备越少
    return int(base * load_factor * concurrency_factor)

逻辑说明:current_load 来自Prometheus实时采集的出口带宽利用率;target_devices 为当前批次待升级设备数;返回值经gRPC下发至边缘升级代理执行TCP窗口限速。

优先级抢占流程

graph TD
    A[新任务入队] --> B{是否高优?}
    B -->|是| C[暂停低优运行中任务]
    B -->|否| D[插入常规队列尾部]
    C --> E[保存断点至Redis]
    E --> F[调度器重分配资源]
策略维度 高优任务 常规任务
抢占阈值 CPU > 70% 或带宽 > 90% 不触发抢占
断点保留时长 72小时 24小时
重试退避 指数退避+随机抖动 固定间隔30s

4.4 升级可观测性建设:实时进度推送、失败回滚日志追踪与版本灰度发布控制

实时进度推送机制

基于 WebSocket + OpenTelemetry TraceID 关联,实现部署流水线各阶段毫秒级状态透出:

# 推送服务核心逻辑(Flask-SocketIO)
@socketio.on('deploy_progress')
def handle_progress(data):
    trace_id = data.get('trace_id')  # 关联分布式追踪上下文
    stage = data['stage']              # e.g., 'build', 'test', 'canary'
    progress = data['percent']        # 0–100 整数
    emit('progress_update', {
        'trace_id': trace_id,
        'stage': stage,
        'percent': progress,
        'ts': time.time_ns() // 1_000_000  # 毫秒时间戳
    }, room=trace_id)  # 按 TraceID 隔离广播

该逻辑确保前端仅接收本部署链路的精准进度,避免跨任务干扰;room=trace_id 实现轻量级会话隔离,无需额外订阅管理。

失败回滚日志追踪

字段 含义 示例
rollback_id 唯一回滚事务标识 rb-20240521-8a3f
origin_deploy_id 触发回滚的原始部署ID dep-20240521-7b2e
affected_pods 受影响实例列表 ["svc-a-7c4d", "svc-b-9f1a"]

灰度发布控制流

graph TD
    A[灰度策略配置] --> B{流量切分是否生效?}
    B -->|是| C[注入OpenTracing Header]
    B -->|否| D[全量切流至v1]
    C --> E[按Header路由至v2-canary]
    E --> F[自动采集错误率/延迟P95]
    F --> G{达标?}
    G -->|是| H[提升灰度比例]
    G -->|否| I[触发自动回滚]

第五章:总结与面向边缘智能的演进路径

边缘智能落地的典型工业场景复盘

在某华东汽车零部件制造工厂,部署基于Jetson AGX Orin的实时缺陷检测系统后,产线质检环节实现毫秒级响应:模型推理延迟稳定控制在23ms以内(

硬件-软件协同演进的关键断点

当前边缘节点普遍面临三类硬约束:

  • 内存墙:ARM64平台常驻内存超限(>3GB)触发OOM Killer;
  • 供电墙:无风扇嵌入式设备在持续AI负载下结温达89°C;
  • 协议墙:OPC UA over TSN与MQTT Sparkplug B协议栈共存时出现时序竞争。
    某智慧电网变电站项目通过重构容器启动顺序(先加载eBPF流量整形模块,再启动TensorFlow Lite服务),将协议冲突导致的丢包率从12.3%压降至0.4%。

模型生命周期管理的现场实践

阶段 工具链 现场问题 解决方案
训练 PyTorch + Weights & Biases 跨厂区数据孤岛导致标注不一致 部署联邦学习框架Flower,本地训练后仅上传梯度加密参数
部署 NVIDIA Fleet Command OTA升级期间PLC通信中断超时 实施双分区A/B镜像+原子化切换,中断时间压缩至412ms
监控 Grafana + Prometheus GPU显存泄漏未被及时捕获 注入自定义Exporter采集nvml指标,设置显存占用斜率告警
flowchart LR
    A[产线摄像头] --> B{边缘网关}
    B --> C[实时预处理:ROI裁剪+直方图均衡]
    C --> D[INT8量化模型推理]
    D --> E[结果缓存至SQLite WAL模式]
    E --> F[每5秒批量推送至Kafka Topic]
    F --> G[中心平台进行跨产线缺陷聚类分析]
    style B fill:#4CAF50,stroke:#388E3C,stroke-width:2px
    style D fill:#2196F3,stroke:#0D47A1,stroke-width:2px

运维保障体系的实战验证

深圳某智慧园区部署237个边缘AI节点后,建立三级故障响应机制:一级(自动恢复)覆盖83%事件,如通过systemd watchdog自动重启卡死的OpenVINO进程;二级(远程干预)由SRE团队使用Ansible Playbook执行标准化修复,平均MTTR为6.2分钟;三级(现场处置)仅针对硬件故障,占比不足0.7%。2023年Q4全网节点可用率达99.992%,其中17台设备连续运行超210天未重启。

开源工具链的深度定制经验

为适配国产RK3588平台的NPU异构计算,团队在Apache TVM基础上扩展了Rockchip专用Pass:

  • 新增rknn_quantize算子替换原生量化逻辑,支持非对称零点校准;
  • 修改graph_runtime模块,将DDR带宽预估误差从±34%收敛至±5.8%;
  • 在编译期注入rknn_profile指令,生成含NPU指令周期数的JSON报告。该定制版已支撑6类视觉模型在12个客户现场稳定运行。

安全加固的现场实施清单

  • 所有边缘节点启用TPM 2.0芯片级密钥保护,模型权重加载前校验SHA-384哈希值;
  • 使用eBPF程序拦截所有非白名单端口的出向连接,阻断潜在C2通信;
  • 日志审计模块强制启用SELinux MLS策略,敏感操作记录包含硬件序列号与可信执行环境PCR值;
  • OTA固件签名采用国密SM2算法,验签过程在TrustZone内完成,杜绝中间人篡改风险。

记录 Golang 学习修行之路,每一步都算数。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注