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
💡 关联知识:[[04-网络编程与项目实战/01-网络基础与socket编程]]、[[04-网络编程与项目实战/03-CMake入门与进阶]]、[[04-网络编程与项目实战/04-实战项目MQTT与视频监控]]。
覆盖《I.MX6U嵌入式Linux C应用编程指南》第二十九章至第三十四章的 5 大主题、25 道题。每题含「答案要点 / 详细解答(代码、对比表)/ 2 条追问」。
答案要点:网络通信分硬件层、驱动层、应用层;socket 是内核向应用层提供的网络编程接口,本质是 socket IPC;OSI 七层对应 TCP/IP 四层。
详细解答:
网络通信本质是不同主机上进程之间的通信,属于 IPC 的一种,称为 socket IPC。socket 是应用层与 TCP/IP 协议之间的软件抽象层(门面模式),把复杂的协议隐藏在简单接口后面,便于跨平台(BSD socket 标准)。
| OSI 七层 | 作用 | TCP/IP 四层 |
|---|---|---|
| 应用层 | HTTP/FTP/MQTT 等 | 应用层 |
| 表示层 | 编码转换、压缩/加密 | 应用层 |
| 会话层 | 建立/管理/终止会话 | 应用层 |
| 传输层 | 端到端、端口号、TCP/UDP | 传输层 |
| 网络层 | IP 寻址、路由 | 网络层 |
| 数据链路层 | MAC、成帧、差错检测 | 网络接口层 |
| 物理层 | 比特流、电平、介质 | 网络接口层 |
数据发送时逐层加首部(封装),接收时逐层去首部(拆封)。
追问
答案要点:TCP 面向连接、可靠、基于字节流;UDP 无连接、不可靠、面向报文、快;TCP 靠应答、超时重传、排序、校验、流控与拥塞控制保证可靠。
详细解答:
| 维度 | TCP | UDP |
|---|---|---|
| 连接 | 面向连接(三次握手) | 无连接 |
| 可靠性 | 可靠,不丢不乱 | 不可靠,出错丢弃无反馈 |
| 传输单位 | 字节流 | 报文 |
| 速度/开销 | 慢、开销大 | 快、无状态 |
| 流控/拥塞控制 | 有(滑动窗口、慢启动等) | 无 |
| 典型应用 | 文件传输、网页、MQTT、RTSP | 直播、网络电话、视频、DNS |
TCP 可靠传输机制:
追问
答案要点:三次握手建立连接(SYN → SYN+ACK → ACK);四次挥手关闭连接(FIN → ACK → FIN → ACK),因 TCP 全双工、每方向需单独关闭。
详细解答:
三次握手:
seq=J,发送后进入 SYN_SENT;ack=J+1,随机 seq=K,进入 SYN_RCVD;ack=K+1,双方进入 ESTABLISHED。注意小写 ack(确认号)与大写 ACK(标志位)不是同一个概念。
四次挥手(假设客户端主动关闭):
FIN_WAIT_1;FIN_WAIT_2;LAST_ACK 状态侧(服务端等待);TIME_WAIT,等待 2MSL 后关闭;服务端收到 ACK 后关闭。因为 TCP 全双工,每个方向要单独关闭:一方发 FIN 只表示该方向没有数据了,另一方向仍可继续发送,所以确认和 FIN 不能像握手那样合并,需四次。
追问
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 可传 NULL。bind 不是必需:客户端可让内核自动选址。
追问
listen() 的 backlog 是什么?(等待连接队列的最大值,队列满时新连接请求可能被丢弃/报错)connect() 会发生握手吗?(不会,只在 sockfd 中记录服务器地址端口,不发送数据)答案要点: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(保留) | — | — |
特殊地址:
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。
追问
INADDR_ANY 是什么?(值为 0,表示绑定本机所有网络接口地址)答案要点:CAN 是控制器局域网络,ISO 标准化的串行通信协议,多主、差分、抗干扰强;显性 0、隐性 1;两端需 120Ω 端接电阻。
详细解答:CAN(Controller Area Network)由德国博世公司开发,最初用于解决汽车电子控制系统之间的通信、减少线束,现广泛用于工业自动化、船舶、医疗等。
主要特点:
电气属性:用 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 更高),速度与总线距离相关。
追问
答案要点:五种帧:数据帧、遥控帧、错误帧、过载帧、间隔帧;数据帧由帧起始/仲裁段/控制段/数据段/CRC段/ACK段/帧结束 7 段构成;按 ID 逐位仲裁。
详细解答:
| 帧 | 用途 |
|---|---|
| 数据帧 | 发送单元向接收单元传送数据 |
| 遥控帧 | 接收单元向具有相同 ID 的发送单元请求数据 |
| 错误帧 | 检测出错误时向其它单元通知错误 |
| 过载帧 | 接收单元通知尚未做好接收准备 |
| 间隔帧 | 将数据帧/遥控帧与前面的帧分离开 |
数据帧有标准格式(11 位 ID)和扩展格式(29 位 ID)两种,由 7 段构成:帧起始、仲裁段、控制段(含数据字节数与保留位)、数据段(0~8 字节)、CRC 段、ACK 段、帧结束。图中 D 表示显性(0)、R 表示隐性(1)。
仲裁:多个单元同时发送时,对 ID 的每个位逐位比较,显性(0)优先,仲裁获胜者继续发送,失利者立即停止改为接收。因此 ID 数值越小优先级越高。
追问
答案要点: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 报文。
追问
can_filter 的两个成员是什么?(can_id 与 can_mask,满足 can_id & can_mask 的帧被接收)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_TIMEOUT、CAN_ERR_LOSTARB、CAN_ERR_BUSOFF、CAN_ERR_ACK 等。
追问
write 返回值与 sizeof(frame),不等即发送失败)can_id & CAN_EFF_FLAG,再用对应 MASK 取 ID)答案要点:回环功能默认开启,发送帧会回环到对应套接字;用 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)。
追问
答案要点: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 目录。
追问
cmake 生成的中间文件有哪些?(CMakeCache.txt、CMakeFiles/、cmake_install.cmake、Makefile 等)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 | 是 | 是 |
追问
target_* 系列?(作用范围明确,避免全局污染,保持工程目录清晰)PUBLIC 等于什么?(PRIVATE + INTERFACE)答案要点: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 的第一个参数是不含前后缀的库名。
追问
add_library(hello hello.c) 不能用来把库改名?(目标名工程内唯一,已存在同名目标)EXECUTABLE_OUTPUT_PATH、LIBRARY_OUTPUT_PATH)答案要点:设置 CMAKE_SYSTEM_NAME、CMAKE_SYSTEM_PROCESSOR、CMAKE_SYSROOT、CMAKE_C_COMPILER、CMAKE_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 创建的是缓存变量(全局,覆盖同名普通变量)。
追问
CMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY 的含义?(find_library() 只在 CMAKE_SYSROOT 中搜索)file 命令,应为 ARM 而非 x86-64)答案要点:函数作用域、目录作用域(值拷贝、向下有效)、全局作用域(缓存变量);函数中用 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
追问
PARENT_SCOPE 写到哪一层?(写到调用者的作用域,即上一层)-D 定义的变量属于哪种作用域?(全局缓存变量)答案要点:MQTT 是基于客户端-服务端、发布/订阅模式的应用层协议,构建于 TCP/IP;特性包括发布订阅、基于 TCP、QoS、小型传输、遗嘱、主题寻址、心跳。
详细解答:MQTT(消息队列遥测传输)轻巧、开放、简单、规范,适合 M2M 与 IoT。三个角色:服务端(Broker)、客户端、主题(Topic)。
应用场景:车联网、智能家居、即时聊天、工业物联网;因需长连接和心跳,不适合低功耗场合。
追问
答案要点:QoS0 最多一次、QoS1 至少一次、QoS2 保证一次;发布与订阅 QoS 不同时,服务端采用较低级别。
详细解答:
| QoS | 语义 | 流程 |
|---|---|---|
| 0 | 最多发一次 | PUBLISH,不确认、不重传 |
| 1 | 至少发一次 | PUBLISH → PUBACK,超时重发,可能重复 |
| 2 | 保证收一次 | PUBLISH → PUBREC → PUBREL → PUBCOMP |
cleanSession=false,否则收不到离线消息;dup 标志处理;追问
cleanSession 有什么作用?保留消息解决什么问题?答案要点:cleanSession=0 建持久会话、可收离线 QoS>0 消息并保存订阅;保留消息让新订阅者立即收到该主题最新值。
详细解答:cleanSession 是布尔值:
=0:持久性会话,客户端再次上线能收到离线期间发来的所有 QoS>0 消息;服务端记住客户端订阅的主题,直到会话超时注销;=1:临时会话,收不到离线消息、服务端不保存订阅,断开后会话销毁。CONNACK 的 sessionPresent 与 cleanSession 配合:cleanSession=0 时返回 1 表示服务端保存了上次会话状态。
保留消息:发布时 retain=true,服务端保存该主题最新一条消息,任何客户端订阅后立即收到,无需等下次发布。每个主题只有一条保留消息,新保留消息覆盖旧的;发布一条空的保留消息即可删除。
追问
cleanSession=false)答案要点:心跳用 PINGREQ/PINGRESP 检测在线;遗嘱在意外断线时由服务端发布;用户名密码用于连接认证与权限管理。
详细解答:
keepAlive 定时发 PINGREQ,服务端回 PINGRESP。服务端长期收不到即判断客户端掉线;客户端发 PINGREQ 收不到 PINGRESP 则认为自身断线。willTopic、willMessage、willRetain、willQoS。仅意外断线(非主动 DISCONNECT)时才由服务端发布。可实现上线通知:客户端上线时向自己的遗嘱主题发布"在线"消息。username/password 可选;服务端开启认证时客户端必须提供正确凭据,有些服务端还用其管理私人主题权限。追问
keepAlive)答案要点: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 回调必须设置,否则收不到消息;MQTTClient.h / libpaho-mqtt3c.so)与异步(MQTTAsync.h / libpaho-mqtt3a.so)。追问
MQTTClient_freeMessage 和 MQTTClient_free?(消息与主题名的内存由库分配,需应用释放)reliable 等控制,默认同步)答案要点:RTSP 实时性最好但实现复杂;RTMP 低延迟、浏览器支持好、生态成熟。方案由推流端(FFmpeg)、流媒体服务器(Nginx + rtmp 模块)、拉流端(VLC)组成。
详细解答:
| 协议 | 特点 | 适用 |
|---|---|---|
| RTSP | 基于文本的多媒体播放控制协议,定义流格式,流经 RTP 传输;实时性最好,实现复杂 | 视频聊天、视频监控 |
| RTMP | Adobe 提出,解决流媒体多路复用与分包;低延迟、稳定性高、支持所有摄像头格式,加载 flash 插件即可播放 | 直播、推流 |
方案结构:推流端负责把视频数据经 RTMP 传给流媒体服务器;服务器接收并转发给拉流客户端;拉流端从服务器获取数据。实现上推流用 FFmpeg、服务器用 Nginx、拉流用 VLC。
追问
答案要点:下载 nginx 与 nginx-rtmp-module;交叉编译前改 auto/cc/name 和 auto/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; } } }。
追问
--add-module 的作用?(添加第三方模块,此处指向 nginx-rtmp-module 源码路径使 Nginx 支持 RTMP)auto/types/sizeof?(交叉编译时 size 探测无法运行目标程序,需固定 ngx_size=4、用主机 gcc 探测)答案要点: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 的音视频处理全靠软件,耗时大;服务器和推流端都在同一块弱性能开发板上。
追问
rtmp://<开发板IP>/live/mytest)答案要点: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); /* 重新入队 */
}
追问
答案要点:服务端 V4L2 采集 RGB565 → QImage → 编码 JPEG → base64 → UDP 广播 8888;客户端绑定端口接收、解码显示;无需流媒体服务器、适合同网段,但带宽大、本地显示与广播互相制约。
详细解答:
服务端(video_server,CaptureThread::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));
}
.pro 需 QT += core gui network。
两条路线对比:
| 维度 | Nginx + FFmpeg(RTMP) | V4L2 + JPEG 广播 |
|---|---|---|
| 是否需要服务器进程 | 需要(Nginx) | 不需要,服务端直接广播 |
| 传输层 | TCP(RTMP) | UDP 广播 |
| 适用网络 | 局域网/公网 | 同网段 |
| 实现复杂度 | 高(移植 Nginx、配 RTMP) | 低(Qt 一个线程) |
| 带宽 | 较低(可 H.264 编码) | 高(逐帧 JPEG + base64) |
| 延迟 | 本板实测 5~6 秒 | 取决于帧率与网络 |
| 局限 | 软编解码耗时 | 本地显示与广播互相制约 |
追问
内容来源:《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。