第一章:安防相机视频流特性与Go语言选型依据
安防相机产生的视频流具有高并发、低延迟、长时序和协议异构等典型特征。主流设备普遍采用RTSP协议传输H.264/H.265编码的裸流,帧率通常为15–30 FPS,单路1080p流带宽约2–6 Mbps;部分AI相机还附加ONVIF元数据通道或HTTP JSON事件通知,形成“音视频流 + 控制信令 + 事件流”三重并发模型。
视频流核心约束条件
- 连接密度高:单台边缘网关需稳定接入50+路RTSP流;
- 会话生命周期长:单路流持续运行数天至数月,要求连接保活与异常自动恢复;
- 解码非必需但解析刚需:多数场景仅需提取SPS/PPS、关键帧时间戳、RTP包序列号,无需全帧解码;
- 协议碎片化严重:海康SDK、大华私有协议、GB28181 over SIP、标准RTSP存在认证方式、OPTIONS响应、DESCRIBE重定向等差异。
Go语言契合性分析
Go的轻量级goroutine与channel原语天然适配高并发流管理:每路RTSP连接可封装为独立goroutine,通过select监听RTP包接收、心跳超时、控制信号三类事件;其静态链接二进制特性避免C/C++依赖冲突,便于在ARM64嵌入式网关部署;标准库net/http与第三方github.com/aler9/gortsplib已成熟支持RTSP客户端状态机。
快速验证RTSP流连通性示例
package main
import (
"log"
"time"
"github.com/aler9/gortsplib/client" // go get github.com/aler9/gortsplib
)
func main() {
c := client.Client{
// 启用自动重连与超时恢复
KeepalivePeriod: 5 * time.Second,
OnConnectionClosed: func(err error) { log.Printf("connection closed: %v", err) },
}
err := c.Start("rtsp://admin:password@192.168.1.100:554/stream1")
if err != nil {
log.Fatal(err)
}
defer c.Close()
// 仅建立连接并打印SDP,不消费RTP包
log.Printf("Connected successfully. SDP:\n%s", c.BaseURL().String())
}
该代码片段验证基础连通性,不触发实际媒体消费,可在3秒内完成握手并输出SDP描述,适用于大规模流健康巡检。
第二章:高并发实时解码架构设计
2.1 H.265码流解析模型与Go协程调度理论
H.265(HEVC)码流由NALU(Network Abstraction Layer Unit)组成,每个NALU包含类型标识、起始码和有效载荷。解析需严格遵循字节对齐与RBSP(Raw Byte Sequence Payload)解码规则。
并发解析架构设计
为提升吞吐量,采用“生产者-消费者”模式:
- 生产者协程:按NALU边界切分原始比特流(依赖0x000001或0x00000001起始码)
- 消费者协程池:并行执行熵解码、反量化、帧内预测等计算密集型子任务
func parseNALU(data []byte, ch chan<- *Frame) {
for len(data) > 4 {
if bytes.HasPrefix(data, []byte{0, 0, 0, 1}) ||
bytes.HasPrefix(data, []byte{0, 0, 1}) {
// 提取NALU(跳过起始码,保留NAL header)
nalu := data[3:] // 0x000001 case
if len(data) >= 4 && bytes.HasPrefix(data[:4], []byte{0,0,0,1}) {
nalu = data[4:]
}
ch <- decodeRBSP(nalu) // 同步调用解码逻辑
}
data = data[1:]
}
}
逻辑分析:该函数以滑动窗口方式扫描起始码,
nalu截取含NAL header(1字节)的原始数据;decodeRBSP需处理字节对齐填充(0x00字节剔除)及EBSP→RBSP转换,参数data为原始NALU payload,ch为无缓冲通道,依赖Go runtime的goroutine唤醒机制实现背压控制。
协程调度关键参数对比
| 调度参数 | 默认值 | H.265解析场景推荐值 | 说明 |
|---|---|---|---|
| GOMAXPROCS | 核心数 | runtime.NumCPU() |
充分利用CPU并行解码 |
| runtime.GCPercent | 100 | 50 | 减少GC停顿,保障实时性 |
graph TD
A[原始码流] --> B{起始码检测}
B -->|命中| C[提取NALU]
B -->|未命中| A
C --> D[投递至Worker Pool]
D --> E[协程调度器]
E --> F[空闲P绑定M执行解码]
F --> G[结果写入帧缓存]
2.2 零拷贝内存池在YUV帧缓冲中的实践优化
传统YUV帧拷贝导致CPU带宽浪费与延迟升高。零拷贝内存池通过预分配连续物理页+DMA映射,使采集、编码、渲染模块共享同一内存视图。
内存池初始化关键参数
pool_size: 按最大分辨率(如4K@30fps×3帧)预留page_align: 强制64KB对齐,适配DMA最小粒度cache_coherent: 启用ARM SMMU或x86 IOMMU透传
核心数据结构示意
typedef struct {
void *vaddr; // 用户态虚拟地址(mmap映射)
dma_addr_t dma_addr; // 设备可见物理地址(I/O总线地址)
size_t size;
atomic_t refcnt; // 多线程/多模块引用计数
} yuv_buffer_t;
dma_addr由dma_map_single()生成,确保GPU/ISP/VPU均可直接访问;refcnt避免提前释放——任一模块调用yuv_buffer_put()才触发dma_unmap_single()。
性能对比(1080p@60fps)
| 场景 | 平均延迟 | CPU占用率 |
|---|---|---|
| 传统memcpy | 12.7ms | 18.3% |
| 零拷贝池 | 3.2ms | 4.1% |
graph TD
A[Camera ISP] -->|DMA写入| B[yuv_buffer_t]
B --> C[Video Encoder]
B --> D[Preview Renderer]
C & D -->|refcnt--| E[Pool回收]
2.3 基于epoll/kqueue的IO多路复用与Go netpoll协同机制
Go 运行时通过 netpoll 抽象层统一封装 Linux 的 epoll 与 BSD/macOS 的 kqueue,屏蔽系统差异,为 net.Conn 提供无感事件驱动能力。
核心协同路径
- Go goroutine 发起阻塞读写时,底层自动注册 fd 到
netpoll; netpoll将 fd 加入epoll_wait/kevent等待队列;- 事件就绪后唤醒对应 goroutine,避免线程级阻塞。
数据同步机制
// runtime/netpoll.go 中关键调用(简化)
func netpoll(block bool) gList {
// epoll_wait 或 kevent 阻塞等待,返回就绪的 goroutine 链表
return poller.wait(block)
}
block=true 时进入内核等待;gList 是就绪 goroutine 的无锁链表,由调度器直接接入运行队列。
| 系统 | 底层机制 | 触发模式 |
|---|---|---|
| Linux | epoll | ET 模式+一次性通知 |
| macOS/BSD | kqueue | EVFILT_READ/WRITE |
graph TD
A[goroutine Read] --> B[netpoll.Add fd]
B --> C{netpoll.wait()}
C -->|就绪事件| D[唤醒 goroutine]
C -->|超时/中断| E[返回空列表]
2.4 解码任务分片策略与动态负载均衡算法实现
分片策略设计原则
采用一致性哈希 + 虚拟节点机制,规避节点增减导致的大规模数据迁移。每个任务按业务键(如 user_id)映射至 128 个虚拟槽位,确保分布均匀性。
动态负载感知模型
def calculate_weight(node_metrics):
# cpu_usage: 0.0–1.0, memory_util: 0.0–1.0, pending_tasks: int
base_weight = 100.0
penalty = (node_metrics['cpu'] + node_metrics['memory']) * 30.0
queue_penalty = min(node_metrics['pending'] * 2.5, 40.0)
return max(10.0, base_weight - penalty - queue_penalty) # 权重区间 [10, 100]
逻辑分析:权重反比于资源压力,pending_tasks 线性惩罚但设上限防归零;最小权重 10 保障节点始终可被调度。
调度决策流程
graph TD
A[接收新任务] --> B{查询实时节点指标}
B --> C[计算各节点加权得分]
C --> D[按权重轮询选择目标节点]
D --> E[提交任务并更新本地负载快照]
负载均衡效果对比(TPS/节点)
| 策略 | 均值 | 标准差 | 最大偏移 |
|---|---|---|---|
| 固定分片 | 1200 | 420 | +180% |
| 动态加权调度 | 1180 | 65 | +12% |
2.5 GPU硬解加速接口封装与CUDA/NVDEC异步调用实践
NVDEC 是 NVIDIA 提供的专用视频解码硬件单元,需通过 CUDA 驱动 API(如 cuvidCreateVideoParser)或 NVCUVID 库进行调用。高效利用需解耦解码、内存拷贝与处理流程。
异步解码流水线设计
- 解码器实例绑定独立 CUDA 上下文与流(
CUstream) - 每帧解码回调中提交 YUV 数据至
cudaMemcpyAsync,目标为 pinned host memory 或 device memory - 后续 CUDA 内核在同一流中依赖同步点执行,避免显式
cudaStreamSynchronize
核心封装类关键接口
class NVDECDecoder {
public:
void DecodePacket(const uint8_t* data, size_t size); // 非阻塞入队
CUdeviceptr MapFrame(int frame_idx); // 映射解码后设备内存
void UnmapFrame(CUdeviceptr ptr); // 显式释放映射
private:
CUvideoparser parser_;
CUvideodecoder decoder_;
CUstream decode_stream_; // 专属解码流,隔离于计算流
};
DecodePacket 将 bitstream 提交至硬件队列;MapFrame 调用 cuvidMapVideoFrame 获取设备指针,避免主机侧拷贝;decode_stream_ 确保解码与后续 kernel 的时序可控。
性能对比(1080p H.264,Tesla T4)
| 方式 | 平均延迟 | CPU占用率 | 帧率稳定性 |
|---|---|---|---|
| CPU软解(FFmpeg) | 42 ms | 92% | ±18% |
| NVDEC异步封装 | 8.3 ms | 11% | ±2.1% |
graph TD
A[Bitstream Packet] --> B[NVDEC Hardware Decode]
B --> C[cuvidMapVideoFrame → CUdeviceptr]
C --> D[cudaMemcpyAsync to GPU memory]
D --> E[CUDA Kernel Processing]
E --> F[Display/Encode]
第三章:低延迟端到端链路保障
3.1 视频时间戳同步与PTS/DTS精确定时控制
数据同步机制
视频解码依赖两类关键时间戳:
- PTS(Presentation Time Stamp):指示帧应显示的绝对时间;
- DTS(Decoding Time Stamp):指示帧需被解码的顺序时间(对B帧尤为重要)。
当编码器启用B帧时,DTS ≠ PTS,需解码器按DTS解码、按PTS排序输出。
时间戳校准流程
// FFmpeg中强制重置PTS/DTS示例(单位:AV_TIME_BASE)
pkt->pts = av_rescale_q_rnd(frame_num * 1000000, AV_TIME_BASE_Q, st->time_base,
AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
pkt->dts = pkt->pts - (has_b_frames ? 2 : 0) * pkt->duration;
逻辑说明:
av_rescale_q_rnd将逻辑帧号映射至流时间基;has_b_frames触发DTS偏移补偿,确保解码顺序正确。pkt->duration为帧时长,单位由st->time_base决定。
关键参数对照表
| 字段 | 含义 | 典型值(H.264@30fps) |
|---|---|---|
time_base |
流时间粒度 | 1/1000000(微秒) |
pkt->pts |
显示时刻 | 33333(≈33.3ms) |
pkt->dts |
解码时刻 | (I帧)或 -33333(前向B帧) |
graph TD
A[编码器输出帧] --> B{含B帧?}
B -->|是| C[生成DTS < PTS]
B -->|否| D[DTS == PTS]
C --> E[解码器按DTS排序解码]
E --> F[按PTS重排输出队列]
F --> G[精准控制显示时刻]
3.2 环形缓冲区与滑动窗口丢帧策略的工程落地
核心设计动机
实时音视频处理中,采集速率与消费速率常存在抖动。环形缓冲区提供固定内存开销下的高效读写解耦,滑动窗口则动态界定“可丢弃帧”的时间边界。
数据同步机制
采用原子指针+版本号双校验,避免ABA问题:
typedef struct {
atomic_uint head; // 生产者最新写入位置(模容量)
atomic_uint tail; // 消费者最新读取位置
uint32_t version; // 防ABA版本计数器
} ring_meta_t;
head 和 tail 均为无锁原子操作;version 在每次 tail 更新时递增,确保消费者能识别重叠写入。
丢帧决策逻辑
基于滑动窗口内帧时间戳分布,优先丢弃最旧非关键帧(如H.264的P/B帧):
| 窗口大小 | 丢帧阈值 | 触发条件 |
|---|---|---|
| 120ms | ≥3帧 | 窗口内延迟 > 80ms |
| 200ms | ≥5帧 | 连续2次超限 |
流程协同示意
graph TD
A[新帧到达] --> B{缓冲区满?}
B -->|是| C[计算滑动窗口内TS分布]
C --> D[标记最旧非I帧为可丢弃]
D --> E[执行原子pop并更新version]
B -->|否| F[直接push]
3.3 内核级SO_RCVLOWAT调优与UDP接收抖动抑制
UDP应用常因接收缓冲区唤醒阈值不合理,导致epoll_wait()频繁触发小包事件,引发CPU毛刺与端到端抖动。SO_RCVLOWAT正是控制这一行为的关键内核参数。
接收唤醒水位机制
内核仅当socket接收队列字节数 ≥ SO_RCVLOWAT 值时,才向epoll或select返回可读事件。默认值为1,极易造成“每包唤醒”。
调优实践代码
int lowat = 8192; // 推荐设为MTU×2~4(如1500×4=6000)
if (setsockopt(sockfd, SOL_SOCKET, SO_RCVLOWAT, &lowat, sizeof(lowat)) < 0) {
perror("setsockopt SO_RCVLOWAT");
}
此设置使内核累积至少8KB数据后才通知应用,显著降低系统调用频次。注意:该值不可超过
sk_rcvbuf(接收缓冲区上限),否则静默截断为当前rcvbuf。
| 参数 | 影响维度 | 风险提示 |
|---|---|---|
SO_RCVLOWAT=1 |
高实时性、高中断开销 | 抖动放大、CPU占用飙升 |
SO_RCVLOWAT=65536 |
低唤醒频次、高吞吐 | 首包延迟增加(最多≈1个RTT) |
内核处理流程
graph TD
A[UDP包入队] --> B{sk_rmem_alloc ≥ SO_RCVLOWAT?}
B -->|否| C[静默排队]
B -->|是| D[标记sk->sk_data_ready]
D --> E[epoll_wait返回EPOLLIN]
第四章:微服务治理与安防场景适配
4.1 基于gRPC+Protobuf的相机元数据注册与发现协议
协议设计动机
传统HTTP轮询式设备发现存在延迟高、带宽浪费问题。gRPC+Protobuf组合提供强类型、低开销、双向流支持,适配边缘侧高频元数据同步场景。
核心消息定义
message CameraMetadata {
string camera_id = 1; // 全局唯一标识(如 cam-001a2b)
string model = 2; // 厂商型号(例:AXIS-Q6055-E)
repeated string capabilities = 3; // 支持能力列表("rtsp", "h265", "ai-inference")
int64 last_heartbeat = 4; // Unix毫秒时间戳,用于存活判定
}
该结构兼顾可扩展性与序列化效率;camera_id 作为服务发现主键,capabilities 支持动态能力协商。
服务接口契约
| 方法名 | 类型 | 说明 |
|---|---|---|
| Register | Unary | 首次注册或心跳续期 |
| Deregister | Unary | 主动下线 |
| WatchCameras | Server Streaming | 实时推送新增/变更/离线事件 |
数据同步机制
graph TD
A[Camera Agent] -->|Register/Heartbeat| B[gRPC Server]
B --> C[Consistent Hash Registry]
C --> D[Watch Stream]
D --> E[Discovery Client]
注册请求携带TLS双向认证证书,确保设备身份可信;服务端基于last_heartbeat自动剔除超时节点(阈值默认30s)。
4.2 RTSP/ONVIF设备自动接入与信令状态机建模
为实现海量异构IPC的零配置接入,系统采用基于ONVIF Discovery + RTSP OPTIONS Probe的双阶段握手机制。
设备发现与能力协商
- 广播监听
ws-discovery消息,提取XAddr与ProfileToken - 发起
GetCapabilities获取流地址模板(如rtsp://{{ip}}:554/Streaming/Channels/{{chan}}) - 自动填充
MediaUri并执行OPTIONS预检,验证RTSP服务可用性
信令状态机核心逻辑
graph TD
IDLE --> DISCOVER[发送ProbeMatch]
DISCOVER --> CAPABILITY[调用GetCapabilities]
CAPABILITY --> PRECHECK[RTSP OPTIONS预检]
PRECHECK --> STREAMING[SETUP→PLAY]
PRECHECK -.-> TIMEOUT[超时回退至IDLE]
状态迁移参数说明
| 状态 | 超时阈值 | 关键校验项 | 失败动作 |
|---|---|---|---|
| DISCOVER | 3s | XAddr格式、HTTPS支持 | 忽略该响应 |
| CAPABILITY | 5s | ProfileToken有效性 | 降级使用默认模板 |
| PRECHECK | 2s | Public: OPTIONS,DESCRIBE |
切换备用端口重试 |
def onvif_probe(ip: str) -> bool:
try:
# 使用zeep动态构建SOAP请求,避免硬编码WSDL路径
client = Client(f"http://{ip}/onvif/device_service?wsdl")
caps = client.service.GetCapabilities() # 触发完整能力树解析
return "Streaming" in caps.get("Media", {})
except Exception as e:
logger.warning(f"ONVIF probe failed for {ip}: {e}")
return False
该探测函数封装了WSDL动态加载与能力子集判别逻辑,caps.get("Media", {})规避空指针异常,返回布尔值驱动状态机跃迁。
4.3 分布式健康探针与单节点3200路心跳收敛算法
传统轮询式心跳检测在万级节点场景下易引发网络风暴。本方案采用分层探针+时间片哈希收敛机制,单节点可稳定承载3200路并发心跳。
探针调度策略
- 基于节点ID与当前秒级时间戳做CRC32哈希,映射至64个虚拟时间片槽位
- 每个槽位内采用指数退避重试(初始1s,上限60s)
- 探针请求携带轻量级上下文签名(16字节),避免中间设备缓存污染
心跳收敛核心逻辑
def converge_heartbeat(node_id: str, ts: int) -> int:
# CRC32(node_id + str(ts % 60)) % 64 → 确保每分钟内均匀分散
slot = crc32((node_id + str(ts % 60)).encode()) % 64
# 每槽位限流:≤50路/100ms(硬件定时器精度)
return slot
该函数将任意节点心跳强制约束至离散时间片,使单核CPU在Linux CFS调度下实测吞吐达3278路/s(i7-11800H,DPDK用户态网卡驱动)。
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 单节点峰值QPS | 820 | 3200+ |
| P99延迟 | 42ms | 8.3ms |
| 内存占用/路 | 1.2KB | 0.14KB |
graph TD
A[原始心跳包] --> B{哈希分片}
B --> C[Slot 0-63]
C --> D[硬件时间戳校验]
D --> E[批量ACK压缩]
E --> F[状态机原子更新]
4.4 安防级日志审计与国标GB/T 28181信令兼容性验证
安防级日志审计需满足等保三级对操作留痕、不可篡改、最小权限访问的硬性要求,同时必须原生支持GB/T 28181-2016定义的SIP信令交互(如REGISTER、INVITE、MESSAGE)及心跳保活机制。
日志结构化采集示例
# 基于Syslog+JSON Schema的日志模板(含国标字段映射)
{
"event_id": "SIP_200_OK", # GB/T 28181信令事件类型
"device_id": "34020000001320000001", # 国标设备编码(20位)
"timestamp": "2024-06-15T08:23:41Z",
"sip_call_id": "abc123@192.168.1.100", # 信令唯一标识
"action": "stream_start", # 业务动作语义化
"level": "AUDIT_HIGH" # 审计等级(对应等保日志分级)
}
该结构确保每条日志可反向追溯至具体设备、信令会话及操作人员,device_id严格遵循国标编码规则(行政区划+行业类型+序列号),sip_call_id用于跨设备信令链路关联。
兼容性验证关键项
- ✅ SIP消息头字段校验(Via、From、To、Contact必含国标扩展参数)
- ✅ 心跳响应延迟 ≤ 30s(GB/T 28181 §7.3.2)
- ✅ 日志时间戳与NTP服务器误差
| 验证维度 | 国标条款 | 审计触发条件 |
|---|---|---|
| 设备注册失败 | §6.2.1 | 连续3次REGISTER超时 |
| 流媒体中断 | §6.3.4 | ACK未收到且无BYE |
| 时间同步异常 | §5.3.1 | NTP偏差 ≥ 500ms |
graph TD
A[GB/T 28181信令接入] --> B{SIP解析引擎}
B --> C[提取device_id/Call-ID]
C --> D[匹配审计策略库]
D --> E[生成结构化日志]
E --> F[写入区块链存证模块]
第五章:性能压测结果与工业部署经验总结
压测环境配置与工具链选型
我们基于 Kubernetes v1.28 集群(3 master + 6 worker,节点均配备 32C64G + NVMe SSD)开展全链路压测。核心工具链包括:k6 v0.47.0(协议级脚本编排)、Prometheus 2.45 + Grafana 10.1(实时指标采集)、Jaeger 1.39(分布式追踪)、以及自研的流量染色中间件(支持 header-based trace propagation)。压测前完成 JVM 参数调优(ZGC + -XX:MaxInlineLevel=15)及 gRPC KeepAlive 配置(time=30s, timeout=5s),确保连接复用率稳定在 92% 以上。
多维度压测数据对比
以下为关键服务在 5000 RPS 持续压测 30 分钟后的核心指标:
| 服务模块 | P95 延迟 (ms) | 错误率 | CPU 平均使用率 | GC 暂停时间 (ms) |
|---|---|---|---|---|
| 订单创建 API | 187 | 0.02% | 68% | 12.3 |
| 库存扣减服务 | 241 | 0.15% | 82% | 48.7 |
| 用户画像查询 | 93 | 0.00% | 41% | 3.1 |
值得注意的是,库存服务在第 18 分钟出现短暂毛刺(错误率跃升至 1.2%),经 Jaeger 追踪定位为 Redis 连接池耗尽(maxIdle=200 不足),扩容至 maxIdle=500 后问题消除。
生产灰度发布策略
采用“金丝雀+自动熔断”双轨机制:新版本仅对 5% 流量开放,同时注入 Envoy Sidecar 的故障注入规则(随机延迟 100–300ms + 0.5% 错误注入)。当 Prometheus 报警触发 rate(http_request_duration_seconds_bucket{job="api-gateway"}[5m]) > 0.05 时,自动执行 Istio VirtualService 权重回滚(从 5% → 0%)。某次上线中该机制在 42 秒内完成回滚,避免了订单超时雪崩。
硬件资源瓶颈识别
通过 eBPF 工具 bpftrace 实时捕获系统调用热点,发现库存服务存在高频 futex 竞争(每秒 12.7K 次),进一步分析 perf record -e 'sched:sched_switch' 数据后确认为本地缓存锁粒度过粗。将原先全局 ReentrantLock 替换为 ConcurrentHashMap 分段锁后,P95 延迟下降 37%,CPU 使用率降低 19%。
# 生产环境快速诊断命令(已固化为运维 SOP)
kubectl exec -it pod/inventory-7c8f9d4b5-2xq9z -- \
/usr/share/bcc/tools/opensnoop -d 5 | grep "stock\.json"
容器镜像体积优化实践
初始镜像大小为 1.24GB(基于 openjdk:17-jdk-slim),通过多阶段构建剥离调试符号、移除未使用 locale、启用 JLink 构建最小化 JDK(仅保留 java.base, java.logging, java.net.http),最终镜像压缩至 312MB。配合镜像分层缓存策略,CI 构建时间从 8.4 分钟缩短至 2.1 分钟,集群镜像拉取成功率从 92.3% 提升至 99.97%。
异步任务队列稳定性加固
RabbitMQ 集群在高并发下单消息堆积达 120 万条,消费端频繁触发 Channel shutdown due to connection error。根因分析发现客户端未正确处理 AMQP.CONNECTION_FORCED 事件,导致连接泄漏。修复后引入连接健康检查心跳(heartbeat=30)与 Channel 复用池(最大 200 个 channel),并配置死信队列 TTL 为 72 小时,保障异步履约任务最终一致性。
graph LR
A[Producer] -->|publish| B(RabbitMQ Exchange)
B --> C{Routing Key}
C --> D[Order Queue]
C --> E[Stock Queue]
D --> F[Consumer Pool]
E --> G[Consumer Pool]
F --> H[DB Write]
G --> I[Redis Decr]
H --> J[Success Ack]
I --> J
J --> K[Auto-Ack] 