面试-网络编程与项目实战.md 43 KB


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() 返回一个新的套接字,代表与某个客户端的连接。

详细解答

服务器:

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);

客户端:

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 可传 NULLbind 不是必需:客户端可让内核自动选址。

追问

  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。

字节序与转换:

#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>

#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):

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_idcan_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 判断错误帧。

详细解答

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

发送数据帧:

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;

接收:

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_TIMEOUTCAN_ERR_LOSTARBCAN_ERR_BUSOFFCAN_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 总线接口对应的套接字上。默认开启,可关闭或开启:

int loopback = 0;   /* 0 关闭,1 开启(默认) */
setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_LOOPBACK, &loopback, sizeof(loopback));

命令行配置与测试(CAN 设备按网络设备管理,用与以太网相同的命令):

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 描述工程,优点是开源、跨平台、语法简单。工作流程:

cmake ../        # 解析 CMakeLists.txt,生成 Makefile 等中间文件
make             # 依据生成的 Makefile 编译

推荐 out-of-source 构建(源码与构建分离):

mkdir build && cd build
cmake .. && make

清理时直接删除 build 目录。

追问

  1. out-of-source 构建有什么好处?(中间文件和产物与源码分离,清理方便,不污染源码树)
  2. cmake 生成的中间文件有哪些?(CMakeCache.txtCMakeFiles/cmake_install.cmakeMakefile 等)

Q3.2 列出常用 CMake 命令,并说明 include_directoriestarget_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 ...)

详细解答

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_PATHLIBRARY_OUTPUT_PATH

Q3.4 如何为 CMake 工程配置 ARM 交叉编译?为什么推荐工具链文件方式?

答案要点:设置 CMAKE_SYSTEM_NAMECMAKE_SYSTEM_PROCESSORCMAKE_SYSROOTCMAKE_C_COMPILERCMAKE_C_FLAGS 等;写成独立工具链文件,用 -DCMAKE_TOOLCHAIN_FILE= 指定,且必须在 project() 之前生效。

详细解答

# 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)
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 可设置到上一层作用域:

    function(xyz)
    set(ABC "Hello China!" PARENT_SCOPE)
    endfunction()
    set(ABC "Hello World!")
    xyz()
    message("${ABC}")   # Hello China!
    
  • 目录作用域:子目录会把父目录变量值拷贝一份(向下有效、值拷贝),子目录内修改不影响父目录。

  • 全局作用域:缓存变量(set(... CACHE ...) 或命令行 -D)在整个工程生命周期有效。

PARENT_SCOPE 实现函数返回值(把变量名当参数传入):

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 的 sessionPresentcleanSession 配合:cleanSession=0 时返回 1 表示服务端保存了上次会话状态。

保留消息:发布时 retain=true,服务端保存该主题最新一条消息,任何客户端订阅后立即收到,无需等下次发布。每个主题只有一条保留消息,新保留消息覆盖旧的;发布一条空的保留消息即可删除。

追问

  1. 想接收离线消息要怎样设置连接参数?(cleanSession=false
  2. 如何删除保留消息?(发布一条 payload 为空的保留消息)

Q4.4 MQTT 的心跳机制、遗嘱机制、用户名密码认证分别是什么?

答案要点:心跳用 PINGREQ/PINGRESP 检测在线;遗嘱在意外断线时由服务端发布;用户名密码用于连接认证与权限管理。

详细解答

  • 心跳:客户端空闲时按 keepAlive 定时发 PINGREQ,服务端回 PINGRESP。服务端长期收不到即判断客户端掉线;客户端发 PINGREQ 收不到 PINGRESP 则认为自身断线。
  • 遗嘱:CONNECT 时设置 willTopicwillMessagewillRetainwillQoS。仅意外断线(非主动 DISCONNECT)时才由服务端发布。可实现上线通知:客户端上线时向自己的遗嘱主题发布"在线"消息。
  • 认证:CONNECT 报文的 username/password 可选;服务端开启认证时客户端必须提供正确凭据,有些服务端还用其管理私人主题权限。

追问

  1. 主动断开连接会触发遗嘱吗?(不会,只有意外断线才触发)
  2. 心跳间隔由哪个参数决定?(CONNECT 报文的 keepAlive

Q4.5 用 paho.mqtt.c 编写同步客户端的基本流程是什么?接收回调有哪些注意事项?

答案要点:create → setCallbacks → connect → publish/subscribe → unsubscribe → disconnect → destroy;回调中必须释放内存,返回 1 表示处理成功、返回 0 会重新投递(此时不可释放内存)。

详细解答

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);

接收回调:

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_freeMessageMQTTClient_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/nameauto/types/sizeof./configure --add-module=...;首次 make 报错后向 objs/ngx_auto_config.h 添加 SYSVSHM 宏;make install 后部署到开发板并配置 rtmp {}

详细解答

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
./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,启动:

./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 无硬件解码、软件处理慢,且服务器与推流端同板。

详细解答

# 推送视频文件
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 释放映射与设备
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_serverCaptureThread::run)关键代码:

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):

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));
}

.proQT += 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_prj31_can/can_write.c30_socket/socket_server.c30_socket/socket_client.c;Qt 例程 Embedded-Qt-Tutorial/Qt/04/05_video_surveillance