【Socket消息传递详细版本】(2) 嵌入式设备间Socket通信传输图片 Common公共函数

文章目录

  • [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,其中:

  1. Client文件夹内存放的为与Client端相关的函数
  2. Host文件夹内存放的为与Host端相关的函数
  3. 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 中:

  1. ltrim / rtrim / trim:去掉字符串左/右/两边的空白字符(空格、换行等)。
  2. tolower_str:把字符串全部转成小写,用来实现"键不区分大小写"。
  3. 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 结构体,若干函数(用于字节序转换、全数据发送和接收等)以及一些字符串处理工具。整体目的是构建、发送、接收图像或文件数据。

代码功能概述:

  1. MsgHeader 结构体 用于定义消息的头部,包含魔数(magic)、版本号、模式长度、名称长度和文件大小等字段,用于确保数据传输过程中的一致性。
  2. htonll / ntohll:用于 64位字节序 的转换,使得不同机器架构(大端、小端)能正确地交换数据。
  3. send_all / recv_all:分别用于 完全发送 和 完全接收 数据,保证数据的完整性。
  4. 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端函数

相关推荐
赖small强3 天前
【Linux驱动开发】Linux网络设备驱动底层原理与实现详解
linux·驱动开发·socket·net_device·sk_buff
小陈又菜11 天前
【QT学习之路】网络通信新次元!Qt TCP双侠:Server监听瞬息,Socket连接万变
qt·网络协议·tcp/ip·socket
huangyuchi.13 天前
【Linux网络】Socket编程实战,基于UDP协议的Dict Server
linux·网络·c++·udp·c·socket
集大周杰伦19 天前
Linux网络编程核心实践:TCP/UDP socket与epoll高并发服务器构建
linux·tcp/ip·网络编程·socket·字节序·套接字·i/o多路复用
矮油0_o20 天前
15.套接字和标准I/O
服务器·c语言·网络·网络编程·socket
NiKo_W20 天前
Linux TcpSocket编程
linux·服务器·网络·udp·socket·多线程·tcp
huangyuchi.24 天前
【Linux网络】Socket编程实战,基于UDP协议的Echo Server
linux·运维·服务器·udp·socket·客户端·网络通信
Ronin3051 个月前
【Linux网络】封装Socket
linux·网络·socket·网络通信
charlie1145141912 个月前
理解C++20的革命特性——协程引用之——利用协程做一个迷你的Echo Server
网络·学习·socket·c++20·协程·epoll·raii