--- 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 #include 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 接口,数据结构与函数定义在 ``。 ```c #include #include #include #include #include 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` 的其他符号位判断,定义在 ``,如 `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`。