Просмотр исходного кода

ingest: 04-网络编程与项目实战(5篇)

OpenCode 12 часов назад
Родитель
Сommit
1c11230eb0

+ 1080 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/04-网络编程与项目实战/01-网络基础与socket编程.md

@@ -0,0 +1,1080 @@
+---
+title: 网络基础与socket编程
+tags: [嵌入式Linux, Linux应用编程, 网络编程, socket, TCP, UDP, OSI, IP地址, 端口号, 字节序, htonl, inet_pton, IMX6ULL]
+created: 2026-09-18
+updated: 2026-09-18
+pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第二十九章 网络基础知识、第三十章 socket编程基础"
+---
+
+# 网络基础与socket编程
+
+> 💡 **关联知识**:[[04-网络编程与项目实战/02-CAN应用编程]]、[[04-网络编程与项目实战/03-CMake入门与进阶]]、[[04-网络编程与项目实战/04-实战项目MQTT与视频监控]];延伸阅读:[[Linux+C+C++技术体系梳理/2. Linux系统编程/14. socket网络编程基础]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/15. TCP编程]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/16. UDP编程]];**深入** → [[Linux+C+C++技术体系梳理/2. Linux系统编程/17. IO多路复用]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/18. 高并发模型]]
+
+网络通信本质上是**进程间通信(IPC)**,只不过通信双方位于不同主机上,因此又称 **socket IPC**。Linux 内核把复杂的 TCP/IP 协议族隐藏在 socket 接口之后,应用层按固定套路调用几个函数即可完成跨主机通信。本篇先补齐网络基础知识(模型、IP、端口、协议),再系统讲解 socket 编程 API,最后落到可编译运行的 TCP 服务端/客户端,并延伸 UDP 与并发服务器。
+
+---
+
+## 1. 网络通信概述
+
+网络通信分三个层次:
+
+| 层次 | 角色 | 说明 |
+| ---- | ---- | ---- |
+| 硬件层 | 网卡设备 | 收发网络数据的物理基础 |
+| 驱动层 | 网卡驱动 | 内核中的网卡驱动,向应用层提供 socket 接口 |
+| 应用层 | 应用程序 | 基于 socket 接口编程,或使用 http、网络控件等更高封装 |
+
+硬件上两台主机各有网卡,通过传输媒介相连(有线:双绞线、光纤;无线:WiFi、蓝牙、ZigBee、4G/5G)。内核驱动底层网卡并暴露 socket 接口;应用层调用 socket 接口即所谓网络编程。
+
+```mermaid
+flowchart TB
+    A["应用层:HTTP / MQTT / 自定义协议(调用 socket 接口)"] --> B["内核层:网卡驱动(提供 socket 接口)"]
+    B --> C["硬件层:网卡设备"]
+    C <-->|"传输媒介:网线 / 光纤 / 无线"| D["对端主机的网卡 / 内核 / 应用"]
+```
+
+---
+
+## 2. 网络互连模型
+
+### 2.1 OSI 七层模型
+
+OSI(Open System Interconnection)是国际标准化组织(ISO)制定的网络互连标准体系,从上到下依次为:
+
+| 层 | 名称 | 主要功能 | 典型协议 / 设备 |
+| -- | ---- | -------- | --------------- |
+| 7 | 应用层 | 为用户提供应用接口与网络服务 | HTTP、FTP、TFTP、SMTP、SNMP、DNS、TELNET、HTTPS、POP3、DHCP |
+| 6 | 表示层 | 数据编码/转换、压缩/解压缩、加密/解密 | — |
+| 5 | 会话层 | 建立、管理和终止通信会话 | — |
+| 4 | 传输层 | 定义端口号、端到端流控和差错校验 | TCP、UDP |
+| 3 | 网络层 | 逻辑地址寻址、路由选择(IP 层) | IP(IPv4/IPv6)、ICMP、IGMP |
+| 2 | 数据链路层 | 成帧、硬件地址寻址、差错检测(分 LLC / MAC 子层) | 以太网、MAC |
+| 1 | 物理层 | 为数据链路层提供物理连接,透明传输比特流 | 集线器、中继器、网线、双绞线、同轴电缆 |
+
+分层的意义:网络通信极其复杂,分层后每层只解决一类问题,层间通过接口协作,便于设计、实现与排错。
+
+### 2.2 TCP/IP 四层 / 五层模型
+
+TCP/IP 模型是 OSI 的简化版本:
+
+| OSI 七层 | TCP/IP 五层 | TCP/IP 四层 |
+| -------- | ----------- | ----------- |
+| 应用层、表示层、会话层 | 应用层 | 应用层 |
+| 传输层 | 传输层 | 传输层 |
+| 网络层 | 网络层 | 网络层 |
+| 数据链路层 + 物理层 | 数据链路层、物理层 | 网络接口层 |
+
+五层模型专为介绍网络原理而设计;四层模型(应用层、传输层、网络层、网络接口层)是实际工程所用。
+
+```mermaid
+flowchart LR
+    subgraph OSI["OSI 七层"]
+        A7["应用层"] --- A6["表示层"] --- A5["会话层"] --- A4["传输层"] --- A3["网络层"] --- A2["数据链路层"] --- A1["物理层"]
+    end
+    subgraph TCPIP["TCP/IP 四层"]
+        B4["应用层"] --- B3["传输层"] --- B2["网络层"] --- B1["网络接口层"]
+    end
+    A7 -.-> B4
+    A6 -.-> B4
+    A5 -.-> B4
+    A4 -.-> B3
+    A3 -.-> B2
+    A2 -.-> B1
+    A1 -.-> B1
+```
+
+### 2.3 数据的封装与拆封
+
+发送时数据自上层向下层交付,每层加一个首部(封装);接收时方向相反,逐层去除首部(拆封)。
+
+```mermaid
+flowchart LR
+    U["用户数据"] --> AP["应用层首部 + 用户数据"]
+    AP --> TP["TCP/UDP 首部 + 数据"]
+    TP --> NP["IP 首部 + 数据"]
+    NP --> DP["以太网首部 + 数据 + 尾部"]
+    DP --> PHY["网卡 → 物理链路上的电平信号"]
+```
+
+应用层(MQTT、HTTP)加应用层首部 → 传输层加 TCP/UDP 首部 → 网络层加 IP 首部 → 链路层加以太网首部后交网卡发到物理链路。接收方执行相反的拆封过程。
+
+---
+
+## 3. IP 地址
+
+Internet 上每个节点依靠唯一的 **IP 地址**相互区分。IP 是**软件地址**,标识主机身份;MAC 是**硬件地址**,存于网卡,用于局域网内寻址。
+
+- 传统 IPv4 地址是 **32 位二进制**,由 4 个 8 位字段组成,交互时用点分十进制表示,如 `192.168.1.1`;
+- IPv6 采用 128 位地址长度,由 8 个 16 位字段组成;
+- 32 位分为**网络地址**和**主机地址**两部分,由**子网掩码**确定各占多少位。
+
+### 3.1 IP 地址分类
+
+| 类别 | 第一字节范围 | 网络/主机地址 | 默认子网掩码 | 高位特征 | 用途 |
+| ---- | ------------ | ------------- | ------------ | -------- | ---- |
+| A | 1 ~ 126 | 1 字节网络 + 3 字节主机 | 255.0.0.0 | 最高位 0 | 特大网络 |
+| B | 128 ~ 191 | 2 字节网络 + 2 字节主机 | 255.255.0.0 | 最高位 10 | 中型网络 |
+| C | 192 ~ 223 | 3 字节网络 + 1 字节主机 | 255.255.255.0 | 最高位 110 | 小型网络、局域网 |
+| D | 224 ~ 239 | 不分网络/主机 | — | 高四位 1110 | 多播 Multicast |
+| E | 240 ~ 255 | 不分网络/主机 | — | 高五位 11110 | 保留,暂未使用 |
+
+三类的私有地址(公网不可用,仅局域网):A 类 `10.0.0.0 ~ 10.255.255.255`;B 类 `172.16.0.0 ~ 172.31.255.255`;C 类 `192.168.0.0 ~ 192.168.255.255`。另外 A 类 `127.X.X.X` 为环回保留地址,B 类 `169.254.X.X` 为自动获取 IP 失败时的保留地址。
+
+### 3.2 特殊 IP 地址
+
+| 名称 | 地址形式 | 说明 |
+| ---- | -------- | ---- |
+| 直接广播地址 | 主机号全 1,如 `192.168.0.255` | 向某网络内所有主机发报文 |
+| 受限广播地址 | `255.255.255.255`(32 位全 1) | 本网络内部广播,启动时不知网络号也可用 |
+| 多播地址 | D 类 `224.0.0.1 ~ 239.255.255.254` | 一对多通信,只能作目的地址 |
+| 环回地址 | `127.X.X.X` | 测试网络协议、本机进程通信;不能作主机 IP |
+| `0.0.0.0` | 32 位全 0 | 表示本网络上的本主机,只能作源地址;不能被 ping 通 |
+
+服务器中 `0.0.0.0`(`INADDR_ANY`)表示监听本机所有 IPv4 地址的该端口。
+
+### 3.3 判断两个 IP 是否同一网段
+
+**网络标识 = IP 地址 & 子网掩码**,网络标识相同即同一网络。
+
+```text
+192.168.1.50  & 255.255.255.0  =  192.168.1.0
+192.168.1.100 & 255.255.255.0  =  192.168.1.0   → 同一网段
+```
+
+---
+
+## 4. 端口号
+
+一台主机通常只有一个 IP,但会运行多个联网进程。IP 只能区分主机,无法区分进程,于是引入端口号:它是用来在主机中**唯一标识一个能上网进程**的数字编号,取值范围 **0 ~ 65535**,通过「IP + 端口号」区分主机内不同进程。
+
+| 服务 | 端口 | 说明 | 服务 | 端口 | 说明 |
+| ---- | ---- | ---- | ---- | ---- | ---- |
+| HTTP | 80 | 超文本传输协议 | SSH | 22 | 安全外壳协议 |
+| FTP | 21 | 文件传输协议 | Telnet | 23 | 终端远程登录 |
+| SMTP | 25 | 简单邮件传输协议 | POP3 | 110 | 邮局协议 v3 |
+| TFTP | 69 | 简单文件传输协议 | — | — | — |
+
+> 自编服务器端口不能与其它服务冲突,**不常用端口通常大于 5000**。
+
+---
+
+## 5. 字节序与转换函数
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。(原书仅在 `bind()` 示例中说明 `htons`/`htonl` 用于避免大小端问题。)
+
+| 概念 | 含义 |
+| ---- | ---- |
+| 大端序 Big-Endian | 低地址存放**高位**字节(网络字节序即大端) |
+| 小端序 Little-Endian | 低地址存放**低位**字节(x86、多数 ARM 的主机字节序) |
+| 网络字节序 | TCP/IP 规定的统一字节序,等同于大端序 |
+
+以 32 位数 `0x12345678` 为例,内存低地址到高地址:大端为 `12 34 56 78`,小端为 `78 56 34 12`。
+
+| 函数 | 含义 | 方向 | 宽度 |
+| ---- | ---- | ---- | ---- |
+| `htons` | host to network short | 主机 → 网络 | 16 位(端口) |
+| `htonl` | host to network long | 主机 → 网络 | 32 位(IP/整数) |
+| `ntohs` | network to host short | 网络 → 主机 | 16 位 |
+| `ntohl` | network to host long | 网络 → 主机 | 32 位 |
+
+使用要点:`sin_port` 必须用 `htons()`;`inet_pton()`/`inet_addr()` 得到的地址**已是网络字节序**,无需再 `htonl`;若手工给 `s_addr` 赋主机字节序整数则需 `htonl()`;`INADDR_ANY` 为 0,转换后仍为 0。
+
+```c
+server_addr.sin_port = htons(SERVER_PORT);        // 端口:主机序 → 网络序
+server_addr.sin_addr.s_addr = htonl(INADDR_ANY);  // 任意本地 IP
+```
+
+---
+
+## 6. TCP 协议
+
+TCP(Transmission Control Protocol,传输控制协议)是**面向连接的、可靠的、基于 IP 的**传输层协议:对上服务 socket 接口,对下调用 IP 层。
+
+### 6.1 核心特点
+
+| 特点 | 说明 |
+| ---- | ---- |
+| 面向连接 | 通信前必须三次握手建立连接 |
+| 确认与重传 | 每个报文段需对端 ACK;超时未收到应答则重传 |
+| 全双工 | 连接建立后数据双向独立流通 |
+| 基于字节流 | 按字节编号,接收端用 ACK 确认,保证有序完整 |
+| 流量控制 | 滑动窗口:接收端告知发送端自己的缓存大小 |
+| 差错控制 | 校验和检验有效性,丢弃重复、重组乱序、请求重传 |
+| 拥塞控制 | 慢启动、拥塞避免、快重传、快恢复 |
+
+**保证可靠传输的三个机制**:① 发送应答机制——每个报文段须得到接收方应答;② 超时重传机制——发送后启动定时器,超时未应答就重发;③ 重排整理机制——IP 数据报可能乱序、重复,TCP 重排去重后再交付应用层。
+
+> 流量控制是**接收方**控制流量;拥塞控制是**发送方**感知网络拥塞后控制流量,二者不同。
+
+### 6.2 TCP 报文格式
+
+TCP 报文 = TCP 首部 + 数据区,首部通常 **20 字节**。
+
+| 字段 | 长度 | 说明 |
+| ---- | ---- | ---- |
+| 源端口号 / 目标端口号 | 各 2 字节 | 标识收发端应用进程 |
+| 序号 Sequence Number | 4 字节 | 本报文段第一个数据字节的编号 |
+| 确认序号 Acknowledge Number | 4 字节 | 期望收到的下一个字节编号,仅 ACK=1 时有效 |
+| 首部长度 | 4 bit | 以 4 字节为单位,最大 15×4=60 字节 |
+| 保留位 | 6 bit | 目前置 0 |
+| 标志位 | 6 bit | URG / ACK / PSH / RST / SYN / FIN |
+| 窗口大小 | 2 字节 | 接收窗口大小,用于流量控制 |
+| 校验和 | 2 字节 | 覆盖首部与数据,强制字段 |
+| 紧急指针 | 2 字节 | 紧急数据最后一个字节的序号 |
+| 选项 | 最多 40 字节 | 可选 |
+
+| 标志 | 置位含义 |
+| ---- | -------- |
+| URG | 紧急指针字段有效 |
+| ACK | 确认序号字段有效 |
+| PSH | 接收方应尽快把本报文段交给应用层 |
+| RST | 连接出现错误,必须释放连接后重建 |
+| SYN | 请求建立连接 |
+| FIN | 发送方无数据要发送,要求释放连接 |
+
+> 小写 **ack** 指首部中的「确认序号」,大写 **ACK** 指标志位,二者不同。
+
+### 6.3 建立连接:三次握手
+
+```mermaid
+sequenceDiagram
+    participant C as 客户端 Client
+    participant S as 服务器 Server
+    Note over C,S: 初始:C=CLOSED, S=LISTEN
+    C->>S: 第一次握手:SYN=1, seq=J(请求连接)
+    Note left of C: 进入 SYN_SENT
+    S->>C: 第二次握手:SYN=1, ACK=1, ack=J+1, seq=K
+    Note right of S: 进入 SYN_RCVD
+    C->>S: 第三次握手:ACK=1, ack=K+1
+    Note over C,S: 双方进入 ESTABLISHED,连接建立成功
+```
+
+| 握手 | 发送方 | 内容 | 状态变化 |
+| ---- | ------ | ---- | -------- |
+| 第一次 | 客户端 | `SYN=1, seq=J` | 客户端 → SYN_SENT |
+| 第二次 | 服务器 | `SYN=1, ACK=1, ack=J+1, seq=K` | 服务器 → SYN_RCVD |
+| 第三次 | 客户端 | `ACK=1, ack=K+1` | 双方 → ESTABLISHED |
+
+握手完成后双方都获得彼此的窗口大小、序列号;传输阶段 SYN 为 0(只用于发起连接、同步序号)。**为什么需要三次握手**:类比打电话——A 问「你能听到我吗?」,B 答「我听到了,你能听到我吗?」,A 再答「我能听到」。三次互相确认后,双方才确信「对方能听到自己」,才能开始可靠通信。
+
+### 6.4 关闭连接:四次挥手
+
+TCP 全双工,每个方向必须单独关闭,首次发起关闭的一方为**主动关闭**,另一方为**被动关闭**,由任一方调用 `close()` 触发。
+
+```mermaid
+sequenceDiagram
+    participant C as 客户端(主动关闭)
+    participant S as 服务器(被动关闭)
+    Note over C,S: 初始:双方 ESTABLISHED
+    C->>S: 第一次挥手:FIN=1, seq=u
+    Note left of C: 进入 FIN_WAIT_1
+    S->>C: 第二次挥手:ACK=1, ack=u+1
+    Note right of S: 进入 CLOSE_WAIT
+    Note left of C: 进入 FIN_WAIT_2
+    S->>C: 第三次挥手:FIN=1, seq=v
+    Note right of S: 进入 LAST_ACK
+    C->>S: 第四次挥手:ACK=1, ack=v+1
+    Note left of C: 进入 TIME_WAIT,等待 2MSL
+    Note over C,S: 服务器收到 ACK 后 CLOSED;客户端 2MSL 后 CLOSED
+```
+
+| 挥手 | 发送方 | 内容 | 状态变化 |
+| ---- | ------ | ---- | -------- |
+| 第一次 | 主动方 | `FIN=1` | 主动方 → FIN_WAIT_1 |
+| 第二次 | 被动方 | `ACK=1, ack=+1` | 被动方 → CLOSE_WAIT,主动方 → FIN_WAIT_2 |
+| 第三次 | 被动方 | `FIN=1` | 被动方 → LAST_ACK |
+| 第四次 | 主动方 | `ACK=1, ack=+1` | 主动方 → TIME_WAIT,等待 2MSL 后 CLOSED |
+
+---
+
+## 7. UDP 协议
+
+UDP(User Datagram Protocol,用户数据报协议)是**无连接、不可靠**的传输层协议。发送端把上层数据加 UDP 首部、简单校验后交给 IP 层;接收端简单校验,出错即丢弃且不通知源主机。到达顺序无法预知,需应用层自行排序。
+
+| 特点 | 说明 |
+| ---- | ---- |
+| 无连接、不可靠 | 不需握手,报文可能丢失、乱序、重复 |
+| 无反馈 | 出现差错直接丢弃,不产生应答 |
+| 面向报文 | 直接添加 UDP 首部,不拆分合并 |
+| 速度快 | 无握手、确认、窗口、重传、拥塞控制 |
+
+UDP 适用于**实时性要求高、可容忍少量差错**的场景,如直播、网络电话、实时视频。
+
+| 维度 | TCP | UDP |
+| ---- | --- | --- |
+| 连接 | 面向连接(三次握手) | 无连接 |
+| 可靠性 | 可靠(确认、重传、排序) | 不可靠 |
+| 流量/拥塞控制 | 有 | 无 |
+| 传输单位 | 字节流 | 数据报 |
+| 速度 | 较慢 | 快 |
+| 典型应用 | 文件传输、Web、SSH | 直播、VoIP、DNS、广播/多播 |
+
+---
+
+## 8. socket 简介
+
+**套接字(socket)** 是 Linux 下的进程间通信机制,可用于不同主机的应用程序间通信,也可用于同一主机的不同应用,通常采用**客户端 ↔ 服务器**模式。socket 是应用层与 TCP/IP 协议通信的中间软件抽象层,在设计模式中是一个**门面模式**:把复杂的 TCP/IP 协议隐藏在接口之后,用户按 socket 规定编程即可。socket 也是标准(**BSD socket**),可移植到任何实现该标准的平台,如 **LwIP**、**Windows**,甚至 RT-Thread 等 RTOS。
+
+---
+
+## 9. socket 编程 API
+
+需包含:
+
+```c
+#include <sys/types.h>
+#include <sys/socket.h>
+```
+
+### 9.1 socket():创建套接字
+
+```c
+int socket(int domain, int type, int protocol);
+```
+
+类似 `open()`,创建一个网络通信端点。成功返回**网络文件描述符**(socket 描述符),失败返回 -1 并设置 `errno`。
+
+常用协议族 `domain`:
+
+| domain | 说明 |
+| ------ | ---- |
+| `AF_UNIX`/`AF_LOCAL` | 本地通信 |
+| `AF_INET` | IPv4 |
+| `AF_INET6` | IPv6 |
+| `AF_NETLINK` | 内核与用户空间接口 |
+| `AF_PACKET` | 底层数据包接口 |
+| `AF_ALG` | 内核加密 API 接口 |
+
+套接字类型 `type`:
+
+| type | 说明 |
+| ---- | ---- |
+| `SOCK_STREAM` | 有序可靠双向、基于连接的字节流,用于 TCP |
+| `SOCK_DGRAM` | 固定长度、无连接、不可靠报文,用于 UDP |
+| `SOCK_SEQPACKET` | 固定长度、有序、可靠、面向连接的报文 |
+| `SOCK_RAW` | 原始套接字,可访问网络层原始数据包 |
+| `SOCK_RDM` | 不保证排序的可靠数据报层 |
+| `SOCK_PACKET` | 已过时,不应使用 |
+
+`protocol` 通常为 0,表示使用默认协议(`AF_INET`+`SOCK_STREAM` 默认 TCP,`AF_INET`+`SOCK_DGRAM` 默认 UDP)。
+
+```c
+int sockfd = socket(AF_INET, SOCK_STREAM, 0);
+if (0 > sockfd) {
+    perror("socket error");
+    exit(-1);
+}
+close(sockfd);
+```
+
+### 9.2 bind():绑定地址与端口
+
+```c
+int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
+```
+
+把 IP / 端口与套接字绑定。服务器通常绑定到众所周知的地址;客户端一般让内核自动选地址,可不调用 `bind()`。
+
+```c
+struct sockaddr {
+    sa_family_t sa_family;
+    char        sa_data[14];   /* IP + 端口,用户难以直接赋值 */
+};
+struct sockaddr_in {
+    sa_family_t    sin_family;  /* 协议族 */
+    in_port_t      sin_port;    /* 端口号,需 htons() */
+    struct in_addr sin_addr;    /* IP 地址 */
+    unsigned char  sin_zero[8]; /* 保留未用 */
+};
+```
+
+两者空间大小一致,可相互强制转换,实际编程用 `sockaddr_in`:
+
+```c
+struct sockaddr_in socket_addr;
+memset(&socket_addr, 0x0, sizeof(socket_addr));
+socket_addr.sin_family = AF_INET;
+socket_addr.sin_addr.s_addr = htonl(INADDR_ANY);
+socket_addr.sin_port = htons(5555);
+bind(sockfd, (struct sockaddr *)&socket_addr, sizeof(socket_addr));
+```
+
+### 9.3 listen():进入监听状态
+
+```c
+int listen(int sockfd, int backlog);
+```
+
+只能用于服务器,一般在 `bind()` 之后、`accept()` 之前调用,不能在已连接的套接字上执行。`backlog` 为等待连接队列的最大长度;请求过多时内核把连接放入队列按先来后到处理,队列满时新请求可能被拒绝。
+
+### 9.4 accept():接受连接
+
+```c
+int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
+```
+
+服务器进入监听后用 `accept()` 获取客户端连接并建立连接。
+
+- 若无等待请求,`accept()` **阻塞**直到有客户端连接;
+- 返回一个**全新的连接套接字**,它连接调用 `connect()` 的客户端,服务器用它与该客户端通信;
+- 原 `sockfd` 继续监听;
+- `addr` 为传出参数,返回客户端 IP 与端口,不关心可传 `NULL`;
+- 出错返回 -1 并设置 `errno`。
+
+服务器流程:`socket()` → `bind()` → `listen()` → `accept()` → `recv/send` → `close()`。
+
+### 9.5 connect():客户端发起连接
+
+```c
+int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
+```
+
+用于客户端连接远程服务器。对 **TCP** 会发生三次握手并最终建立连接;对 **UDP** 只是在 `sockfd` 中记录服务器 IP 与端口,不发送数据。成功返回 0,失败返回 -1 并设置 `errno`。
+
+### 9.6 发送与接收函数
+
+```c
+ssize_t recv(int sockfd, void *buf, size_t len, int flags);
+ssize_t send(int sockfd, const void *buf, size_t len, int flags);
+```
+
+`recv` 标志:
+
+| recv 标志 | 描述 |
+| --------- | ---- |
+| `MSG_CMSG_CLOEXEC` | 为 UNIX 域套接字接收的文件描述符设置执行时关闭标志 |
+| `MSG_DONTWAIT` | 非阻塞操作(相当于 `O_NONBLOCK`) |
+| `MSG_ERRQUEUE` | 接收错误信息作为辅助数据 |
+| `MSG_OOB` | 如果协议支持,获取带外数据 |
+| `MSG_PEEK` | 返回数据包内容但不真正取走 |
+| `MSG_TRUNC` | 即使数据包被截断也返回其长度 |
+| `MSG_WAITALL` | 等待所有数据可用(仅 `SOCK_STREAM`) |
+
+`send` 标志:
+
+| send 标志 | 描述 |
+| --------- | ---- |
+| `MSG_CONFIRM` | 提供链路层反馈以保持地址映射有效 |
+| `MSG_DONTROUTE` | 勿将数据包路由出本地网络 |
+| `MSG_DONTWAIT` | 允许非阻塞操作 |
+| `MSG_EOR` | 如果协议支持,标志记录结束 |
+| `MSG_MORE` | 延迟发送数据包,允许写更多数据 |
+| `MSG_NOSIGNAL` | 写无连接套接字时不产生 `SIGPIPE` |
+| `MSG_OOB` | 如果协议支持,发送带外数据 |
+
+要点:通常 `flags` 为 0;对 `SOCK_STREAM` 一次 `recv` 可能少于请求字节数,`MSG_WAITALL` 可阻止;对 `SOCK_DGRAM`/`SOCK_SEQPACKET` 一次读取返回整个报文;发送者关闭后数据收完 `recv` 返回 0;`send()` 成功不代表对方进程已收到,只保证数据无错误地发到网络驱动。
+
+### 9.7 close():关闭套接字
+
+不再需要套接字描述符时调用 `close()` 关闭并释放资源。
+
+### 9.8 IP 地址格式转换函数
+
+需包含 `<sys/socket.h>`、`<arpa/inet.h>`、`<netinet/in.h>`。
+
+| 函数 | 作用 | 说明 |
+| ---- | ---- | ---- |
+| `inet_aton` | 字符串 → 二进制 | 已废弃,旧代码可见 |
+| `inet_addr` | 字符串 → 二进制 | 已废弃 |
+| `inet_ntoa` | 二进制 → 字符串 | 已废弃 |
+| `inet_pton` | 字符串 → 二进制 | 支持 IPv4/IPv6,推荐 |
+| `inet_ntop` | 二进制 → 字符串 | 支持 IPv4/IPv6,推荐 |
+
+```c
+int inet_pton(int af, const char *src, void *dst);
+const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
+```
+
+- `inet_pton`:`af` 为 `AF_INET` 时 `dst` 指向 `struct in_addr`,为 `AF_INET6` 时指向 `struct in6_addr`;成功返回 1,字符串无效返回 0,地址族无效返回 -1 且 `errno=EAFNOSUPPORT`。
+- `inet_ntop`:把二进制地址转为点分十进制字符串存入 `dst`,`size` 为缓冲区大小;成功返回 `dst`,缓冲区太小返回 `NULL` 且 `errno=ENOSPC`。
+
+```c
+struct in_addr addr;
+char buf[20] = {0};
+inet_pton(AF_INET, "192.168.1.222", &addr);   // 字符串 → 二进制(网络字节序)
+inet_ntop(AF_INET, &addr, buf, sizeof(buf));  // 二进制 → 字符串
+```
+
+---
+
+## 10. socket 通信流程总览
+
+```mermaid
+flowchart TB
+    subgraph S["服务端 Server"]
+        S1["socket() 创建监听套接字"] --> S2["bind() 绑定 IP + 端口"]
+        S2 --> S3["listen() 进入监听,等待连接"]
+        S3 --> S4["accept() 阻塞等待客户端"]
+        S4 --> S5["recv()/send() 用新套接字收发数据"]
+        S5 --> S6["close() 关闭"]
+    end
+    subgraph C["客户端 Client"]
+        C1["socket() 创建套接字"] --> C2["connect() 发起连接"]
+        C2 --> C3["send()/recv() 收发数据"]
+        C3 --> C4["close() 关闭"]
+    end
+    C2 -. "三次握手" .-> S3
+    S4 -. "返回新套接字 connfd" .-> C3
+```
+
+---
+
+## 11. TCP 编程实战:服务器与客户端
+
+### 11.1 服务器程序(socket_server.c)
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#define SERVER_PORT     8888    // 端口号不能发生冲突,不常用的端口号通常大于 5000
+int main(void)
+{
+    struct sockaddr_in server_addr = {0};
+    struct sockaddr_in client_addr = {0};
+    char ip_str[20] = {0};
+    int sockfd, connfd;
+    int addrlen = sizeof(client_addr);
+    char recvbuf[512];
+    int ret;
+    /* 打开套接字,得到套接字描述符 */
+    sockfd = socket(AF_INET, SOCK_STREAM, 0);
+    if (0 > sockfd) {
+        perror("socket error");
+        exit(EXIT_FAILURE);
+    }
+    /* 将套接字与指定端口号进行绑定 */
+    server_addr.sin_family = AF_INET;
+    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
+    server_addr.sin_port = htons(SERVER_PORT);
+    ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
+    if (0 > ret) {
+        perror("bind error");
+        close(sockfd);
+        exit(EXIT_FAILURE);
+    }
+    /* 使服务器进入监听状态 */
+    ret = listen(sockfd, 50);
+    if (0 > ret) {
+        perror("listen error");
+        close(sockfd);
+        exit(EXIT_FAILURE);
+    }
+    /* 阻塞等待客户端连接 */
+    connfd = accept(sockfd, (struct sockaddr *)&client_addr, &addrlen);
+    if (0 > connfd) {
+        perror("accept error");
+        close(sockfd);
+        exit(EXIT_FAILURE);
+    }
+    printf("有客户端接入...\n");
+    inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip_str, sizeof(ip_str));
+    printf("客户端主机的IP地址: %s\n", ip_str);
+    printf("客户端进程的端口号: %d\n", client_addr.sin_port);
+    /* 接收客户端发送过来的数据 */
+    for ( ; ; ) {
+        // 接收缓冲区清零
+        memset(recvbuf, 0x0, sizeof(recvbuf));
+        // 读数据
+        ret = recv(connfd, recvbuf, sizeof(recvbuf), 0);
+        if (0 >= ret) {
+            perror("recv error");
+            close(connfd);
+            break;
+        }
+        // 将读取到的数据以字符串形式打印出来
+        printf("from client: %s\n", recvbuf);
+        // 如果读取到"exit"则关闭套接字退出程序
+        if (0 == strncmp("exit", recvbuf, 4)) {
+            printf("server exit...\n");
+            close(connfd);
+            break;
+        }
+    }
+    /* 关闭套接字 */
+    close(sockfd);
+    exit(EXIT_SUCCESS);
+}
+```
+
+| 代码段 | 作用 |
+| ------ | ---- |
+| `socket(AF_INET, SOCK_STREAM, 0)` | 创建 IPv4 TCP 套接字,返回监听套接字 `sockfd` |
+| `sin_family = AF_INET` | 协议族设为 IPv4 |
+| `sin_addr.s_addr = htonl(INADDR_ANY)` | 绑定本机所有 IPv4 地址 |
+| `sin_port = htons(8888)` | 绑定端口 8888,注意字节序转换 |
+| `bind()` | 把套接字与地址、端口关联 |
+| `listen(sockfd, 50)` | 进入监听状态,等待队列上限 50 |
+| `accept()` | 阻塞等待客户端,返回代表该连接的 `connfd` |
+| `inet_ntop()` | 把客户端 IP 从二进制转为点分十进制字符串 |
+| `recv()` 循环 | 不断读取客户端数据;返回 ≤0 表示连接关闭或出错 |
+| `strncmp("exit", recvbuf, 4)` | 收到 `exit` 时结束服务 |
+| `close(connfd)` / `close(sockfd)` | 关闭连接套接字与监听套接字 |
+
+> 服务器用 `%d` 直接打印 `client_addr.sin_port` 时得到的仍是**网络字节序**,要显示正确端口应先 `ntohs()`。
+
+### 11.2 客户端程序(socket_client.c)
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#define SERVER_PORT     8888            // 服务器的端口号
+#define SERVER_IP       "192.168.1.150" // 服务器的IP地址
+int main(void)
+{
+    struct sockaddr_in server_addr = {0};
+    char buf[512];
+    int sockfd;
+    int ret;
+    /* 打开套接字,得到套接字描述符 */
+    sockfd = socket(AF_INET, SOCK_STREAM, 0);
+    if (0 > sockfd) {
+        perror("socket error");
+        exit(EXIT_FAILURE);
+    }
+    /* 调用connect连接远端服务器 */
+    server_addr.sin_family = AF_INET;
+    server_addr.sin_port = htons(SERVER_PORT);  // 端口号
+    inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr); // IP地址
+    ret = connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
+    if (0 > ret) {
+        perror("connect error");
+        close(sockfd);
+        exit(EXIT_FAILURE);
+    }
+    printf("服务器连接成功...\n\n");
+    /* 向服务器发送数据 */
+    for ( ; ; ) {
+        // 清理缓冲区
+        memset(buf, 0x0, sizeof(buf));
+        // 接收用户输入的字符串数据
+        printf("Please enter a string: ");
+        fgets(buf, sizeof(buf), stdin);
+        // 将用户输入的数据发送给服务器
+        ret = send(sockfd, buf, strlen(buf), 0);
+        if (0 > ret) {
+            perror("send error");
+            break;
+        }
+        // 输入了"exit",退出循环
+        if (0 == strncmp(buf, "exit", 4))
+            break;
+    }
+    close(sockfd);
+    exit(EXIT_SUCCESS);
+}
+```
+
+| 代码段 | 作用 |
+| ------ | ---- |
+| `socket(AF_INET, SOCK_STREAM, 0)` | 创建 TCP 套接字 |
+| `sin_port = htons(SERVER_PORT)` | 服务器端口,需网络字节序 |
+| `inet_pton(AF_INET, SERVER_IP, ...)` | 把点分十进制字符串转为二进制地址 |
+| `connect()` | 发起三次握手连接服务器 |
+| `fgets()` + `send()` | 读取用户输入并发送给服务器 |
+| `strncmp("exit", buf, 4)` | 输入 `exit` 时结束 |
+
+> `SERVER_IP` 需按实际修改;服务器绑定 8888 端口,客户端也必须用 8888。
+
+### 11.3 交叉编译与实验步骤
+
+把服务器跑在开发板、客户端跑在 Ubuntu:
+
+```bash
+# 交叉编译服务器(Ubuntu 上执行)
+arm-linux-gnueabihf-gcc -o server socket_server.c
+# 客户端用本机 gcc 编译
+gcc -o client socket_client.c
+# 把服务器可执行文件拷贝到开发板
+scp server root@192.168.1.150:/home/root/
+# 开发板上先运行服务器
+./server
+# Ubuntu 上运行客户端,输入字符串测试
+./client
+```
+
+运行结果:客户端打印 `服务器连接成功...`;服务器打印 `有客户端接入...` 及客户端 IP、端口;客户端输入字符串回车后服务器打印 `from client: ...`;输入 `exit` 双方退出。
+
+---
+
+## 12. UDP 编程与广播、组播
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。(原书第三十章只给出 TCP 服务端/客户端例程。)
+
+UDP 不需 `listen()`/`accept()`/`connect()`,用 `recvfrom()`/`sendto()` 收发:
+
+```c
+ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
+                 struct sockaddr *src_addr, socklen_t *addrlen);
+ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
+               const struct sockaddr *dest_addr, socklen_t addrlen);
+```
+
+UDP 服务器:
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#define SERVER_PORT     8888
+int main(void)
+{
+    struct sockaddr_in server_addr = {0};
+    struct sockaddr_in client_addr = {0};
+    socklen_t addrlen = sizeof(client_addr);
+    char buf[512];
+    int sockfd, ret;
+    sockfd = socket(AF_INET, SOCK_DGRAM, 0);   // UDP:SOCK_DGRAM
+    if (0 > sockfd) {
+        perror("socket error");
+        exit(EXIT_FAILURE);
+    }
+    server_addr.sin_family = AF_INET;
+    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
+    server_addr.sin_port = htons(SERVER_PORT);
+    ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
+    if (0 > ret) {
+        perror("bind error");
+        close(sockfd);
+        exit(EXIT_FAILURE);
+    }
+    for ( ; ; ) {
+        memset(buf, 0x0, sizeof(buf));
+        ret = recvfrom(sockfd, buf, sizeof(buf), 0,
+                       (struct sockaddr *)&client_addr, &addrlen);
+        if (0 > ret) {
+            perror("recvfrom error");
+            break;
+        }
+        printf("from client: %s\n", buf);
+    }
+    close(sockfd);
+    exit(EXIT_SUCCESS);
+}
+```
+
+UDP 客户端:
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#define SERVER_PORT     8888
+#define SERVER_IP       "192.168.1.150"
+int main(void)
+{
+    struct sockaddr_in server_addr = {0};
+    char buf[512];
+    int sockfd, ret;
+    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
+    if (0 > sockfd) {
+        perror("socket error");
+        exit(EXIT_FAILURE);
+    }
+    server_addr.sin_family = AF_INET;
+    server_addr.sin_port = htons(SERVER_PORT);
+    inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);
+    for ( ; ; ) {
+        memset(buf, 0x0, sizeof(buf));
+        printf("Please enter a string: ");
+        fgets(buf, sizeof(buf), stdin);
+        ret = sendto(sockfd, buf, strlen(buf), 0,
+                     (struct sockaddr *)&server_addr, sizeof(server_addr));
+        if (0 > ret) {
+            perror("sendto error");
+            break;
+        }
+        if (0 == strncmp(buf, "exit", 4))
+            break;
+    }
+    close(sockfd);
+    exit(EXIT_SUCCESS);
+}
+```
+
+### 12.1 广播
+
+目的地址为主机号全 1 的直接广播地址(如 `192.168.0.255`)或 `255.255.255.255`。发送前必须打开 `SO_BROADCAST`:
+
+```c
+int opt = 1;
+setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &opt, sizeof(opt));
+server_addr.sin_addr.s_addr = htonl(INADDR_BROADCAST);   // 255.255.255.255
+sendto(sockfd, buf, len, 0, (struct sockaddr *)&server_addr, sizeof(server_addr));
+```
+
+### 12.2 组播(多播)
+
+组播地址属 D 类(`224.0.0.1 ~ 239.255.255.254`)。接收方先加入组播组:
+
+```c
+struct ip_mreq mreq;
+inet_pton(AF_INET, "239.0.0.1", &mreq.imr_multiaddr);  // 组播组地址
+mreq.imr_interface.s_addr = htonl(INADDR_ANY);         // 本地任意网卡
+setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
+```
+
+发送方把目的地址设为组播地址后用 `sendto()` 发送即可;组播地址只能作目的地址。
+
+---
+
+## 13. 多客户端并发服务器
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+### 13.1 fork 版并发服务器
+
+每接受一个连接就 `fork()` 一个子进程专门处理,父进程继续 `accept()`。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <signal.h>
+#include <sys/wait.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#define SERVER_PORT     8888
+static void sigchld_handler(int sig)
+{
+    (void)sig;
+    while (0 < waitpid(-1, NULL, WNOHANG))   // 回收所有已终止子进程,避免僵尸
+        ;
+}
+static void handle_client(int connfd)
+{
+    char buf[512];
+    int ret;
+    for ( ; ; ) {
+        memset(buf, 0x0, sizeof(buf));
+        ret = recv(connfd, buf, sizeof(buf), 0);
+        if (0 >= ret)
+            break;
+        printf("from client: %s\n", buf);
+        if (0 == strncmp("exit", buf, 4))
+            break;
+    }
+    close(connfd);
+}
+int main(void)
+{
+    struct sockaddr_in server_addr = {0};
+    struct sockaddr_in client_addr = {0};
+    socklen_t addrlen = sizeof(client_addr);
+    int sockfd, connfd, ret;
+    pid_t pid;
+    signal(SIGCHLD, sigchld_handler);
+    sockfd = socket(AF_INET, SOCK_STREAM, 0);
+    if (0 > sockfd) {
+        perror("socket error");
+        exit(EXIT_FAILURE);
+    }
+    server_addr.sin_family = AF_INET;
+    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
+    server_addr.sin_port = htons(SERVER_PORT);
+    ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
+    if (0 > ret) {
+        perror("bind error");
+        close(sockfd);
+        exit(EXIT_FAILURE);
+    }
+    listen(sockfd, 50);
+    for ( ; ; ) {
+        connfd = accept(sockfd, (struct sockaddr *)&client_addr, &addrlen);
+        if (0 > connfd) {
+            perror("accept error");
+            continue;
+        }
+        pid = fork();
+        if (0 > pid) {
+            perror("fork error");
+            close(connfd);
+            continue;
+        }
+        if (0 == pid) {        // 子进程:处理客户端
+            close(sockfd);     // 子进程不用监听套接字
+            handle_client(connfd);
+            exit(0);
+        }
+        close(connfd);         // 父进程关闭连接套接字,继续 accept
+    }
+    close(sockfd);
+    exit(EXIT_SUCCESS);
+}
+```
+
+### 13.2 select 版并发服务器
+
+`select()` 在单进程内同时监控监听套接字与所有客户端套接字:
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/select.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#define SERVER_PORT     8888
+#define MAX_CLIENTS     64
+int main(void)
+{
+    int clientfds[MAX_CLIENTS];
+    int maxfd, i, nready, sockfd, connfd;
+    fd_set rset, allset;
+    char buf[512];
+    struct sockaddr_in addr = {0};
+    for (i = 0; i < MAX_CLIENTS; i++)
+        clientfds[i] = -1;
+    sockfd = socket(AF_INET, SOCK_STREAM, 0);
+    addr.sin_family = AF_INET;
+    addr.sin_addr.s_addr = htonl(INADDR_ANY);
+    addr.sin_port = htons(SERVER_PORT);
+    bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
+    listen(sockfd, 50);
+    FD_ZERO(&allset);
+    FD_SET(sockfd, &allset);
+    maxfd = sockfd;
+    for ( ; ; ) {
+        rset = allset;                     // 每轮重新赋值:select 会修改集合
+        nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
+        if (0 > nready)
+            continue;
+        if (FD_ISSET(sockfd, &rset)) {     // 有新连接
+            connfd = accept(sockfd, NULL, NULL);
+            for (i = 0; i < MAX_CLIENTS; i++) {
+                if (0 > clientfds[i]) {
+                    clientfds[i] = connfd;
+                    break;
+                }
+            }
+            FD_SET(connfd, &allset);
+            if (connfd > maxfd)
+                maxfd = connfd;
+        }
+        for (i = 0; i < MAX_CLIENTS; i++) { // 已有客户端可读
+            if (0 > clientfds[i] || !FD_ISSET(clientfds[i], &rset))
+                continue;
+            memset(buf, 0x0, sizeof(buf));
+            if (0 >= recv(clientfds[i], buf, sizeof(buf), 0)) {
+                close(clientfds[i]);
+                FD_CLR(clientfds[i], &allset);
+                clientfds[i] = -1;
+            } else {
+                printf("from client: %s\n", buf);
+            }
+        }
+    }
+    close(sockfd);
+    return 0;
+}
+```
+
+> `select()` 有描述符数量上限(通常 1024),高并发场景应用 `epoll`。详见 [[Linux+C+C++技术体系梳理/2. Linux系统编程/17. IO多路复用]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/18. 高并发模型]]。
+
+---
+
+## 14. socket 选项:SO_REUSEADDR
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+服务器重启时若前一进程仍处于 `TIME_WAIT`,`bind()` 常报 `Address already in use`。设置 `SO_REUSEADDR` 可立即复用该地址(应在 `bind()` 之前调用):
+
+```c
+int opt = 1;
+setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
+```
+
+---
+
+## 15. 网络调试工具
+
+| 工具 | 用途 | 示例 |
+| ---- | ---- | ---- |
+| `ifconfig` | 查看/配置网络接口 IP、状态 | `ifconfig eth0` |
+| `ip addr` | 查看/配置地址(`ifconfig` 的现代替代) | `ip addr show eth0` |
+| `ip link` | 查看/配置链路层(启用、关闭接口) | `ip link set eth0 up` |
+| `ping` | 测试与目标主机连通性 | `ping 192.168.1.150`、`ping 127.0.0.1` |
+| `netstat` | 查看 TCP/UDP 连接、监听状态、路由 | `netstat -anp`、`netstat -tlnp` |
+| `ss` | 更现代的 socket 统计工具 | `ss -tlnp` |
+| `tcpdump` | 抓包分析网络流量 | `tcpdump -i eth0 port 8888` |
+
+`ping 127.0.0.1` 可测试本机 TCP/IP 协议栈是否正常;用 `netstat -an` 可在握手/挥手过程中观察 `SYN_SENT`、`ESTABLISHED`、`FIN_WAIT_2`、`CLOSE_WAIT`、`TIME_WAIT` 等状态。
+
+---
+
+## 16. 跨平台对比:Linux socket vs 其它平台
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+| 维度 | Linux(本教程) | Windows Winsock | RT-Thread / LwIP |
+| ---- | --------------- | --------------- | ---------------- |
+| 头文件 | `<sys/socket.h>`、`<netinet/in.h>` | `<winsock2.h>` | `<sys/socket.h>` |
+| 初始化 | 无需 | 需 `WSAStartup()` | 无需 |
+| 关闭 | `close()` | `closesocket()` | `close()` |
+| 错误码 | `errno` | `WSAGetLastError()` | `errno` |
+| 接口风格 | BSD socket | BSD socket(略有差异) | BSD socket(兼容) |
+| 非阻塞 | `fcntl(O_NONBLOCK)` | `ioctlsocket(FIONBIO)` | `fcntl` |
+
+BSD socket 是事实标准,跨平台移植时主要差异集中在初始化、关闭函数与错误码获取方式上。
+
+---
+
+## 17. 面试精选
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+### Q1:TCP 为什么需要三次握手?两次握手会有什么问题?
+
+**答**:三次握手让通信双方都确认「对方能收到并理解自己的报文」,同时交换初始序列号与窗口大小。若只握手两次,服务器无法确认客户端已收到自己的 SYN+ACK;此外网络中滞留的旧连接请求可能在延迟后到达服务器,导致服务器单方面建立无效连接、浪费资源。三次握手使双方都进入 `ESTABLISHED` 后才开始传输数据,保证可靠。
+
+### Q2:TCP 与 UDP 的区别是什么?分别适用于哪些场景?
+
+**答**:TCP 面向连接、可靠、基于字节流,有确认重传、流量控制、拥塞控制,速度较慢;UDP 无连接、不可靠、面向报文,没有这些控制机制,速度快、实时性好。TCP 适用于文件传输、Web、远程登录等要求数据完整可靠的场景;UDP 适用于直播、网络电话、实时视频、DNS 等对实时性要求高、可容忍少量丢包的场景。
+
+### Q3:服务器端 `accept()` 返回的套接字和 `socket()` 返回的套接字有什么区别?
+
+**答**:`socket()` 返回的是**监听套接字**,用于绑定地址、监听连接,在服务器整个生命周期内存在;`accept()` 返回的是**新的连接套接字**,代表服务器与某个具体客户端之间的一条 TCP 连接,专门用于与该客户端收发数据。服务器保留监听套接字继续 `accept()`,而用每个连接套接字分别服务对应客户端。
+
+### Q4:为什么 `bind()` 时端口号要用 `htons()`,而 IP 地址用 `inet_pton()` 后不用再 `htonl()`?
+
+**答**:不同主机字节序可能不同,网络传输统一使用大端序。端口号是主机字节序的整数,必须用 `htons()` 转成网络字节序;而 `inet_pton()`/`inet_addr()` 把点分十进制字符串转成二进制时,输出的 `struct in_addr` 已经是网络字节序,无需再 `htonl()`。若手工给 `s_addr` 赋主机字节序整数,则必须 `htonl()`。
+
+### Q5:TCP 四次挥手为什么是四次?`TIME_WAIT` 状态的作用是什么?
+
+**答**:TCP 全双工,每个方向必须单独关闭。主动方发 FIN 关闭自己的发送方向(第一次),被动方回 ACK(第二次);被动方可能还有数据要发,待发完后再发 FIN(第三次),主动方回 ACK(第四次),因此是四次。`TIME_WAIT` 出现在主动关闭方发出最后一个 ACK 之后:一方面确保该 ACK 能被对方收到,若丢失可重发对方的 FIN;另一方面让本次连接的残留报文在网络中消失,避免影响使用相同四元组的新连接。等待时间为 2MSL。
+
+---
+
+## 延伸阅读
+
+- socket 基础:[[Linux+C+C++技术体系梳理/2. Linux系统编程/14. socket网络编程基础]]
+- TCP 编程:[[Linux+C+C++技术体系梳理/2. Linux系统编程/15. TCP编程]]
+- UDP 编程:[[Linux+C+C++技术体系梳理/2. Linux系统编程/16. UDP编程]]
+- 深入 → IO 多路复用:[[Linux+C+C++技术体系梳理/2. Linux系统编程/17. IO多路复用]]
+- 深入 → 高并发模型:[[Linux+C+C++技术体系梳理/2. Linux系统编程/18. 高并发模型]]
+- 本库相关:[[04-网络编程与项目实战/02-CAN应用编程]]、[[04-网络编程与项目实战/03-CMake入门与进阶]]、[[04-网络编程与项目实战/04-实战项目MQTT与视频监控]]
+
+---
+
+**内容来源**:《I.MX6U嵌入式Linux C应用编程指南》第二十九章 网络基础知识、第三十章 socket编程基础;例程源码 `30_socket/socket_server.c`、`30_socket/socket_client.c`

