文章目录
- [1 概要](#1 概要)
- [2 代码文件结构](#2 代码文件结构)
-
- [2.1 Common文件夹下函数介绍](#2.1 Common文件夹下函数介绍)
-
- [2.1.1 Config.h文件](#2.1.1 Config.h文件)
- [2.1.2 Config.cpp](#2.1.2 Config.cpp)
- [2.1.3 ImageTransferCommon.h](#2.1.3 ImageTransferCommon.h)
- [2.1.4 lan_image_transfer.conf](#2.1.4 lan_image_transfer.conf)
- [3 总结](#3 总结)
- [4 其余章节](#4 其余章节)
1 概要
博主最近因为工程需求,需要在两个嵌入式设备之间传输图片,具体功能如下描述:
硬件资源:
①米联客安路F3P-CZ02-FPSoc(FPGA) (HOST端)
②rk3568 (Client端)
满足功能:
①rk3568可以将消息包通过Socket接口发送给FPGA
②FPGA通过解包消息报,读取图片数据,并存入相应的文件夹中
2 代码文件结构

整个工程代码分为三个结构,Client,Host,Common,其中:
- Client文件夹内存放的为与Client端相关的函数
- Host文件夹内存放的为与Host端相关的函数
- Common文件夹内存放的为公共配置函数及部分工具包
2.1 Common文件夹下函数介绍

2.1.1 Config.h文件
代码如下:
c
#pragma once
#include <cstdint>
#include <string>
struct AppConfig
{
std::string host_ip; // client 用来连接
std::string bind_ip; // host 绑定(可选,留空=全部网卡)
uint16_t port = 0; // 必填
std::string base_dir = "./images";
};
// 从指定路径加载 .conf,成功返回 true;失败返回 false,并在 err(可选)中给出原因
bool loadConfig(const std::string &path, AppConfig &out, std::string *err = nullptr);
这段代码定义了一个 AppConfig 配置结构体,用于存放程序运行所需要的 IP、端口、目录等配置信息;并声明了一个 loadConfig 函数,用来从配置文件(.conf)里读取这些配置并写入 AppConfig。
其中#pragma once这是一个头文件保护指令,防止同一个头文件被重复包含,避免出现重复定义错误。功能与传统写法( #ifndef/#define/#endif )一样,但更简洁。
工程设置中Host会监听当前设备上所有ip的8899号端口(如果bind_ip留空的话)。
2.1.2 Config.cpp
其中代码主要是工具函数,如
在匿名 namespace 中:
- ltrim / rtrim / trim:去掉字符串左/右/两边的空白字符(空格、换行等)。
- tolower_str:把字符串全部转成小写,用来实现"键不区分大小写"。
- parseUint16:把字符串转换成 uint16_t(0~65535 的整数),如果包含非数字或超范围就返回 false。
其中匿名 namespace是用来组织代码的,它可以避免不同代码模块间命名冲突。匿名 namespace 是指没有名字的命名空间,它的作用是限制其内部成员(函数、变量等)的作用域,仅在当前文件中可见。
在loadConfig 函数中:
主要用于读取配置文件中的配置数据。
代码如下:
c
#include "Config.h"
#include <fstream>
#include <sstream>
#include <algorithm>
#include <unordered_map>
#include <cctype>
namespace
{
/**
* @brief 去掉字符串左侧的空白字符(空格、制表符、换行等)。
* 直接在原字符串上修改。
*/
inline void ltrim(std::string &s)
{
s.erase(s.begin(), std::find_if(s.begin(), s.end(),
[](unsigned char c)
{ return !std::isspace(c); }));
}
/**
* @brief 去掉字符串右侧的空白字符(空格、制表符、换行等)。
* 直接在原字符串上修改。
*/
inline void rtrim(std::string &s)
{
s.erase(std::find_if(s.rbegin(), s.rend(),
[](unsigned char c)
{ return !std::isspace(c); })
.base(),
s.end());
}
/**
* @brief 同时去掉字符串左右两侧的空白字符。
* 等价于先 ltrim 再 rtrim。
*/
inline void trim(std::string &s)
{
ltrim(s);
rtrim(s);
}
/**
* @brief 将字符串中所有字符转换为小写,返回转换后的新字符串。
* 常用于对 key 做大小写不敏感处理。
*/
inline std::string tolower_str(std::string s)
{
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c)
{ return (char)std::tolower(c); });
return s;
}
/**
* @brief 将十进制数字字符串解析为 uint16_t 整数。
*
* @param s 输入的数字字符串(只能包含 0-9)。
* @param out 解析成功后输出的 uint16_t 结果。
* @return true 解析成功且在 0~65535 范围内
* @return false 解析失败(含非数字字符或溢出/空字符串)
*/
inline bool parseUint16(const std::string &s, uint16_t &out)
{
if (s.empty())
return false;
unsigned long v = 0;
for (char c : s)
{
if (!std::isdigit((unsigned char)c))
return false;
v = v * 10 + (c - '0');
if (v > 65535UL)
return false;
}
out = static_cast<uint16_t>(v);
return true;
}
} // namespace
/**
* @brief 从指定路径的 .conf 配置文件加载配置到 AppConfig。
*
* 支持的特性:
* - 行级注释:以 '#' 或 ';' 开头的行会被忽略
* - section:形如 [network] / [host],内部 key 会保存为 "section.key"
* - 键名大小写不敏感:内部统一转为小写
* - 可选前缀访问:既支持 "network.port" 也支持直接 "port"
* - 自动去除 key / value 两端空白
* - 自动去除 value 两端成对引号("xxx" 或 'xxx')
* - 端口使用 network.port / port,并校验为合法 uint16_t 且非 0
*
* @param path 配置文件路径
* @param out 解析后输出的 AppConfig(部分字段可能沿用默认值)
* @param err 可选,用于返回错误原因(如文件打不开、端口非法)
* @return true 加载成功
* @return false 加载失败,err 中带有原因(若不为 nullptr)
*/
bool loadConfig(const std::string &path, AppConfig &out, std::string *err)
{
std::ifstream ifs(path);
if (!ifs.is_open())
{
if (err)
*err = "cannot open config: " + path;
return false;
}
std::unordered_map<std::string, std::string> kv;
std::string line, section;
while (std::getline(ifs, line))
{
// 去掉 UTF-8 BOM(若存在)
if (!line.empty() && (unsigned char)line[0] == 0xEF)
{
if (line.size() >= 3 &&
(unsigned char)line[1] == 0xBB &&
(unsigned char)line[2] == 0xBF)
{
line.erase(0, 3);
}
}
std::string raw = line;
trim(line);
// 空行或注释行直接跳过
if (line.empty() || line[0] == '#' || line[0] == ';')
continue;
// 处理 [section] 行
if (line.front() == '[' && line.back() == ']')
{
section = tolower_str(line.substr(1, line.size() - 2));
trim(section);
continue;
}
// 处理 key = value 行
auto pos = line.find('=');
if (pos == std::string::npos)
continue;
std::string key = line.substr(0, pos);
std::string val = line.substr(pos + 1);
trim(key);
trim(val);
key = tolower_str(key);
// 去掉值两边的引号("xxx" 或 'xxx',必须成对)
if (!val.empty() && (val.front() == '"' || val.front() == '\''))
{
if (val.size() >= 2 && val.back() == val.front())
{
val = val.substr(1, val.size() - 2);
}
}
// fullkey = section.key(如果有 section)
std::string fullkey = key;
if (!section.empty())
fullkey = section + "." + key;
// 带 section 前缀的键
kv[fullkey] = val;
// 同时允许无 section 的平铺键(如果之前不存在)
if (kv.find(key) == kv.end())
kv[key] = val;
}
// 取端口(优先 network.port,否则 port)
std::string port_s =
kv.count("network.port") ? kv["network.port"] : kv.count("port") ? kv["port"]
: "";
if (!parseUint16(port_s, out.port) || out.port == 0)
{
if (err)
*err = "invalid or missing 'port'";
return false;
}
// host_ip:network.host_ip 优先,其次 host_ip
if (kv.count("network.host_ip"))
out.host_ip = kv["network.host_ip"];
else if (kv.count("host_ip"))
out.host_ip = kv["host_ip"];
// bind_ip:host.bind_ip 优先,其次 bind_ip
if (kv.count("host.bind_ip"))
out.bind_ip = kv["host.bind_ip"];
else if (kv.count("bind_ip"))
out.bind_ip = kv["bind_ip"];
return true;
}
2.1.3 ImageTransferCommon.h
这段代码主要是与 网络通信 和 数据传输 相关的工具函数,通常用于在网络通信过程中处理数据传输、消息头结构等内容。它包含了一个 MsgHeader 结构体,若干函数(用于字节序转换、全数据发送和接收等)以及一些字符串处理工具。整体目的是构建、发送、接收图像或文件数据。
代码功能概述:
- MsgHeader 结构体 用于定义消息的头部,包含魔数(magic)、版本号、模式长度、名称长度和文件大小等字段,用于确保数据传输过程中的一致性。
- htonll / ntohll:用于 64位字节序 的转换,使得不同机器架构(大端、小端)能正确地交换数据。
- send_all / recv_all:分别用于 完全发送 和 完全接收 数据,保证数据的完整性。
- sanitize:对输入的字符串进行清洗,去掉不允许的字符,只保留字母、数字和特定的符号。
完整代码如下:
c
#pragma once
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
#include <cctype>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#if defined(__GNUC__) || defined(__clang__)
#pragma pack(push, 1)
#endif
// 定义消息头结构体
struct MsgHeader
{
char magic[4]; // 魔数,标识消息类型 ('I','M','T','X')
uint16_t version; // 版本号,当前为1
uint16_t mode_len; // 模式长度,<= 65535
uint16_t name_len; // 名称长度,<= 65535
uint64_t file_size; // 文件大小(字节)
};
#if defined(__GNUC__) || defined(__clang__)
#pragma pack(pop)
#endif
// 常量定义
static constexpr char MAGIC[4] = {'I', 'M', 'T', 'X'}; // 魔数,表示图像传输
static constexpr uint16_t VERSION = 1; // 版本号,当前为1
namespace itx
{ // image transfer aux(图像传输辅助)
/**
* @brief 将 64 位无符号整数从主机字节序转换为网络字节序(大端序)。
* @param v 主机字节序的 64 位数值
* @return 网络字节序的 64 位数值
*/
inline uint64_t htonll(uint64_t v)
{
uint32_t hi = htonl(static_cast<uint32_t>(v >> 32)); // 高 32 位转换
uint32_t lo = htonl(static_cast<uint32_t>(v & 0xFFFFFFFFULL)); // 低 32 位转换
return (static_cast<uint64_t>(lo) << 32) | hi; // 组合成 64 位结果
}
/**
* @brief 将 64 位无符号整数从网络字节序转换为主机字节序。
* @param v 网络字节序的 64 位数值
* @return 主机字节序的 64 位数值
*/
inline uint64_t ntohll(uint64_t v)
{
uint32_t lo = ntohl(static_cast<uint32_t>(v >> 32)); // 高 32 位转换
uint32_t hi = ntohl(static_cast<uint32_t>(v & 0xFFFFFFFFULL)); // 低 32 位转换
return (static_cast<uint64_t>(hi) << 32) | lo; // 组合成 64 位结果
}
/**
* @brief 向套接字中完全发送指定长度的数据。
* 会循环调用 send 直到所有数据都发送完成。
* @param fd 套接字描述符
* @param data 数据指针
* @param len 数据长度
* @return true 发送成功
* @return false 发送失败
*/
inline bool send_all(int fd, const void *data, size_t len)
{
const char *p = static_cast<const char *>(data); // 转换为字符指针
size_t sent = 0;
while (sent < len)
{
ssize_t n = ::send(fd, p + sent, len - sent, 0); // 发送数据
if (n <= 0)
{
if (errno == EINTR) // 如果被信号中断,继续发送
continue;
return false; // 否则发送失败
}
sent += static_cast<size_t>(n); // 累计已发送的数据长度
}
return true;
}
/**
* @brief 向套接字中完全接收指定长度的数据。
* 会循环调用 recv 直到所有数据都接收完成。
* @param fd 套接字描述符
* @param data 数据指针
* @param len 数据长度
* @return true 接收成功
* @return false 接收失败
*/
inline bool recv_all(int fd, void *data, size_t len)
{
char *p = static_cast<char *>(data); // 转换为字符指针
size_t recvd = 0;
while (recvd < len)
{
ssize_t n = ::recv(fd, p + recvd, len - recvd, 0); // 接收数据
if (n <= 0)
{
if (n < 0 && errno == EINTR) // 如果被信号中断,继续接收
continue;
return false; // 否则接收失败
}
recvd += static_cast<size_t>(n); // 累计已接收的数据长度
}
return true;
}
/**
* @brief 清理字符串,替换其中的非字母、数字、_、-、. 字符为 _。
* @param s 输入字符串
* @return 返回清理后的字符串,所有不符合要求的字符被替换为 '_'
*/
inline std::string sanitize(const std::string &s)
{
std::string out;
out.reserve(s.size()); // 预分配内存以提高效率
for (unsigned char c : s)
{
// 如果字符是字母、数字或 _、-、.,就保留
if (std::isalnum(c) || c == '_' || c == '-' || c == '.')
out.push_back(static_cast<char>(c));
else
out.push_back('_'); // 否则替换为 _
}
if (out.empty()) // 如果最终字符串为空,返回 "_"
out = "_";
return out;
}
} // namespace itx
2.1.4 lan_image_transfer.conf
这个为配置文件,指定host端的ip,此ip用于client进行连接,也指定了Host端需要监听的port号与接收到图片后存储的文件夹位置。
代码如下:
c
# LAN Image Transfer 配置
# 注:以 '#' 或 ';' 开头的行是注释
[network]
# client 侧将连接到这个 IP
host_ip = 127.0.0.1
# host 监听端口,同时 client 也用该端口进行连接
port = 8899
base_dir = /home/uisrc/qt_play_v2
3 总结
本章节详细介绍了嵌入式设备间Socket通信传输图片的公共函数。
4 其余章节
【Socket消息传递】(1) 嵌入式设备间Socket通信传输图片
【Socket消息传递详细版本】(2) 嵌入式设备间Socket通信传输图片 Common公共函数
【Socket消息传递详细版本】(3) 嵌入式设备间Socket通信传输图片 Client端函数
【Socket消息传递详细版本】(4) 嵌入式设备间Socket通信传输图片 Host端函数