网络编程与应用
1. 概述
1.1 功能简介
网络编程是嵌入式 Linux 上最基础、也最常用的应用开发方向之一。RK3588 内置 GMAC0 千兆以太网控制器(GMAC0@fe1c0000),支持 10/100/1000Mbps 自适应以太网连接,配合 Linux 6.1 内核自带的完整 TCP/IP 协议栈,就能做出高性能的网络通信应用。本文档依次介绍:网络环境配置、TCP/UDP 套接字(Socket,程序之间通过网络收发数据的标准接口)编程、多线程并发服务器、HTTP 请求、MQTT 通信、NFS 远程挂载、网络性能测试、SSH 远程访问。所有示例都用 C 语言编写,并在 RK3588 平台上验证过。
1.2 应用场景
- 工业网关:Modbus TCP/RTU 网关、协议转换
- 边缘计算节点:MQTT 数据采集与上报、HTTP REST API 服务
- 远程管理:SSH 远程登录、NFS 文件共享、OTA 远程升级
- 数据传输:大文件 TCP/UDP 传输、实时流媒体推送
- 网络测试:带宽测试、延迟测量、丢包率分析
- 物联网终端:传感器数据采集、云端连接、远程控制
1.3 工作原理
先看全貌:应用程序发出的数据要经过传输层(TCP/UDP)、网络层(IP),交给内核网络栈,再由网卡驱动送到 GMAC 硬件,最后经 PHY 芯片从 RJ45 网口出去。分层结构如下:
┌─────────────────────────────────────────────────────────────┐
│ 应用层 │
│ TCP Server/Client UDP Socket HTTP(libcurl) MQTT │
│ SSH NFS iperf │
├─────────────────────────────────────────────────────────────┤
│ 传输层 │
│ TCP (可靠流) UDP (不可靠数据报) │
├─────────────────────────────────────────────────────────────┤
│ 网络层 │
│ IPv4 / IPv6 ICMP ARP │
├─────────────────────────────────────────────────────────────┤
│ Linux 内核网络栈 (6.1) │
│ Socket API Netfilter /proc/net 网络设备管理 │
├─────────────────────────────────────────────────────────────┤
│ 驱动层 │
│ stmmac (Synopsys DesignWare MAC) │
│ GMAC0@fe1c0000 — 千兆以太网 MAC │
├─────────────────────────────────────────────────────────────┤
│ 硬件层 │
│ RK3588 GMAC0 ──► RGMII/SGMII ──► PHY ──► RJ45 网口 │
└─────────────────────────────────────────────────────────────┘RK3588 网络相关硬件信息:
| 模块 | 设备树节点 | 寄存器基地址 | 功能 |
|---|---|---|---|
| GMAC0 | gmac0: ethernet@fe1c0000 | 0xFE1C0000 | 千兆以太网 MAC |
| GMAC1 | gmac1: ethernet@fe1b0000 | 0xFE1B0000 | 千兆以太网 MAC(第二路,如有) |
| PCIe | pcie3x4@fe170000 | 0xFE170000 | 可扩展 WiFi/万兆网卡 |
2. 硬件准备
2.1 硬件清单
常规配置如下,网线准备一到两根:
| 组件 | 型号/规格 | 数量 | 备注 |
|---|---|---|---|
| 主控板 | C-3588LQ 核心板 + SHIMETAPI-M5-R2A 底板 | 1 | 运行 Linux 6.1 系统 |
| 网线 | 标准 CAT5e/CAT6 RJ45 | 1~2 | 连接路由器/交换机或 PC 直连 |
| 调试串口线 | USB 转 TTL(3.3V) | 1 | 波特率 1500000 |
| 电源适配器 | 12V/2A DC | 1 | |
| 电脑(测试机) | x86 Linux/Windows/macOS | 1 | 用于交叉编译和网络测试 |
| 路由器/交换机(可选) | 支持 DHCP | 1 | 多设备组网测试 |
2.2 硬件连接
接路由器最省事,IP 自动分配;没有路由器时也可以网线直连 PC,两边配静态 IP:
SHIMETAPI-M5-R2A 底板网络连接:
方案一:连接路由器(DHCP 自动获取 IP)
├── RJ45 网口 (GMAC0) ──► 路由器 LAN 口
├── USB 调试口 ──► PC(串口调试)
└── DC 12V 电源 ──► 电源适配器
方案二:PC 直连(静态 IP)
├── RJ45 网口 (GMAC0) ──► PC 网口(交叉网线或直连线)
├── USB 调试口 ──► PC(串口调试)
└── DC 12V 电源 ──► 电源适配器2.3 硬件确认
逐条执行下面的命令,对照注释确认:架构 aarch64、内核 6.1.xx、网卡接口(end1 或 end0)存在、stmmac 驱动已加载:
# 确认系统架构
uname -m
# 输出:aarch64
# 确认内核版本
uname -r
# 输出:6.1.xx
# 确认网卡是否存在
ip link show
# 输出中应包含 end1 或 end0(GMAC0 对应的网络接口)
# 确认网卡驱动
dmesg | grep -i gmac
# 应看到 stmmac 驱动加载信息
# 确认网络接口详细信息
ip addr show end1
# 查看 MAC 地址、IP 地址等3. 软件准备
3.1 交叉编译工具链
交叉编译就是在 PC 上编译出能在板子上运行的程序。RK3588 Linux SDK 自带 GCC 10.3 交叉编译工具链,加进 PATH 就能用:
# SDK 工具链路径
SDK_DIR/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/
# 添加到 PATH
export PATH=$SDK_DIR/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin:$PATH
# 验证
aarch64-linux-gnu-gcc --version
# 输出:aarch64-linux-gnu-gcc (GCC) 10.3.1 20210621也可以使用系统安装的交叉编译器:
# Ubuntu/Debian
sudo apt-get install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
# 验证
aarch64-linux-gnu-gcc --version3.2 目标板依赖安装
下面把后面各节要用的库和工具一次装齐,每条命令的用途见注释:
# 更新软件源
sudo apt-get update
# 安装基础网络工具
sudo apt-get install -y net-tools iputils-ping
# 安装 libcurl 开发库(HTTP 请求)
sudo apt-get install -y libcurl4-openssl-dev curl
# 安装 mosquitto 客户端和开发库(MQTT)
sudo apt-get install -y mosquitto-clients libmosquitto-dev
# 安装 iperf(网络性能测试)
sudo apt-get install -y iperf
# 安装 OpenSSH 服务端
sudo apt-get install -y openssh-server
# 安装 NFS 工具
sudo apt-get install -y nfs-common nfs-kernel-server
# 安装 pthread 开发支持(通常已内置)
# 编译时使用 -lpthread 参数即可3.3 主机端工具
主机这边装交叉编译器和网络调试工具:
# 主机端安装交叉编译器和网络调试工具
sudo apt-get install -y \
gcc-aarch64-linux-gnu \
g++-aarch64-linux-gnu \
iperf \
net-tools \
openssh-client \
nfs-kernel-server4. 操作步骤
4.1 网络环境配置
联网是一切网络编程的前提。板上的网卡接口名通常叫 end1 或 end0,以 ip addr 的实际输出为准。
DHCP 自动获取 IP
拿到 IP 后,ip addr show end1 里能看到 inet 开头的一行、ping 网关能通,DHCP 就算配好了:
# 查看当前网络接口状态
ip addr show
# 使用 DHCP 获取 IP(接口名以实际为准,通常为 end1 或 end0)
sudo dhclient end1
# 或使用 systemd-networkd
sudo systemctl enable systemd-networkd
sudo systemctl start systemd-networkd
# 查看获取的 IP 地址
ip addr show end1
# 输出示例:
# inet 192.168.49.100/24 brd 192.168.49.255 scope global dynamic end1
# 查看默认网关
ip route show default
# 输出示例:default via 192.168.49.1 dev end1
# 查看 DNS 配置
cat /etc/resolv.conf静态 IP 配置
方法一用 ip 命令临时配置,立即生效但重启后失效;方法二写进 systemd-networkd 的配置文件,重启后仍然有效:
# 方法一:使用 ip 命令临时配置
sudo ip addr add 192.168.49.100/24 dev end1
sudo ip link set end1 up
sudo ip route add default via 192.168.49.1
# 方法二:写入网络配置文件(以 systemd-networkd 为例)
sudo mkdir -p /etc/systemd/network
sudo tee /etc/systemd/network/10-end1.network << 'EOF'
[Match]
Name=end1
[Network]
Address=192.168.49.100/24
Gateway=192.168.49.1
DNS=8.8.8.8
DNS=114.114.114.114
[Link]
_MTU=1500"
EOF
sudo systemctl restart systemd-networkd网络连通性测试
按"网关 → 外网 IP → 域名"的顺序测:ping 不通网关是链路问题;IP 通但域名解析不了是 DNS 问题:
# 测试网关连通性
ping -c 4 192.168.49.1
# 测试外网连通性
ping -c 4 8.8.8.8
# 测试 DNS 解析
nslookup www.baidu.com
# 查看网络路由
ip route show
# 查看网络接口统计
cat /proc/net/dev4.2 TCP 服务器与客户端
TCP 是面向连接的可靠传输。先跑通一对最简单的回显程序:客户端发什么,服务器原样发回来。两边各自跑起来后,客户端收到的内容和发出的一致,链路就通了。
TCP 服务器
服务器的固定套路是四步:socket 创建套接字、bind 绑定地址和端口、listen 开始监听、accept 等待连接进来:
/* tcp_server.c — RK3588 TCP 服务器示例 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define SERVER_PORT 8080
#define BUFFER_SIZE 1024
int main(void)
{
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
char buffer[BUFFER_SIZE];
ssize_t recv_len;
/* 创建 TCP 套接字 */
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
perror("socket");
return EXIT_FAILURE;
}
/* 允许端口复用 */
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
/* 绑定地址和端口 */
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(SERVER_PORT);
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("bind");
close(server_fd);
return EXIT_FAILURE;
}
/* 监听 */
if (listen(server_fd, 5) < 0) {
perror("listen");
close(server_fd);
return EXIT_FAILURE;
}
printf("TCP Server listening on port %d...\n", SERVER_PORT);
/* 接受客户端连接 */
client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
if (client_fd < 0) {
perror("accept");
close(server_fd);
return EXIT_FAILURE;
}
printf("Client connected: %s:%d\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
/* 接收并回显数据 */
while ((recv_len = recv(client_fd, buffer, BUFFER_SIZE - 1, 0)) > 0) {
buffer[recv_len] = '\0';
printf("Received: %s", buffer);
/* 回显给客户端 */
send(client_fd, buffer, recv_len, 0);
}
printf("Client disconnected.\n");
close(client_fd);
close(server_fd);
return EXIT_SUCCESS;
}TCP 客户端
客户端更简单:socket 之后 connect 连上服务器,就能 send/recv 收发数据:
/* tcp_client.c — RK3588 TCP 客户端示例 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define SERVER_IP "192.168.49.100"
#define SERVER_PORT 8080
#define BUFFER_SIZE 1024
int main(void)
{
int sock_fd;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE];
ssize_t recv_len;
/* 创建 TCP 套接字 */
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd < 0) {
perror("socket");
return EXIT_FAILURE;
}
/* 设置服务器地址 */
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
perror("inet_pton");
close(sock_fd);
return EXIT_FAILURE;
}
/* 连接服务器 */
if (connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("connect");
close(sock_fd);
return EXIT_FAILURE;
}
printf("Connected to server %s:%d\n", SERVER_IP, SERVER_PORT);
/* 发送数据并接收回显 */
const char *msg = "Hello from RK3588 TCP Client!\n";
send(sock_fd, msg, strlen(msg), 0);
printf("Sent: %s", msg);
recv_len = recv(sock_fd, buffer, BUFFER_SIZE - 1, 0);
if (recv_len > 0) {
buffer[recv_len] = '\0';
printf("Echo: %s", buffer);
}
close(sock_fd);
return EXIT_SUCCESS;
}编译与测试
在主机上交叉编译,scp 传到板子,然后两个终端分别跑服务器和客户端:
# 交叉编译(在主机上执行)
aarch64-linux-gnu-gcc -Wall -O2 -o tcp_server tcp_server.c
aarch64-linux-gnu-gcc -Wall -O2 -o tcp_client tcp_client.c
# 传输到目标板
scp tcp_server tcp_client root@192.168.49.100:/root/
# 在目标板上运行
# 终端 1:启动服务器
./tcp_server
# 输出:TCP Server listening on port 8080...
# 终端 2(另一台设备或同一板上新开终端):启动客户端
./tcp_client
# 输出:Connected to server 192.168.49.100:8080
# 输出:Sent: Hello from RK3588 TCP Client!
# 输出:Echo: Hello from RK3588 TCP Client!4.3 UDP 通信
UDP 不建立连接,每个数据报独立发送,适合对实时性要求高、能容忍少量丢包的场景。服务器省去 listen/accept:recvfrom 一次就能拿到数据和对方的地址,照着地址回发即可。
UDP 服务器
/* udp_server.c — RK3588 UDP 服务器示例 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define SERVER_PORT 9090
#define BUFFER_SIZE 1024
int main(void)
{
int sock_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
char buffer[BUFFER_SIZE];
ssize_t recv_len;
/* 创建 UDP 套接字 */
sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (sock_fd < 0) {
perror("socket");
return EXIT_FAILURE;
}
/* 绑定地址和端口 */
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(SERVER_PORT);
if (bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("bind");
close(sock_fd);
return EXIT_FAILURE;
}
printf("UDP Server listening on port %d...\n", SERVER_PORT);
/* 循环接收和回复 */
while (1) {
recv_len = recvfrom(sock_fd, buffer, BUFFER_SIZE - 1, 0,
(struct sockaddr *)&client_addr, &client_len);
if (recv_len < 0) {
perror("recvfrom");
continue;
}
buffer[recv_len] = '\0';
printf("Received from %s:%d: %s",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buffer);
/* 回显给客户端 */
sendto(sock_fd, buffer, recv_len, 0,
(struct sockaddr *)&client_addr, client_len);
}
close(sock_fd);
return EXIT_SUCCESS;
}UDP 客户端
/* udp_client.c — RK3588 UDP 客户端示例 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define SERVER_IP "192.168.49.100"
#define SERVER_PORT 9090
#define BUFFER_SIZE 1024
int main(void)
{
int sock_fd;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE];
socklen_t addr_len = sizeof(server_addr);
ssize_t recv_len;
/* 创建 UDP 套接字 */
sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (sock_fd < 0) {
perror("socket");
return EXIT_FAILURE;
}
/* 设置服务器地址 */
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);
/* 发送数据 */
const char *msg = "Hello from RK3588 UDP Client!\n";
sendto(sock_fd, msg, strlen(msg), 0,
(struct sockaddr *)&server_addr, sizeof(server_addr));
printf("Sent: %s", msg);
/* 接收回显 */
recv_len = recvfrom(sock_fd, buffer, BUFFER_SIZE - 1, 0, NULL, NULL);
if (recv_len > 0) {
buffer[recv_len] = '\0';
printf("Echo: %s", buffer);
}
close(sock_fd);
return EXIT_SUCCESS;
}编译运行的方式和 TCP 一致:
# 交叉编译
aarch64-linux-gnu-gcc -Wall -O2 -o udp_server udp_server.c
aarch64-linux-gnu-gcc -Wall -O2 -o udp_client udp_client.c
# 传输到目标板
scp udp_server udp_client root@192.168.49.100:/root/
# 运行测试
./udp_server # 终端 1
./udp_client # 终端 24.4 多线程并发服务器
前面的 TCP 服务器一次只能服务一个客户端。RK3588 配备 4 核 Cortex-A76 + 4 核 Cortex-A55,共 8 个 CPU 核心,适合用"一个客户端一个线程"的方式接待大量连接:主线程只管 accept,新连接交给新线程处理,互不阻塞。
/* threaded_server.c — RK3588 多线程并发 TCP 服务器 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sched.h>
#define SERVER_PORT 8080
#define BUFFER_SIZE 1024
#define MAX_CLIENTS 64
/* 客户端线程参数 */
typedef struct {
int client_fd;
struct sockaddr_in client_addr;
int thread_id;
} client_info_t;
/* 客户端处理线程 */
void *handle_client(void *arg)
{
client_info_t *info = (client_info_t *)arg;
char buffer[BUFFER_SIZE];
ssize_t recv_len;
printf("[Thread-%d] Handling client %s:%d on CPU %d\n",
info->thread_id,
inet_ntoa(info->client_addr.sin_addr),
ntohs(info->client_addr.sin_port),
sched_getcpu());
/* 接收并回显数据 */
while ((recv_len = recv(info->client_fd, buffer, BUFFER_SIZE - 1, 0)) > 0) {
buffer[recv_len] = '\0';
printf("[Thread-%d] Received: %s", info->thread_id, buffer);
send(info->client_fd, buffer, recv_len, 0);
}
printf("[Thread-%d] Client disconnected.\n", info->thread_id);
close(info->client_fd);
free(info);
pthread_detach(pthread_self());
return NULL;
}
int main(void)
{
int server_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
int thread_count = 0;
/* 创建 TCP 套接字 */
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
perror("socket");
return EXIT_FAILURE;
}
/* 允许端口复用 */
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
/* 绑定地址和端口 */
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(SERVER_PORT);
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("bind");
close(server_fd);
return EXIT_FAILURE;
}
if (listen(server_fd, MAX_CLIENTS) < 0) {
perror("listen");
close(server_fd);
return EXIT_FAILURE;
}
printf("Threaded TCP Server listening on port %d (max %d clients)...\n",
SERVER_PORT, MAX_CLIENTS);
/* 循环接受客户端连接 */
while (1) {
client_info_t *info = malloc(sizeof(client_info_t));
if (!info) {
perror("malloc");
continue;
}
info->client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
if (info->client_fd < 0) {
perror("accept");
free(info);
continue;
}
info->client_addr = client_addr;
info->thread_id = thread_count++;
/* 创建处理线程 */
pthread_t tid;
if (pthread_create(&tid, NULL, handle_client, info) != 0) {
perror("pthread_create");
close(info->client_fd);
free(info);
continue;
}
}
close(server_fd);
return EXIT_SUCCESS;
}编译时记得加 -lpthread 链接线程库:
# 交叉编译(链接 pthread 库)
aarch64-linux-gnu-gcc -Wall -O2 -o threaded_server threaded_server.c -lpthread
# 传输到目标板
scp threaded_server root@192.168.49.100:/root/
# 运行
./threaded_server
# 输出:Threaded TCP Server listening on port 8080 (max 64 clients)...4.5 HTTP 请求(libcurl)
libcurl 是 C 语言里最常用的 HTTP 客户端库。例子实现 GET 和 POST 两种请求:服务器返回的数据会分块送到回调函数,代码在回调里把数据拼进内存:
/* http_client.c — RK3588 HTTP 请求示例(基于 libcurl) */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <curl/curl.h>
/* 回调函数:接收 HTTP 响应数据 */
static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp)
{
size_t total = size * nmemb;
char **response = (char **)userp;
char *ptr = realloc(*response, strlen(*response) + total + 1);
if (!ptr) {
fprintf(stderr, "realloc failed\n");
return 0;
}
*response = ptr;
strncat(*response, contents, total);
return total;
}
/* GET 请求 */
static int http_get(const char *url)
{
CURL *curl;
CURLcode res;
char *response = NULL;
curl = curl_easy_init();
if (!curl) {
fprintf(stderr, "curl_easy_init failed\n");
return -1;
}
response = calloc(1, 1);
if (!response) {
curl_easy_cleanup(curl);
return -1;
}
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
res = curl_easy_perform(curl);
if (res != CURLE_OK) {
fprintf(stderr, "GET failed: %s\n", curl_easy_strerror(res));
} else {
long http_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
printf("HTTP GET %s (status %ld)\n", url, http_code);
printf("Response length: %zu bytes\n", strlen(response));
/* 打印前 200 字符 */
if (strlen(response) > 0) {
printf("Response preview: %.200s\n", response);
}
}
free(response);
curl_easy_cleanup(curl);
return (res == CURLE_OK) ? 0 : -1;
}
/* POST 请求 */
static int http_post(const char *url, const char *data)
{
CURL *curl;
CURLcode res;
char *response = NULL;
curl = curl_easy_init();
if (!curl)
return -1;
response = calloc(1, 1);
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
struct curl_slist *headers = NULL;
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
res = curl_easy_perform(curl);
if (res != CURLE_OK) {
fprintf(stderr, "POST failed: %s\n", curl_easy_strerror(res));
} else {
long http_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
printf("HTTP POST %s (status %ld)\n", url, http_code);
printf("Response: %s\n", response);
}
curl_slist_free_all(headers);
free(response);
curl_easy_cleanup(curl);
return (res == CURLE_OK) ? 0 : -1;
}
int main(int argc, char *argv[])
{
curl_global_init(CURL_GLOBAL_DEFAULT);
if (argc < 2) {
printf("Usage: %s <url> [post_data]\n", argv[0]);
printf(" GET example: %s http://httpbin.org/get\n", argv[0]);
printf(" POST example: %s http://httpbin.org/post '{\"key\":\"value\"}'\n", argv[0]);
curl_global_cleanup();
return 1;
}
if (argc >= 3) {
http_post(argv[1], argv[2]);
} else {
http_get(argv[1]);
}
curl_global_cleanup();
return 0;
}运行时先试 GET 再试 POST。输出里有状态码(200 表示成功)和响应内容:
# 交叉编译(链接 libcurl 和 libssl)
aarch64-linux-gnu-gcc -Wall -O2 -o http_client http_client.c \
-lcurl -lssl -lcrypto
# 传输到目标板
scp http_client root@192.168.49.100:/root/
# 运行
./http_client http://httpbin.org/get
# 输出:HTTP GET http://httpbin.org/get (status 200)
# 输出:Response preview: { "args": {}, "headers": { ... }, ...
# POST 请求
./http_client http://httpbin.org/post '{"device":"rk3588","status":"online"}'4.6 MQTT 通信(mosquitto)
MQTT 是物联网领域常用的轻量级消息协议,采用发布/订阅模式:设备之间不直接互发消息,而是把消息发给 Broker(代理服务器),谁订阅了对应主题(Topic)谁就能收到。先用命令行工具把流程跑通,再看 C 代码。
安装与启动 MQTT Broker
# 在目标板上安装 mosquitto
sudo apt-get install -y mosquitto mosquitto-clients libmosquitto-dev
# 启动 MQTT Broker(默认端口 1883)
sudo systemctl start mosquitto
sudo systemctl enable mosquitto
# 或手动启动(前台模式,方便调试)
mosquitto -v
# 输出:Opening ipv4 listen socket on port 1883.
# 输出:mosquitto version 2.x.x runningMQTT 发布/订阅测试
一个终端订阅、一个终端发布,订阅端能立刻看到消息,说明 Broker 工作正常:
# 终端 1:订阅主题
mosquitto_sub -h 127.0.0.1 -t "rk3588/sensor" -v
# 等待接收消息...
# 终端 2:发布消息
mosquitto_pub -h 127.0.0.1 -t "rk3588/sensor" -m "temperature=36.5"
# 终端 1 输出:rk3588/sensor temperature=36.5MQTT 客户端 C 代码
/* mqtt_client.c — RK3588 MQTT 客户端示例(基于 libmosquitto) */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <mosquitto.h>
#define MQTT_BROKER "127.0.0.1"
#define MQTT_PORT 1883
#define MQTT_TOPIC_PUB "rk3588/status"
#define MQTT_TOPIC_SUB "rk3588/command"
#define CLIENT_ID "rk3588_client"
/* 消息回调 */
static void on_message(struct mosquitto *mosq, void *userdata,
const struct mosquitto_message *msg)
{
printf("Received on topic [%s]: %s\n", msg->topic, (char *)msg->payload);
/* 处理命令 */
if (strcmp(msg->topic, MQTT_TOPIC_SUB) == 0) {
if (strcmp((char *)msg->payload, "reboot") == 0) {
printf("Received reboot command!\n");
} else if (strcmp((char *)msg->payload, "status") == 0) {
/* 回复状态 */
mosquitto_publish(mosq, NULL, MQTT_TOPIC_PUB, 7, "online", 0, false);
printf("Published status: online\n");
}
}
}
/* 连接回调 */
static void on_connect(struct mosquitto *mosq, void *userdata, int rc)
{
if (rc == 0) {
printf("Connected to MQTT broker.\n");
/* 订阅命令主题 */
mosquitto_subscribe(mosq, NULL, MQTT_TOPIC_SUB, 0);
printf("Subscribed to: %s\n", MQTT_TOPIC_SUB);
} else {
fprintf(stderr, "Connection failed: %d\n", rc);
}
}
int main(void)
{
struct mosquitto *mosq;
int rc;
/* 初始化 mosquitto 库 */
mosquitto_lib_init();
/* 创建客户端实例 */
mosq = mosquitto_new(CLIENT_ID, true, NULL);
if (!mosq) {
fprintf(stderr, "mosquitto_new failed\n");
mosquitto_lib_cleanup();
return EXIT_FAILURE;
}
/* 设置回调 */
mosquitto_connect_callback_set(mosq, on_connect);
mosquitto_message_callback_set(mosq, on_message);
/* 连接到 Broker */
rc = mosquitto_connect(mosq, MQTT_BROKER, MQTT_PORT, 60);
if (rc != MOSQ_ERR_SUCCESS) {
fprintf(stderr, "mosquitto_connect failed: %s\n", mosquitto_strerror(rc));
mosquitto_destroy(mosq);
mosquitto_lib_cleanup();
return EXIT_FAILURE;
}
printf("MQTT client started, broker: %s:%d\n", MQTT_BROKER, MQTT_PORT);
/* 发布上线消息 */
mosquitto_publish(mosq, NULL, MQTT_TOPIC_PUB, 10, "connecting", 0, false);
/* 主循环:处理网络事件 */
while (1) {
rc = mosquitto_loop(mosq, 1000, 1);
if (rc != MOSQ_ERR_SUCCESS) {
fprintf(stderr, "Connection lost, reconnecting...\n");
usleep(5000000);
mosquitto_reconnect(mosq);
}
}
/* 清理 */
mosquitto_disconnect(mosq);
mosquitto_destroy(mosq);
mosquitto_lib_cleanup();
return EXIT_SUCCESS;
}编译运行后,客户端连上 Broker 并订阅 rk3588/command 主题,就算就绪:
# 交叉编译
aarch64-linux-gnu-gcc -Wall -O2 -o mqtt_client mqtt_client.c -lmosquitto
# 传输到目标板
scp mqtt_client root@192.168.49.100:/root/
# 运行
./mqtt_client
# 输出:Connected to MQTT broker.
# 输出:Subscribed to: rk3588/command4.7 NFS 远程挂载
NFS(Network File System)让板子把主机上的目录当成自己的本地目录来用。交叉编译完直接在板子上运行,不用每次 scp,调试效率高很多。
主机端配置(NFS 服务器)
# 安装 NFS 服务器
sudo apt-get install -y nfs-kernel-server
# 创建共享目录
sudo mkdir -p /srv/nfs/rk3588_rootfs
# 配置导出目录
sudo tee -a /etc/exports << 'EOF'
/srv/nfs/rk3588_rootfs *(rw,sync,no_subtree_check,no_root_squash)
EOF
# 重新加载导出配置
sudo exportfs -ra
# 启动 NFS 服务
sudo systemctl restart nfs-kernel-server
# 查看导出目录
sudo exportfs -v
# 输出:/srv/nfs/rk3588_rootfs 0.0.0.0/0(rw,async,wdelay,hide,...)目标板端配置(NFS 客户端)
挂载成功后,ls 能看到主机共享目录的内容,df -h 里会多出一行 NFS 挂载记录:
# 安装 NFS 客户端工具
sudo apt-get install -y nfs-common
# 创建挂载点
sudo mkdir -p /mnt/nfs_share
# 手动挂载(假设主机 IP 为 192.168.49.200)
sudo mount -t nfs 192.168.49.200:/srv/nfs/rk3588_rootfs /mnt/nfs_share
# 验证挂载
ls /mnt/nfs_share
df -h /mnt/nfs_share
# 输出:192.168.49.200:/srv/nfs/rk3588_rootfs xxxG xxxG xxxG xx% /mnt/nfs_share
# 卸载
sudo umount /mnt/nfs_share开机自动挂载
# 写入 fstab 实现开机自动挂载
echo "192.168.49.200:/srv/nfs/rk3588_rootfs /mnt/nfs_share nfs defaults,_netdev 0 0" | \
sudo tee -a /etc/fstab4.8 iperf 网络性能测试
iperf 是专门的带宽测试工具,一端起服务、一端打流量,用来测带宽、延迟和丢包率。
安装
# 目标板(服务端)
sudo apt-get install -y iperf
# 主机(客户端)
sudo apt-get install -y iperfTCP 带宽测试
# 目标板作为服务端
iperf -s
# 输出:-----------------------------------------------------------
# 输出:Server listening on 5201
# 主机作为客户端(目标板 IP 为 192.168.49.100)
iperf -c 192.168.49.100
# 输出示例:
# [ ID] Interval Transfer Bitrate
# [ 5] 0.00-10.00 sec 10.9 GBytes 9.36 Gbits/sec
# 反向测试(主机作为服务端,目标板作为客户端)
iperf -c 192.168.49.100 -RUDP 测试
UDP 模式加 -u 和目标带宽 -b,输出里会多出 Jitter(抖动)和丢包率两列:
# 目标板服务端
iperf -s
# 主机客户端(UDP 模式,目标带宽 100Mbps)
iperf -c 192.168.49.100 -u -b 100M
# 输出示例:
# [ ID] Interval Transfer Bitrate Jitter Lost/Total
# [ 5] 0.00-10.00 sec 119 MBytes 100 Mbits/sec 0.034 ms 0/122058 (0%)并发连接测试
# 4 线程并发 TCP 测试
iperf -c 192.168.49.100 -P 4本机 iperf 自测(loopback)
板上同时装着 iperf v2.1.8 和 iperf3 3.12 两个版本,注意两者协议不兼容:iperf3 客户端连 iperf v2 server 会报 "control socket has closed unexpectedly"。下面是在同一块板上做的本机回环自测,主要用来验证工具本身可用:
$ which iperf iperf3
/usr/bin/iperf
/usr/bin/iperf3
$ iperf -v | head -1
iperf version 2.1.8 (12 August 2022) pthreads
$ iperf3 -v | head -1
iperf 3.12 (cJSON 1.7.15)
# 板端启 v2 server,板端作 client 自测(loopback,仅作 API 验证)
$ (iperf -s -p 5001 > /tmp/iperf_s.log 2>&1 &) && sleep 2
$ iperf -c 127.0.0.1 -p 5001 -t 2
[ 1] local 127.0.0.1 port 58234 connected with 127.0.0.1 port 5001
[ ID] Interval Transfer Bandwidth
[ 1] 0.0000-2.0141 sec 12.5 GBytes 53.3 Gbits/sec # loopback 极限
# iperf3 试连同一个 server
$ iperf3 -c 127.0.0.1 -p 5001 -t 2
iperf3: error - control socket has closed unexpectedly实际测两台 RK3588 之间的网速时,两端必须用相同版本:要么都是 iperf v2,要么都是 iperf3。
4.9 SSH 远程访问
串口之外,SSH 是更顺手的远程方式:登录、传文件、端口转发都能做。
安装与配置 SSH 服务
# 安装 OpenSSH 服务端
sudo apt-get install -y openssh-server
# 启动 SSH 服务
sudo systemctl start ssh
sudo systemctl enable ssh
# 查看 SSH 服务状态
sudo systemctl status ssh
# 修改 SSH 端口(可选)
sudo sed -i 's/#Port 22/Port 2222/' /etc/ssh/sshd_config
sudo systemctl restart sshSSH 密钥登录
生成密钥对、把公钥放到板子上,之后 ssh 登录不再需要密码:
# 主机端:生成密钥对
ssh-keygen -t ed25519 -C "rk3588_dev"
# 主机端:复制公钥到目标板
ssh-copy-id -p 22 root@192.168.49.100
# 主机端:免密码登录
ssh root@192.168.49.100SSH 文件传输
# 从主机传输文件到目标板
scp file.txt root@192.168.49.100:/root/
# 从目标板传输文件到主机
scp root@192.168.49.100:/root/data.txt ./
# 递归复制目录
scp -r ./project root@192.168.49.100:/root/
# 使用 rsync 同步(支持增量同步)
rsync -avz ./project/ root@192.168.49.100:/root/project/SSH 远程端口转发
# 将目标板的 8080 端口转发到主机的 8080
ssh -L 8080:localhost:8080 root@192.168.49.100
# 远程端口转发(将主机的 3000 端口暴露给目标板访问)
ssh -R 3000:localhost:3000 root@192.168.49.1005. 代码详解
5.1 Socket 编程模型
TCP 和 UDP 的调用顺序对比如下,写套接字程序前把这两张时序记牢,步骤就不会乱:
TCP 通信流程:
客户端 服务器
│ │
│ socket() │ socket()
│ │ bind()
│ │ listen()
│ connect() ──────────────────────►│ accept()
│ ◄────────────────────────────────│
│ send()/recv() ◄──────────────► send()/recv()
│ close() │ close()
│ │
UDP 通信流程:
客户端 服务器
│ │
│ socket() │ socket()
│ │ bind()
│ sendto() ──────────────────────►│ recvfrom()
│ recvfrom() ◄────────────────────│ sendto()
│ close() │ close()
│ │5.2 并发服务器模型对比
一台服务器同时接待多个客户端,常见三种做法:多线程(每个连接一个线程)、多进程(每个连接一个进程)、IO 多路复用(用一个线程同时盯住所有连接,select/poll/epoll 都是这类接口)。三者的取舍如下:
┌─────────────────────────────────────────────────────────────┐
│ 并发模型对比 │
├──────────────┬──────────────┬──────────────┬────────────────┤
│ │ 多线程 │ 多进程 │ IO 多路复用 │
├──────────────┼──────────────┼──────────────┼────────────────┤
│ 实现方式 │ pthread │ fork() │ select/poll/ │
│ │ │ │ epoll │
├──────────────┼──────────────┼──────────────┼────────────────┤
│ 资源开销 │ 中等 │ 高 │ 低 │
├──────────────┼──────────────┼──────────────┼────────────────┤
│ 编程复杂度 │ 中等 │ 中等 │ 高 │
├──────────────┼──────────────┼──────────────┼────────────────┤
│ 适用场景 │ CPU 密集 │ 隔离性强 │ 高并发 IO │
│ │ 计算任务 │ 不稳定进程 │ 连接数多 │
├──────────────┼──────────────┼──────────────┼────────────────┤
│ RK3588 适配 │ 推荐(8核) │ 一般 │ 推荐 │
│ │ 线程亲和性强 │ 开销大 │ 性能最优 │
└──────────────┴──────────────┴──────────────┴────────────────┘RK3588 的 8 核 CPU(4 x Cortex-A76 + 4 x Cortex-A55)跑多线程并发服务器有天然优势;还可以用 pthread_setaffinity_np() 把线程绑定到指定 CPU 核心,避免线程来回迁移带来的缓存失效。
5.3 libcurl 工作原理
libcurl 的调用套路是固定五步:初始化句柄、设置参数、执行请求、取结果信息、释放资源。中间的 curl_easy_perform() 内部会依次完成 DNS 解析、建连、握手、收发:
libcurl 请求流程:
curl_easy_init() 初始化 curl 句柄
│
curl_easy_setopt() 设置 URL、回调、超时等参数
│
curl_easy_perform() 执行请求(阻塞直到完成)
│
├── DNS 解析
├── TCP 连接
├── TLS 握手(HTTPS)
├── 发送 HTTP 请求
├── 接收 HTTP 响应
└── 回调写入数据
│
curl_easy_getinfo() 获取状态码、耗时等信息
│
curl_easy_cleanup() 释放资源5.4 MQTT 协议架构
发布者和订阅者互不认识,全靠 Broker 按 Topic 转发。QoS 是消息送达保证等级,从"最多一次"到"恰好一次",可靠性越高开销越大:
MQTT 发布/订阅模型:
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Publisher │────►│ │────►│Subscriber│
│ (发布者) │ │ MQTT Broker │ │ (订阅者) │
└──────────┘ │ (代理服务器) │ └──────────┘
│ │
┌──────────┐ │ Port: 1883 │ ┌──────────┐
│Subscriber │◄────│ │◄────│Publisher │
└──────────┘ └──────────────┘ └──────────┘
Topic(主题)示例:
rk3588/sensor/temperature
rk3588/sensor/humidity
rk3588/command/reboot
QoS 等级:
QoS 0 — 最多一次(不确认)
QoS 1 — 至少一次(确认重传)
QoS 2 — 恰好一次(四步握手)6. 常见问题
6.1 网络接口无法获取 IP 地址
现象:ip addr show end1 里没有 inet 地址。
先看接口是不是 UP 状态,再手动跑一次 dhclient,然后检查网线物理连接和驱动加载:
# 检查网卡是否处于 UP 状态
ip link show end1
# 如果是 DOWN 状态,手动启动
sudo ip link set end1 up
# 检查 DHCP 客户端
sudo dhclient end1
# 检查网线物理连接
ethtool end1
# 确认 Link detected: yes
# 检查网卡驱动加载
dmesg | grep -i eth6.2 TCP 连接被拒绝
现象:客户端 connect() 返回 ECONNREFUSED。
多半是对面没人监听,或者被防火墙拦了。先确认服务器在监听 8080 端口,再查防火墙规则:
# 检查服务器是否在监听
ss -tlnp | grep 8080
# 或
netstat -tlnp | grep 8080
# 检查防火墙规则
sudo iptables -L -n
# 临时关闭防火墙
sudo iptables -F
# 检查 SELinux(如有)
getenforce
sudo setenforce 06.3 iperf 测试带宽异常低
现象:TCP 带宽远低于千兆预期。
先看网卡协商出的速率和双工模式是不是 1000Mb/s、Full,再排查中断分布和 TSO:
# 检查网卡协商速率
ethtool end1 | grep Speed
# 应显示:Speed: 1000Mb/s
# 检查网卡双工模式
ethtool end1 | grep Duplex
# 应显示:Duplex: Full
# 强制千兆全双工
sudo ethtool -s end1 speed 1000 duplex full autoneg off
# 检查网卡中断亲和性(多核优化)
cat /proc/interrupts | grep eth
# 关闭 TCP Segmentation Offload 进行对比测试
sudo ethtool -K end1 tso off6.4 SSH 连接超时
现象:SSH 卡在 "Connecting..." 没有响应。
先用串口登上板子确认 SSH 服务在跑、网络通,再从客户端侧加超时参数、开调试模式看卡在哪一步:
# 检查目标板 SSH 服务是否运行
sudo systemctl status ssh
# 使用串口登录目标板,检查网络连通性
ping 192.168.49.100
# 检查 SSH 配置
sudo cat /etc/ssh/sshd_config | grep -v "^#"
# 指定 SSH 协议版本
ssh -o ConnectTimeout=10 -4 root@192.168.49.100
# 调试模式连接
ssh -vvv root@192.168.49.1006.5 MQTT 连接失败
现象:mosquitto_sub/pub 报 "Connection refused"。
先看 mosquitto 服务和 1883 端口,再确认配置允许匿名连接:
# 检查 mosquitto 服务状态
sudo systemctl status mosquitto
# 检查监听端口
ss -tlnp | grep 1883
# 手动启动 mosquitto 并查看日志
mosquitto -v -c /etc/mosquitto/mosquitto.conf
# 检查配置文件是否允许匿名连接
cat /etc/mosquitto/conf.d/default.conf
# 确认包含:allow_anonymous true
# 修改配置后重启
sudo systemctl restart mosquitto6.6 NFS 挂载失败
现象:mount.nfs: mount system call failed。
按顺序查:服务器可达、NFS 端口开放、目录确实已导出;都不行就指定 NFSv3 重试:
# 检查 NFS 服务器是否可达
ping 192.168.49.200
# 检查 NFS 端口是否开放
rpcinfo -p 192.168.49.200
# 检查共享目录是否导出
showmount -e 192.168.49.200
# 使用 NFSv3 挂载(默认可能使用 v4)
sudo mount -t nfs -o vers=3,nolock 192.168.49.200:/srv/nfs/rk3588_rootfs /mnt/nfs_share
# 主机端重启 NFS 服务
sudo systemctl restart nfs-kernel-server相关文档
- 01_C-C++应用开发入门 — C/C++ 交叉编译基础
- ch02 Python 应用开发 — Python 网络编程
- ch04 GStreamer 多媒体开发 — 多媒体流网络传输
- 02_内核配置与裁剪 — 网络相关内核配置