+ 744 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/04-网络编程与项目实战/02-CAN应用编程.md

@@ -0,0 +1,744 @@
+---
+title: CAN应用编程
+tags: [嵌入式Linux, Linux应用编程, CAN, SocketCAN, CAN总线, can_frame, can_filter, can-utils, IMX6ULL]
+created: 2026-09-18
+updated: 2026-09-18
+pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第三十一章 CAN应用编程基础"
+---
+
+# CAN应用编程
+
+> 💡 **关联知识**:[[04-网络编程与项目实战/01-网络基础与socket编程]]、[[04-网络编程与项目实战/04-实战项目MQTT与视频监控]];延伸阅读:[[嵌入式Linux驱动开发实战/04-Linux总线与接口驱动/04-CAN驱动开发]]
+
+CAN(Controller Area Network,控制器局域网络)是目前应用非常广泛的现场总线之一,主要应用于汽车电子和工业领域。汽车上大量传感器与模块都通过 CAN 总线连接。Linux 把 CAN 设备当**网络设备**管理,因此 CAN 应用编程近似于以太网 socket 通信——内核提供了 **SocketCAN** 接口。本篇先讲 CAN 总线原理(差分信号、仲裁、帧格式),再系统讲解 SocketCAN 编程 API 与 `can-utils` 工具,最后给出两个完整可编译运行的程序(发送 / 接收)。
+
+---
+
+## 1. 什么是 CAN
+
+CAN 是 ISO 国际标准化的串行通信协议:
+
+| 时间/标准 | 内容 |
+| --------- | ---- |
+| 1986 年 | 德国博世公司开发出面向汽车的 CAN 通信协议,动机是解决汽车中庞大电子控制系统之间的通讯、减少信号线束 |
+| ISO11898 / ISO11519 | CAN 标准化 |
+| 现状 | 欧洲汽车网络标准协议,当前应用最广泛的现场总线之一 |
+
+CAN 是一种**多主方式的串行通讯总线**,基本设计规范要求高比特率、高抗电磁干扰性,并且能检测产生的任何错误。经过几十年发展,其高性能、高可靠性和高实时性已被广泛认可,应用于工业自动化、船舶、医疗设备、工业设备等方面。汽车上的空调、车门、发动机、大量传感器等部件、模块都通过 CAN 总线连成一个网络。
+
+---
+
+## 2. CAN 的特点
+
+| 特点 | 说明 |
+| ---- | ---- |
+| 多主控制 | 总线空闲时所有单元都可开始发送(CSMA/CA);最先访问总线者获得发送权;多个单元同时发送时,**高优先级 ID** 的单元获得发送权 |
+| 消息发送 | 所有消息以固定格式发送;两个以上单元同时发送时,根据标识符 ID 决定优先级;ID 不表示目的地址,而表示访问总线的消息优先级。逐位仲裁比较,仲裁获胜者继续发送,失利者立即停止发送转为接收 |
+| 系统柔软性 | 连接在总线上的单元没有类似「地址」的信息;增加单元时其它单元的软硬件及应用层无需改变 |
+| 通信速度 | 可按网络规模设定合适速度;同一网络中所有单元必须设定**统一速度**,否则会输出错误信号妨碍整个网络;不同网络间可不同 |
+| 远程数据请求 | 可通过发送「遥控帧」请求其它单元发送数据 |
+| 错误检测/通知/恢复 | 所有单元都可检测错误;检测到错误的单元立即通知其它所有单元;发送单元检测出错误会强制结束当前发送并反复重发直到成功 |
+| 故障封闭 | 可判断是总线暂时性数据错误(外部噪声)还是持续性错误(单元内部故障、驱动器故障、断线);持续错误时可将故障单元从总线隔离 |
+| 连接 | 可同时连接多个单元,理论上数量无限制;实际受总线时间延迟及电气负载限制。降低速度可增加单元数,提高速度则减少 |
+
+---
+
+## 3. CAN 的电气属性
+
+CAN 总线用两根线连接各单元:**CAN_H** 和 **CAN_L**。CAN 控制器通过判断两根线上的**电位差**得到总线电平,分为**显性电平**和**隐性电平**:
+
+| 电平 | 逻辑值 | CAN_H | CAN_L | 电位差 |
+| ---- | ------ | ----- | ----- | ------ |
+| 显性电平 | 逻辑 0 | 约 3.5V | 约 1.5V | 2V |
+| 隐性电平 | 逻辑 1 | 约 2.5V | 约 2.5V | 0V |
+
+CAN 总线上没有节点传输数据时一直处于隐性状态(总线空闲即隐性)。CAN 总线就通过显性、隐性电平的变化把数据发送出去。**显性电平覆盖隐性电平**,这是仲裁机制的基础。
+
+```mermaid
+flowchart LR
+    X["显性电平(逻辑 0)<br/>CAN_H=3.5V, CAN_L=1.5V<br/>压差 2V"] -->|"覆盖"| Y["隐性电平(逻辑 1)<br/>CAN_H=2.5V, CAN_L=2.5V<br/>压差 0V"]
+    Y -.->|"总线空闲时保持隐性"| Z["空闲状态"]
+```
+
+---
+
+## 4. CAN 网络拓扑
+
+CAN 是分布式控制总线,与以太网一样由许多 CAN 节点构成。每个节点都非常简单:
+
+```mermaid
+flowchart TB
+    subgraph N1["CAN 节点 1"]
+        M1["MCU 微控制器"] --> C1["CAN 控制器"] --> T1["CAN 收发器"]
+    end
+    subgraph N2["CAN 节点 2"]
+        M2["MCU 微控制器"] --> C2["CAN 控制器"] --> T2["CAN 收发器"]
+    end
+    T1 ===|"CAN_H"| T2
+    T1 ===|"CAN_L"| T2
+    R1["120Ω 端接电阻"] --- T1
+    R2["120Ω 端接电阻"] --- T2
+```
+
+- 每个节点 = **MCU + CAN 控制器 + CAN 收发器**三部分;
+- 通过 CAN_H、CAN_L 两条线连接成 CAN 局域网络;
+- CAN 可使用多种物理介质(双绞线、光纤等),最常用双绞线,信号使用**差分电压**传送;
+- **CAN 总线两端各接一个 120Ω 端接电阻**,用于匹配总线阻抗、吸收信号反射及回拨,提高抗干扰能力与可靠性;
+- 传输速度可达 **1Mbps**,最新 CAN-FD 最高可达 **5Mbps** 甚至更高;速度与总线距离有关,距离越短速度越快。
+
+每个 CAN 节点相当于局域网络中的一台主机。
+
+---
+
+## 5. CAN 总线通信模型
+
+CAN 传输协议参考了 OSI 开放系统互连模型,但实际只定义了**传输层、数据链路层、物理层**三层,应用层协议可以由用户定义成适合特定工业领域的方案。
+
+| 层 | CAN 的内容 |
+| -- | ---------- |
+| 应用层 | 用户自定义;常见标准:DeviceNet(工业控制/制造业,为 PLC 和智能传感器设计)、CANopen(工业控制)、OBD / UDS(统一诊断服务, ISO14229,乘用车诊断)、SAEJ1939(商用车) |
+| 数据链路层 | 分 LLC 子层和 MAC 子层,**MAC 子层是 CAN 协议的核心**;负责把物理层信号组织成有意义的消息,提供传送错误控制等流程:消息的帧化、仲裁、应答、错误的检测或报告,通常在 CAN 控制器硬件中执行 |
+| 物理层 | 定义信号实际的发送方式、位时序、位的编码方式及同步步骤;信号电平、通信速度、采样点、驱动器/总线电气特性、连接器形态等由用户根据系统需求确定 |
+
+```mermaid
+flowchart TB
+    A["应用层:DeviceNet / CANopen / OBD / UDS / SAEJ1939(用户自定义)"] --> B["数据链路层:LLC 子层 + MAC 子层(CAN 核心)"]
+    B --> C["物理层:位时序 / 位编码 / 同步"]
+```
+
+---
+
+## 6. CAN 帧的种类
+
+CAN 定义了 **5 种类型**的报文帧:
+
+| 帧 | 用途 |
+| -- | ---- |
+| 数据帧 | 发送单元向接收单元传送数据的帧 |
+| 遥控帧 | 接收单元向具有相同 ID 的发送单元请求数据的帧 |
+| 错误帧 | 检测出错误时向其它单元通知错误的帧 |
+| 过载帧 | 接收单元通知其尚未做好接收准备的帧 |
+| 间隔帧 | 将数据帧及遥控帧与前面的帧分离开来的帧 |
+
+其中数据帧和遥控帧都有**标准格式**(11 位标识符 ID)和**扩展格式**(29 位标识符 ID)。数据帧是使用最多的帧类型。
+
+### 6.1 数据帧的结构
+
+数据帧由 **7 个段**构成:
+
+| 段 | 说明 |
+| -- | ---- |
+| 帧起始 | 表示数据帧开始的段 |
+| 仲裁段 | 表示该帧优先级的段 |
+| 控制段 | 表示数据的字节数及保留位的段 |
+| 数据段 | 数据的内容,可发送 **0~8 个字节** |
+| CRC 段 | 检查帧的传输错误的段 |
+| ACK 段 | 表示确认正常接收的段 |
+| 帧结束 | 表示数据帧结束的段 |
+
+```mermaid
+flowchart LR
+    SOF["帧起始<br/>SOF 1位<br/>显性"] --> ARB["仲裁段<br/>ID + RTR"]
+    ARB --> CTRL["控制段<br/>IDE + r0 + DLC 4位"]
+    CTRL --> DATA["数据段<br/>0~8 字节"]
+    DATA --> CRC["CRC段<br/>CRC 15位 + 界定符"]
+    CRC --> ACK["ACK段<br/>ACK槽 + 界定符"]
+    ACK --> EOF["帧结束<br/>EOF 7位 隐性"]
+```
+
+> ⚠️ **来源说明**:以下位级字段细节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。原书仅给出「7 个段」的概述,并指引读者参考瑞萨电子《CAN 入门教程》。
+
+标准帧与扩展帧的位级构成对比:
+
+| 字段 | 标准帧 | 扩展帧 | 说明 |
+| ---- | ------ | ------ | ---- |
+| 帧起始 SOF | 1 位 | 1 位 | 显性,标志帧开始 |
+| 标识符 ID | 11 位 | 11 位基本 ID | 决定优先级,ID 越小优先级越高 |
+| SRR / IDE | IDE(1 位,显性) | SRR(1 位,隐性)+ IDE(1 位,隐性) | 区分标准/扩展格式 |
+| 扩展 ID | — | 18 位 | 与基本 ID 组成 29 位扩展 ID |
+| RTR | 1 位 | 1 位 | 数据帧为显性,遥控帧为隐性 |
+| 控制段 | IDE + r0 + DLC(4 位) | 同左 | DLC 表示数据字节数 0~8 |
+| 数据段 | 0~8 字节 | 0~8 字节 | 实际数据 |
+| CRC 段 | 15 位 + 界定符 | 15 位 + 界定符 | 校验传输错误 |
+| ACK 段 | ACK 槽 + 界定符 | 同左 | 发送方发隐性,接收方正确接收后拉为显性 |
+| 帧结束 EOF | 7 位 | 7 位 | 隐性 |
+
+### 6.2 仲裁机制
+
+> ⚠️ **来源说明**:位级仲裁原理不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+当多个节点同时开始发送时,CAN 对消息 ID 的每个位进行**逐个仲裁比较**:
+
+1. 每个发送节点在发送每一位的同时监听总线电平;
+2. 发出隐性(1)却检测到显性(0)的节点,判定仲裁失利,立即停止发送转为接收;
+3. 仲裁获胜的节点继续发送,无需重发。
+
+由于显性覆盖隐性,**ID 二进制值越小(前面显性位越多)优先级越高**。这种机制称为非破坏性仲裁(Non-destructive Arbitration),不会浪费已发送的报文,保证高优先级消息的实时性。
+
+### 6.3 位时序
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+CAN 是异步串行总线,收发双方靠位时序同步。一个位时间(Bit Time)被划分为若干时间段,每段由若干个最小时间单位 **Tq(Time Quantum)** 组成:
+
+| 段 | 作用 |
+| -- | ---- |
+| 同步段 SYNC_SEG | 1 个 Tq,用于硬同步,期望信号跳变发生在此段 |
+| 传播时间段 PROP_SEG | 补偿总线上的物理传播延迟和收发器延迟 |
+| 相位缓冲段 1 PHASE_SEG1 | 补偿正相位误差,可通过重同步延长 |
+| 相位缓冲段 2 PHASE_SEG2 | 补偿负相位误差,可通过重同步缩短 |
+
+**采样点**通常位于 PHASE_SEG1 结束处,大致在位时间的 75%~87.5% 位置(常用 87.5%)。配置 bitrate 时,采样点、波特率分频(brp)、各段长度需匹配晶振与网络延迟。
+
+```text
+位时间 = SYNC_SEG + PROP_SEG + PHASE_SEG1 + PHASE_SEG2
+波特率 = 1 / 位时间
+采样点 = (SYNC_SEG + PROP_SEG + PHASE_SEG1) / 位时间 × 100%
+```
+
+---
+
+## 7. SocketCAN 应用编程
+
+Linux 将 CAN 设备作为**网络设备**统一管理,提供 **SocketCAN** 应用编程接口,使 CAN 通信近似于以太网通信。SocketCAN 中大部分数据结构和函数在头文件 **`linux/can.h`** 中定义,应用程序必须包含:
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <linux/can.h>
+#include <linux/can/raw.h>
+#include <net/if.h>
+```
+
+### 7.1 创建 CAN 套接字
+
+CAN 套接字采用标准网络套接字操作,第一个参数设为 `PF_CAN`,类型设为 `SOCK_RAW`,协议设为 `CAN_RAW`:
+
+```c
+int sockfd = -1;
+sockfd = socket(PF_CAN, SOCK_RAW, CAN_RAW);
+if (0 > sockfd) {
+    perror("socket error");
+    exit(EXIT_FAILURE);
+}
+```
+
+| 参数 | 取值 | 说明 |
+| ---- | ---- | ---- |
+| domain | `PF_CAN` | 指定 CAN 通信协议 |
+| type | `SOCK_RAW` | 原始套接字类型 |
+| protocol | `CAN_RAW` | CAN 原始协议 |
+
+### 7.2 将套接字与 CAN 设备绑定
+
+需要用 `ioctl(SIOCGIFINDEX)` 根据接口名(如 `can0`)取得接口索引,再填充 `struct sockaddr_can` 后 `bind()`:
+
+```c
+struct ifreq ifr = {0};
+struct sockaddr_can can_addr = {0};
+int ret;
+
+strcpy(ifr.ifr_name, "can0");          // 指定设备名
+ioctl(sockfd, SIOCGIFINDEX, &ifr);     // 获取接口索引
+
+can_addr.can_family = AF_CAN;          // 填充数据
+can_addr.can_ifindex = ifr.ifr_ifindex;
+
+ret = bind(sockfd, (struct sockaddr *)&can_addr, sizeof(can_addr));
+if (0 > ret) {
+    perror("bind error");
+    close(sockfd);
+    exit(EXIT_FAILURE);
+}
+```
+
+| 结构体 | 头文件 | 关键成员 |
+| ------ | ------ | -------- |
+| `struct ifreq` | `<net/if.h>` | `ifr_name`(接口名)、`ifr_ifindex`(接口索引) |
+| `struct sockaddr_can` | `<linux/can.h>` | `can_family`(`AF_CAN`)、`can_ifindex`(接口索引) |
+
+> ⚠️ **来源说明**:以下 `struct sockaddr_can` 完整定义不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识(原书未展开该结构体)。
+
+```c
+struct sockaddr_can {
+    __kernel_sa_family_t can_family;  /* AF_CAN */
+    int                  can_ifindex; /* CAN 网络设备接口索引 */
+    union {
+        struct { canid_t rx_id, tx_id; } tp;  /* 传输协议地址(ISO-TP) */
+    } can_addr;
+};
+```
+
+### 7.3 设置过滤规则
+
+不设置过滤规则时,应用程序默认接收**所有 ID** 的报文。若只接收特定 ID,可用 `setsockopt` 设置 `CAN_RAW_FILTER`。`struct can_filter` 只有两个成员 `can_id` 和 `can_mask`:
+
+```c
+struct can_filter rfilter[2];          // 定义一个 can_filter 结构体对象
+
+// 填充过滤规则:只接收 ID 为 (can_id & can_mask) 的报文
+rfilter[0].can_id = 0x60A;
+rfilter[0].can_mask = 0x7FF;
+rfilter[1].can_id = 0x60B;
+rfilter[1].can_mask = 0x7FF;
+
+// 调用 setsockopt 设置过滤规则
+setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));
+```
+
+规则说明:内核把收到报文的 `can_id` 与 `can_mask` 按位与,再与 `can_id` 比较,相等则接收。`can_mask` 中为 1 的位参与比较,为 0 的位忽略。例如 `can_id=0x60A, can_mask=0x7FF` 表示精确匹配标准帧 ID `0x60A`。
+
+如果应用**只发送、不接收任何报文**,可在内核中省略接收队列以减少 CPU 消耗,此时把第 4 个参数设为 `NULL`、第 5 个参数设为 `0`:
+
+```c
+setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, NULL, 0);
+```
+
+### 7.4 数据发送与接收
+
+CAN 每次通信都采用 `struct can_frame` 把数据封装成帧:
+
+```c
+struct can_frame {
+    canid_t can_id; /* CAN 标识符 */
+    __u8    can_dlc;/* 数据长度(最长为 8 个字节) */
+    __u8    __pad;  /* padding */
+    __u8    __res0; /* reserved / padding */
+    __u8    __res1; /* reserved / padding */
+    __u8    data[8];/* 数据 */
+};
+```
+
+- 标准帧使用 `can_id` 的低 **11 位**;
+- 扩展帧使用 **0~28 位**;
+- `can_id` 的第 29、30、31 位是帧的标志位,用于定义帧类型。
+
+```c
+/* special address description flags for the CAN_ID */
+#define CAN_EFF_FLAG 0x80000000U  /* 扩展帧的标识 */
+#define CAN_RTR_FLAG 0x40000000U  /* 远程帧的标识 */
+#define CAN_ERR_FLAG 0x20000000U  /* 错误帧的标识,用于错误检查 */
+
+/* mask */
+#define CAN_SFF_MASK 0x000007FFU  /* <can_id & CAN_SFF_MASK> 获取标准帧 ID */
+#define CAN_EFF_MASK 0x1FFFFFFFU  /* <can_id & CAN_EFF_MASK> 获取扩展帧 ID */
+#define CAN_ERR_MASK 0x1FFFFFFFU  /* omit EFF, RTR, ERR flags */
+```
+
+**数据发送**使用 `write()`:
+
+```c
+struct can_frame frame;
+int ret;
+
+frame.can_id = 123;         // 标准帧 ID;扩展帧写作 CAN_EFF_FLAG | 123
+frame.can_dlc = 3;          // 数据长度为 3
+frame.data[0] = 0xA0;
+frame.data[1] = 0xB0;
+frame.data[2] = 0xC0;
+
+ret = write(sockfd, &frame, sizeof(frame));  // 发送数据
+if (sizeof(frame) != ret)                    // ret 不等于帧长度说明发送失败
+    perror("write error");
+```
+
+发送遥控帧(远程帧):
+
+```c
+frame.can_id = CAN_RTR_FLAG | 123;   // 帧 ID 为 123 的远程帧
+write(sockfd, &frame, sizeof(frame));
+```
+
+**数据接收**使用 `read()`:
+
+```c
+struct can_frame frame;
+int ret = read(sockfd, &frame, sizeof(frame));
+```
+
+### 7.5 错误处理
+
+接收到一帧数据后,可通过判断 `can_id` 中的 `CAN_ERR_FLAG` 位判断是否为错误帧;若为错误帧,可通过 `can_id` 的其它符号位判断具体原因。错误帧符号位在 `<linux/can/error.h>` 中定义:
+
+```c
+/* error class (mask) in can_id */
+#define CAN_ERR_TX_TIMEOUT  0x00000001U /* TX timeout (by netdevice driver) */
+#define CAN_ERR_LOSTARB     0x00000002U /* lost arbitration    / data[0] */
+#define CAN_ERR_CRTL        0x00000004U /* controller problems / data[1] */
+#define CAN_ERR_PROT        0x00000008U /* protocol violations / data[2..3] */
+#define CAN_ERR_TRX         0x00000010U /* transceiver status  / data[4] */
+#define CAN_ERR_ACK         0x00000020U /* received no ACK on transmission */
+#define CAN_ERR_BUSOFF      0x00000040U /* bus off */
+#define CAN_ERR_BUSERROR    0x00000080U /* bus error (may flood!) */
+#define CAN_ERR_RESTARTED   0x00000100U /* controller restarted */
+```
+
+### 7.6 回环功能设置
+
+默认情况下 CAN 的**本地回环功能是开启的**。可用 `CAN_RAW_LOOPBACK` 关闭或开启:
+
+```c
+int loopback = 0;   // 0 表示关闭,1 表示开启(默认)
+setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_LOOPBACK, &loopback, sizeof(loopback));
+```
+
+回环开启时,所有发送帧都会被回环到与 CAN 总线接口对应的套接字上,因此**单块开发板也能自收自发做测试**。
+
+### 7.7 其它 SocketCAN 套接字选项
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+| 选项 | 层级 | 作用 |
+| ---- | ---- | ---- |
+| `CAN_RAW_FILTER` | `SOL_CAN_RAW` | 设置接收过滤规则 |
+| `CAN_RAW_ERR_FILTER` | `SOL_CAN_RAW` | 设置要接收的错误帧类型掩码 |
+| `CAN_RAW_LOOPBACK` | `SOL_CAN_RAW` | 本地回环开关,默认开启 |
+| `CAN_RAW_RECV_OWN_MSGS` | `SOL_CAN_RAW` | 是否接收自己发送的报文(回环开启后本 socket 是否收到) |
+| `CAN_RAW_JOIN_FILTERS` | `SOL_CAN_RAW` | 多个过滤器之间改为「或」的并集关系 |
+| `CAN_RAW_FD_FRAMES` | `SOL_CAN_RAW` | 使能 CAN-FD 帧(`struct canfd_frame`) |
+
+---
+
+## 8. can-utils 工具
+
+开发板出厂系统提供了一些 CAN 测试工具。在 Linux 中 CAN 设备作为网络设备管理,配置命令与以太网相同。
+
+### 8.1 查看与配置 CAN 设备
+
+```bash
+# 查看 CAN 设备(ALPHA 板上只有 can0)
+ifconfig -a | grep can
+ifconfig can0
+
+# 配置波特率:先关闭设备,再以 1000000 波特率启用(triple-sampling 三次采样)
+ifconfig can0 down
+ip link set can0 up type can bitrate 1000000 triple-sampling on
+
+# 查看详细参数(扩展)
+ip -details link show can0
+```
+
+> CAN 分析仪等测试设备的波特率必须和开发板 CAN 设备一致,否则通信失败。
+
+### 8.2 发送与接收
+
+```bash
+# 发送:# 前是帧 ID,后面是数据(十六进制,用 . 分隔)
+cansend can0 123#01.02.03.04.05.06.07.08
+
+# 接收:显示 CAN 报文,-ta 表示显示绝对时间戳
+candump -ta can0
+
+# 按 Ctrl+C 退出
+```
+
+> ⚠️ **来源说明**:以下工具不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+```bash
+candump can0                 # 列出报文,Ctrl+C 退出
+cangen can0 -g 10 -I 123     # 以 10ms 间隔生成 ID=0x123 的随机数据帧
+canbusload can0@1000000      # 统计总线负载率
+cansniffer can0              # 动态过滤、按 ID 归类显示变化中的报文
+candump can0,123:7FF         # 只显示 ID=0x123 的报文(过滤)
+```
+
+---
+
+## 9. CAN 应用编程实战
+
+### 9.1 CAN 数据发送实例(can_write.c)
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <linux/can.h>
+#include <linux/can/raw.h>
+#include <net/if.h>
+
+int main(void)
+{
+    struct ifreq ifr = {0};
+    struct sockaddr_can can_addr = {0};
+    struct can_frame frame = {0};
+    int sockfd = -1;
+    int ret;
+
+    /* 打开套接字 */
+    sockfd = socket(PF_CAN, SOCK_RAW, CAN_RAW);
+    if (0 > sockfd) {
+        perror("socket error");
+        exit(EXIT_FAILURE);
+    }
+
+    /* 指定 can0 设备 */
+    strcpy(ifr.ifr_name, "can0");
+    ioctl(sockfd, SIOCGIFINDEX, &ifr);
+    can_addr.can_family = AF_CAN;
+    can_addr.can_ifindex = ifr.ifr_ifindex;
+
+    /* 将 can0 与套接字进行绑定 */
+    ret = bind(sockfd, (struct sockaddr *)&can_addr, sizeof(can_addr));
+    if (0 > ret) {
+        perror("bind error");
+        close(sockfd);
+        exit(EXIT_FAILURE);
+    }
+
+    /* 设置过滤规则:不接受任何报文、仅发送数据 */
+    setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, NULL, 0);
+
+    /* 发送数据 */
+    frame.data[0] = 0xA0;
+    frame.data[1] = 0xB0;
+    frame.data[2] = 0xC0;
+    frame.data[3] = 0xD0;
+    frame.data[4] = 0xE0;
+    frame.data[5] = 0xF0;
+    frame.can_dlc = 6;      // 一次发送 6 个字节数据
+    frame.can_id = 0x123;   // 帧 ID 为 0x123,标准帧
+
+    for ( ; ; ) {
+        ret = write(sockfd, &frame, sizeof(frame)); // 发送数据
+        if (sizeof(frame) != ret) {                 // ret 不等于帧长度说明发送失败
+            perror("write error");
+            goto out;
+        }
+        sleep(1);                                   // 一秒钟发送一次
+    }
+
+out:
+    /* 关闭套接字 */
+    close(sockfd);
+    exit(EXIT_SUCCESS);
+}
+```
+
+逐段解释:
+
+| 代码段 | 作用 |
+| ------ | ---- |
+| `socket(PF_CAN, SOCK_RAW, CAN_RAW)` | 创建 CAN 原始套接字 |
+| `strcpy(ifr.ifr_name, "can0")` | 指定 CAN 设备名为 can0 |
+| `ioctl(SIOCGIFINDEX, &ifr)` | 取得 can0 的接口索引,存入 `ifr.ifr_ifindex` |
+| `can_addr.can_family/can_ifindex` | 填充 CAN 套接字地址 |
+| `bind()` | 把套接字与 can0 绑定 |
+| `CAN_RAW_FILTER, NULL, 0` | 只发不收,省略内核接收队列 |
+| `frame.can_id = 0x123` | 标准帧 ID,低 11 位有效 |
+| `frame.can_dlc = 6` | 数据长度 6 字节 |
+| `write()` | 发送一帧,返回值为 `sizeof(frame)` 表示成功 |
+| `sleep(1)` | 每秒发送一次 |
+
+### 9.2 CAN 数据接收实例(can_read.c)
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <linux/can.h>
+#include <linux/can/raw.h>
+#include <net/if.h>
+
+int main(void)
+{
+    struct ifreq ifr = {0};
+    struct sockaddr_can can_addr = {0};
+    struct can_frame frame = {0};
+    int sockfd = -1;
+    int i;
+    int ret;
+
+    /* 打开套接字 */
+    sockfd = socket(PF_CAN, SOCK_RAW, CAN_RAW);
+    if (0 > sockfd) {
+        perror("socket error");
+        exit(EXIT_FAILURE);
+    }
+
+    /* 指定 can0 设备 */
+    strcpy(ifr.ifr_name, "can0");
+    ioctl(sockfd, SIOCGIFINDEX, &ifr);
+    can_addr.can_family = AF_CAN;
+    can_addr.can_ifindex = ifr.ifr_ifindex;
+
+    /* 将 can0 与套接字进行绑定 */
+    ret = bind(sockfd, (struct sockaddr *)&can_addr, sizeof(can_addr));
+    if (0 > ret) {
+        perror("bind error");
+        close(sockfd);
+        exit(EXIT_FAILURE);
+    }
+
+    /* 设置过滤规则 */
+    //setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, NULL, 0);
+
+    /* 接收数据 */
+    for ( ; ; ) {
+        if (0 > read(sockfd, &frame, sizeof(struct can_frame))) {
+            perror("read error");
+            break;
+        }
+
+        /* 校验是否接收到错误帧 */
+        if (frame.can_id & CAN_ERR_FLAG) {
+            printf("Error frame!\n");
+            break;
+        }
+
+        /* 校验帧格式 */
+        if (frame.can_id & CAN_EFF_FLAG)    // 扩展帧
+            printf("扩展帧 <0x%08x> ", frame.can_id & CAN_EFF_MASK);
+        else                                // 标准帧
+            printf("标准帧 <0x%03x> ", frame.can_id & CAN_SFF_MASK);
+
+        /* 校验帧类型:数据帧还是远程帧 */
+        if (frame.can_id & CAN_RTR_FLAG) {
+            printf("remote request\n");
+            continue;
+        }
+
+        /* 打印数据长度 */
+        printf("[%d] ", frame.can_dlc);
+
+        /* 打印数据 */
+        for (i = 0; i < frame.can_dlc; i++)
+            printf("%02x ", frame.data[i]);
+        printf("\n");
+    }
+
+    /* 关闭套接字 */
+    close(sockfd);
+    exit(EXIT_SUCCESS);
+}
+```
+
+逐段解释:
+
+| 代码段 | 作用 |
+| ------ | ---- |
+| `socket()` / `ioctl()` / `bind()` | 与发送端完全相同的创建与绑定流程 |
+| 注释掉的 `setsockopt` | 若打开,则「不接受任何报文」;默认不设置即接收所有 ID |
+| `read()` | 阻塞读取一帧 CAN 报文,存入 `frame` |
+| `can_id & CAN_ERR_FLAG` | 判断是否为错误帧,是则退出 |
+| `can_id & CAN_EFF_FLAG` | 判断扩展帧,用 `CAN_EFF_MASK` 取出 29 位 ID |
+| `can_id & CAN_SFF_MASK` | 标准帧取 11 位 ID |
+| `can_id & CAN_RTR_FLAG` | 判断是否为远程帧(遥控帧) |
+| `can_dlc` + `data[]` 循环 | 按长度打印数据字节 |
+
+### 9.3 编译、部署与测试步骤
+
+```bash
+# 交叉编译(Ubuntu 上执行)
+arm-linux-gnueabihf-gcc -o can_write can_write.c
+arm-linux-gnueabihf-gcc -o can_read  can_read.c
+
+# 拷贝到开发板
+scp can_write can_read root@192.168.1.10:/home/root/
+
+# 开发板上配置 CAN 设备(波特率必须与对端一致)
+ifconfig can0 down
+ip link set can0 up type can bitrate 1000000 triple-sampling on
+```
+
+**方式一:单板回环测试**(CAN 默认开启本地回环)
+
+```bash
+# 终端 1:运行接收程序
+./can_read
+# 终端 2:运行发送程序
+./can_write
+# can_read 会不断打印:标准帧 <123> [6] a0 b0 c0 d0 e0 f0
+```
+
+**方式二:双板通信**(两块开发板 CAN_H↔CAN_H、CAN_L↔CAN_L 对接,两端各接 120Ω 端接电阻)
+
+```bash
+# A 板运行 can_read,B 板运行 can_write(或反之)
+# B 板每 1 秒发送一帧 ID=0x123 的 6 字节数据,A 板打印接收结果
+```
+
+**方式三:配合 CAN 分析仪 / can-utils**
+
+```bash
+# 开发板发,上位机(或另一块板)收
+./can_write
+# 上位机 candump 或分析仪可见 ID=0x123 的报文
+
+# 上位机发,开发板收
+cansend can0 123#01.02.03.04.05.06.07.08
+./can_read
+```
+
+### 9.4 调试方法
+
+| 现象 | 可能原因 | 排查手段 |
+| ---- | -------- | -------- |
+| `socket error` | 内核不支持 SocketCAN 或未加载 CAN 驱动 | `ifconfig -a` 看是否有 can0;`dmesg` 看驱动加载日志 |
+| `bind error` | 设备名写错或 can0 未 up | `ifconfig can0` 检查状态,`ip link set can0 up ...` |
+| 收不到数据 | 双端波特率不一致、接线错误、缺端接电阻 | `ip -details link show can0` 核对 bitrate;检查 CAN_H/CAN_L;两端接 120Ω |
+| 收到 `Error frame!` | 总线错误 / bus-off | 查看 `dmesg` 内核 CAN 报错,检查布线、终端电阻、波特率 |
+| `write error` | 接口 down 或被过滤 | 确认 `ifconfig can0 up`,返回值是否等于 `sizeof(frame)` |
+| 单板回环收不到 | 回环被关闭 | 用 `CAN_RAW_LOOPBACK` 设为 1(默认即 1) |
+
+---
+
+## 10. 跨平台对比:Linux SocketCAN vs STM32 vs 裸机
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+| 维度 | Linux SocketCAN(本教程) | STM32(HAL/裸机) | 说明 |
+| ---- | ------------------------- | ----------------- | ---- |
+| 接口层次 | 标准 socket(`PF_CAN`) | 直接操作 bxCAN 寄存器 | Linux 把 CAN 当网络设备 |
+| 发送 | `write(sockfd, &frame, sizeof(frame))` | 填充发送邮箱 / `HAL_CAN_AddTxMessage` | 数据都封装为帧 |
+| 接收 | `read()` + `can_frame` | 中断/轮询接收邮箱 | Linux 由内核管理接收队列 |
+| 过滤 | `can_filter` + `CAN_RAW_FILTER` | 配置筛选器组(Filter Bank) | 硬件/内核过滤 |
+| 帧格式 | `struct can_frame`(`can_id/can_dlc/data[]`) | `CAN_TxHeaderTypeDef` 等 | 字段概念一致 |
+| 配置波特率 | `ip link set can0 up type can bitrate ...` | 配置位时序寄存器 | 都需匹配对端 |
+| 工具 | `candump` / `cansend` | 无,需自行实现 | SocketCAN 生态完善 |
+
+---
+
+## 11. 面试精选
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+### Q1:CAN 为什么使用差分信号?显性电平和隐性电平分别对应什么逻辑值?
+
+**答**:CAN 通过 CAN_H、CAN_L 两根线之间的电位差判断总线电平,差分传输抗共模干扰能力强、可靠性高。显性电平表示逻辑 0,此时 CAN_H 约 3.5V、CAN_L 约 1.5V,电位差约 2V;隐性电平表示逻辑 1,此时两线都约 2.5V,电位差约 0V。总线空闲时保持隐性状态,且显性电平会覆盖隐性电平,这是仲裁和 ACK 的基础。
+
+### Q2:CAN 的仲裁机制是怎样的?为什么 ID 越小优先级越高?
+
+**答**:多个节点同时发送时,CAN 对 ID 的每一位逐位比较:节点在发送每一位的同时监听总线,若它发出隐性(1)却检测到显性(0),说明有更高优先级节点在发送,该节点立即退出仲裁转为接收,获胜者继续发送,这种非破坏性仲裁不浪费报文。由于显性覆盖隐性,ID 二进制值越小(高位显性位越多)优先级越高,因此需要实时响应的消息应分配更小的 ID。
+
+### Q3:数据帧由哪 7 个段组成?标准帧与扩展帧的区别是什么?
+
+**答**:数据帧由帧起始、仲裁段、控制段、数据段、CRC 段、ACK 段、帧结束 7 个段构成,数据段可携带 0~8 字节数据。标准帧使用 11 位标识符,扩展帧使用 29 位标识符(11 位基本 ID + 18 位扩展 ID);两者通过 IDE 位区分,扩展帧还多了 SRR 位。遥控帧(远程帧)与数据帧结构类似,通过 RTR 位区分,用于请求具有相同 ID 的发送单元发送数据。
+
+### Q4:SocketCAN 中创建并绑定一个 CAN 套接字的流程是什么?
+
+**答**:先 `socket(PF_CAN, SOCK_RAW, CAN_RAW)` 创建套接字;然后用 `strcpy(ifr.ifr_name, "can0")` 指定设备名,`ioctl(sockfd, SIOCGIFINDEX, &ifr)` 获取接口索引;接着填充 `struct sockaddr_can` 的 `can_family = AF_CAN` 和 `can_ifindex = ifr.ifr_ifindex`;最后 `bind()` 把套接字与 can0 绑定。收发数据分别用 `write()`/`read()` 配合 `struct can_frame`,过滤规则通过 `setsockopt(SOL_CAN_RAW, CAN_RAW_FILTER, ...)` 设置。
+
+### Q5:`struct can_frame` 与 `struct can_filter` 各有什么作用?`CAN_RAW_LOOPBACK` 有什么意义?
+
+**答**:`struct can_frame` 用于封装一帧 CAN 数据,包含 `can_id`(标识符及 EFF/RTR/ERR 标志位)、`can_dlc`(数据长度)、`data[8]`(数据),发送和接收都通过它。`struct can_filter` 含 `can_id` 和 `can_mask`,用于设置只接收哪些 ID 的报文,内核将收到报文的 `can_id & can_mask` 与 `can_id` 比较,相等则接收;把 `CAN_RAW_FILTER` 的第四、五参数设为 `NULL, 0` 表示只发不收。`CAN_RAW_LOOPBACK` 默认开启,发送帧会回环到对应 CAN 接口的套接字,因此单块开发板即可自收自发完成测试。
+
+---
+
+## 延伸阅读
+
+- CAN 驱动原理:[[嵌入式Linux驱动开发实战/04-Linux总线与接口驱动/04-CAN驱动开发]]
+- 本库相关:[[04-网络编程与项目实战/01-网络基础与socket编程]]、[[04-网络编程与项目实战/04-实战项目MQTT与视频监控]]
+
+---
+
+**内容来源**:《I.MX6U嵌入式Linux C应用编程指南》第三十一章 CAN应用编程基础(基础内容参考瑞萨电子《CAN 入门教程》);例程源码 `31_can/can_write.c`、`31_can/can_read.c`

+ 1249 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/04-网络编程与项目实战/03-CMake入门与进阶.md

@@ -0,0 +1,1249 @@
+---
+title: CMake入门与进阶
+tags: [嵌入式Linux, Linux应用编程, CMake, CMakeLists, Makefile, 构建系统, add_executable, add_library, target_link_libraries, target_include_directories, 静态库, 动态库, 交叉编译, 工具链文件, arm-linux-setup.cmake, install, IMX6ULL]
+created: 2026-09-18
+updated: 2026-09-18
+pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第三十二章 CMake入门与进阶"
+---
+
+# CMake入门与进阶
+
+> 💡 **关联知识**:[[04-网络编程与项目实战/04-实战项目MQTT与视频监控]]、[[04-网络编程与项目实战/01-网络基础与socket编程]];延伸阅读:[[Linux+C+C++技术体系梳理/3. 编译调试与驱动预留/2. Makefile]]
+
+直接敲 `gcc` 编译一两个文件还行,工程一大就会遇到"源文件太多、依赖太乱、换个平台就得重写 Makefile"的问题。CMake 用一个与平台无关的 `CMakeLists.txt` 描述整个工程的编译流程,再由它**根据当前平台生成对应的 Makefile**,最后仍由 `make` 完成编译。本篇从零开始,用五个递进示例讲清 CMake 的用法,再系统整理命令、变量、作用域与交叉编译配置,最后给出一个可直接复制的多目录工程。
+
+> 约定:本篇命令基于 Ubuntu 主机;示例中的交叉编译工具链路径(`/opt/fsl-imx-x11/...`)来自正点原子 I.MX6U 出厂开发环境,读者需按自己的实际安装路径修改。
+
+---
+
+## 1. 为什么需要 CMake
+
+### 1.1 构建系统的演进
+
+| 阶段 | 工具 | 说明 |
+| ---- | ---- | ---- |
+| 手动编译 | `gcc main.c hello.c -o hello` | 文件一多就要手敲一长串,改一个文件全量重编 |
+| 脚本化 | Makefile + `make` | 描述依赖关系与规则,支持增量编译;但语法复杂、各平台不通用 |
+| 跨平台构建 | CMake | 写与平台无关的 `CMakeLists.txt`,自动生成本地化 Makefile / 工程文件 |
+
+CMake 的主要优点(教材总结):
+
+- **开放源代码**:官网 <https://cmake.org/> 可下载源码;
+- **跨平台**:CMake 不直接编译出可执行文件/库,而是解析 `CMakeLists.txt`,按当前平台生成 Makefile 和工程文件,最终还是调用 `make` 编译,但 CMake 本身跨平台;
+- **语法规则简单**:`CMakeLists.txt` 语法与平台无关,比 Makefile 简单易懂,由 CMake 自动生成 Makefile,无需手写。
+
+### 1.2 CMake 与 Makefile 对比
+
+| 维度 | Makefile | CMake |
+| ---- | -------- | ----- |
+| 编写者 | 开发者手工编写 | 开发者写 `CMakeLists.txt`,Makefile 由 CMake 生成 |
+| 语法 | 复杂、易错,制表符敏感 | 命令 + 变量,类脚本,简单 |
+| 跨平台 | 各平台规则往往不同 | `CMakeLists.txt` 与平台无关 |
+| 依赖扫描 | 需手工维护或配合工具 | 自动处理头文件依赖 |
+| 产物 | 直接给 `make` 用 | 中间层,再生成 Makefile |
+| 适合规模 | 小工程、学习原理 | 中大型工程、跨平台工程 |
+
+```mermaid
+flowchart LR
+    A["CMakeLists.txt<br/>(平台无关)"] --> B["cmake 工具"]
+    B -->|Linux| C["Makefile"]
+    B -->|其它平台| D["本地化工程文件"]
+    C --> E["make"]
+    E --> F["可执行文件 / 库文件"]
+
+    classDef cfg fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
+    classDef tool fill:#fef9c3,stroke:#ca8a04,color:#713f12
+    classDef out fill:#dcfce7,stroke:#16a34a,color:#14532d
+    class A cfg
+    class B,E tool
+    class F out
+```
+
+### 1.3 安装与查看版本
+
+```bash
+sudo apt-get install cmake    # Ubuntu 在线安装
+cmake --version               # 查看版本号
+```
+
+> 实测提示:Ubuntu 自带的 CMake 可能是 3.5.1 这类老版本,在配置交叉编译时会报错。建议改用 CMake 3.16.0(GitHub Releases 下载 `cmake-3.16.0-Linux-x86_64.tar.gz`,解压即用,无需自己编译),后文示例均以该版本为准。
+
+官方文档:<https://cmake.org/documentation/>(总链接)、<https://cmake.org/cmake/help/latest/guide/tutorial/index.html>(培训教程)。
+
+---
+
+## 2. 从零开始:五个递进示例
+
+### 2.1 示例一:单个源文件
+
+```c
+// main.c
+#include <stdio.h>
+
+int main()
+{
+    printf("Hello World!\n");
+    return 0;
+}
+```
+
+```cmake
+# CMakeLists.txt
+project(HELLO)
+add_executable(hello ./main.c)
+```
+
+- `project(HELLO)`:设置工程名称(非强制,但推荐)。
+- `add_executable(hello ./main.c)`:生成名为 `hello` 的可执行文件,源文件为 `./main.c`。
+
+在工程目录执行:
+
+```bash
+cmake ./      # 解析当前目录的 CMakeLists.txt
+make          # 编译,得到可执行文件 hello
+file hello    # 查看架构,x86-64 说明只能在 Ubuntu 上跑
+./hello
+```
+
+执行 `cmake` 后会额外生成 `CMakeCache.txt`、`CMakeFiles/`、`cmake_install.cmake`、`Makefile` 等文件。
+
+### 2.2 out-of-source 构建
+
+上面的做法把中间文件和源码混在一起,清理麻烦。正确做法是**源码与构建分离**(out-of-source):
+
+```text
+├── build/            # 构建目录
+├── CMakeLists.txt
+└── main.c
+```
+
+```bash
+cd build/
+cmake ../
+make
+```
+
+所有中间文件和可执行文件都落在 `build/`,清理时直接删掉 `build/` 即可。
+
+### 2.3 示例二:多个源文件
+
+```c
+// hello.h
+#ifndef __TEST_HELLO_
+#define __TEST_HELLO_
+
+void hello(const char *name);
+
+#endif
+```
+
+```c
+// hello.c
+#include <stdio.h>
+#include "hello.h"
+
+void hello(const char *name)
+{
+    printf("Hello %s!\n", name);
+}
+```
+
+```c
+// main.c
+#include "hello.h"
+
+int main(void)
+{
+    hello("World");
+    return 0;
+}
+```
+
+```cmake
+project(HELLO)
+set(SRC_LIST main.c hello.c)
+add_executable(hello ${SRC_LIST})
+```
+
+`set(SRC_LIST main.c hello.c)` 定义变量 `SRC_LIST`,用 `${SRC_LIST}` 引用;也可直接写成 `add_executable(hello main.c hello.c)`。
+
+### 2.4 示例三:生成库文件
+
+```cmake
+project(HELLO)
+add_library(libhello hello.c)
+add_executable(hello main.c)
+target_link_libraries(hello libhello)
+```
+
+编译后在 `build/` 下同时得到可执行文件 `hello` 和静态库 `liblibhello.a`。
+
+- `add_library(libhello hello.c)`:生成库文件。第一个参数是**不含前后缀**的库名,Linux 下静态库自动加 `lib` + `.a`,动态库自动加 `lib` + `.so`。
+- 指定库类型:
+
+```cmake
+add_library(libhello SHARED hello.c)   # 生成动态库 liblibhello.so
+add_library(libhello STATIC hello.c)   # 生成静态库 liblibhello.a
+```
+
+- 想得到 `libhello.a` 而不是 `liblibhello.a`,用 `set_target_properties` 改输出名:
+
+```cmake
+set_target_properties(libhello PROPERTIES OUTPUT_NAME "hello")
+```
+
+> **目标名唯一**:`add_executable`、`add_library` 定义的目标名在整个工程内必须唯一,所以不能直接用 `add_library(hello hello.c)` 改名,只能用 `OUTPUT_NAME` 属性。
+
+### 2.5 示例四:源文件分目录 + `add_subdirectory`
+
+```text
+├── build/
+├── CMakeLists.txt
+├── libhello/
+│   ├── CMakeLists.txt
+│   ├── hello.c
+│   └── hello.h
+└── src/
+    ├── CMakeLists.txt
+    └── main.c
+```
+
+```cmake
+# 顶层 CMakeLists.txt
+cmake_minimum_required(VERSION 3.5)
+project(HELLO)
+add_subdirectory(libhello)
+add_subdirectory(src)
+```
+
+```cmake
+# src/CMakeLists.txt
+include_directories(${PROJECT_SOURCE_DIR}/libhello)
+add_executable(hello main.c)
+target_link_libraries(hello libhello)
+```
+
+```cmake
+# libhello/CMakeLists.txt
+add_library(libhello hello.c)
+set_target_properties(libhello PROPERTIES OUTPUT_NAME "hello")
+```
+
+`add_subdirectory(dir)` 告诉 CMake 去子目录寻找并解析新的 `CMakeLists.txt`;`PROJECT_SOURCE_DIR` 指工程顶层源码目录。
+
+### 2.6 示例五:把产物放到单独的目录
+
+```cmake
+# src/CMakeLists.txt
+include_directories(${PROJECT_SOURCE_DIR}/libhello)
+set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)   # 可执行文件输出路径
+add_executable(hello main.c)
+target_link_libraries(hello libhello)
+```
+
+```cmake
+# libhello/CMakeLists.txt
+set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)      # 库文件输出路径
+add_library(libhello hello.c)
+set_target_properties(libhello PROPERTIES OUTPUT_NAME "hello")
+```
+
+| 变量 | 作用 |
+| ---- | ---- |
+| `EXECUTABLE_OUTPUT_PATH` | 控制可执行文件输出目录 |
+| `LIBRARY_OUTPUT_PATH` | 控制库文件输出目录 |
+
+最终目录结构:
+
+```text
+build/
+├── bin/hello
+└── lib/libhello.a
+```
+
+---
+
+## 3. CMakeLists.txt 语法规则
+
+### 3.1 注释、命令、变量
+
+```cmake
+# 这是单行注释
+cmake_minimum_required(VERSION 3.5)
+project(HELLO)
+
+set(MY_VAL "Hello World!")   # 设置变量
+message(${MY_VAL})           # 用 ${} 引用变量
+```
+
+- 命令格式:`command(参数1 参数2 ...)`,参数用**空格**分隔(不是逗号)。命令名大小写不敏感(`project` 与 `PROJECT` 等价),内置变量习惯用大写以示区分。
+- 必要参数用 `<参数>` 表示,可选参数(选项)用 `[参数]` 表示。
+- 变量分为内置变量与自定义变量,引用一律用 `${变量名}`。
+
+### 3.2 常用命令一览
+
+| 命令 | 说明 |
+| ---- | ---- |
+| `add_executable` | 定义可执行程序目标 |
+| `add_library` | 定义库文件目标 |
+| `add_subdirectory` | 去指定目录中寻找新的 `CMakeLists.txt` |
+| `aux_source_directory` | 收集目录中的源文件名并赋值给变量 |
+| `cmake_minimum_required` | 设置 CMake 最低版本要求 |
+| `get_target_property` | 获取目标属性 |
+| `include_directories` | 设置**所有目标**的头文件搜索路径(≈ `gcc -I`) |
+| `link_directories` | 设置**所有目标**的库文件搜索路径(≈ `gcc -L`) |
+| `link_libraries` | 设置**所有目标**需要链接的库(≈ `gcc -l`) |
+| `list` | 列表相关操作 |
+| `message` | 打印输出信息 |
+| `project` | 设置工程名称 |
+| `set` | 设置变量 |
+| `set_target_properties` | 设置目标属性 |
+| `target_include_directories` | 设置**指定目标**的头文件搜索路径 |
+| `target_link_libraries` | 设置**指定目标**链接的库 |
+| `target_sources` | 设置指定目标所需的源文件 |
+| `target_link_directories` | 设置**指定目标**的库搜索路径 |
+
+### 3.3 常用命令详解
+
+#### 3.3.1 `add_executable`
+
+```cmake
+add_executable(<name> [WIN32] [MACOSX_BUNDLE] [EXCLUDE_FROM_ALL] source1 [source2 ...])
+```
+
+```cmake
+add_executable(hello 1.c 2.c 3.c)   # 生成可执行文件 hello
+```
+
+源文件路径可用相对路径(相对当前源码路径)或绝对路径。
+
+#### 3.3.2 `add_library`
+
+```cmake
+add_library(<name> [STATIC | SHARED | MODULE] [EXCLUDE_FROM_ALL] source1 [source2 ...])
+```
+
+```cmake
+add_library(mylib STATIC 1.c 2.c 3.c)   # 静态库 libmylib.a
+add_library(mylib SHARED 1.c 2.c 3.c)   # 动态库 libmylib.so
+```
+
+#### 3.3.3 `add_subdirectory`
+
+```cmake
+add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
+```
+
+- `source_dir`:子源码目录(必须有 `CMakeLists.txt`)。
+- `binary_dir`:子源码的输出文件目录(`BINARY_DIR`),可选。不指定时,默认在当前源码的 `BINARY_DIR` 下创建与子目录同名的文件夹。
+
+```cmake
+add_subdirectory(src)          # 子目录在源码树内,可省略 binary_dir
+add_subdirectory(src output)   # 指定子源码 BINARY_DIR 为 build/output
+add_subdirectory(../lib output)# 加载平级目录,必须显式指定 binary_dir
+```
+
+> 注意:加载**非当前源码子目录**(平级、上级)时,如果不显式指定 `binary_dir`,执行 `cmake` 会报错。相对路径的 `binary_dir` 是相对于**当前源码的 BINARY_DIR**,不是当前源码路径。
+
+#### 3.3.4 `aux_source_directory`
+
+```cmake
+aux_source_directory(<dir> <variable>)
+```
+
+扫描目录下所有源文件,存进变量,各元素用分号 `;` 分隔。
+
+```cmake
+aux_source_directory(src SRC_LIST)
+message("${SRC_LIST}")   # 加双引号才能看到列表全貌
+```
+
+#### 3.3.5 `include_directories`
+
+```cmake
+include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 ...])
+```
+
+相当于 `gcc -I`。默认添加到头文件搜索列表**末尾**,可用 `BEFORE`/`AFTER` 调整;设置 `CMAKE_INCLUDE_DIRECTORIES_BEFORE=ON` 可改变默认行为。调用 `add_subdirectory` 时,该列表会**向下传递**给子源码。
+
+#### 3.3.6 `link_directories` 与 `link_libraries`
+
+```cmake
+link_directories(directory1 directory2 ...)
+link_libraries([item1 [item2 [...]]] [[debug|optimized|general] <item>] ...)
+```
+
+```cmake
+include_directories(include)
+link_directories(lib)
+link_libraries(hello)                 # 简写
+link_libraries(libhello.so)           # 全称
+link_libraries(${PROJECT_SOURCE_DIR}/lib/libhello.so)   # 绝对路径
+```
+
+库文件搜索列表同样会向下传递给子源码。
+
+#### 3.3.7 `list`
+
+```cmake
+list(LENGTH <list> <output variable>)
+list(GET <list> <element index> [...] <output variable>)
+list(APPEND <list> [<element> ...])
+list(FIND <list> <value> <output variable>)
+list(INSERT <list> <element_index> <element> [...])
+list(REMOVE_ITEM <list> <value> [...])
+list(REMOVE_AT <list> <index> [...])
+list(REMOVE_DUPLICATES <list>)
+list(REVERSE <list>)
+list(SORT <list>)
+```
+
+示例:
+
+```cmake
+set(SRC_LIST main.c world.c hello.c)
+message("SRC_LIST: ${SRC_LIST}")
+
+list(LENGTH SRC_LIST L_LEN)
+message("列表长度: ${L_LEN}")
+
+list(GET SRC_LIST 1 VAR1)     # 取 index=1 的元素
+message("index=1: ${VAR1}")
+
+list(APPEND SRC_LIST hello_world.c)   # 追加
+list(SORT SRC_LIST)                   # 排序
+```
+
+#### 3.3.8 `message`
+
+```cmake
+message([<mode>] "message to display" ...)
+```
+
+| mode | 说明 |
+| ---- | ---- |
+| 无 | 重要信息、普通信息 |
+| `STATUS` | 附带信息 |
+| `WARNING` | CMake 警告,继续处理 |
+| `AUTHOR_WARNING` | CMake 警告(开发),继续处理 |
+| `SEND_ERROR` | CMake 错误,继续处理,但跳过生成 |
+| `FATAL_ERROR` | CMake 错误,停止处理和生成 |
+| `DEPRECATION` | 弃用错误/警告 |
+
+```cmake
+message("Hello World!")
+message(STATUS "CMake version: " ${CMAKE_VERSION})
+```
+
+#### 3.3.9 `project` 与 `set`
+
+```cmake
+project(HELLO)                 # 设置工程名称
+project(HELLO VERSION 1.1.0)   # 同时设置版本号
+```
+
+执行 `project(HELLO)` 后引入 `HELLO_SOURCE_DIR`、`HELLO_BINARY_DIR` 两个变量(前缀就是工程名)。CMake 还定义了两个等价的 `PROJECT_SOURCE_DIR`、`PROJECT_BINARY_DIR`,通常只用这两个,且只在顶层调用一次 `project`。
+
+```cmake
+set(<variable> <value>... [PARENT_SCOPE])   # 设置变量
+set(SRC_LIST 1.c 2.c 3.c 4.c 5.c)           # 字符串列表,元素以 ; 分隔
+set(BUILD_SHARED_LIBS on)                   # 改变 add_library 默认行为
+```
+
+#### 3.3.10 `target_include_directories` 与 `target_link_libraries`
+
+```cmake
+target_include_directories(<target> [SYSTEM] [BEFORE]
+    <INTERFACE|PUBLIC|PRIVATE> [items1...]
+    [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])
+
+target_link_libraries(<target>
+    <PRIVATE|PUBLIC|INTERFACE> <item>...
+    [<PRIVATE|PUBLIC|INTERFACE> <item>...]...)
+```
+
+它们与 `include_directories`/`link_libraries` 功能相同,但作用范围可控,只影响**指定目标**。关键是三个作用域关键字:
+
+| 关键字 | 当前目标是否使用 | 是否传递给依赖目标 | 等价关系 |
+| ------ | ---------------- | ------------------ | -------- |
+| `PRIVATE` | 使用 | 不传递 | 私有 |
+| `INTERFACE` | 不使用 | 传递 | 只给依赖者用 |
+| `PUBLIC` | 使用 | 传递 | `PRIVATE` + `INTERFACE` |
+
+```cmake
+# hello_world 内部用 hello,且 hello_world.h 也对外暴露 hello.h
+target_link_libraries(hello_world PUBLIC hello)
+target_include_directories(hello_world PUBLIC hello)
+```
+
+> 强烈建议统一使用 `target_include_directories()` / `target_link_libraries()`,而不是全局的 `include_directories()` / `link_libraries()`——后者作用于当前源码所有目标并向下传递,大工程里容易混乱、出错。保持工程目录清晰。
+
+`target_link_directories()` 则为**指定目标**设置库文件搜索路径(对应 `gcc -L`),只对该目标生效:
+
+```cmake
+target_link_directories(mqttClient PRIVATE /home/alientek/tools/paho.mqtt.c-1.3.8/install/lib)
+target_link_libraries(mqttClient PRIVATE paho-mqtt3c)
+```
+
+### 3.4 部分常用变量
+
+#### 3.4.1 提供信息的变量
+
+| 变量 | 说明 |
+| ---- | ---- |
+| `PROJECT_SOURCE_DIR` | 工程顶层目录(顶层 `CMakeLists.txt` 所在目录) |
+| `PROJECT_BINARY_DIR` | 工程 `BINARY_DIR`(顶层源码的输出目录) |
+| `CMAKE_SOURCE_DIR` | 与 `PROJECT_SOURCE_DIR` 等价 |
+| `CMAKE_BINARY_DIR` | 与 `PROJECT_BINARY_DIR` 等价 |
+| `CMAKE_CURRENT_SOURCE_DIR` | 当前源码所在路径 |
+| `CMAKE_CURRENT_BINARY_DIR` | 当前源码的 `BINARY_DIR` |
+| `CMAKE_MAJOR_VERSION` / `CMAKE_MINOR_VERSION` / `CMAKE_VERSION` | CMake 主/次/完整版本号 |
+| `PROJECT_VERSION` / `PROJECT_VERSION_MAJOR` / `PROJECT_VERSION_MINOR` | 工程版本号 |
+| `CMAKE_PROJECT_NAME` / `PROJECT_NAME` | 工程名(二者等价) |
+
+#### 3.4.2 改变行为的变量
+
+| 变量 | 说明 |
+| ---- | ---- |
+| `BUILD_SHARED_LIBS` | 控制 `add_library` 未显式指定时是否生成动态库 |
+| `CMAKE_BUILD_TYPE` | 构建类型,`Debug` 或 `Release` |
+| `CMAKE_SYSROOT` | 对应编译器 `--sysroot` 选项,交叉编译时使用 |
+| `CMAKE_IGNORE_PATH` | 被 `find_xxx` 忽略的目录列表 |
+| `CMAKE_INCLUDE_PATH` | `find_file()` / `find_path()` 的搜索路径 |
+| `CMAKE_INCLUDE_DIRECTORIES_BEFORE` | 控制 `include_directories()` 默认行为 |
+| `CMAKE_LIBRARY_PATH` | `find_library()` 的搜索路径 |
+| `CMAKE_MODULE_PATH` | `include()` / `find_package()` 加载模块的搜索路径 |
+| `CMAKE_PROGRAM_PATH` | `find_program()` 的搜索路径 |
+
+```cmake
+set(BUILD_SHARED_LIBS on)        # add_library 默认生成动态库
+set(CMAKE_BUILD_TYPE Debug)      # 带调试信息,可用 GDB
+set(CMAKE_INCLUDE_PATH ${PROJECT_SOURCE_DIR}/src)
+```
+
+`CMAKE_INCLUDE_PATH` 实例:
+
+```cmake
+set(CMAKE_INCLUDE_PATH ${PROJECT_SOURCE_DIR}/src)
+find_file(P_VAR hello.c)   # 找到后返回 hello.c 的全路径
+message(${P_VAR})
+```
+
+#### 3.4.3 描述系统的变量
+
+| 变量 | 说明 |
+| ---- | ---- |
+| `CMAKE_HOST_SYSTEM_NAME` | 运行 CMake 的操作系统名(`uname -s`) |
+| `CMAKE_HOST_SYSTEM_PROCESSOR` | 运行 CMake 的处理器名(`uname -p`) |
+| `CMAKE_HOST_SYSTEM` | 运行 CMake 的系统(复合信息) |
+| `CMAKE_HOST_SYSTEM_VERSION` | 运行 CMake 的系统版本(`uname -r`) |
+| `CMAKE_HOST_UNIX` / `UNIX` | 主机是类 UNIX 时为真 |
+| `CMAKE_HOST_WIN32` / `WIN32` | 主机是 Windows 时为真 |
+| `CMAKE_SYSTEM_NAME` | 目标主机的操作系统名 |
+| `CMAKE_SYSTEM_PROCESSOR` | 目标主机的处理器名 |
+| `CMAKE_SYSTEM` / `CMAKE_SYSTEM_VERSION` | 目标主机的复合信息 / 版本号 |
+| `ENV` | 访问环境变量,用法 `$ENV{VAR}` |
+
+```cmake
+message(${CMAKE_HOST_SYSTEM_NAME})
+message(${CMAKE_SYSTEM_NAME})
+message($ENV{XXX})              # 读取环境变量 XXX
+```
+
+#### 3.4.4 控制编译的变量
+
+| 变量 | 说明 |
+| ---- | ---- |
+| `EXECUTABLE_OUTPUT_PATH` | 可执行程序的输出路径 |
+| `LIBRARY_OUTPUT_PATH` | 库文件的输出路径 |
+
+默认情况下,最终目标文件的输出目录就是源码的 `BINARY_DIR`。
+
+### 3.5 双引号的作用
+
+**命令参数**:双引号把内容当成一个整体参数。
+
+```cmake
+message(Hello World)     # 两个参数,打印 HelloWorld
+message("Hello World")   # 一个参数,打印 Hello World
+```
+
+**引用变量**:`${VAR}` 不加引号时,列表元素被拆开、无分隔地打印;加引号时按整体处理,CMake 用分号保持列表语义。
+
+```cmake
+set(MY_LIST Hello World China)
+message(${MY_LIST})     # HelloWorldChina
+message("${MY_LIST}")   # Hello;World;China
+```
+
+### 3.6 条件判断 `if`
+
+```cmake
+if(expression)
+  # ...
+elseif(expression2)
+  # ...
+else()
+  # ...
+endif()
+```
+
+`else` / `endif` 括号中的表达式可写可不写,写了必须与 `if` 一致。常用表达式:
+
+| 表达式 | 为真条件 | 常用写法示例 |
+| ------ | -------- | ------------ |
+| `<constant>` | `1`/`ON`/`YES`/`TRUE`/`Y`/非零数字 | `if(ON)` |
+| `<variable\|string>` | 变量已定义且不为假常量 | `if(GG)` |
+| `NOT <expr>` | expr 为假 | `if(NOT GG)` |
+| `<e1> AND <e2>` | 两者同为真 | `if(yes AND on)` |
+| `<e1> OR <e2>` | 至少一个为真 | `if(yes OR no)` |
+| `COMMAND name` | name 是已定义的命令/宏/函数 | `if(COMMAND project)` |
+| `TARGET name` | name 是已定义的目标 | `if(TARGET hello)` |
+| `EXISTS path` | 文件或目录存在(需绝对路径) | `if(EXISTS ${PROJECT_BINARY_DIR})` |
+| `IS_DIRECTORY path` | path 是目录 | `if(IS_DIRECTORY ${PROJECT_BINARY_DIR}/hello)` |
+| `IS_ABSOLUTE path` | path 是绝对路径 | `if(IS_ABSOLUTE ${PROJECT_BINARY_DIR})` |
+| `<var\|str> MATCHES regex` | 正则匹配成功 | `if(MY_STR MATCHES "Hello World")` |
+| `<var\|str> IN_LIST <var>` | 元素在列表中 | `if(Hello IN_LIST MY_LIST)` |
+| `DEFINED <var>` | 变量已定义(值真假无关) | `if(DEFINED yyds)` |
+| `<a> LESS/GREATER/EQUAL <b>` | 数值比较 | `if(20 LESS 100)` |
+
+常量真假规则:`1`、`ON`、`YES`、`TRUE`、`Y`、非零数字为真;`0`、`OFF`、`NO`、`FALSE`、`N`、`IGNORE`、`NOTFOUND`、空字符串、以 `-NOTFOUND` 结尾为假,大小写不敏感。不匹配这些常量时,才当作变量或字符串处理。
+
+### 3.7 循环与数学运算
+
+#### 3.7.1 `foreach`
+
+```cmake
+foreach(loop_var arg1 arg2 ...)     # 遍历参数列表
+    message("${loop_var}")
+endforeach()
+
+set(my_list A B C D)
+foreach(loop_var ${my_list})        # 遍历列表
+endforeach()
+
+foreach(loop_var RANGE 4)           # 0..4
+foreach(loop_var RANGE 1 4 1)       # start stop step
+
+foreach(loop_var IN LISTS my_list)  # 遍历列表
+foreach(loop_var IN ITEMS A B C D)  # 遍历显式元素
+```
+
+#### 3.7.2 `while`、`break`、`continue` 与 `math`
+
+```cmake
+set(loop_var 4)
+while(loop_var GREATER 0)
+    message("${loop_var}")
+    math(EXPR loop_var "${loop_var} - 1")
+endwhile()
+
+# break / continue
+while(loop_var GREATER 0)
+    if(loop_var LESS 6)
+        break()        # 跳出循环
+    endif()
+    math(EXPR loop_var "${loop_var} - 1")
+endwhile()
+
+# 打印偶数
+while(loop_var GREATER 0)
+    math(EXPR var "${loop_var} % 2")
+    if(var EQUAL 0)
+        message("${loop_var}")
+        math(EXPR loop_var "${loop_var} - 1")
+        continue()     # 进入下一次循环
+    endif()
+    math(EXPR loop_var "${loop_var} - 1")
+endwhile()
+```
+
+`math()` 支持 `+ - * / %`、`| & ^ ~ << >>` 及组合运算,含义与 C 语言相同:
+
+```cmake
+math(EXPR out_var "100 + 100")
+math(EXPR out_var "(100 & 100) * 50 - 2")
+```
+
+### 3.8 函数与宏
+
+#### 3.8.1 `function`
+
+```cmake
+function(<name> [arg1 [arg2 ...]])
+    # ...
+    return()      # 可提前退出,但 return 不能返回参数
+endfunction()
+```
+
+调用函数时实际参数个数**可以多于**定义个数,甚至定义 0 个也行。函数内部内置变量:
+
+| 内部变量 | 说明 |
+| -------- | ---- |
+| `ARGVX` | 第 X 个参数,如 `ARGV0`、`ARGV1` |
+| `ARGV` | 实际传入的所有参数(列表) |
+| `ARGN` | 超出形参个数的剩余参数(列表) |
+| `ARGC` | 实际传入的参数个数 |
+
+```cmake
+function(xyz arg1 arg2)
+    message("ARGC: ${ARGC}")
+    message("ARGV: ${ARGV}")
+    message("ARGN: ${ARGN}")
+    message("ARGV0: ${ARGV0}")
+endfunction()
+
+xyz(A B C D E F G)
+```
+
+函数作用域是**全局的**:父源码定义的函数子源码能用,子源码定义的函数父源码也能用(调用前需已定义)。
+
+#### 3.8.2 `macro`
+
+```cmake
+macro(<name> [arg1 [arg2 ...]])
+    # ...
+endmacro()
+```
+
+宏与函数用法相似,也支持 `ARGVX`/`ARGC`/`ARGV`/`ARGN`,但**宏是字符串替换**,其参数与这些值不是变量:
+
+```cmake
+macro(abc arg1 arg2)
+    if(DEFINED ARGC)     # 宏里 ARGC 被替换成数字 4,故不成立
+        message(true)
+    else()
+        message(false)
+    endif()
+endmacro()
+
+function(xyz arg1 arg2)
+    if(DEFINED ARGC)     # 函数里 ARGC 是变量,成立
+        message(true)
+    else()
+        message(false)
+    endif()
+endfunction()
+```
+
+区别:宏无作用域概念、纯文本替换;函数有自己的作用域。
+
+### 3.9 变量的作用域
+
+CMake 有三种作用域:
+
+```mermaid
+flowchart TB
+    G["全局作用域<br/>缓存变量 / -D 定义的变量"]
+    D1["目录作用域<br/>顶层 CMakeLists"]
+    D2["目录作用域<br/>子目录 CMakeLists<br/>(值拷贝)"]
+    F["函数作用域<br/>function 内部"]
+
+    G --> D1 --> D2
+    D1 --> F
+
+    classDef scope fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
+    class G,D1,D2,F scope
+```
+
+**函数作用域**:函数内 `set` 只创建函数内的变量;引用未在函数内定义的变量时,逐层向外查找。要在函数内修改外部变量,`set` 末尾加 `PARENT_SCOPE`(设置到上一层作用域):
+
+```cmake
+function(xyz)
+    set(ABC "Hello China!" PARENT_SCOPE)
+endfunction()
+
+set(ABC "Hello World!")
+xyz()
+message("${ABC}")   # Hello China!
+```
+
+利用 `PARENT_SCOPE` 可以实现函数"返回值"——把变量名当参数传入,在函数里用该名字 `set` 到上层:
+
+```cmake
+function(xyz out var1 var2)
+    math(EXPR temp "${var1} + ${var2}")
+    set(${out} ${temp} PARENT_SCOPE)
+endfunction()
+
+xyz(out_var 5 10)
+message("${out_var}")   # 15
+```
+
+**目录作用域**:子目录会把父目录变量**值拷贝**一份,子目录内 `set` 不影响父目录(向下有效、值拷贝)。
+
+**全局作用域**:缓存变量在整个工程生命周期有效,可用 `set(... CACHE ...)` 或命令行 `-D` 定义:
+
+```bash
+cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/arm-linux-setup.cmake ..
+```
+
+`-D` 创建的缓存变量是全局变量,会覆盖 `CMakeLists.txt` 中定义的同名变量。
+
+### 3.10 属性
+
+属性分为全局属性、目录属性(源码属性)、目标属性等。常用命令:
+
+```cmake
+get_directory_property(<variable> [DIRECTORY <dir>] <prop-name>)
+set_directory_properties(PROPERTIES prop1 value1 prop2 value2)
+get_target_property(<variable> <target> <prop-name>)
+set_target_properties(<target> PROPERTIES prop1 value1 ...)
+```
+
+- 目录属性:`INCLUDE_DIRECTORIES`(`include_directories()` 添加的目录)、`LINK_DIRECTORIES`(`link_directories()` 添加的目录)、`CACHE_VARIABLES`、`VARIABLES`、`MACROS`、`PARENT_DIRECTORY` 等。用 `set_directory_properties` 设置时必须用**绝对路径**。
+- 目标属性:`OUTPUT_NAME`、`TYPE`(`STATIC_LIBRARY`/`SHARED_LIBRARY`/`EXECUTABLE` 等)、`INCLUDE_DIRECTORIES`、`INTERFACE_INCLUDE_DIRECTORIES`、`INTERFACE_LINK_LIBRARIES`、`LINK_LIBRARIES`、`BINARY_DIR`、`SOURCE_DIR` 等。
+
+```cmake
+include_directories(include)
+get_directory_property(out_var INCLUDE_DIRECTORIES)
+message("${out_var}")
+
+add_library(mylib STATIC mylib.c)
+get_target_property(type mylib TYPE)
+message("${type}")     # STATIC_LIBRARY
+```
+
+### 3.11 `file()` 文件操作
+
+| 用法 | 说明 |
+| ---- | ---- |
+| `file(WRITE <file> <content>...)` | 写文件,存在则覆盖 |
+| `file(APPEND <file> <content>...)` | 追加到文件末尾 |
+| `file(GENERATE OUTPUT <out> INPUT <in> \| CONTENT <c> [CONDITION <expr>])` | 由内容/输入文件生成文件 |
+| `file(READ <file> <var> [OFFSET <o>] [LIMIT <n>] [HEX])` | 按字节读取 |
+| `file(STRINGS <file> <var> [options...])` | 按字符串列表读取(忽略二进制、CR) |
+| `file(<MD5\|SHA1\|SHA256\|...> <file> <var>)` | 计算 hash 值 |
+| `file(RENAME <old> <new>)` | 重命名 |
+| `file(REMOVE <files>...)` | 删除文件 |
+| `file(REMOVE_RECURSE <files>...)` | 删除文件或目录(含非空目录) |
+
+```cmake
+file(WRITE wtest.txt "Hello World!")
+file(APPEND wtest.txt " China")
+
+file(GENERATE OUTPUT out1.txt INPUT "${PROJECT_SOURCE_DIR}/wtest.txt")
+file(GENERATE OUTPUT out2.txt CONTENT "This is the out2.txt file")
+
+file(READ "${PROJECT_SOURCE_DIR}/wtest.txt" out_var)
+file(READ "${PROJECT_SOURCE_DIR}/wtest.txt" out_var OFFSET 0 LIMIT 10)
+file(STRINGS "${PROJECT_SOURCE_DIR}/input.txt" out_var LENGTH_MAXIMUM 5)
+file(SHA256 "${PROJECT_SOURCE_DIR}/input.txt" out_var)
+file(RENAME "${PROJECT_SOURCE_DIR}/input.txt" "${PROJECT_SOURCE_DIR}/output.txt")
+file(REMOVE "${PROJECT_SOURCE_DIR}/out1.txt")
+file(REMOVE_RECURSE "${PROJECT_SOURCE_DIR}/Non_empty-dir")
+```
+
+> 路径规则:`WRITE`/`APPEND`/`READ`/`STRINGS`/`RENAME`/`REMOVE` 的相对路径相对于**当前源码路径**;`GENERATE` 与 hash 计算的相对路径相对于**当前源码的 BINARY_DIR**。`STRINGS` 的常用选项包括 `LENGTH_MAXIMUM`、`LENGTH_MINIMUM`、`LIMIT_COUNT`、`LIMIT_INPUT`、`LIMIT_OUTPUT`、`NEWLINE_CONSUME`、`NO_HEX_CONVERSION`、`REGEX`、`ENCODING`。
+
+---
+
+## 4. 生成静态库与动态库
+
+```cmake
+# 方式一:命令中显式指定
+add_library(mylib STATIC 1.c 2.c 3.c)   # libmylib.a
+add_library(mylib SHARED 1.c 2.c 3.c)   # libmylib.so
+
+# 方式二:用 BUILD_SHARED_LIBS 改变默认行为
+set(BUILD_SHARED_LIBS on)
+add_library(hello hello/hello.c)        # 生成 libhello.so
+add_library(world world/world.c)        # 生成 libworld.so
+```
+
+| 库类型 | 选项 | 产物前缀/后缀 | 链接方式 |
+| ------ | ---- | ------------- | -------- |
+| 静态库 | `STATIC`(默认) | `lib` + `.a` | 链接时把代码复制进可执行文件 |
+| 动态库 | `SHARED` | `lib` + `.so` | 运行时加载,需保证目标机有对应 `.so` |
+| 模块库 | `MODULE` | 视平台而定 | 供运行时动态加载,一般不参与链接 |
+
+想要自定义输出文件名,用属性而非目标名:
+
+```cmake
+set_target_properties(mylib PROPERTIES OUTPUT_NAME "hello")   # libhello.a / libhello.so
+```
+
+---
+
+## 5. 交叉编译
+
+不配置交叉编译时,CMake 默认用主机的 gcc,产物只能在 Ubuntu 上运行。要让产物跑在 ARM 开发板上,需要设置若干变量。I.MX6U 使用的交叉编译器为:
+
+```bash
+arm-poky-linux-gnueabi-gcc     # C 编译器
+arm-poky-linux-gnueabi-g++     # C++ 编译器
+```
+
+### 5.1 工具链文件 `arm-linux-setup.cmake`
+
+```cmake
+##################################
+# 配置 ARM 交叉编译
+#################################
+set(CMAKE_SYSTEM_NAME Linux)    # 设置目标系统名字
+set(CMAKE_SYSTEM_PROCESSOR arm) # 设置目标处理器架构
+
+# 指定编译器的 sysroot 路径
+set(TOOLCHAIN_DIR /opt/fsl-imx-x11/4.1.15-2.1.0/sysroots)
+set(CMAKE_SYSROOT ${TOOLCHAIN_DIR}/cortexa7hf-neon-poky-linux-gnueabi)
+
+# 指定交叉编译器 arm-linux-gcc 和 arm-linux-g++
+set(CMAKE_C_COMPILER ${TOOLCHAIN_DIR}/x86_64-pokysdk-linux/usr/bin/arm-poky-linux-gnueabi/arm-poky-linux-gnueabi-gcc)
+set(CMAKE_CXX_COMPILER ${TOOLCHAIN_DIR}/x86_64-pokysdk-linux/usr/bin/arm-poky-linux-gnueabi/arm-poky-linux-gnueabi-g++)
+
+# 为编译器添加编译选项
+set(CMAKE_C_FLAGS "-march=armv7ve -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a7")
+set(CMAKE_CXX_FLAGS "-march=armv7ve -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a7")
+
+set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
+set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
+#################################
+# end
+##################################
+```
+
+各变量含义:
+
+| 变量 | 含义 |
+| ---- | ---- |
+| `CMAKE_SYSTEM_NAME` | 目标主机操作系统名,`Linux` 表示目标是 Linux 系统 |
+| `CMAKE_SYSTEM_PROCESSOR` | 目标架构名,`arm` |
+| `CMAKE_SYSROOT` | 传给 gcc 的 `--sysroot` 选项,编译时去该目录找标准库与头文件 |
+| `CMAKE_C_COMPILER` | C 编译器(交叉编译时指向 arm-gcc) |
+| `CMAKE_CXX_COMPILER` | C++ 编译器(交叉编译时指向 arm-g++) |
+| `CMAKE_C_FLAGS` / `CMAKE_CXX_FLAGS` | 分别为 C/C++ 编译器追加编译选项 |
+| `CMAKE_FIND_ROOT_PATH_MODE_LIBRARY` | `ONLY` 表示 `find_library()` 只在 `CMAKE_SYSROOT` 中搜索;`NEVER` 只用主机路径;`BOTH` 都搜 |
+| `CMAKE_FIND_ROOT_PATH_MODE_INCLUDE` | 控制 `find_file()`/`find_path()` 是否使用 `CMAKE_SYSROOT`,取值同上 |
+| `CMAKE_FIND_ROOT_PATH_MODE_PROGRAM` | 控制查找**可执行程序**时使用主机还是 sysroot 路径;通常设为 `NEVER`,即用主机工具 |
+
+### 5.2 两种使用方式
+
+**方式一(不推荐)**:把配置直接写进 `CMakeLists.txt`,且必须放在 `project()` 之前,否则不生效。
+
+**方式二(推荐)**:单独写成工具链文件,用 `-DCMAKE_TOOLCHAIN_FILE` 指定:
+
+```bash
+cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/arm-linux-setup.cmake -DCMAKE_BUILD_TYPE=Release ..
+make
+```
+
+`-D` 创建缓存变量,`CMAKE_TOOLCHAIN_FILE` 的值就是工具链文件路径,CMake 会执行它来配置交叉编译。
+
+### 5.3 验证
+
+```bash
+file main    # 应显示 ARM 架构,而不是 x86-64
+```
+
+---
+
+## 6. 完整可复制工程示例
+
+下面是一个多目录工程,综合运用 `add_subdirectory`、库目标、目标级属性、输出路径与交叉编译工具链。
+
+```text
+cmake_demo/
+├── CMakeLists.txt              # 顶层
+├── cmake/
+│   └── arm-linux-setup.cmake   # 交叉编译工具链文件(第 5.1 节)
+├── include/
+│   └── demo.h                  # 对外公共头文件
+├── src/
+│   ├── CMakeLists.txt
+│   └── main.c
+└── lib/
+    ├── hello/
+    │   ├── CMakeLists.txt
+    │   ├── hello.c
+    │   └── hello.h
+    └── math/
+        ├── CMakeLists.txt
+        ├── math.c
+        └── math.h
+```
+
+**顶层 `CMakeLists.txt`**
+
+```cmake
+cmake_minimum_required(VERSION 3.5)
+project(cmake_demo C VERSION 1.0.0)
+
+# 输出路径
+set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
+set(LIBRARY_OUTPUT_PATH    ${PROJECT_BINARY_DIR}/lib)
+
+# 公共头文件目录(只影响下面的目标,靠目标级命令传递)
+include_directories(${PROJECT_SOURCE_DIR}/include)
+
+message(STATUS "CMake version: " ${CMAKE_VERSION})
+message(STATUS "Project source: " ${PROJECT_SOURCE_DIR})
+message(STATUS "Project binary: " ${PROJECT_BINARY_DIR})
+
+# 两个库子目录 + 一个应用子目录
+add_subdirectory(lib/hello)
+add_subdirectory(lib/math)
+add_subdirectory(src)
+```
+
+**`include/demo.h`**
+
+```c
+#ifndef __DEMO_H_
+#define __DEMO_H_
+
+#define DEMO_VERSION "1.0.0"
+
+#endif
+```
+
+**`lib/hello/hello.h` 与 `hello.c`**
+
+```c
+#ifndef __HELLO_H_
+#define __HELLO_H_
+
+void hello(const char *name);
+
+#endif
+```
+
+```c
+#include <stdio.h>
+#include "hello.h"
+
+void hello(const char *name)
+{
+    printf("Hello %s!\n", name);
+}
+```
+
+**`lib/hello/CMakeLists.txt`**
+
+```cmake
+# 生成静态库 libhello.a
+add_library(hello STATIC hello.c)
+
+# 头文件目录:本目标自己用,也传给依赖者
+target_include_directories(hello PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
+```
+
+**`lib/math/math.h` 与 `math.c`**
+
+```c
+#ifndef __MATH_H_
+#define __MATH_H_
+
+int add(int a, int b);
+int mul(int a, int b);
+
+#endif
+```
+
+```c
+#include "math.h"
+
+int add(int a, int b) { return a + b; }
+int mul(int a, int b) { return a * b; }
+```
+
+**`lib/math/CMakeLists.txt`**
+
+```cmake
+add_library(math STATIC math.c)
+target_include_directories(math PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
+```
+
+**`src/main.c`**
+
+```c
+#include <stdio.h>
+#include "demo.h"
+#include "hello.h"
+#include "math.h"
+
+int main(void)
+{
+    hello("CMake");
+    printf("demo version: %s\n", DEMO_VERSION);
+    printf("3 + 5 = %d\n", add(3, 5));
+    printf("3 * 5 = %d\n", mul(3, 5));
+    return 0;
+}
+```
+
+**`src/CMakeLists.txt`**
+
+```cmake
+add_executable(cmake_demo main.c)
+
+# 链接两个库,库的 PUBLIC 头文件目录会自动带过来
+target_link_libraries(cmake_demo PRIVATE hello math)
+
+# 如需额外头文件搜索路径(示例,仅本目标生效):
+# target_include_directories(cmake_demo PRIVATE ${PROJECT_SOURCE_DIR}/include)
+```
+
+### 6.1 构建(主机验证)
+
+```bash
+mkdir -p build && cd build
+cmake -DCMAKE_BUILD_TYPE=Release ..
+make
+tree .
+```
+
+预期产物:
+
+```text
+build/
+├── bin/cmake_demo
+└── lib/
+    ├── libhello.a
+    └── libmath.a
+```
+
+### 6.2 交叉编译(ARM 开发板)
+
+```bash
+cd build
+rm -rf *
+cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/arm-linux-setup.cmake \
+      -DCMAKE_BUILD_TYPE=Release ..
+make
+file bin/cmake_demo      # 应为 ARM 架构
+```
+
+把 `bin/cmake_demo` 拷贝到开发板 `/home/root` 下运行即可。
+
+### 6.3 扩展:`option()` 与 `install()`
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+```cmake
+# option() 定义一个布尔缓存变量,默认值 OFF;命令行 -DENABLE_DEBUG=ON 可覆盖
+option(ENABLE_DEBUG "Enable debug build" OFF)
+if(ENABLE_DEBUG)
+    set(CMAKE_BUILD_TYPE Debug)
+    add_definitions(-DDEBUG)
+else()
+    set(CMAKE_BUILD_TYPE Release)
+endif()
+
+# install() 定义安装规则,配合 make install 使用
+# CMAKE_INSTALL_PREFIX 默认 /usr/local,可用 -DCMAKE_INSTALL_PREFIX=... 覆盖
+install(TARGETS cmake_demo RUNTIME DESTINATION bin)
+install(TARGETS hello math ARCHIVE DESTINATION lib)
+install(FILES include/demo.h DESTINATION include)
+
+# file(GLOB) 通配收集源文件(不推荐用于正式工程)
+file(GLOB SRC_FILES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/src/*.c)
+# 注意:CMake 官方建议显式列出源文件,GLOB 在新增文件时不一定触发重新配置。
+```
+
+---
+
+## 7. 实验步骤与调试
+
+| 现象 | 原因 | 处理 |
+| ---- | ---- | ---- |
+| `cmake` 报交叉编译配置相关错误 | Ubuntu 自带 CMake 版本太旧(如 3.5.1) | 换用 CMake 3.16.0(解压即用的二进制包) |
+| 交叉编译不生效 | 配置写进了 `CMakeLists.txt` 但放在了 `project()` 之后 | 交叉编译配置必须在 `project()` 之前,或改用工具链文件 |
+| `add_subdirectory(../lib)` 报错 | 加载平级/上级目录未指定 `binary_dir` | 显式写 `add_subdirectory(../lib output)` |
+| 找不到头文件 | 未设置头文件搜索路径,或未向下/向上传递 | 用 `target_include_directories(... PUBLIC ...)` |
+| 生成的库名是 `liblibhello.a` | 库目标名与 `OUTPUT_NAME` 混用 | 用 `set_target_properties(... PROPERTIES OUTPUT_NAME "hello")` |
+| `make` 找不到目标 | 没在 `build` 目录执行 `cmake` | 回到 `build` 目录,先 `cmake ..` 再 `make` |
+| 想彻底重来 | 缓存了旧变量 | 删除 `build` 目录(或 `CMakeCache.txt`)后重新 `cmake` |
+
+推荐构建流程:
+
+```bash
+cd build && rm -rf ./*
+cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/arm-linux-setup.cmake -DCMAKE_BUILD_TYPE=Release ..
+make
+```
+
+---
+
+## 8. 跨平台 / 工具对比
+
+| 维度 | 手写 gcc 命令 | Makefile | CMake | Qt qmake |
+| ---- | ------------- | -------- | ----- | -------- |
+| 跨平台 | 差 | 一般 | 好 | 好 |
+| 学习成本 | 低 | 中 | 中 | 低(Qt 专用) |
+| 依赖管理 | 无 | 手工 | 自动扫描 + 目标属性 | `QT +=` 模块 |
+| 大型工程 | 不可行 | 尚可 | 推荐 | Qt 工程推荐 |
+| 与 IDE 集成 | 差 | 一般 | 好(生成工程文件) | 好 |
+| 典型场景 | 单文件验证 | 小型 C 工程 | 中大型跨平台 C/C++ | Qt 应用 |
+
+对同一份多文件源码,三种方式的对比:
+
+```bash
+# 1) 手写 gcc
+gcc -Iinclude src/main.c lib/hello/hello.c lib/math/math.c -o demo
+
+# 2) Makefile(节选)
+demo: src/main.o lib/hello/hello.o lib/math/math.o
+	$(CC) $^ -o $@
+
+# 3) CMake
+cmake -B build && cmake --build build
+```
+
+---
+
+## 9. 面试精选(5 题)
+
+### Q1 CMake 和 Makefile 的关系是什么?为什么说 CMake 跨平台?
+
+**要点**:CMake 不直接编译,而是解析 `CMakeLists.txt` 生成本地化的 `Makefile`(或工程文件),最终仍由 `make` 编译。
+
+**详解**:`CMakeLists.txt` 与平台无关,CMake 根据当前平台/工具链生成对应构建脚本;Makefile 的语法和规则在不同平台往往不同,不能通用。因此跨平台的本质是"用统一描述文件 + 生成器适配各平台",而真正干活的仍是 `make`。
+
+**追问**:
+1. 为什么不直接写 Makefile?(语法复杂、跨平台差、大工程依赖难维护)
+2. `cmake` 和 `make` 分别负责什么?(cmake 生成构建文件,make 依据构建文件执行编译)
+
+### Q2 `include_directories` 与 `target_include_directories` 有什么区别?该用哪个?
+
+**要点**:前者对当前源码**所有目标**生效并向下传递;后者只对**指定目标**生效,作用域由 `PRIVATE/INTERFACE/PUBLIC` 控制。
+
+**详解**:`target_include_directories(t PRIVATE d)` 只给 `t` 用;`INTERFACE` 只传给依赖 `t` 的目标;`PUBLIC` 两者都传。全局命令在大工程里容易污染其它目标、产生隐蔽错误,因此推荐目标级命令以保持目录清晰。
+
+**追问**:
+1. `PRIVATE` 和 `PUBLIC` 的差别在哪?(头文件目录是否传递给依赖该目标的目标)
+2. 静态库的 `PUBLIC` 头文件目录会怎样传给可执行文件?(通过链接关系自动带入)
+
+### Q3 CMake 中如何生成静态库和动态库?如何自定义库文件名?
+
+**要点**:`add_library(name [STATIC|SHARED] src)`;用 `set_target_properties(NAME PROPERTIES OUTPUT_NAME ...)` 改名。
+
+**详解**:`add_library` 默认生成静态库,`SHARED` 生成动态库;库名不含 `lib` 前缀与 `.a/.so` 后缀,由 CMake 自动补齐。目标名工程内必须唯一,所以不能用目标名去改文件名,只能用 `OUTPUT_NAME` 属性。
+
+**追问**:
+1. `BUILD_SHARED_LIBS` 的作用?(设为 `on` 时 `add_library` 默认生成动态库)
+2. 动态库拷到开发板后程序运行报 "cannot open shared object file" 怎么办?(把 `.so` 放到 `/usr/lib` 或设置 `LD_LIBRARY_PATH`)
+
+### Q4 如何为 CMake 工程配置 ARM 交叉编译?
+
+**要点**:编写工具链文件,设置 `CMAKE_SYSTEM_NAME`、`CMAKE_SYSTEM_PROCESSOR`、`CMAKE_SYSROOT`、`CMAKE_C_COMPILER` 等,再用 `-DCMAKE_TOOLCHAIN_FILE=` 指定。
+
+**详解**:配置不要写进 `CMakeLists.txt`,而是放到独立工具链文件,且必须在 `project()` 之前生效。示例见本篇第 5.1 节。`CMAKE_SYSROOT` 会作为 `--sysroot` 传给 gcc,`CMAKE_FIND_ROOT_PATH_MODE_LIBRARY/INCLUDE` 设为 `ONLY` 可保证只搜 sysroot。
+
+**追问**:
+1. `-D` 选项创建的是什么变量?(缓存变量,全局生效,覆盖同名普通变量)
+2. 怎么验证交叉编译成功?(`file` 命令查看产物架构,应为 ARM 而非 x86-64)
+
+### Q5 CMake 变量有哪些作用域?函数内如何修改外部变量?
+
+**要点**:函数作用域、目录作用域(值拷贝、向下有效)、全局作用域(缓存变量);函数内用 `PARENT_SCOPE` 修改上层变量。
+
+**详解**:函数内 `set` 默认创建局部变量,不改变外层同名变量;加 `PARENT_SCOPE` 后设置到上一层作用域,可借"传入变量名"的技巧实现返回值。目录之间是值拷贝,子目录改父目录变量无效。`-D` 定义的缓存变量全局有效。
+
+**追问**:
+1. `PARENT_SCOPE` 在嵌套函数中写到哪一层?(调用者的作用域,即上一层)
+2. 为什么每次 `cmake` 都要清理 `build`?(缓存变量会保留并覆盖新配置,可能导致旧设置残留)
+
+---
+
+**内容来源**:《I.MX6U嵌入式Linux C应用编程指南V1.6》第三十二章 CMake入门与进阶;例程 `33_mqtt/mqtt_prj/CMakeLists.txt`、`33_mqtt/mqtt_prj/cmake/arm-linux-setup.cmake`。第 6.3 节 `option()`/`install()`/`file(GLOB)` 为扩展知识。

+ 1543 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/04-网络编程与项目实战/04-实战项目MQTT与视频监控.md

@@ -0,0 +1,1543 @@
+---
+title: 实战项目MQTT与视频监控
+tags: [嵌入式Linux, 项目实战, MQTT, mosquitto, paho.mqtt.c, MQTTClient, 发布订阅, QoS, 遗嘱, 保留消息, 物联网, RTMP, Nginx, FFmpeg, VLC, 视频监控, V4L2, MJPEG, Qt, IMX6ULL]
+created: 2026-09-18
+updated: 2026-09-18
+pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第三十三章 实战小项目之MQTT物联网、第三十四章 实战小项目之视频监控;《嵌入式Qt开发指南》视频监控例程"
+---
+
+# 实战项目MQTT与视频监控
+
+> 💡 **关联知识**:[[04-网络编程与项目实战/01-网络基础与socket编程]]、[[04-网络编程与项目实战/03-CMake入门与进阶]];延伸阅读:[[Linux+C+C++技术体系梳理/2. Linux系统编程/15. TCP编程]]、[[04-网络编程与项目实战/面试-网络编程与项目实战]]
+
+本篇把前面的 socket、文件 I/O、CMake 知识落到两个完整项目上:**MQTT 物联网**(开发板作为客户端,远程控制 LED、上报 SoC 温度)和**视频监控**(两条技术路线:Nginx + FFmpeg 的 RTMP 推拉流,以及 V4L2 采集 + JPEG 广播的局域网 MJPEG 方案)。每个项目都给出移植步骤、完整可复制源码与测试方法。
+
+---
+
+# 第一部分:MQTT 物联网
+
+## 1. MQTT 简介
+
+MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是一种基于**客户端-服务端架构的发布/订阅模式**消息传输协议,与 HTTP 一样工作在 TCP/IP 四层模型的**应用层**,构建于 TCP/IP 之上。它的设计思想是轻巧、开放、简单、规范,易于实现,因此特别适合机器与机器(M2M)、物联网(IoT)等受限环境。
+
+### 1.1 主要特性
+
+| 特性 | 说明 |
+| ---- | ---- |
+| 发布/订阅模式 | 一对多的消息发布,解除应用程序耦合 |
+| 基于 TCP/IP | 主流 MQTT 基于 TCP;也有基于 UDP 的 MQTT-SN |
+| 支持 QoS | 按消息重要性设置不同服务质量等级(0/1/2) |
+| 小型传输 | 开销很小,协议交换最小化,降低网络流量 |
+| 遗嘱机制(will) | 客户端异常断线时通知其它客户端 |
+| 主题寻址 | 基于主题发布/订阅,对负载内容屏蔽 |
+| 心跳机制 | 检测客户端是否在线 |
+
+### 1.2 历史与版本
+
+- 1999 年由 Andy Stanford-Clark 和 Arlen Nipper 发明,最初用于石油管道传感器与卫星之间的数据传输。
+- 面向"低带宽、高延迟或不可靠网络",要求:易于实现、服务质量可控、占用带宽小、能适应掉线断网、云端与设备保持长连接。
+- 主流版本:**MQTT 3.1.1**(2014-10,经典主流)与 **MQTT 5**(2019-03,完全兼容 3.1.1 并增加新功能)。
+- MQTT 更适合需要实时控制的场合(尤其执行器);由于要保持长连接、定时发心跳,**并不适合低功耗场合**。
+
+## 2. MQTT 协议原理
+
+### 2.1 三个核心角色
+
+| 角色 | 说明 |
+| ---- | ---- |
+| 服务端(Broker) | MQTT 信息传输的枢纽,负责把客户端发来的信息转发给其它客户端,并管理客户端 |
+| 客户端 | 可以发布消息,也可以订阅主题后接收消息;角色不固定,随主题切换 |
+| 主题(Topic) | 发布/订阅的纽带,服务端按主题决定消息转发给哪些订阅者 |
+
+客户端发布消息时要指定主题;订阅者订阅主题后,服务端就把该主题的新消息推送给订阅者。一个客户端对不同主题可以既是发布者又是订阅者。
+
+```mermaid
+flowchart LR
+    subgraph P["发布者"]
+        Dev["开发板客户端"]
+    end
+    subgraph B["MQTT 服务端 Broker"]
+        T1["主题: dt_mqtt/temperature"]
+        T2["主题: dt_mqtt/led"]
+    end
+    subgraph S["订阅者"]
+        Phone["手机客户端"]
+        PC["电脑 MQTT.fx"]
+    end
+
+    Dev -->|"发布 temperature"| T1
+    T1 --> Phone
+    T1 --> PC
+    Phone -->|"发布 led=1/0/2"| T2
+    PC -->|"发布 led=1/0/2"| T2
+    T2 --> Dev
+
+    classDef pub fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
+    classDef broker fill:#fef9c3,stroke:#ca8a04,color:#713f12
+    classDef sub fill:#dcfce7,stroke:#16a34a,color:#14532d
+    class Dev pub
+    class T1,T2 broker
+    class Phone,PC sub
+```
+
+发布/订阅的三个特性都带一个"可"字,体现灵活性:
+
+- **客户端相互独立**:发布者不知道有多少订阅者,订阅者之间也不知道彼此存在。
+- **空间上可分离**:双方只要联网,无论远近都能通信。
+- **时间上可异步**:发送与接收无需同步,服务端可暂存离线消息。
+
+### 2.2 连接服务端:CONNECT 与 CONNACK
+
+客户端连接服务端分两步:客户端发 **CONNECT** 报文 → 服务端回 **CONNACK** 报文。MQTT 报文由**固定头、可变头、消息体(Payload)**三部分组成。
+
+CONNECT 报文关键字段:
+
+| 字段 | 说明 |
+| ---- | ---- |
+| `clientId` | 客户端标识,必须唯一;两个客户端用同一 clientId 会被当成同一个客户端 |
+| `keepAlive` | 心跳时间间隔,如 60 表示约每 60 秒发一次心跳包 |
+| `cleanSession` | 清除会话标志 |
+
+`cleanSession` 的作用:
+
+- `cleanSession = 0`:创建**持久性会话**,客户端再次上线时可收到离线期间所有 QoS>0 的消息;服务端会记住客户端订阅过的主题,直到会话超时注销。
+- `cleanSession = 1`:创建**临时会话**,不接收离线消息、服务端不保存订阅主题,每次连接都是全新会话,断开后会话自动销毁。
+
+CONNACK 报文包含 `returnCode`(连接返回码)与 `sessionPresent`:
+
+| returnCode | 说明 |
+| ---------- | ---- |
+| 0 | 连接成功 |
+| 1 | 拒绝:不支持的 MQTT 协议版本 |
+| 2 | 拒绝:不支持客户端标识符的编码 |
+| 3 | 拒绝:服务端不可用 |
+| 4 | 拒绝:用户名或密码无效 |
+| 5 | 拒绝:客户端未授权 |
+| 6-255 | 保留备用 |
+
+`sessionPresent` 与 `cleanSession` 配合:`cleanSession=0` 时,若服务端保存了上次会话状态则返回 1,否则 0;`cleanSession=1` 时恒为 0。
+
+### 2.3 发布、订阅、取消订阅
+
+| 报文 | 作用 |
+| ---- | ---- |
+| PUBLISH | 发布消息(含 packetId、topicName、payload、qos、retain、dup) |
+| SUBSCRIBE / SUBACK | 订阅主题 / 订阅确认(一个 SUBSCRIBE 可含多个主题) |
+| UNSUBSCRIBE / UNSUBACK | 取消订阅 / 取消订阅确认 |
+| DISCONNECT | 客户端主动断开连接 |
+
+SUBACK 的订阅返回码:
+
+| 返回码 | 说明 |
+| ------ | ---- |
+| 0x00 | 订阅成功——QoS0 |
+| 0x01 | 订阅成功——QoS1 |
+| 0x02 | 订阅成功——QoS2 |
+| 0x80 | 订阅失败 |
+
+注意事项:
+
+- `packetId`(报文标识符)只有 QoS>0 时才为非零数值。
+- `dup` 重发标志只在 QoS>0 时使用;接收方没有及时回确认时,发送方重发并置 dup=true。
+- `retain` 保留标志见 2.6 节。
+
+### 2.4 主题进阶
+
+- **区分大小写**,可用空格但建议不用,**不要用中文**(多数服务器不支持),建议使用 ASCII 字符。
+- **分级**:用 `/` 分隔,如 `home/sensor/led/brightness` 共四级;主题不要以 `/` 开头。
+- **通配符**(只能在订阅时用):
+  - 单级通配符 `+`:匹配一个主题级别,如 `home/sensor/+/status`。
+  - 多级通配符 `#`:匹配任意数量级别,如 `home/sensor/#`。
+- 以 `$` 开头的是服务端系统保留主题(如 `$SYS/#`),客户端不可随意订阅或发布。
+
+### 2.5 QoS 服务质量
+
+| QoS | 语义 | 传输流程 |
+| --- | ---- | -------- |
+| 0 | 最多发一次(最多一次) | 发送即结束,不确认、不重传,靠 TCP 保证 |
+| 1 | 至少发一次(最少一次) | PUBLISH → PUBACK,超时未收到 PUBACK 则重发,可能重复 |
+| 2 | 保证收一次 | PUBLISH → PUBREC → PUBREL → PUBCOMP,最安全也最慢 |
+
+```mermaid
+sequenceDiagram
+    participant S as 发送端
+    participant R as 接收端
+
+    Note over S,R: QoS 0:最多一次
+    S->>R: PUBLISH
+
+    Note over S,R: QoS 1:至少一次
+    S->>R: PUBLISH
+    R-->>S: PUBACK
+    Note right of S: 未收到 PUBACK 则重发(dup=1)
+
+    Note over S,R: QoS 2:保证收一次
+    S->>R: PUBLISH
+    R-->>S: PUBREC
+    S->>R: PUBREL
+    R-->>S: PUBCOMP
+```
+
+要点:
+
+- 要实现 QoS>0,连接时 **`cleanSession` 必须设为 false**,否则收不到任何离线消息(含 QoS1/QoS2)。
+- QoS=1 时协议本身不去重,需应用根据 dup 标志自行处理。
+- **服务质量降级**:若发布用 QoS2、订阅用 QoS1,服务端会采用两者中**较低**的 QoS 提供服务。
+
+### 2.6 保留消息(retain)
+
+- 作用:让服务端保留该主题的**最新一条**消息,任何客户端订阅该主题后立即收到它,不必等下一次发布。
+- 每个主题只能有一条保留消息,发布新的保留消息会覆盖旧的。
+- 删除保留消息:向该主题发布一条**空的保留消息**即可。
+
+### 2.7 心跳机制
+
+客户端在空闲时定时发送 **PINGREQ** 报文(心跳请求),服务端收到后回 **PINGRESP**(心跳响应)。服务端据此判断客户端是否在线;客户端若发 PINGREQ 后收不到 PINGRESP,则认为自己已断线。发送间隔由 CONNECT 报文的 `keepAlive` 指定。
+
+### 2.8 遗嘱机制(will)
+
+客户端在"活着"时通过 CONNECT 报文预先写好遗嘱,一旦**意外断线**(非主动发 DISCONNECT,如断网、断电),服务端就把遗嘱消息发布出去。主动断开不会触发遗嘱。
+
+| 字段 | 说明 |
+| ---- | ---- |
+| `willTopic` | 遗嘱主题,订阅该主题的客户端才会收到遗嘱消息 |
+| `willMessage` | 遗嘱内容 |
+| `willRetain` | 遗嘱的保留标志 |
+| `willQoS` | 遗嘱的 QoS 等级(0/1/2) |
+
+小技巧:客户端上线时主动向自己的遗嘱主题发一条"在线"消息,订阅者即可据此实现上线通知。
+
+### 2.9 用户名密码认证
+
+CONNECT 报文中的 `username`、`password` 是可选字段。服务端若开启认证,则客户端必须提供正确的用户名密码才能连接;有些服务端还用它识别客户端归属、管理私人主题权限。
+
+## 3. 搭建 MQTT 服务器
+
+### 3.1 公用 MQTT 服务器
+
+| 服务器 | 地址 | 端口 |
+| ------ | ---- | ---- |
+| test.mosquitto.org(国外) | `test.mosquitto.org` | 1883(TCP)、8883(TLS)、8080(WebSocket) |
+| broker.hivemq.com(国外) | `broker.hivemq.com` | 1883、8000(WebSocket) |
+| 然也物联(国内) | `test.ranye-iot.net` | 1883、8883(TLS) |
+| 通信猫(国内) | `mq.tongxinmao.com` | 1883 |
+
+> 公用服务器仅供学习测试,不可商用;国内服务器连接快、延迟低。
+
+### 3.2 用 mosquitto 自建服务器(Ubuntu)
+
+```bash
+sudo apt-add-repository ppa:mosquitto-dev/mosquitto-ppa   # 加入软件源
+sudo apt-get update                                       # 更新软件源
+sudo apt-get install mosquitto                            # 安装 mosquitto 服务端
+```
+
+`/etc/mosquitto/conf.d/` 下所有 `*.conf` 会被自动加载。新建 `myconfig.conf`:
+
+```conf
+# /etc/mosquitto/conf.d/myconfig.conf
+# 添加监听端口 1883
+listener 1883
+# 关闭匿名访问,客户端必须使用用户名
+allow_anonymous false
+# 指定用于存放用户名和密码的文件
+password_file /etc/mosquitto/pwfile.txt
+```
+
+创建用户名密码文件:
+
+```bash
+sudo touch /etc/mosquitto/pwfile.txt
+sudo mosquitto_passwd /etc/mosquitto/pwfile.txt mqtt1    # 用户名 mqtt1,密码 123456(需输入两次)
+```
+
+启停与查看状态:
+
+```bash
+sudo service mosquitto start     # 启动服务
+sudo service mosquitto status    # 查看服务状态
+sudo service mosquitto stop      # 停止服务
+```
+
+> 网络要求:开发板、Ubuntu、手机/电脑处于同一局域网且能互相 `ping` 通。
+
+### 3.3 用 MQTT.fx 初体验
+
+1. 下载安装 MQTT.fx(官网 <http://mqttfx.jensd.de/>,教程使用 1.7.1 版本)。
+2. 点击齿轮按钮新建配置:填写 Broker 地址(如 `192.168.6.161`,即 Ubuntu 的 IP)、Client ID(如 `test1`)、用户名 `mqtt1`、密码 `123456`,Apply 后连接。
+3. 订阅主题:切到 Subscribe 页,填写主题(如 `dt2914/testTopic`),点 Subscribe。
+4. 发布消息:切到 Publish 页,填写主题与内容,点 Publish;订阅同一个主题的客户端即可收到。
+5. 取消订阅:在已订阅主题上点击取消即可。
+
+手机端也可用 MQTT Client 软件连接同一服务器(Client ID 不能与其它客户端重复),实现跨设备收发。
+
+## 4. 移植 paho.mqtt.c 客户端库
+
+### 4.1 下载源码
+
+官网 <https://mqtt.org/> → Software → Client libraries → **Eclipse Paho C**。示例使用 **1.3.8** 版本(当前最新为 1.3.13)。
+
+```bash
+tar -xzf paho.mqtt.c-1.3.8.tar.gz
+cd paho.mqtt.c-1.3.8
+```
+
+该工程顶级目录有 `CMakeLists.txt`,是 CMake 构建的工程。
+
+### 4.2 交叉编译
+
+在 `cmake/` 目录新建 `arm-linux-setup.cmake`(内容与本库 [[04-网络编程与项目实战/03-CMake入门与进阶]] 第 5.1 节相同,按实际工具链路径修改):
+
+```cmake
+##################################
+# 配置 ARM 交叉编译
+#################################
+set(CMAKE_SYSTEM_NAME Linux)    # 设置目标系统名字
+set(CMAKE_SYSTEM_PROCESSOR arm) # 设置目标处理器架构
+
+# 指定编译器的 sysroot 路径
+set(TOOLCHAIN_DIR /opt/fsl-imx-x11/4.1.15-2.1.0/sysroots)
+set(CMAKE_SYSROOT ${TOOLCHAIN_DIR}/cortexa7hf-neon-poky-linux-gnueabi)
+
+# 指定交叉编译器 arm-linux-gcc
+set(CMAKE_C_COMPILER ${TOOLCHAIN_DIR}/x86_64-pokysdk-linux/usr/bin/arm-poky-linux-gnueabi/arm-poky-linux-gnueabi-gcc)
+
+# 为编译器添加编译选项
+set(CMAKE_C_FLAGS "-march=armv7ve -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a7")
+
+set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
+set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
+#################################
+# end
+##################################
+```
+
+```bash
+mkdir build && cd build
+
+cmake -DCMAKE_BUILD_TYPE=Release \
+      -DCMAKE_INSTALL_PREFIX=~/tools/paho.mqtt.c-1.3.8/install \
+      -DCMAKE_TOOLCHAIN_FILE=../cmake/arm-linux-setup.cmake \
+      -DPAHO_WITH_SSL=TRUE \
+      -DPAHO_BUILD_SAMPLES=TRUE ..
+
+make
+make install
+```
+
+- `CMAKE_INSTALL_PREFIX` 指定安装路径(示例为源码目录下的 `install`)。
+- `PAHO_WITH_SSL`、`PAHO_BUILD_SAMPLES` 是缓存变量,含义见工程 `README.md`。
+- MQTT 库依赖 openssl;示例未单独移植 openssl,因为开发板出厂系统已带 openssl,且交叉编译器会经 sysroot 链接它。
+
+### 4.3 安装产物
+
+`install/` 下有 `bin`、`include`、`lib`、`share`:
+
+| 库文件 | 说明 |
+| ------ | ---- |
+| `libpaho-mqtt3a.so` | 异步模式,不支持 SSL |
+| `libpaho-mqtt3as.so` | 异步模式,支持 SSL |
+| `libpaho-mqtt3c.so` | 同步模式,不支持 SSL |
+| `libpaho-mqtt3cs.so` | 同步模式,支持 SSL |
+
+头文件:同步模式包含 `MQTTClient.h`,异步模式包含 `MQTTAsync.h`(其它头文件被这两个包含)。本项目使用**同步模式** `libpaho-mqtt3c.so` + `MQTTClient.h`。
+
+### 4.4 拷贝库到开发板
+
+```bash
+# 在 Ubuntu 上打包
+tar -czf libmqtt.tar.gz -C ~/tools/paho.mqtt.c-1.3.8/install/lib .
+
+# 拷贝到开发板 /home/root 后,解压到 /usr/lib
+tar -xzf libmqtt.tar.gz -C /usr/lib
+```
+
+> 不要破坏库原有的软链接关系,建议打包 `.so` 及其符号链接一起拷贝。
+
+## 5. MQTT 客户端库 API(同步模式)
+
+以下函数定义在 `MQTTClient.h`。文档见源码 `docs/MQTTClient/html/index.html`。
+
+### 5.1 `MQTTClient_message` 结构体
+
+```c
+typedef struct
+{
+    int payloadlen;   // 负载长度
+    void* payload;    // 负载
+    int qos;          // 消息的 qos 等级
+    int retained;     // 消息的保留标志
+    int dup;          // dup 标志(重复标志)
+    int msgid;        // 消息标识符,即 packetId
+    /* ... */
+} MQTTClient_message;
+```
+
+发布消息需实例化一个 `MQTTClient_message` 对象;接收消息也是收到该对象。常用 `MQTTClient_message_initializer` 宏初始化。
+
+### 5.2 创建客户端对象
+
+```c
+int MQTTClient_create(MQTTClient *handle,
+                      const char *serverURI,
+                      const char *clientId,
+                      int persistence_type,
+                      void *persistence_context);
+```
+
+| 参数 | 说明 |
+| ---- | ---- |
+| `handle` | MQTT 客户端句柄 |
+| `serverURI` | MQTT 服务器地址 |
+| `clientId` | 客户端 ID |
+| `persistence_type` | 持久化类型,见下表 |
+| `persistence_context` | `NONE` 时传 `NULL`;`DEFAULT` 时为持久化目录(传 `NULL` 表示工作目录) |
+
+| persistence_type | 说明 |
+| ---------------- | ---- |
+| `MQTTCLIENT_PERSISTENCE_NONE` | 内存持久化,系统故障时传输中消息状态可能丢失 |
+| `MQTTCLIENT_PERSISTENCE_DEFAULT` | 默认(基于文件系统)持久化,提供一定防丢失保护 |
+| `MQTTCLIENT_PERSISTENCE_USER` | 应用自定义持久化实现 |
+
+返回值:成功 `MQTTCLIENT_SUCCESS`,失败返回错误码。
+
+```c
+MQTTClient client;
+int rc;
+if (MQTTCLIENT_SUCCESS !=
+        (rc = MQTTClient_create(&client, "192.168.6.161", "dt_mqtt_2_id",
+                MQTTCLIENT_PERSISTENCE_NONE, NULL))) {
+    printf("Failed to create client, return code %d\n", rc);
+    return EXIT_FAILURE;
+}
+```
+
+### 5.3 连接服务端
+
+```c
+int MQTTClient_connect(MQTTClient handle, MQTTClient_connectOptions *options);
+```
+
+```c
+typedef struct
+{
+    int keepAliveInterval;          // keepAlive
+    int cleansession;               // cleanSession
+    MQTTClient_willOptions *will;   // 遗嘱相关
+    const char *username;           // 用户名
+    const char *password;           // 密码
+    int reliable;                   // 同步/异步发布控制(默认 1=同步)
+    /* ... */
+} MQTTClient_connectOptions;
+
+typedef struct
+{
+    const char *topicName;   // 遗嘱主题
+    const char *message;     // 遗嘱内容
+    int retained;            // 遗嘱消息的保留标志
+    int qos;                 // 遗嘱消息的 QoS 等级
+    /* ... */
+} MQTTClient_willOptions;
+```
+
+返回值错误码与 CONNACK 的连接返回码一致(1~5)。
+
+```c
+MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
+MQTTClient_willOptions will_opts = MQTTClient_willOptions_initializer;
+
+will_opts.topicName = "dt_mqtt/willTopic";
+will_opts.message   = "Abnormally dropped";
+will_opts.retained  = 1;
+will_opts.qos       = 0;
+
+conn_opts.will = &will_opts;
+conn_opts.keepAliveInterval = 30;
+conn_opts.cleansession = 0;
+conn_opts.username = "mqtt1";
+conn_opts.password = "123456";
+
+if (MQTTCLIENT_SUCCESS != (rc = MQTTClient_connect(client, &conn_opts))) {
+    printf("Failed to connect, return code %d\n", rc);
+    return EXIT_FAILURE;
+}
+```
+
+### 5.4 设置回调
+
+```c
+int MQTTClient_setCallbacks(MQTTClient handle,
+                            void *context,
+                            MQTTClient_connectionLost *cl,
+                            MQTTClient_messageArrived *ma,
+                            MQTTClient_deliveryComplete *dc);
+```
+
+| 回调 | 类型 | 说明 |
+| ---- | ---- | ---- |
+| `cl` | `void (void *context, char *cause)` | 断线时执行;设 `NULL` 表示不处理断线 |
+| `ma` | `int (void *context, char *topicName, int topicLen, MQTTClient_message *message)` | 收到消息时执行,**必须设置** |
+| `dc` | `void (void *context, MQTTClient_deliveryToken dt)` | 发布确认时执行,可设 `NULL` |
+
+`ma` 回调的两个关键点:
+
+- 退出前必须释放内存:`MQTTClient_freeMessage(&message)` 与 `MQTTClient_free(topicName)`。
+- 返回值必须是 0 或 1:返回 1 表示处理成功;返回 0 表示处理有问题,客户端库会**重新投递**,此时不要释放内存,否则重新投递失败。
+
+```c
+static void delivered(void *context, MQTTClient_deliveryToken dt)
+{
+    printf("Message with token value %d delivery confirmed\n", dt);
+}
+
+static int msgarrvd(void *context, char *topicName, int topicLen,
+                    MQTTClient_message *message)
+{
+    printf("Message arrived\n");
+    printf("topic: %s\n", topicName);
+    printf("message: <%d>%s\n", message->payloadlen, (char *)message->payload);
+    MQTTClient_freeMessage(&message);
+    MQTTClient_free(topicName);
+    return 1;
+}
+
+static void connlost(void *context, char *cause)
+{
+    printf("\nConnection lost\n");
+    printf("    cause: %s \n", cause);
+}
+```
+
+> `MQTTClient_setCallbacks` 必须在连接服务器**之前**调用。`reliable`(默认 1)控制同步/异步发布:`reliable=1` 时同步发布,必须完成当前消息确认后才能发布下一条。
+
+### 5.5 发布消息
+
+```c
+int MQTTClient_publishMessage(MQTTClient handle,
+                              const char *topicName,
+                              MQTTClient_message *msg,
+                              MQTTClient_deliveryToken *dt);
+```
+
+```c
+MQTTClient_message pubmsg = MQTTClient_message_initializer;
+MQTTClient_deliveryToken token;
+
+pubmsg.payload = "online";
+pubmsg.payloadlen = 6;
+pubmsg.qos = 0;
+pubmsg.retained = 1;
+if (MQTTCLIENT_SUCCESS !=
+        (rc = MQTTClient_publishMessage(client, "dt_mqtt/testTopic", &pubmsg, &token))) {
+    printf("Failed to publish message, return code %d\n", rc);
+}
+```
+
+### 5.6 订阅 / 取消订阅 / 断开 / 销毁
+
+```c
+int MQTTClient_subscribe(MQTTClient handle, const char *topic, int qos);
+int MQTTClient_unsubscribe(MQTTClient handle, const char *topic);
+int MQTTClient_disconnect(MQTTClient handle, int timeout);   // timeout 单位毫秒
+void MQTTClient_destroy(MQTTClient *handle);
+```
+
+```c
+MQTTClient_subscribe(client, "dt_mqtt/testTopic", 0);
+MQTTClient_unsubscribe(client, "dt_mqtt/testTopic");
+MQTTClient_disconnect(client, 10000);
+MQTTClient_destroy(&client);
+```
+
+## 6. 完整可复制工程:mqtt_prj
+
+工程路径:开发板光盘 → `11、Linux C 应用编程例程源码` → `33_mqtt/mqtt_prj`。功能设计:
+
+- 基于 mosquitto 搭建的本地服务器,实现个人物联网小项目;
+- 手机/电脑可远程控制开发板上的一颗 LED;
+- 开发板每隔 30 秒向服务端发布 SoC 温度,手机/电脑可订阅查看。
+
+### 6.1 工程目录结构
+
+```text
+mqtt_prj/
+├── CMakeLists.txt              # 顶层 CMakeLists
+├── cmake/
+│   └── arm-linux-setup.cmake   # 交叉编译工具链文件
+├── mqttClient.c                # 客户端应用程序
+└── build/                      # out-of-source 构建目录
+```
+
+### 6.2 `cmake/arm-linux-setup.cmake`
+
+```cmake
+##################################
+# 配置 ARM 交叉编译
+#################################
+set(CMAKE_SYSTEM_NAME Linux)    #设置目标系统名字
+set(CMAKE_SYSTEM_PROCESSOR arm) #设置目标处理器架构
+
+# 指定编译器的sysroot路径
+set(TOOLCHAIN_DIR /opt/fsl-imx-x11/4.1.15-2.1.0/sysroots)
+set(CMAKE_SYSROOT ${TOOLCHAIN_DIR}/cortexa7hf-neon-poky-linux-gnueabi)
+
+# 指定交叉编译器arm-linux-gcc
+set(CMAKE_C_COMPILER ${TOOLCHAIN_DIR}/x86_64-pokysdk-linux/usr/bin/arm-poky-linux-gnueabi/arm-poky-linux-gnueabi-gcc)
+
+# 为编译器添加编译选项
+set(CMAKE_C_FLAGS "-march=armv7ve -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a7")
+
+set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
+set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
+#################################
+# end
+##################################
+```
+
+### 6.3 `CMakeLists.txt`
+
+```cmake
+cmake_minimum_required(VERSION 2.8.12)
+project(MQTTClient C)
+message(STATUS "CMake version: " ${CMAKE_VERSION})
+message(STATUS "CMake system name: " ${CMAKE_SYSTEM_NAME})
+message(STATUS "CMake system processor: " ${CMAKE_SYSTEM_PROCESSOR})
+
+# 设置可执行文件输出路径
+set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
+
+# 定义可执行文件目标
+add_executable(mqttClient mqttClient.c)
+
+# 指定MQTT客户端库头文件路径、库路径以及链接库
+# ***大家需要根据MQTT的实际安装路径设置***
+target_include_directories(mqttClient PRIVATE /home/alientek/tools/paho.mqtt.c-1.3.8/install/include)  #MQTT头文件搜索路径
+target_link_directories(mqttClient PRIVATE /home/alientek/tools/paho.mqtt.c-1.3.8/install/lib)          #MQTT库文件搜索路径
+target_link_libraries(mqttClient PRIVATE paho-mqtt3c)   #MQTT链接库 libpaho-mqtt3c.so
+```
+
+逐段解释:
+
+- `cmake_minimum_required(VERSION 2.8.12)`:设置最低 CMake 版本要求。
+- `project(MQTTClient C)`:工程名为 `MQTTClient`,语言 C。
+- `message(STATUS ...)`:打印 CMake 版本、目标系统名、目标处理器,确认交叉编译配置生效。
+- `set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)`:可执行文件输出到 `build/bin`。
+- `add_executable(mqttClient mqttClient.c)`:定义目标 `mqttClient`。
+- `target_include_directories(... PRIVATE ...)`:只给该目标加 MQTT 头文件搜索路径。
+- `target_link_directories(... PRIVATE ...)`:只给该目标加 MQTT 库搜索路径。
+- `target_link_libraries(mqttClient PRIVATE paho-mqtt3c)`:链接同步模式 MQTT 库。
+
+### 6.4 `mqttClient.c`(完整源码)
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include "MQTTClient.h"     //包含MQTT客户端库头文件
+
+/* ########################宏定义##################### */
+/*
+ * 因为本地服务器是搭建在您的ubuntu上,所以此处的服务器地址
+ * 要填写您ubuntu的ip地址,笔者ubuntu的ip地址为192.168.6.161,
+ * 你们填写自己的ip地址即可
+ */
+#define BROKER_ADDRESS  "192.168.6.161"   //服务器地址
+
+/*
+ * 客户端id、用户名、密码 *
+ * 客户端id可以随意填写,只需要保证跟MQTT.fx客户端使用的不同,
+ * 用户名要使用MQTT服务端注册的用户,即 mqtt1
+ * 密码也是之前设置的 123456
+ */
+#define CLIENTID        "test3"           //客户端id
+#define USERNAME        "mqtt1"           //用户名
+#define PASSWORD        "123456"          //密码
+
+/*
+ * 以下 dt_mqtt/ 便是笔者的个人主题级别
+ */
+#define WILL_TOPIC      "dt_mqtt/will"          //遗嘱主题
+#define LED_TOPIC       "dt_mqtt/led"           //LED主题
+#define TEMP_TOPIC      "dt_mqtt/temperature"   //温度主题
+/* ################################################# */
+
+static int msgarrvd(void *context, char *topicName, int topicLen,
+            MQTTClient_message *message)
+{
+    if (!strcmp(topicName, LED_TOPIC)) {  //校验消息的主题
+        if (!strcmp("2", message->payload))  //如果接收到的消息是"2"则设置LED为呼吸灯模式
+            system("echo heartbeat > /sys/class/leds/sys-led/trigger");
+        if (!strcmp("1", message->payload)) {  //如果是"1"则LED常亮
+            system("echo none > /sys/class/leds/sys-led/trigger");
+            system("echo 1 > /sys/class/leds/sys-led/brightness");
+        }
+        else if (!strcmp("0", message->payload)) {  //如果是"0"则LED熄灭
+            system("echo none > /sys/class/leds/sys-led/trigger");
+            system("echo 0 > /sys/class/leds/sys-led/brightness");
+        }
+
+        // 接收到其它数据 不做处理
+    }
+
+    /* 释放占用的内存空间 */
+    MQTTClient_freeMessage(&message);
+    MQTTClient_free(topicName);
+
+    /* 退出 */
+    return 1;
+}
+
+static void connlost(void *context, char *cause)
+{
+    printf("\nConnection lost\n");
+    printf("    cause: %s \n", cause);
+}
+
+int main(int argc, char *argv[])
+{
+    MQTTClient client;
+    MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
+    MQTTClient_willOptions will_opts = MQTTClient_willOptions_initializer;
+    MQTTClient_message pubmsg = MQTTClient_message_initializer;
+    int rc;
+
+    /* 创建mqtt客户端对象 */
+    if (MQTTCLIENT_SUCCESS !=
+            (rc = MQTTClient_create(&client, BROKER_ADDRESS, CLIENTID,
+            MQTTCLIENT_PERSISTENCE_NONE, NULL))) {
+        printf("Failed to create client, return code %d\n", rc);
+        rc = EXIT_FAILURE;
+        goto exit;
+    }
+
+    /* 设置回调 */
+    if (MQTTCLIENT_SUCCESS !=
+            (rc = MQTTClient_setCallbacks(client, NULL, connlost,
+            msgarrvd, NULL))) {
+        printf("Failed to set callbacks, return code %d\n", rc);
+        rc = EXIT_FAILURE;
+        goto destroy_exit;
+    }
+
+    /* 连接MQTT服务器 */
+    will_opts.topicName = WILL_TOPIC;    //遗嘱主题
+    will_opts.message = "Unexpected disconnection";  //遗嘱消息
+    will_opts.retained = 1;              //保留消息
+    will_opts.qos = 0;                   //QoS0
+
+    conn_opts.will = &will_opts;
+    conn_opts.keepAliveInterval = 30;    //心跳包间隔时间
+    conn_opts.cleansession = 0;          //cleanSession标志
+    conn_opts.username = USERNAME;       //用户名
+    conn_opts.password = PASSWORD;       //密码
+    if (MQTTCLIENT_SUCCESS !=
+            (rc = MQTTClient_connect(client, &conn_opts))) {
+        printf("Failed to connect, return code %d\n", rc);
+        rc = EXIT_FAILURE;
+        goto destroy_exit;
+    }
+
+    printf("MQTT服务器连接成功!\n");
+
+    /* 发布上线消息 */
+    pubmsg.payload = "Online";   //消息的内容
+    pubmsg.payloadlen = 6;       //内容的长度
+    pubmsg.qos = 0;              //QoS等级
+    pubmsg.retained = 1;         //保留消息
+    if (MQTTCLIENT_SUCCESS !=
+        (rc = MQTTClient_publishMessage(client, WILL_TOPIC, &pubmsg, NULL))) {
+        printf("Failed to publish message, return code %d\n", rc);
+        rc = EXIT_FAILURE;
+        goto disconnect_exit;
+    }
+
+    /* 订阅主题 dt_mqtt/led */
+    if (MQTTCLIENT_SUCCESS !=
+            (rc = MQTTClient_subscribe(client, LED_TOPIC, 0))) {
+        printf("Failed to subscribe, return code %d\n", rc);
+        rc = EXIT_FAILURE;
+        goto disconnect_exit;
+    }
+
+    /* 向服务端发布芯片温度信息 */
+    for ( ; ; ) {
+
+        MQTTClient_message tempmsg = MQTTClient_message_initializer;
+        char temp_str[10] = {0};
+        int fd;
+
+        /* 读取温度值 */
+        fd = open("/sys/class/thermal/thermal_zone0/temp", O_RDONLY);
+        read(fd, temp_str, sizeof(temp_str));  //读取temp属性文件即可获取温度
+        close(fd);
+
+        /* 发布温度信息 */
+        tempmsg.payload = temp_str;              //消息的内容
+        tempmsg.payloadlen = strlen(temp_str);   //内容的长度
+        tempmsg.qos = 0;                         //QoS等级
+        tempmsg.retained = 1;                    //保留消息
+        if (MQTTCLIENT_SUCCESS !=
+            (rc = MQTTClient_publishMessage(client, TEMP_TOPIC, &tempmsg, NULL))) {
+            printf("Failed to publish message, return code %d\n", rc);
+            rc = EXIT_FAILURE;
+            goto unsubscribe_exit;
+        }
+
+        sleep(30);      //每隔30秒 更新一次数据
+    }
+
+unsubscribe_exit:
+    if (MQTTCLIENT_SUCCESS !=
+        (rc = MQTTClient_unsubscribe(client, LED_TOPIC))) {
+        printf("Failed to unsubscribe, return code %d\n", rc);
+        rc = EXIT_FAILURE;
+    }
+disconnect_exit:
+    if (MQTTCLIENT_SUCCESS !=
+        (rc = MQTTClient_disconnect(client, 10000))) {
+        printf("Failed to disconnect, return code %d\n", rc);
+        rc = EXIT_FAILURE;
+    }
+destroy_exit:
+    MQTTClient_destroy(&client);
+exit:
+    return rc;
+}
+```
+
+三个主题的作用:
+
+| 宏 | 主题 | 作用 |
+| -- | ---- | ---- |
+| `WILL_TOPIC` | `dt_mqtt/will` | 遗嘱主题,正常上线时发布 "Online",意外断线时由服务端发布遗嘱消息 |
+| `LED_TOPIC` | `dt_mqtt/led` | 开发板订阅;收到 "0"/"1"/"2" 分别控制 LED 熄灭/常亮/呼吸灯 |
+| `TEMP_TOPIC` | `dt_mqtt/temperature` | 开发板每 30 秒发布一次 SoC 温度(读 `/sys/class/thermal/thermal_zone0/temp`) |
+
+### 6.5 构建与编译
+
+```bash
+cd mqtt_prj/build
+~/tools/cmake-3.16.0-Linux-x86_64/bin/cmake \
+    -DCMAKE_TOOLCHAIN_FILE=../cmake/arm-linux-setup.cmake \
+    -DCMAKE_BUILD_TYPE=Release ..
+make
+```
+
+编译成功后生成 `build/bin/mqttClient`,拷贝到开发板 `/home/root`。
+
+### 6.6 测试演示
+
+1. 确保开发板与 Ubuntu 在同一局域网、能互相 `ping` 通。
+2. 开发板执行 `./mqttClient`,打印"MQTT服务器连接成功!"。
+3. 用 MQTT.fx(Client ID 用 `test1`)连接服务器,订阅 `dt_mqtt/temperature`,立即收到保留温度值,之后每 30 秒收到一次。
+4. 向 `dt_mqtt/led` 发布 `0`/`1`/`2`,观察开发板 LED 熄灭/常亮/呼吸灯。
+5. 也可用手机 MQTT Client(Client ID 不能重复)订阅温度、发布 LED 控制。
+
+## 7. 扩展:JSON 数据处理与云平台接入
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。教材示例仅上报纯文本温度值,接入真实云平台时通常改为 JSON 格式并增加设备认证。
+
+**JSON 数据处理(cJSON / parson)**:物联网平台多要求负载为 JSON。可在开发板端引入轻量 JSON 库(如 cJSON、parson),把温度、状态打包成 JSON 发布,订阅端再解析。典型负载:
+
+```json
+{ "temp": 45, "led": 1, "ts": 1730000000 }
+```
+
+cJSON 的典型用法(需自行移植该库并链接):
+
+```c
+/* 扩展示例,非教材内容 */
+cJSON *root = cJSON_CreateObject();
+cJSON_AddNumberToObject(root, "temp", temperature);
+cJSON_AddNumberToObject(root, "led", led_state);
+char *out = cJSON_PrintUnformatted(root);
+/* out 作为 pubmsg.payload 发布,payloadlen = strlen(out) */
+cJSON_Delete(root);
+```
+
+**原子云接入**:正点原子原子云提供 MQTT 接入(设备三元组:产品 ID / 设备名 / 设备密钥),流程是先用三元组连接云平台,再按平台规定的主题上下行数据。具体主题、鉴权算法以原子云官方文档为准,此处不展开。
+
+---
+
+# 第二部分:视频监控
+
+## 8. 方案概览
+
+常见视频监控/直播使用 **RTMP** 和 **RTSP** 流媒体传输协议:
+
+| 协议 | 提出方 | 特点 | 适用 |
+| ---- | ------ | ---- | ---- |
+| RTSP(Real-Time Stream Protocol) | Real Networks 与 Netscape | 基于文本的多媒体播放控制协议,定义流格式,流数据经 RTP 传输;实时性最好 | 视频聊天、视频监控 |
+| RTMP(Real Time Message Protocol) | Adobe | 解决流媒体多路复用与分包,低延迟、稳定性高,支持所有摄像头格式,浏览器加载 flash 插件可直接播放 | 直播、推流 |
+
+选择:RTSP 实时性最好但实现复杂;RTMP 浏览器支持好、生态成熟。本章用 **FFmpeg + Nginx** 通过 RTMP 推流实现视频监控/直播。
+
+```mermaid
+flowchart LR
+    Push["推流端<br/>开发板 (FFmpeg)"] -->|"RTMP 推流"| Srv["流媒体服务器<br/>Nginx + nginx-rtmp-module"]
+    Srv -->|"RTMP 拉流"| Pull["拉流端<br/>Windows VLC"]
+
+    classDef client fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
+    classDef server fill:#fef9c3,stroke:#ca8a04,color:#713f12
+    class Push,Pull client
+    class Srv server
+```
+
+三部分:推流客户端(FFmpeg)、流媒体服务器(Nginx + RTMP 模块)、拉流客户端(VLC)。
+
+## 9. Nginx 移植
+
+> 说明:本方案中开发板既是流媒体服务器又是推流端。出厂系统通常已移植好 Nginx 并在启动时自动运行,这里为了演示,重新手动移植。
+
+### 9.1 下载源码与 RTMP 模块
+
+```bash
+wget http://nginx.org/download/nginx-1.20.0.tar.gz
+git clone https://github.com/arut/nginx-rtmp-module.git
+tar -xzf nginx-1.20.0.tar.gz
+cd nginx-1.20.0
+```
+
+原生 Nginx 不支持 RTMP,需要第三方模块 `nginx-rtmp-module`。
+
+### 9.2 交叉编译前的源码修改
+
+初始化交叉编译环境:
+
+```bash
+source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
+```
+
+修改两处源码,否则配置通不过:
+
+| 文件 | 位置 | 修改 |
+| ---- | ---- | ---- |
+| `auto/cc/name` | 第 21 行 | 注释掉 `exit 1` |
+| `auto/types/sizeof` | 第 15 行 | `ngx_size=` 改为 `ngx_size=4` |
+| `auto/types/sizeof` | 第 36 行 | `$CC` 改为 `gcc` |
+
+### 9.3 配置、编译、安装
+
+```bash
+./configure --prefix=/home/dt/tools/nginx-1.20.0/install \
+    --with-http_ssl_module \
+    --with-http_mp4_module \
+    --with-http_v2_module \
+    --without-http_upstream_zone_module \
+    --add-module=/home/dt/tools/nginx-rtmp-module
+```
+
+- `--prefix`:Nginx 安装路径。
+- `--add-module`:添加第三方模块(指向 `nginx-rtmp-module` 源码路径)。
+
+```bash
+make
+```
+
+首次 `make` 会报错,此时编辑 `objs/ngx_auto_config.h`,添加:
+
+```c
+#ifndef NGX_HAVE_SYSVSHM
+#define NGX_HAVE_SYSVSHM 1
+#endif
+```
+
+再次 `make` 即可编译成功,然后:
+
+```bash
+make install
+```
+
+### 9.4 部署到开发板
+
+安装目录下:
+
+- `conf/`:配置文件,重点是 `nginx.conf`;
+- `sbin/nginx`:可执行程序("not stripped",约 7.5MB,可执行 `arm-poky-linux-gnueabi-strip --strip-debug nginx` 去掉调试信息);
+- `logs/`、`html/`。
+
+```bash
+# 开发板上先移除出厂系统自带的 nginx
+rm -rf /usr/sbin/nginx
+rm -rf /etc/nginx/*
+
+# 拷贝新编译的 nginx 到 /home/root
+# 拷贝 conf、logs、html 到 /etc/nginx
+```
+
+### 9.5 测试 Nginx
+
+```bash
+./nginx -V                 # 查看版本信息
+./nginx -h                 # 查看帮助
+./nginx -p /etc/nginx      # 启动(-p 指定前缀路径)
+ps -aux                    # 查看服务进程
+```
+
+在电脑浏览器输入开发板 IP,出现 "welcome to nginx" 即正常。
+
+### 9.6 配置 RTMP
+
+编辑 `/etc/nginx/conf/nginx.conf`,添加:
+
+```conf
+rtmp {
+    server {
+        listen 1935;          # 监听 1935 端口
+        chunk_size 4096;
+
+        application live{
+            allow publish 127.0.0.1;
+            allow play all;
+            live on;          # 打开直播
+            record off;       # 关闭 record
+            meta copy;
+        }
+
+        application hls {
+            live on;
+            hls on;
+            hls_path /tmp/hls;
+            hls_fragment 8s;
+        }
+    }
+}
+```
+
+```bash
+./nginx -p /etc/nginx -s reload    # 重载配置
+```
+
+## 10. 用 FFmpeg 推流、VLC 拉流
+
+### 10.1 推送视频文件
+
+```bash
+ffmpeg -re -i /run/media/mmcblk0p1/testVideo.mp4 -c:av copy -f flv rtmp://127.0.0.1/live/mytest
+```
+
+- `-re`:按原始帧率读取(实时推流)。
+- `-i`:输入视频文件。
+- `rtmp://127.0.0.1/live/mytest`:推给本机流媒体服务器的 `live` 应用,流名 `mytest`(服务器与推流端都是开发板,故 IP 为 127.0.0.1)。
+
+拉流:Windows 上打开 VLC → 媒体 → 打开网络串流 → 输入 `rtmp://<开发板IP>/live/mytest` → 播放,既有画面也有声音。
+
+### 10.2 推送摄像头(USB 摄像头)
+
+```bash
+ffmpeg -f v4l2 -video_size 320x240 -framerate 15 -i /dev/video2 -q 10 -f flv rtmp://127.0.0.1/live/mytest
+```
+
+- `-f v4l2`:使用 V4L2 采集。
+- `-video_size 320x240 -framerate 15`:分辨率与帧率。
+- `-i /dev/video2`:摄像头设备节点(USB 摄像头编号需按实际确认)。
+- `-q 10`:视频质量参数。
+- `-f flv`:输出 FLV 封装,便于 RTMP 传输。
+
+在 Windows 下用 VLC 拉流即可看到摄像头画面。
+
+### 10.3 延迟分析
+
+实测延迟约 5~6 秒,原因:
+
+- I.MX6U 没有硬件视频解码,FFmpeg 内部对音视频的处理全靠软件,耗时大;
+- 服务器和推流端都在同一块性能较弱的开发板上,负担更重。
+
+结论:普通开发板软编解码能力有限,追求低延迟应使用带硬件编解码的更高性能平台。
+
+## 11. 方案二:V4L2 + JPEG 广播的局域网 MJPEG 监控
+
+> 该实现来自本知识库配套的 Qt 例程 `Embedded-Qt-Tutorial/Qt/04/05_video_surveillance`,与 PDF 第三十四章的 Nginx 方案是两条不同路线:**Nginx 方案**适合广域/公网推拉流,**本方案**适合开发板与客户端同网段的轻量预览,结构简单、无需服务器进程。
+> 摄像头采集部分"请参考正点原子 C 应用编程 V4L2 章节"(源码注释原文)。
+
+### 11.1 整体结构
+
+```mermaid
+flowchart LR
+    subgraph Server["video_server(开发板)"]
+        V4L2["/dev/video1<br/>V4L2 采集 RGB565"] --> QImg["QImage (640x480)"]
+        QImg -->|"save JPEG"| B64["base64 编码"]
+        B64 -->|"UDP 广播 :8888"| Net["QUdpSocket"]
+    end
+    subgraph Client["video_client(PC / 开发板)"]
+        Rcv["QUdpSocket bind :8888"] --> Dec["base64 解码 + loadFromData"]
+        Dec --> Show["QLabel 显示"]
+    end
+    Net --> Rcv
+
+    classDef dev fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
+    classDef pc fill:#dcfce7,stroke:#16a34a,color:#14532d
+    class V4L2,QImg,B64,Net dev
+    class Rcv,Dec,Show pc
+```
+
+- 服务端:V4L2 采集一帧 RGB565 → 转 `QImage` → 编码 JPEG → base64 → UDP 广播到 8888 端口。
+- 客户端:绑定 8888 端口接收 UDP 数据报 → base64 解码 → 从数据加载 `QImage` → 显示。
+
+### 11.2 服务端采集线程 `capture_thread.h`
+
+```cpp
+#ifndef CAPTURE_THREAD_H
+#define CAPTURE_THREAD_H
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <string.h>
+#include <pthread.h>
+#ifdef linux
+#include <linux/fb.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <linux/videodev2.h>
+#include <linux/input.h>
+#endif
+
+#include <QThread>
+#include <QDebug>
+#include <QPushButton>
+#include <QImage>
+#include <QByteArray>
+#include <QBuffer>
+#include <QTime>
+#include <QUdpSocket>
+
+#define VIDEO_DEV           "/dev/video1"
+#define FB_DEV              "/dev/fb0"
+#define VIDEO_BUFFER_COUNT  3
+
+struct buffer_info {
+    void *start;
+    unsigned int length;
+};
+
+class CaptureThread : public QThread
+{
+    Q_OBJECT
+
+signals:
+    void imageReady(QImage);
+    void sendImage(QImage);
+
+private:
+    bool startFlag = false;          // 线程开启flag
+    bool startBroadcast = false;     // 开启广播flag
+    bool startLocalDisplay = false;  // 本地显示flag
+    void run() override;
+
+public:
+    CaptureThread(QObject *parent = nullptr) {
+        Q_UNUSED(parent);
+    }
+
+public slots:
+    void setThreadStart(bool start) {
+        startFlag = start;
+        if (start) {
+            if (!this->isRunning())
+                this->start();
+        } else {
+            this->quit();
+        }
+    }
+    void setBroadcast(bool start)        { startBroadcast = start; }
+    void setLocalDisplay(bool start)     { startLocalDisplay = start; }
+};
+
+#endif // CAPTURE_THREAD_H
+```
+
+### 11.3 服务端采集线程 `capture_thread.cpp`(V4L2 + UDP 广播核心)
+
+```cpp
+#include "capture_thread.h"
+
+void CaptureThread::run()
+{
+    /* 下面的代码请参考正点原子C应用编程V4L2章节,摄像头编程,这里不作解释 */
+#ifdef linux
+#ifndef __arm__
+    return;
+#endif
+    int video_fd = -1;
+    struct v4l2_format fmt;
+    struct v4l2_requestbuffers req_bufs;
+    static struct v4l2_buffer buf;
+    int n_buf;
+    struct buffer_info bufs_info[VIDEO_BUFFER_COUNT];
+    enum v4l2_buf_type type;
+
+    video_fd = open(VIDEO_DEV, O_RDWR);
+    if (0 > video_fd) {
+        printf("ERROR: failed to open video device %s\n", VIDEO_DEV);
+        return ;
+    }
+
+    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    fmt.fmt.pix.width = 640;
+    fmt.fmt.pix.height = 480;
+    fmt.fmt.pix.colorspace = V4L2_COLORSPACE_SRGB;
+    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565;
+
+    if (0 > ioctl(video_fd, VIDIOC_S_FMT, &fmt)) {
+        printf("ERROR: failed to VIDIOC_S_FMT\n");
+        close(video_fd);
+        return ;
+    }
+
+    req_bufs.count = VIDEO_BUFFER_COUNT;
+    req_bufs.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    req_bufs.memory = V4L2_MEMORY_MMAP;
+
+    if (0 > ioctl(video_fd, VIDIOC_REQBUFS, &req_bufs)) {
+        printf("ERROR: failed to VIDIOC_REQBUFS\n");
+        return ;
+    }
+
+    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    buf.memory = V4L2_MEMORY_MMAP;
+    for (n_buf = 0; n_buf < VIDEO_BUFFER_COUNT; n_buf++) {
+
+        buf.index = n_buf;
+        if (0 > ioctl(video_fd, VIDIOC_QUERYBUF, &buf)) {
+            printf("ERROR: failed to VIDIOC_QUERYBUF\n");
+            return ;
+        }
+
+        bufs_info[n_buf].length = buf.length;
+        bufs_info[n_buf].start = mmap(NULL, buf.length,
+                                      PROT_READ | PROT_WRITE, MAP_SHARED,
+                                      video_fd, buf.m.offset);
+        if (MAP_FAILED == bufs_info[n_buf].start) {
+            printf("ERROR: failed to mmap video buffer, size 0x%x\n", buf.length);
+            return ;
+        }
+    }
+
+    for (n_buf = 0; n_buf < VIDEO_BUFFER_COUNT; n_buf++) {
+        buf.index = n_buf;
+        if (0 > ioctl(video_fd, VIDIOC_QBUF, &buf)) {
+            printf("ERROR: failed to VIDIOC_QBUF\n");
+            return ;
+        }
+    }
+
+    type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    if (0 > ioctl(video_fd, VIDIOC_STREAMON, &type)) {
+        printf("ERROR: failed to VIDIOC_STREAMON\n");
+        return ;
+    }
+
+    while (startFlag) {
+
+        for (n_buf = 0; n_buf < VIDEO_BUFFER_COUNT; n_buf++) {
+
+            buf.index = n_buf;
+
+            if (0 > ioctl(video_fd, VIDIOC_DQBUF, &buf)) {
+                printf("ERROR: failed to VIDIOC_DQBUF\n");
+                return;
+            }
+
+            QImage qImage((unsigned char*)bufs_info[n_buf].start,
+                          fmt.fmt.pix.width, fmt.fmt.pix.height,
+                          QImage::Format_RGB16);
+
+            /* 是否开启本地显示,开启本地显示可能会导致开启广播卡顿,它们互相制约 */
+            if (startLocalDisplay)
+                emit imageReady(qImage);
+
+            /* 是否开启广播,开启广播会导致本地显示卡顿,它们互相制约 */
+            if (startBroadcast) {
+                QUdpSocket udpSocket;       /* udp套接字 */
+                QByteArray byte;            /* QByteArray类型 */
+                QBuffer buff(&byte);        /* 建立一个用于IO读写的缓冲区 */
+
+                qImage.save(&buff, "JPEG", -1);         /* image转为byte的类型,再存入buff */
+                QByteArray base64Byte = byte.toBase64(); /* 转换为base64Byte类型 */
+
+                /* 由udpSocket以广播的形式传输数据,端口号为8888 */
+                udpSocket.writeDatagram(base64Byte.data(), base64Byte.size(),
+                                        QHostAddress::Broadcast, 8888);
+            }
+
+            if (0 > ioctl(video_fd, VIDIOC_QBUF, &buf)) {
+                printf("ERROR: failed to VIDIOC_QBUF\n");
+                return;
+            }
+        }
+    }
+
+    msleep(800);//at lease 650
+
+    for (int i = 0; i < VIDEO_BUFFER_COUNT; i++) {
+        munmap(bufs_info[i].start, buf.length);
+    }
+
+    close(video_fd);
+#endif
+}
+```
+
+V4L2 采集流程(与 C 应用编程 V4L2 章节一致):
+
+| 步骤 | ioctl / 操作 | 说明 |
+| ---- | ------------ | ---- |
+| 打开设备 | `open(VIDEO_DEV, O_RDWR)` | `/dev/video1` 为摄像头节点(按实际确认) |
+| 设置格式 | `VIDIOC_S_FMT` | 640×480、`V4L2_PIX_FMT_RGB565` |
+| 申请缓冲 | `VIDIOC_REQBUFS` | 3 个缓冲区,`V4L2_MEMORY_MMAP` |
+| 查询并映射 | `VIDIOC_QUERYBUF` + `mmap` | 得到每个缓冲区的地址与长度 |
+| 入队 | `VIDIOC_QBUF` | 把所有缓冲区放入采集队列 |
+| 启动流 | `VIDIOC_STREAMON` | 开始采集 |
+| 循环取帧 | `VIDIOC_DQBUF` → 处理 → `VIDIOC_QBUF` | 取出一帧、转 `QImage`、处理完再入队 |
+| 停止 | 循环退出后 `munmap` + `close` | `while(startFlag)` 控制 |
+
+广播处理链:`QImage` → `QBuffer`(内存 IO)→ `save(..., "JPEG", -1)` 编码为 JPEG → `toBase64()` → `QUdpSocket::writeDatagram(..., QHostAddress::Broadcast, 8888)`。
+
+> 注意:本地显示与广播会互相制约(同一线程里串行处理),源码注释也明确说明开启广播会导致本地显示卡顿,反之亦然。
+
+### 11.4 服务端窗口 `mainwindow.cpp`
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    this->setGeometry(0, 0, 800, 480);
+
+    videoLabel = new QLabel(this);
+    videoLabel->setText("未获取到图像数据或未开启本地显示");
+    videoLabel->setStyleSheet("QWidget {color: white;}");
+    videoLabel->setAlignment(Qt::AlignCenter);
+    videoLabel->resize(640, 480);
+
+    checkBox1 = new QCheckBox(this);
+    checkBox2 = new QCheckBox(this);
+    checkBox1->resize(120, 50);
+    checkBox2->resize(120, 50);
+    checkBox1->setText("本地显示");
+    checkBox2->setText("开启广播");
+
+    checkBox1->setStyleSheet("QCheckBox {color: yellow;}"
+                             "QCheckBox:indicator {width: 40; height: 40;}");
+    checkBox2->setStyleSheet("QCheckBox {color: yellow;}"
+                             "QCheckBox:indicator {width: 40; height: 40}");
+
+    startCaptureButton = new QPushButton(this);
+    startCaptureButton->setCheckable(true);
+    startCaptureButton->setText("开始采集摄像头数据");
+
+    QColor color = QColor(Qt::black);
+    QPalette p;
+    p.setColor(QPalette::Window, color);
+    this->setPalette(p);
+
+    startCaptureButton->setStyleSheet("QPushButton {background-color: white; border-radius: 30}"
+                                      "QPushButton:pressed  {background-color: red;}");
+
+    captureThread = new CaptureThread(this);
+
+    connect(startCaptureButton, SIGNAL(clicked(bool)), captureThread, SLOT(setThreadStart(bool)));
+    connect(startCaptureButton, SIGNAL(clicked(bool)), this, SLOT(startCaptureButtonClicked(bool)));
+    connect(captureThread, SIGNAL(imageReady(QImage)), this, SLOT(showImage(QImage)));
+    connect(checkBox1, SIGNAL(clicked(bool)), captureThread, SLOT(setLocalDisplay(bool)));
+    connect(checkBox2, SIGNAL(clicked(bool)), captureThread, SLOT(setBroadcast(bool)));
+}
+
+MainWindow::~MainWindow() {}
+
+void MainWindow::showImage(QImage image)
+{
+    videoLabel->setPixmap(QPixmap::fromImage(image));
+}
+
+void MainWindow::startCaptureButtonClicked(bool start)
+{
+    if (start)
+        startCaptureButton->setText("停止采集摄像头数据");
+    else
+        startCaptureButton->setText("开始采集摄像头数据");
+}
+```
+
+### 11.5 客户端 `mainwindow.cpp`(接收并显示)
+
+```cpp
+#include "mainwindow.h"
+#include <QDebug>
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    QColor color = QColor(Qt::black);
+    QPalette p;
+    p.setColor(QPalette::Window, color);
+    this->setPalette(p);
+
+    udpSocket = new QUdpSocket(this);
+    udpSocket->bind(QHostAddress::Any, 8888);   /* 绑定端口号 */
+
+    videoLabel = new QLabel(this);
+    videoLabel->resize(640, 480);
+    videoLabel->setText("未获取到图像数据");
+    videoLabel->setStyleSheet("QWidget {color: white;}");
+    videoLabel->setAlignment(Qt::AlignCenter);
+    connect(udpSocket, SIGNAL(readyRead()), this, SLOT(videoUpdate()));
+
+    this->setGeometry(0, 0, 800, 480);
+}
+
+MainWindow::~MainWindow() {}
+
+void MainWindow::videoUpdate()
+{
+    QByteArray datagram;
+    datagram.resize(udpSocket->pendingDatagramSize());       /* 数据大小重置 */
+    udpSocket->readDatagram(datagram.data(), datagram.size()); /* 读入datagram */
+
+    QByteArray decryptedByte;
+    decryptedByte = QByteArray::fromBase64(datagram.data());  /* base64 解码 */
+
+    QImage image;
+    image.loadFromData(decryptedByte);                        /* 从数据加载图像 */
+
+    videoLabel->setPixmap(QPixmap::fromImage(image));          /* 显示图像 */
+}
+```
+
+### 11.6 工程文件(`.pro`)
+
+`video_server.pro` 与 `video_client.pro` 都需要网络模块:
+
+```pro
+QT       += core gui network
+
+greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
+
+CONFIG += c++11
+
+# video_server.pro
+SOURCES += \
+    capture_thread.cpp \
+    main.cpp \
+    mainwindow.cpp
+
+HEADERS += \
+    capture_thread.h \
+    mainwindow.h
+
+# video_client.pro
+# SOURCES += main.cpp mainwindow.cpp
+# HEADERS += mainwindow.h
+```
+
+### 11.7 运行与测试
+
+1. 在开发板上交叉编译、运行 `video_server`(需摄像头 `/dev/video1`,且仅 ARM 平台编译采集代码)。
+2. 勾选"本地显示"在开发板屏幕预览;勾选"开启广播"后,同网段客户端才能收到画面。
+3. 在 PC(或另一块开发板)上运行 `video_client`,绑定 8888 端口接收广播并显示。
+4. 注意:本地显示与广播互相制约,追求流畅广播时建议关闭本地显示。
+
+## 12. 扩展:H.264 编码与低延迟方向
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+RTMP 推流常见编码为 H.264;而上面 Qt 方案用 JPEG 逐帧广播(MJPEG 风格),优点是实现简单、无需编码库,缺点是带宽占用大、无法跨公网高效传输。若要在开发板上做低延迟、低带宽的监控,思路是:
+
+- 采集端用 V4L2 取 YUV 帧,经硬件/软件 H.264 编码器压缩后再传输;
+- 传输可用 RTSP/RTMP(配合 FFmpeg/Nginx)或自定义 TCP/UDP 协议;
+- I.MX6U 无硬件视频编解码单元,软编码 H.264 耗 CPU,实际低延迟方案应评估平台能力。
+
+## 13. 实验步骤与调试
+
+| 现象 | 原因 | 处理 |
+| ---- | ---- | ---- |
+| 开发板连不上 MQTT | 服务器 IP/端口错、用户名密码错、不在同一网段 | 确认 `BROKER_ADDRESS`、`ping` 通、`mosquitto` 已启动 |
+| 连接返回码 4/5 | 用户名或密码无效 / 未授权 | 用 `mosquitto_passwd` 重新注册,核对 `myconfig.conf` |
+| 编译 mqttClient 找不到 `MQTTClient.h` | 头文件路径未指定 | `target_include_directories` 指向 install/include |
+| 链接时报找不到 `paho-mqtt3c` | 库路径未指定或库未安装 | `target_link_directories` + `target_link_libraries`,并 `make install` |
+| 开发板运行报找不到 `libpaho-mqtt3c.so` | 动态库未拷入 | 打包 `.so` 解压到 `/usr/lib`,或设置 `LD_LIBRARY_PATH` |
+| VLC 拉不到 RTMP 流 | nginx 未启动/未 reload、端口被占、推流地址错 | `ps` 查进程、`nginx -s reload`、核对 `1935` 与流名 |
+| 视频延迟大 | 软编解码 + 服务器与推流端同板 | 降低分辨率/帧率,或换高性能平台 |
+| ARM 上采集线程直接返回 | 采集代码用 `#ifndef __arm__ return;` 限定 ARM | 确认交叉编译宏 `__arm__`,PC 上不采集 |
+
+## 14. 跨平台对比
+
+| 项目 | IMX6U(本方案) | STM32(如 F4/F7 + LwIP) | 高性能 ARM Linux(RK3568 等) |
+| ---- | --------------- | ------------------------ | ----------------------------- |
+| MQTT 客户端 | paho.mqtt.c(POSIX,同步/异步) | MQTT 嵌入式库(如 Paho Embedded / coreMQTT) | 同 paho / 各云 SDK |
+| 视频方案 | Nginx+FFmpeg(软编) 或 V4L2+JPEG 广播 | 一般无 Linux,靠专用编码/传输 | 硬件 H.264 编码 + RTSP/RTMP,低延迟 |
+| 网络栈 | 内核 TCP/IP + BSD socket | LwIP | 内核 TCP/IP |
+| 构建 | CMake + 交叉工具链 | Keil/IAR/Makefile | CMake/Yocto/Buildroot |
+
+## 15. 面试精选(5 题)
+
+### Q1 简述 MQTT 的发布/订阅模型与三大角色。它和 HTTP 的区别?
+
+**要点**:Broker 为枢纽,客户端可发布/订阅,围绕主题通信;发布/订阅解耦,且可空间分离、时间异步。
+
+**详解**:发布者把消息发到某个主题,Broker 按订阅关系转发给订阅者;客户端角色随主题切换,彼此无需知道对方存在。相比 HTTP 的请求-响应、短连接,MQTT 保持长连接、低开销、支持一对多推送与离线消息,更适合物联网。区别:
+
+| 维度 | MQTT | HTTP |
+| ---- | ---- | ---- |
+| 模型 | 发布/订阅 | 请求/响应 |
+| 连接 | 长连接 + 心跳 | 多为短连接 |
+| 开销 | 极小 | 头部冗余大 |
+| 推送 | 服务端可主动推送 | 需轮询/长轮询/WS |
+
+**追问**:
+1. 发布/订阅的"三个可"是什么?(客户端相互独立、空间可分离、时间可异步)
+2. Broker 的职责有哪些?(接收、存储、转发消息,管理客户端与会话)
+
+### Q2 MQTT 的 QoS 0/1/2 分别是什么?如何选择?
+
+**要点**:0 最多一次、1 至少一次、2 保证一次;越高质量越安全也越慢。
+
+**详解**:QoS0 发完即弃,靠 TCP;QoS1 用 PUBACK 确认,超时重发,可能重复(协议不去重,需按 dup 处理);QoS2 经 PUBREC/PUBREL/PUBCOMP 四次交互,保证接收端只收一次。实现 QoS>0 必须 `cleanSession=false`。发布与订阅 QoS 不同时,服务端按较低值降级。
+
+**追问**:
+1. 为什么 QoS1 需要应用自己去重?(协议只保证至少一次,重复由重发引起)
+2. QoS2 的报文顺序?(PUBLISH → PUBREC → PUBREL → PUBCOMP)
+
+### Q3 `cleanSession` 和会话保持有什么关系?保留消息有什么用?
+
+**要点**:`cleanSession=0` 建持久会话、可收离线消息并保留订阅;保留消息让新订阅者立刻拿到该主题最新值。
+
+**详解**:`cleanSession=1` 每次连接都是全新会话,不保存订阅、不收离线消息,`sessionPresent` 恒为 0;`cleanSession=0` 时服务端保存会话,`sessionPresent` 表示是否恢复。保留消息针对"发布在前、订阅在后"的场景,每个主题只保留一条最新值,发布空保留消息可删除。
+
+**追问**:
+1. 想接收离线 QoS1/QoS2 消息该怎样设置?(`cleanSession=false`)
+2. 如何删除某主题的保留消息?(向该主题发布一条 payload 为空的保留消息)
+
+### Q4 MQTT 心跳与遗嘱机制分别在解决什么问题?
+
+**要点**:心跳检测客户端在线状态;遗嘱在客户端意外断线时通知其它客户端。
+
+**详解**:客户端空闲时按 `keepAlive` 定时发 PINGREQ,服务端回 PINGRESP,长时间收不到即判定掉线;客户端发 PINGREQ 收不到响应则认为自身断线。遗嘱在 CONNECT 时设置(willTopic/willMessage/willRetain/willQoS),仅**意外断线**触发,主动 DISCONNECT 不触发。
+
+**追问**:
+1. 遗嘱在什么情况下不会发布?(客户端主动发送 DISCONNECT 断开)
+2. 如何用遗嘱实现上线通知?(上线时主动向自己的遗嘱主题发布一条"在线"消息)
+
+### Q5 RTMP 视频监控方案由哪几部分组成?延迟来源是什么?另一条 V4L2+JPEG 路线有何特点?
+
+**要点**:推流端 FFmpeg + 服务器 Nginx(rtmp 模块) + 拉流端 VLC;延迟主要来自软编解码和同板部署。
+
+**详解**:`ffmpeg -re -i ... -f flv rtmp://.../live/xxx` 推流,VLC 拉流;Nginx 加 `nginx-rtmp-module` 并配置 `rtmp { server { application live {...} } }`。延迟原因:I.MX6U 无硬件视频解码,全靠软件处理,且服务器与推流端在同一弱性能板子上。另一条 V4L2+JPEG 广播路线(Qt 例程)无需流媒体服务器:采集 RGB565 → JPEG → base64 → UDP 广播,客户端解码显示;实现简单、适合同网段,但带宽大、本地显示与广播互相制约。
+
+**追问**:
+1. RTMP 与 RTSP 的主要区别?(RTSP 实时性最好但实现复杂;RTMP 浏览器支持好、低延迟、生态成熟)
+2. 为什么 Qt 方案选择 UDP 广播而不是 TCP?(一对多、无连接、实现简单;代价是可能丢包)
+
+---
+
+**内容来源**:《I.MX6U嵌入式Linux C应用编程指南V1.6》第三十三章 实战小项目之MQTT物联网、第三十四章 实战小项目之视频监控;例程 `33_mqtt/mqtt_prj`(CMakeLists.txt、mqttClient.c、cmake/arm-linux-setup.cmake);Qt 例程 `Embedded-Qt-Tutorial/Qt/04/05_video_surveillance`(video_server、video_client)。第 7 节(JSON/原子云)与第 12 节(H.264)为扩展知识。

+ 860 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/04-网络编程与项目实战/面试-网络编程与项目实战.md

@@ -0,0 +1,860 @@
+---
+title: 面试-网络编程与项目实战
+tags: [嵌入式Linux, 面试, 网络编程, socket, TCP, UDP, OSI, IP地址, 端口号, CAN, SocketCAN, can_frame, CMake, 交叉编译, MQTT, QoS, 遗嘱, paho, RTMP, RTSP, Nginx, FFmpeg, V4L2, 视频监控, IMX6ULL]
+created: 2026-09-18
+updated: 2026-09-18
+pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第二十九章~第三十四章:网络基础知识、socket编程基础、CAN应用编程基础、CMake入门与进阶、实战小项目之MQTT物联网、实战小项目之视频监控"
+---
+
+# 面试-网络编程与项目实战
+
+> 💡 **关联知识**:[[04-网络编程与项目实战/01-网络基础与socket编程]]、[[04-网络编程与项目实战/03-CMake入门与进阶]]、[[04-网络编程与项目实战/04-实战项目MQTT与视频监控]]。
+
+覆盖《I.MX6U嵌入式Linux C应用编程指南》第二十九章至第三十四章的 5 大主题、25 道题。每题含「答案要点 / 详细解答(代码、对比表)/ 2 条追问」。
+
+---
+
+## 一、网络基础与 socket(5 题)
+
+### Q1.1 网络通信分哪几个层次?socket 是什么?OSI 七层与 TCP/IP 四层如何对应?
+
+**答案要点**:网络通信分硬件层、驱动层、应用层;socket 是内核向应用层提供的网络编程接口,本质是 socket IPC;OSI 七层对应 TCP/IP 四层。
+
+**详细解答**:
+
+- 硬件层:网卡设备,收发网络数据;
+- 驱动层:内核网卡驱动,向上提供 socket 接口;
+- 应用层:应用程序调用 socket 接口(或 HTTP 等更高级封装)。
+
+网络通信本质是不同主机上进程之间的通信,属于 IPC 的一种,称为 socket IPC。socket 是应用层与 TCP/IP 协议之间的软件抽象层(门面模式),把复杂的协议隐藏在简单接口后面,便于跨平台(BSD socket 标准)。
+
+| OSI 七层 | 作用 | TCP/IP 四层 |
+| -------- | ---- | ----------- |
+| 应用层 | HTTP/FTP/MQTT 等 | 应用层 |
+| 表示层 | 编码转换、压缩/加密 | 应用层 |
+| 会话层 | 建立/管理/终止会话 | 应用层 |
+| 传输层 | 端到端、端口号、TCP/UDP | 传输层 |
+| 网络层 | IP 寻址、路由 | 网络层 |
+| 数据链路层 | MAC、成帧、差错检测 | 网络接口层 |
+| 物理层 | 比特流、电平、介质 | 网络接口层 |
+
+数据发送时逐层加首部(封装),接收时逐层去首部(拆封)。
+
+**追问**
+1. socket 属于哪一层?(应用层与传输层之间的接口层,把 TCP/IP 隐藏在接口后面)
+2. 为什么说网络通信也是 IPC?(它是不同主机上进程之间的通信,属于进程间通信范畴)
+
+### Q1.2 TCP 与 UDP 有什么区别?TCP 靠什么保证可靠传输?
+
+**答案要点**:TCP 面向连接、可靠、基于字节流;UDP 无连接、不可靠、面向报文、快;TCP 靠应答、超时重传、排序、校验、流控与拥塞控制保证可靠。
+
+**详细解答**:
+
+| 维度 | TCP | UDP |
+| ---- | --- | --- |
+| 连接 | 面向连接(三次握手) | 无连接 |
+| 可靠性 | 可靠,不丢不乱 | 不可靠,出错丢弃无反馈 |
+| 传输单位 | 字节流 | 报文 |
+| 速度/开销 | 慢、开销大 | 快、无状态 |
+| 流控/拥塞控制 | 有(滑动窗口、慢启动等) | 无 |
+| 典型应用 | 文件传输、网页、MQTT、RTSP | 直播、网络电话、视频、DNS |
+
+TCP 可靠传输机制:
+
+1. **应答机制**:每个报文段都要得到接收方 ACK 才认为成功;
+2. **超时重传**:发送后启动定时器,超时未收到 ACK 则重发;
+3. **重排整理**:对乱序/重复的报文段重排、去重后再交给应用层;
+4. **校验和**:检测数据有效性;此外还有**窗口流量控制**与**拥塞控制**。
+
+**追问**
+1. TCP 流量控制与拥塞控制分别由谁控制?(流量控制由接收方通过窗口控制;拥塞控制由发送方控制)
+2. 为什么视频直播常用 UDP?(实时性要求高、可容忍少量丢包,无重传带来的延迟)
+
+### Q1.3 什么是三次握手和四次挥手?为什么建立连接是三次而关闭是四次?
+
+**答案要点**:三次握手建立连接(SYN → SYN+ACK → ACK);四次挥手关闭连接(FIN → ACK → FIN → ACK),因 TCP 全双工、每方向需单独关闭。
+
+**详细解答**:
+
+**三次握手**:
+
+1. 第一次:客户端置 SYN=1,随机序号 `seq=J`,发送后进入 `SYN_SENT`;
+2. 第二次:服务端置 SYN=1、ACK=1,`ack=J+1`,随机 `seq=K`,进入 `SYN_RCVD`;
+3. 第三次:客户端置 ACK=1,`ack=K+1`,双方进入 `ESTABLISHED`。
+
+注意小写 `ack`(确认号)与大写 `ACK`(标志位)不是同一个概念。
+
+**四次挥手**(假设客户端主动关闭):
+
+1. 客户端发 FIN,进入 `FIN_WAIT_1`;
+2. 服务端回 ACK,客户端进入 `FIN_WAIT_2`;
+3. 服务端发 FIN,客户端进入 `LAST_ACK` 状态侧(服务端等待);
+4. 客户端回 ACK,进入 `TIME_WAIT`,等待 2MSL 后关闭;服务端收到 ACK 后关闭。
+
+因为 TCP 全双工,每个方向要单独关闭:一方发 FIN 只表示该方向没有数据了,另一方向仍可继续发送,所以确认和 FIN 不能像握手那样合并,需四次。
+
+**追问**
+1. TIME_WAIT 为什么等 2MSL?(确保最后的 ACK 能到达对端,并让本连接的旧报文在网络中消失)
+2. 为什么握手需要三次而不能两次?(两次无法让服务端确认客户端已收到自己的 SYN/ACK,且可防止历史失效连接请求)
+
+### Q1.4 写出 TCP 服务器与客户端的基本流程,`accept()` 返回的套接字有什么用?
+
+**答案要点**:服务器 socket→bind→listen→accept→read/write→close;客户端 socket→connect→read/write→close;`accept()` 返回一个**新的**套接字,代表与某个客户端的连接。
+
+**详细解答**:
+
+服务器:
+
+```c
+int sockfd = socket(AF_INET, SOCK_STREAM, 0);
+/* bind:绑定 IP 与端口 */
+struct sockaddr_in server_addr = {0};
+server_addr.sin_family = AF_INET;
+server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
+server_addr.sin_port = htons(8888);
+bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
+
+listen(sockfd, 50);                          /* 进入监听 */
+connfd = accept(sockfd, (struct sockaddr *)&client_addr, &addrlen);  /* 阻塞等待 */
+recv(connfd, recvbuf, sizeof(recvbuf), 0);   /* 与客户端通信 */
+close(connfd);
+close(sockfd);
+```
+
+客户端:
+
+```c
+int sockfd = socket(AF_INET, SOCK_STREAM, 0);
+server_addr.sin_port = htons(8888);
+inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);
+connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
+send(sockfd, buf, strlen(buf), 0);
+close(sockfd);
+```
+
+关键点:`socket()` 返回的是**监听套接字**;`accept()` 返回的是**已连接套接字**,服务器用它和对应客户端收发电数据。`accept()` 在无连接请求时阻塞;若对客户端地址不关心,`addr`/`addrlen` 可传 `NULL`。`bind` 不是必需:客户端可让内核自动选址。
+
+**追问**
+1. `listen()` 的 `backlog` 是什么?(等待连接队列的最大值,队列满时新连接请求可能被丢弃/报错)
+2. UDP 客户端调用 `connect()` 会发生握手吗?(不会,只在 sockfd 中记录服务器地址端口,不发送数据)
+
+### Q1.5 IP 地址如何分类?如何判断两个 IP 在同一网段?端口号与字节序转换函数各是什么?
+
+**答案要点**:IPv4 分 A/B/C/D/E 五类;网络标识 = IP & 子网掩码,相同即同网段;端口号 0~65535;字节序用 `htonl/htons/ntohl/ntohs`,地址转换用 `inet_pton/inet_ntop`。
+
+**详细解答**:
+
+| 类别 | 首字节范围 | 默认掩码 | 私有地址 |
+| ---- | ---------- | -------- | -------- |
+| A | 1~126 | 255.0.0.0 | 10.0.0.0~10.255.255.255 |
+| B | 128~191 | 255.255.0.0 | 172.16.0.0~172.31.255.255 |
+| C | 192~223 | 255.255.255.0 | 192.168.0.0~192.168.255.255 |
+| D | 224~239(多播) | — | — |
+| E | 240~255(保留) | — | — |
+
+特殊地址:
+
+- 直接广播地址:主机号全 1(如 C 类 `192.168.0.255`);
+- 受限广播地址:`255.255.255.255`;
+- 环回地址:`127.x.x.x`;
+- `0.0.0.0`:本网络本主机,只能作源地址;监听 `0.0.0.0` 即监听本机所有 IPv4 地址。
+
+判断同网段:`网络标识 = IP 地址 & 子网掩码`,两个 IP 网络标识相同即处于同一网络。
+
+端口号用来在一台主机上唯一标识一个能上网的进程,取值 0~65535,"IP + 端口号"区分不同进程。常见端口:HTTP 80、FTP 21、SMTP 25、TFTP 69、SSH 22、Telnet 23、POP3 110。
+
+字节序与转换:
+
+```c
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+socket_addr.sin_port = htons(5555);          /* 主机序 -> 网络序(16位) */
+socket_addr.sin_addr.s_addr = htonl(INADDR_ANY);  /* 主机序 -> 网络序(32位) */
+
+inet_pton(AF_INET, "192.168.1.222", &addr);  /* 点分十进制字符串 -> 二进制 */
+inet_ntop(AF_INET, &addr, buf, sizeof(buf)); /* 二进制 -> 点分十进制字符串 */
+```
+
+`inet_aton/inet_addr/inet_ntoa` 已废弃,且不支持 IPv6;新代码用 `inet_pton/inet_ntop`。
+
+**追问**
+1. `INADDR_ANY` 是什么?(值为 0,表示绑定本机所有网络接口地址)
+2. 端口号为什么不能随便选?(要避免与已有服务冲突;自定义服务通常选大于 5000 的不常用端口)
+
+---
+
+## 二、CAN 应用编程(5 题)
+
+### Q2.1 CAN 是什么?有哪些特点?电气属性和网络拓扑是怎样的?
+
+**答案要点**:CAN 是控制器局域网络,ISO 标准化的串行通信协议,多主、差分、抗干扰强;显性 0、隐性 1;两端需 120Ω 端接电阻。
+
+**详细解答**:CAN(Controller Area Network)由德国博世公司开发,最初用于解决汽车电子控制系统之间的通信、减少线束,现广泛用于工业自动化、船舶、医疗等。
+
+主要特点:
+
+- **多主控制**:总线空闲时所有单元都可发送,最先访问总线者获得发送权;同时发送时高优先级 ID 获胜;
+- **消息发送**:以固定格式发送,ID 不表示目的地址,而表示访问总线的优先级;
+- **系统柔软性**:节点无"地址"信息,增减节点不影响其它节点;
+- **通信速度**:同一网络所有单元必须统一速度,不同网络可不同;
+- **远程数据请求**:可发"遥控帧"请求数据;
+- **错误检测/通知/恢复**:所有单元可检测错误、通知、强制结束并重发;
+- **故障封闭**:区分暂时性错误与持续错误,可将故障单元隔离;
+- **连接**:理论节点数不限,实际受时延和电气负载限制,速度越低可连节点越多。
+
+电气属性:用 CAN_H、CAN_L 两根线的电位差表示电平。**显性电平**逻辑 0,CAN_H 约 3.5V、CAN_L 约 1.5V,电位差 2V;**隐性电平**逻辑 1,两线均约 2.5V,电位差 0V。总线空闲时保持隐性。
+
+网络拓扑:每个节点由 MCU + CAN 控制器 + CAN 收发器构成,通过 CAN_H/CAN_L 连成总线;总线两端各接一个 **120Ω 端接电阻**,匹配阻抗、吸收反射。CAN 速度可达 1Mbps(CAN-FD 更高),速度与总线距离相关。
+
+**追问**
+1. 为什么总线两端要接 120Ω 电阻?(匹配总线阻抗,吸收信号反射,提高抗干扰能力与可靠性)
+2. CAN 的 ID 是目的地址吗?(不是,ID 表示消息访问总线的优先级)
+
+### Q2.2 CAN 有哪几种帧?数据帧由哪些段构成?优先级如何仲裁?
+
+**答案要点**:五种帧:数据帧、遥控帧、错误帧、过载帧、间隔帧;数据帧由帧起始/仲裁段/控制段/数据段/CRC段/ACK段/帧结束 7 段构成;按 ID 逐位仲裁。
+
+**详细解答**:
+
+| 帧 | 用途 |
+| -- | ---- |
+| 数据帧 | 发送单元向接收单元传送数据 |
+| 遥控帧 | 接收单元向具有相同 ID 的发送单元请求数据 |
+| 错误帧 | 检测出错误时向其它单元通知错误 |
+| 过载帧 | 接收单元通知尚未做好接收准备 |
+| 间隔帧 | 将数据帧/遥控帧与前面的帧分离开 |
+
+数据帧有标准格式(11 位 ID)和扩展格式(29 位 ID)两种,由 7 段构成:帧起始、仲裁段、控制段(含数据字节数与保留位)、数据段(0~8 字节)、CRC 段、ACK 段、帧结束。图中 D 表示显性(0)、R 表示隐性(1)。
+
+仲裁:多个单元同时发送时,对 ID 的每个位逐位比较,显性(0)优先,仲裁获胜者继续发送,失利者立即停止改为接收。因此 ID 数值越小优先级越高。
+
+**追问**
+1. 标准帧和扩展帧的 ID 位宽分别是多少?(标准 11 位,扩展 29 位)
+2. 数据段最多发送多少字节?(8 字节)
+
+### Q2.3 SocketCAN 中如何创建套接字、绑定 can0、设置过滤规则?
+
+**答案要点**:`socket(PF_CAN, SOCK_RAW, CAN_RAW)`;用 `ifreq` + `ioctl(SIOCGIFINDEX)` 得到索引后 `bind`;用 `setsockopt(..., CAN_RAW_FILTER, ...)` 设置过滤。
+
+**详细解答**:Linux 把 CAN 设备当网络设备管理,提供 SocketCAN 接口,数据结构与函数定义在 `<linux/can.h>`。
+
+```c
+#include <sys/socket.h>
+#include <linux/can.h>
+#include <linux/can/raw.h>
+#include <net/if.h>
+#include <sys/ioctl.h>
+
+int sockfd = socket(PF_CAN, SOCK_RAW, CAN_RAW);
+if (0 > sockfd) { perror("socket error"); exit(EXIT_FAILURE); }
+
+struct ifreq ifr = {0};
+struct sockaddr_can can_addr = {0};
+strcpy(ifr.ifr_name, "can0");                 /* 指定 can0 设备 */
+ioctl(sockfd, SIOCGIFINDEX, &ifr);            /* 获取接口索引 */
+can_addr.can_family = AF_CAN;
+can_addr.can_ifindex = ifr.ifr_ifindex;
+
+if (0 > bind(sockfd, (struct sockaddr *)&can_addr, sizeof(can_addr))) {
+    perror("bind error"); close(sockfd); exit(EXIT_FAILURE);
+}
+```
+
+过滤规则(只接收 ID 0x60A、0x60B):
+
+```c
+struct can_filter rfilter[2];
+rfilter[0].can_id = 0x60A;  rfilter[0].can_mask = 0x7FF;
+rfilter[1].can_id = 0x60B;  rfilter[1].can_mask = 0x7FF;
+setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));
+```
+
+仅发送、不接收时,可用 `setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, NULL, 0)` 省略内核接收队列以省 CPU。不设置过滤规则则默认接收所有 ID 报文。
+
+**追问**
+1. `can_filter` 的两个成员是什么?(`can_id` 与 `can_mask`,满足 `can_id & can_mask` 的帧被接收)
+2. 仅发送的应用如何减少 CPU 消耗?(设置 CAN_RAW_FILTER 为 NULL、长度为 0,省略内核接收队列)
+
+### Q2.4 `struct can_frame` 各字段含义?如何发送、接收和判断错误帧?
+
+**答案要点**:`can_id`(标识符,含帧标志位)、`can_dlc`(数据长度,最长 8)、`data[8]`;用 `write()` 发送、`read()` 接收;用 `can_id & CAN_ERR_FLAG` 判断错误帧。
+
+**详细解答**:
+
+```c
+struct can_frame {
+    canid_t can_id;   /* CAN 标识符 */
+    __u8 can_dlc;     /* 数据长度(最长 8 字节) */
+    __u8 __pad;
+    __u8 __res0;
+    __u8 __res1;
+    __u8 data[8];     /* 数据 */
+};
+```
+
+`can_id` 低 11 位为标准帧 ID,0~28 位为扩展帧 ID;第 29/30/31 位是帧类型标志:
+
+| 宏 | 值 | 含义 |
+| -- | -- | ---- |
+| `CAN_EFF_FLAG` | 0x80000000 | 扩展帧标识 |
+| `CAN_RTR_FLAG` | 0x40000000 | 远程帧标识 |
+| `CAN_ERR_FLAG` | 0x20000000 | 错误帧标识 |
+| `CAN_SFF_MASK` | 0x000007FF | 取标准帧 ID |
+| `CAN_EFF_MASK` | 0x1FFFFFFF | 取扩展帧 ID |
+
+发送数据帧:
+
+```c
+struct can_frame frame;
+frame.can_id = 123;          /* 扩展帧则 frame.can_id = CAN_EFF_FLAG | 123; */
+frame.can_dlc = 3;
+frame.data[0] = 0xA0;
+frame.data[1] = 0xB0;
+frame.data[2] = 0xC0;
+int ret = write(sockfd, &frame, sizeof(frame));
+if (sizeof(frame) != ret) perror("write error");
+```
+
+发送远程帧:`frame.can_id = CAN_RTR_FLAG | 123;`。
+
+接收:
+
+```c
+struct can_frame frame;
+int ret = read(sockfd, &frame, sizeof(frame));
+
+if (frame.can_id & CAN_ERR_FLAG) { printf("Error frame!\n"); }
+if (frame.can_id & CAN_EFF_FLAG)
+    printf("扩展帧 <0x%08x> ", frame.can_id & CAN_EFF_MASK);
+else
+    printf("标准帧 <0x%03x> ", frame.can_id & CAN_SFF_MASK);
+if (frame.can_id & CAN_RTR_FLAG) { printf("remote request\n"); }
+for (i = 0; i < frame.can_dlc; i++) printf("%02x ", frame.data[i]);
+```
+
+错误帧的具体原因通过 `can_id` 的其他符号位判断,定义在 `<linux/can/error.h>`,如 `CAN_ERR_TX_TIMEOUT`、`CAN_ERR_LOSTARB`、`CAN_ERR_BUSOFF`、`CAN_ERR_ACK` 等。
+
+**追问**
+1. 发送数据后如何判断是否成功?(比较 `write` 返回值与 `sizeof(frame)`,不等即发送失败)
+2. 如何判断收到的是扩展帧还是标准帧?(看 `can_id & CAN_EFF_FLAG`,再用对应 MASK 取 ID)
+
+### Q2.5 CAN 的本地回环功能是什么?如何用命令行配置和测试 CAN?
+
+**答案要点**:回环功能默认开启,发送帧会回环到对应套接字;用 `setsockopt(..., CAN_RAW_LOOPBACK, ...)` 开关;命令行用 `ip link` 配波特率、`cansend/candump` 收发。
+
+**详细解答**:回环开启时,所有发送帧都会被回环到与 CAN 总线接口对应的套接字上。默认开启,可关闭或开启:
+
+```c
+int loopback = 0;   /* 0 关闭,1 开启(默认) */
+setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_LOOPBACK, &loopback, sizeof(loopback));
+```
+
+命令行配置与测试(CAN 设备按网络设备管理,用与以太网相同的命令):
+
+```bash
+ifconfig can0 down                                            # 先关闭 can0
+ip link set can0 up type can bitrate 1000000 triple-sampling on  # 设置波特率
+cansend can0 123#01.02.03.04.05.06.07.08                      # 发送:ID=123,数据 8 字节
+candump -ta can0                                              # 接收并显示
+```
+
+`#` 前的 `123` 是帧 ID,后面是数据。测试时要用 CAN 分析仪或另一块开发板对接,且**分析仪波特率必须与开发板一致**(示例为 1000000)。
+
+**追问**
+1. 回环功能默认是开还是关?(默认开启)
+2. 两块开发板对接时,CAN_H/CAN_L 如何连接?(CAN_H 接 CAN_H、CAN_L 接 CAN_L,且需 120Ω 端接)
+
+---
+
+## 三、CMake(5 题)
+
+### Q3.1 CMake 和 Makefile 是什么关系?CMake 的工作流程是怎样的?
+
+**答案要点**:CMake 是生成 Makefile 的工具,解析平台无关的 `CMakeLists.txt`,根据当前平台生成本地化 Makefile,最终仍由 make 编译。
+
+**详细解答**:Makefile 语法复杂、跨平台差;CMake 用统一的 `CMakeLists.txt` 描述工程,优点是开源、跨平台、语法简单。工作流程:
+
+```bash
+cmake ../        # 解析 CMakeLists.txt,生成 Makefile 等中间文件
+make             # 依据生成的 Makefile 编译
+```
+
+推荐 **out-of-source** 构建(源码与构建分离):
+
+```bash
+mkdir build && cd build
+cmake .. && make
+```
+
+清理时直接删除 `build` 目录。
+
+**追问**
+1. out-of-source 构建有什么好处?(中间文件和产物与源码分离,清理方便,不污染源码树)
+2. `cmake` 生成的中间文件有哪些?(`CMakeCache.txt`、`CMakeFiles/`、`cmake_install.cmake`、`Makefile` 等)
+
+### Q3.2 列出常用 CMake 命令,并说明 `include_directories` 与 `target_include_directories` 的区别。
+
+**答案要点**:常用命令见表格;`include_directories` 作用于当前源码所有目标并向下传递,`target_include_directories` 只作用于指定目标,用 PRIVATE/PUBLIC/INTERFACE 控制传播。
+
+**详细解答**:
+
+| 命令 | 说明 |
+| ---- | ---- |
+| `add_executable` | 可执行程序目标 |
+| `add_library` | 库文件目标 |
+| `add_subdirectory` | 加载子目录的 CMakeLists.txt |
+| `aux_source_directory` | 收集目录源文件到变量 |
+| `include_directories` / `link_directories` / `link_libraries` | 全局头文件/库搜索路径、链接库 |
+| `target_include_directories` / `target_link_libraries` / `target_sources` | 针对指定目标的头文件/库/源文件 |
+| `cmake_minimum_required` | 最低版本要求 |
+| `project` | 工程名(可带 VERSION) |
+| `set` | 设置变量 |
+| `set_target_properties` / `get_target_property` | 设置/获取目标属性 |
+| `list` | 列表操作 |
+| `message` | 打印信息 |
+
+关键区别:`include_directories()`/`link_libraries()` 针对当前源码中**所有目标**并向下传递(经 `add_subdirectory` 传递给子源码),大工程易混乱;`target_*` 只影响指定目标,传播范围由关键字决定:
+
+| 关键字 | 当前目标使用 | 传给依赖目标 |
+| ------ | ------------ | ------------ |
+| PRIVATE | 是 | 否 |
+| INTERFACE | 否 | 是 |
+| PUBLIC | 是 | 是 |
+
+**追问**
+1. 为什么推荐 `target_*` 系列?(作用范围明确,避免全局污染,保持工程目录清晰)
+2. `PUBLIC` 等于什么?(`PRIVATE` + `INTERFACE`)
+
+### Q3.3 CMake 中如何生成静态库和动态库?如何修改生成的库文件名?
+
+**答案要点**:`add_library(name [STATIC|SHARED] src...)`,默认静态库;改名用 `set_target_properties(name PROPERTIES OUTPUT_NAME ...)`。
+
+**详细解答**:
+
+```cmake
+add_library(mylib STATIC 1.c 2.c 3.c)   # libmylib.a
+add_library(mylib SHARED 1.c 2.c 3.c)   # libmylib.so
+
+# 用 BUILD_SHARED_LIBS 改变 add_library 默认行为
+set(BUILD_SHARED_LIBS on)
+add_library(hello hello/hello.c)        # 生成 libhello.so
+
+# 修改输出名(目标名唯一,不能直接改名)
+set_target_properties(libhello PROPERTIES OUTPUT_NAME "hello")  # libhello.a / .so
+```
+
+Linux 下库名自动加 `lib` 前缀和 `.a`/`.so` 后缀,`add_library` 的第一个参数是**不含前后缀**的库名。
+
+**追问**
+1. 为什么 `add_library(hello hello.c)` 不能用来把库改名?(目标名工程内唯一,已存在同名目标)
+2. 可执行文件与库文件的输出路径由什么变量控制?(`EXECUTABLE_OUTPUT_PATH`、`LIBRARY_OUTPUT_PATH`)
+
+### Q3.4 如何为 CMake 工程配置 ARM 交叉编译?为什么推荐工具链文件方式?
+
+**答案要点**:设置 `CMAKE_SYSTEM_NAME`、`CMAKE_SYSTEM_PROCESSOR`、`CMAKE_SYSROOT`、`CMAKE_C_COMPILER`、`CMAKE_C_FLAGS` 等;写成独立工具链文件,用 `-DCMAKE_TOOLCHAIN_FILE=` 指定,且必须在 `project()` 之前生效。
+
+**详细解答**:
+
+```cmake
+# cmake/arm-linux-setup.cmake
+set(CMAKE_SYSTEM_NAME Linux)
+set(CMAKE_SYSTEM_PROCESSOR arm)
+
+set(TOOLCHAIN_DIR /opt/fsl-imx-x11/4.1.15-2.1.0/sysroots)
+set(CMAKE_SYSROOT ${TOOLCHAIN_DIR}/cortexa7hf-neon-poky-linux-gnueabi)
+
+set(CMAKE_C_COMPILER ${TOOLCHAIN_DIR}/x86_64-pokysdk-linux/usr/bin/arm-poky-linux-gnueabi/arm-poky-linux-gnueabi-gcc)
+set(CMAKE_CXX_COMPILER ${TOOLCHAIN_DIR}/x86_64-pokysdk-linux/usr/bin/arm-poky-linux-gnueabi/arm-poky-linux-gnueabi-g++)
+
+set(CMAKE_C_FLAGS "-march=armv7ve -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a7")
+set(CMAKE_CXX_FLAGS "-march=armv7ve -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a7")
+
+set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
+set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
+```
+
+```bash
+cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/arm-linux-setup.cmake -DCMAKE_BUILD_TYPE=Release ..
+make
+file main      # 应显示 ARM 架构
+```
+
+工具链方式的优点:配置与业务解耦,不污染 `CMakeLists.txt`;若直接写进 `CMakeLists.txt`,必须放在 `project()` 之前否则不生效。`-D` 创建的是缓存变量(全局,覆盖同名普通变量)。
+
+**追问**
+1. `CMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY` 的含义?(`find_library()` 只在 `CMAKE_SYSROOT` 中搜索)
+2. 交叉编译后如何验证产物架构?(`file` 命令,应为 ARM 而非 x86-64)
+
+### Q3.5 CMake 变量有哪些作用域?函数中如何修改外部变量或返回值?
+
+**答案要点**:函数作用域、目录作用域(值拷贝、向下有效)、全局作用域(缓存变量);函数中用 `set(... PARENT_SCOPE)` 修改上层变量,借此实现返回值。
+
+**详细解答**:
+
+- **函数作用域**:函数内引用未定义的变量时逐层向外查找;函数内 `set` 创建的是函数内变量,不影响外部同名变量。加 `PARENT_SCOPE` 可设置到上一层作用域:
+
+```cmake
+function(xyz)
+    set(ABC "Hello China!" PARENT_SCOPE)
+endfunction()
+set(ABC "Hello World!")
+xyz()
+message("${ABC}")   # Hello China!
+```
+
+- **目录作用域**:子目录会把父目录变量**值拷贝**一份(向下有效、值拷贝),子目录内修改不影响父目录。
+- **全局作用域**:缓存变量(`set(... CACHE ...)` 或命令行 `-D`)在整个工程生命周期有效。
+
+用 `PARENT_SCOPE` 实现函数返回值(把变量名当参数传入):
+
+```cmake
+function(xyz out var1 var2)
+    math(EXPR temp "${var1} + ${var2}")
+    set(${out} ${temp} PARENT_SCOPE)
+endfunction()
+
+xyz(out_var 5 10)
+message("${out_var}")   # 15
+```
+
+**追问**
+1. 嵌套函数中 `PARENT_SCOPE` 写到哪一层?(写到调用者的作用域,即上一层)
+2. `-D` 定义的变量属于哪种作用域?(全局缓存变量)
+
+---
+
+## 四、MQTT 物联网(5 题)
+
+### Q4.1 MQTT 是什么?有哪些主要特性?发布/订阅模型怎么理解?
+
+**答案要点**:MQTT 是基于客户端-服务端、发布/订阅模式的应用层协议,构建于 TCP/IP;特性包括发布订阅、基于 TCP、QoS、小型传输、遗嘱、主题寻址、心跳。
+
+**详细解答**:MQTT(消息队列遥测传输)轻巧、开放、简单、规范,适合 M2M 与 IoT。三个角色:服务端(Broker)、客户端、主题(Topic)。
+
+- 客户端发布消息到某主题,Broker 检查哪些客户端订阅了该主题并转发;
+- 客户端角色不固定,可对不同主题既发布又订阅;
+- 发布/订阅三特性:客户端相互独立、空间可分离、时间可异步。
+
+应用场景:车联网、智能家居、即时聊天、工业物联网;因需长连接和心跳,不适合低功耗场合。
+
+**追问**
+1. MQTT 工作在 TCP/IP 的哪一层?(应用层,构建于 TCP/IP 之上)
+2. 为什么说 MQTT 不适合低功耗场合?(需保持长连接、定时发心跳包,比较耗电)
+
+### Q4.2 QoS 三个级别分别是什么含义?服务质量如何降级?
+
+**答案要点**:QoS0 最多一次、QoS1 至少一次、QoS2 保证一次;发布与订阅 QoS 不同时,服务端采用较低级别。
+
+**详细解答**:
+
+| QoS | 语义 | 流程 |
+| --- | ---- | ---- |
+| 0 | 最多发一次 | PUBLISH,不确认、不重传 |
+| 1 | 至少发一次 | PUBLISH → PUBACK,超时重发,可能重复 |
+| 2 | 保证收一次 | PUBLISH → PUBREC → PUBREL → PUBCOMP |
+
+- 实现 QoS>0 必须 `cleanSession=false`,否则收不到离线消息;
+- QoS1 协议本身不去重,需应用根据 `dup` 标志处理;
+- 降级:客户端 A 以 QoS2 发布、客户端 B 以 QoS1 订阅,则服务端对 B 采用 QoS1。
+
+**追问**
+1. QoS1 为什么可能重复投递?(发送方未及时收到 PUBACK 会重发,协议不做去重)
+2. QoS2 为什么最慢?(需要两次确认、四次报文交互)
+
+### Q4.3 `cleanSession` 有什么作用?保留消息解决什么问题?
+
+**答案要点**:`cleanSession=0` 建持久会话、可收离线 QoS>0 消息并保存订阅;保留消息让新订阅者立即收到该主题最新值。
+
+**详细解答**:`cleanSession` 是布尔值:
+
+- `=0`:持久性会话,客户端再次上线能收到离线期间发来的所有 QoS>0 消息;服务端记住客户端订阅的主题,直到会话超时注销;
+- `=1`:临时会话,收不到离线消息、服务端不保存订阅,断开后会话销毁。
+
+CONNACK 的 `sessionPresent` 与 `cleanSession` 配合:`cleanSession=0` 时返回 1 表示服务端保存了上次会话状态。
+
+保留消息:发布时 `retain=true`,服务端保存该主题**最新一条**消息,任何客户端订阅后立即收到,无需等下次发布。每个主题只有一条保留消息,新保留消息覆盖旧的;发布一条空的保留消息即可删除。
+
+**追问**
+1. 想接收离线消息要怎样设置连接参数?(`cleanSession=false`)
+2. 如何删除保留消息?(发布一条 payload 为空的保留消息)
+
+### Q4.4 MQTT 的心跳机制、遗嘱机制、用户名密码认证分别是什么?
+
+**答案要点**:心跳用 PINGREQ/PINGRESP 检测在线;遗嘱在意外断线时由服务端发布;用户名密码用于连接认证与权限管理。
+
+**详细解答**:
+
+- **心跳**:客户端空闲时按 `keepAlive` 定时发 PINGREQ,服务端回 PINGRESP。服务端长期收不到即判断客户端掉线;客户端发 PINGREQ 收不到 PINGRESP 则认为自身断线。
+- **遗嘱**:CONNECT 时设置 `willTopic`、`willMessage`、`willRetain`、`willQoS`。仅**意外断线**(非主动 DISCONNECT)时才由服务端发布。可实现上线通知:客户端上线时向自己的遗嘱主题发布"在线"消息。
+- **认证**:CONNECT 报文的 `username`/`password` 可选;服务端开启认证时客户端必须提供正确凭据,有些服务端还用其管理私人主题权限。
+
+**追问**
+1. 主动断开连接会触发遗嘱吗?(不会,只有意外断线才触发)
+2. 心跳间隔由哪个参数决定?(CONNECT 报文的 `keepAlive`)
+
+### Q4.5 用 paho.mqtt.c 编写同步客户端的基本流程是什么?接收回调有哪些注意事项?
+
+**答案要点**:create → setCallbacks → connect → publish/subscribe → unsubscribe → disconnect → destroy;回调中必须释放内存,返回 1 表示处理成功、返回 0 会重新投递(此时不可释放内存)。
+
+**详细解答**:
+
+```c
+MQTTClient client;
+MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
+MQTTClient_willOptions will_opts = MQTTClient_willOptions_initializer;
+MQTTClient_message pubmsg = MQTTClient_message_initializer;
+
+MQTTClient_create(&client, BROKER_ADDRESS, CLIENTID,
+                  MQTTCLIENT_PERSISTENCE_NONE, NULL);
+MQTTClient_setCallbacks(client, NULL, connlost, msgarrvd, NULL);   /* 必须在 connect 前 */
+
+will_opts.topicName = "dt_mqtt/will";
+will_opts.message = "Unexpected disconnection";
+will_opts.retained = 1; will_opts.qos = 0;
+conn_opts.will = &will_opts;
+conn_opts.keepAliveInterval = 30;
+conn_opts.cleansession = 0;
+conn_opts.username = "mqtt1";
+conn_opts.password = "123456";
+MQTTClient_connect(client, &conn_opts);
+
+pubmsg.payload = "Online"; pubmsg.payloadlen = 6;
+pubmsg.qos = 0; pubmsg.retained = 1;
+MQTTClient_publishMessage(client, "dt_mqtt/will", &pubmsg, NULL);
+
+MQTTClient_subscribe(client, "dt_mqtt/led", 0);
+/* ... 循环读取温度并发布 ... */
+MQTTClient_unsubscribe(client, "dt_mqtt/led");
+MQTTClient_disconnect(client, 10000);
+MQTTClient_destroy(&client);
+```
+
+接收回调:
+
+```c
+static int msgarrvd(void *context, char *topicName, int topicLen,
+                    MQTTClient_message *message)
+{
+    /* 处理消息 ... */
+    MQTTClient_freeMessage(&message);   /* 必须释放消息内存 */
+    MQTTClient_free(topicName);         /* 必须释放主题名内存 */
+    return 1;                           /* 返回 1 表示成功处理 */
+}
+```
+
+注意事项:
+
+- `setCallbacks` 必须在 `MQTTClient_connect` **之前**调用;`ma` 回调必须设置,否则收不到消息;
+- 返回 0 表示处理失败,客户端库会重新投递,此时**不要**释放 message/topicName,否则重新投递失败;
+- 库分同步(`MQTTClient.h` / `libpaho-mqtt3c.so`)与异步(`MQTTAsync.h` / `libpaho-mqtt3a.so`)。
+
+**追问**
+1. 为什么回调里要调用 `MQTTClient_freeMessage` 和 `MQTTClient_free`?(消息与主题名的内存由库分配,需应用释放)
+2. 同步与异步模式的区别体现在哪里?(是否在发布后等待确认;由 `reliable` 等控制,默认同步)
+
+---
+
+## 五、视频监控(5 题)
+
+### Q5.1 RTSP 与 RTMP 有什么区别?RTMP 视频监控方案由哪几部分组成?
+
+**答案要点**:RTSP 实时性最好但实现复杂;RTMP 低延迟、浏览器支持好、生态成熟。方案由推流端(FFmpeg)、流媒体服务器(Nginx + rtmp 模块)、拉流端(VLC)组成。
+
+**详细解答**:
+
+| 协议 | 特点 | 适用 |
+| ---- | ---- | ---- |
+| RTSP | 基于文本的多媒体播放控制协议,定义流格式,流经 RTP 传输;实时性最好,实现复杂 | 视频聊天、视频监控 |
+| RTMP | Adobe 提出,解决流媒体多路复用与分包;低延迟、稳定性高、支持所有摄像头格式,加载 flash 插件即可播放 | 直播、推流 |
+
+方案结构:推流端负责把视频数据经 RTMP 传给流媒体服务器;服务器接收并转发给拉流客户端;拉流端从服务器获取数据。实现上推流用 FFmpeg、服务器用 Nginx、拉流用 VLC。
+
+**追问**
+1. 为什么在浏览器里播放 RTSP 很困难?(浏览器原生不支持 RTSP,RTMP 可通过 flash 插件播放而流行)
+2. 本方案中开发板扮演哪些角色?(既是流媒体服务器,也是推流端)
+
+### Q5.2 移植 Nginx 支持 RTMP 的关键步骤有哪些?
+
+**答案要点**:下载 nginx 与 nginx-rtmp-module;交叉编译前改 `auto/cc/name` 和 `auto/types/sizeof`;`./configure --add-module=...`;首次 make 报错后向 `objs/ngx_auto_config.h` 添加 SYSVSHM 宏;make install 后部署到开发板并配置 `rtmp {}`。
+
+**详细解答**:
+
+```bash
+wget http://nginx.org/download/nginx-1.20.0.tar.gz
+git clone https://github.com/arut/nginx-rtmp-module.git
+tar -xzf nginx-1.20.0.tar.gz && cd nginx-1.20.0
+source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
+```
+
+源码修改:
+
+| 文件 | 位置 | 修改 |
+| ---- | ---- | ---- |
+| `auto/cc/name` | 第 21 行 | 注释掉 `exit 1` |
+| `auto/types/sizeof` | 第 15 行 | `ngx_size=` 改为 `ngx_size=4` |
+| `auto/types/sizeof` | 第 36 行 | `$CC` 改为 `gcc` |
+
+```bash
+./configure --prefix=/home/dt/tools/nginx-1.20.0/install \
+    --with-http_ssl_module --with-http_mp4_module --with-http_v2_module \
+    --without-http_upstream_zone_module \
+    --add-module=/home/dt/tools/nginx-rtmp-module
+make    # 首次报错后向 objs/ngx_auto_config.h 添加 NGX_HAVE_SYSVSHM 宏
+make install
+arm-poky-linux-gnueabi-strip --strip-debug nginx   # 可选,去调试信息
+```
+
+部署到开发板:移除出厂 nginx(`rm -rf /usr/sbin/nginx /etc/nginx/*`),拷贝新 nginx 到 `/home/root`,拷贝 conf/logs/html 到 `/etc/nginx`,启动:
+
+```bash
+./nginx -p /etc/nginx
+./nginx -p /etc/nginx -s reload
+```
+
+`nginx.conf` 加 RTMP 配置:`rtmp { server { listen 1935; application live { live on; record off; } } }`。
+
+**追问**
+1. `--add-module` 的作用?(添加第三方模块,此处指向 nginx-rtmp-module 源码路径使 Nginx 支持 RTMP)
+2. 为什么编译前要改 `auto/types/sizeof`?(交叉编译时 size 探测无法运行目标程序,需固定 `ngx_size=4`、用主机 gcc 探测)
+
+### Q5.3 FFmpeg 推流的命令参数含义?延迟大的原因是什么?
+
+**答案要点**:`ffmpeg -re -i 输入 -c:av copy -f flv rtmp://...`;延迟主要来自 I.MX6U 无硬件解码、软件处理慢,且服务器与推流端同板。
+
+**详细解答**:
+
+```bash
+# 推送视频文件
+ffmpeg -re -i /run/media/mmcblk0p1/testVideo.mp4 -c:av copy -f flv rtmp://127.0.0.1/live/mytest
+
+# 推送 USB 摄像头
+ffmpeg -f v4l2 -video_size 320x240 -framerate 15 -i /dev/video2 -q 10 -f flv rtmp://127.0.0.1/live/mytest
+```
+
+| 参数 | 含义 |
+| ---- | ---- |
+| `-re` | 按原始帧率读取,实时推流 |
+| `-i` | 输入(视频文件或 V4L2 设备) |
+| `-c:av copy` | 音视频直接拷贝(不重新编码) |
+| `-f v4l2` | 使用 V4L2 采集摄像头 |
+| `-video_size` / `-framerate` | 分辨率 / 帧率 |
+| `-q 10` | 视频质量参数 |
+| `-f flv` | 输出 FLV 封装(RTMP 传输) |
+| `rtmp://127.0.0.1/live/mytest` | 推流地址:服务器/live 应用/流名 |
+
+延迟原因:I.MX6U 没有硬件视频解码,FFmpeg 的音视频处理全靠软件,耗时大;服务器和推流端都在同一块弱性能开发板上。
+
+**追问**
+1. 拉流端用什么工具?(VLC,媒体 → 打开网络串流,输入 `rtmp://<开发板IP>/live/mytest`)
+2. 如何降低延迟?(降低分辨率/帧率,或换带硬件编解码的高性能平台)
+
+### Q5.4 基于 C 应用编程的 V4L2 摄像头采集流程是什么?
+
+**答案要点**:open → VIDIOC_S_FMT → VIDIOC_REQBUFS → VIDIOC_QUERYBUF + mmap → VIDIOC_QBUF → VIDIOC_STREAMON → 循环 DQBUF/QBUF → munmap + close。
+
+**详细解答**(与 Qt 视频监控例程 `capture_thread.cpp` 一致):
+
+| 步骤 | 操作 | 说明 |
+| ---- | ---- | ---- |
+| 打开设备 | `open("/dev/video1", O_RDWR)` | 摄像头节点按实际确认 |
+| 设置格式 | `VIDIOC_S_FMT` | 如 640×480、`V4L2_PIX_FMT_RGB565` |
+| 申请缓冲 | `VIDIOC_REQBUFS` | 如 3 个缓冲、`V4L2_MEMORY_MMAP` |
+| 查询映射 | `VIDIOC_QUERYBUF` + `mmap` | 得到缓冲地址与长度 |
+| 入队 | `VIDIOC_QBUF` | 所有缓冲放入采集队列 |
+| 启动 | `VIDIOC_STREAMON` | 开始采集 |
+| 循环取帧 | `VIDIOC_DQBUF` → 处理 → `VIDIOC_QBUF` | 取出、处理、再入队 |
+| 结束 | `munmap` + `close` | 释放映射与设备 |
+
+```c
+fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+fmt.fmt.pix.width = 640;
+fmt.fmt.pix.height = 480;
+fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565;
+ioctl(video_fd, VIDIOC_S_FMT, &fmt);
+
+req_bufs.count = 3;
+req_bufs.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+req_bufs.memory = V4L2_MEMORY_MMAP;
+ioctl(video_fd, VIDIOC_REQBUFS, &req_bufs);
+
+/* QUERYBUF + mmap 每个缓冲 */
+/* QBUF 所有缓冲 */
+ioctl(video_fd, VIDIOC_STREAMON, &type);
+
+while (startFlag) {
+    ioctl(video_fd, VIDIOC_DQBUF, &buf);   /* 取出一帧 */
+    /* 处理(显示 / 编码 / 广播) */
+    ioctl(video_fd, VIDIOC_QBUF, &buf);    /* 重新入队 */
+}
+```
+
+**追问**
+1. 为什么要用 mmap 而不是 read?(mmap 零拷贝、效率高,适合高帧率视频采集)
+2. DQBUF 和 QBUF 为什么成对出现?(出队的缓冲处理完必须重新入队,采集才能继续)
+
+### Q5.5 Qt 的 V4L2 + JPEG 广播视频监控方案如何实现?它与 Nginx 方案相比有何取舍?
+
+**答案要点**:服务端 V4L2 采集 RGB565 → `QImage` → 编码 JPEG → base64 → UDP 广播 8888;客户端绑定端口接收、解码显示;无需流媒体服务器、适合同网段,但带宽大、本地显示与广播互相制约。
+
+**详细解答**:
+
+服务端(`video_server`,`CaptureThread::run`)关键代码:
+
+```cpp
+QImage qImage((unsigned char*)bufs_info[n_buf].start,
+              fmt.fmt.pix.width, fmt.fmt.pix.height,
+              QImage::Format_RGB16);
+
+if (startLocalDisplay)
+    emit imageReady(qImage);          /* 本地显示 */
+
+if (startBroadcast) {
+    QUdpSocket udpSocket;
+    QByteArray byte;
+    QBuffer buff(&byte);
+    qImage.save(&buff, "JPEG", -1);                   /* 编码 JPEG */
+    QByteArray base64Byte = byte.toBase64();          /* base64 编码 */
+    udpSocket.writeDatagram(base64Byte.data(), base64Byte.size(),
+                            QHostAddress::Broadcast, 8888);   /* UDP 广播 */
+}
+```
+
+客户端(`video_client`):
+
+```cpp
+udpSocket->bind(QHostAddress::Any, 8888);
+/* readyRead -> videoUpdate */
+void MainWindow::videoUpdate() {
+    QByteArray datagram;
+    datagram.resize(udpSocket->pendingDatagramSize());
+    udpSocket->readDatagram(datagram.data(), datagram.size());
+    QByteArray decryptedByte = QByteArray::fromBase64(datagram.data());
+    QImage image;
+    image.loadFromData(decryptedByte);
+    videoLabel->setPixmap(QPixmap::fromImage(image));
+}
+```
+
+`.pro` 需 `QT += core gui network`。
+
+两条路线对比:
+
+| 维度 | Nginx + FFmpeg(RTMP) | V4L2 + JPEG 广播 |
+| ---- | ---------------------- | ---------------- |
+| 是否需要服务器进程 | 需要(Nginx) | 不需要,服务端直接广播 |
+| 传输层 | TCP(RTMP) | UDP 广播 |
+| 适用网络 | 局域网/公网 | 同网段 |
+| 实现复杂度 | 高(移植 Nginx、配 RTMP) | 低(Qt 一个线程) |
+| 带宽 | 较低(可 H.264 编码) | 高(逐帧 JPEG + base64) |
+| 延迟 | 本板实测 5~6 秒 | 取决于帧率与网络 |
+| 局限 | 软编解码耗时 | 本地显示与广播互相制约 |
+
+**追问**
+1. 为什么广播会导致本地显示卡顿?(同一采集线程串行处理显示与广播,二者互相制约)
+2. 想用公网或更低带宽该怎么做?(走 RTMP/RTSP 并做 H.264 编码;但 I.MX6U 无硬件编码,需评估性能)
+
+---
+
+**内容来源**:《I.MX6U嵌入式Linux C应用编程指南V1.6》第二十九章 网络基础知识、第三十章 socket编程基础、第三十一章 CAN应用编程基础、第三十二章 CMake入门与进阶、第三十三章 实战小项目之MQTT物联网、第三十四章 实战小项目之视频监控;例程 `33_mqtt/mqtt_prj`、`31_can/can_write.c`、`30_socket/socket_server.c`、`30_socket/socket_client.c`;Qt 例程 `Embedded-Qt-Tutorial/Qt/04/05_video_surveillance`。