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

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文件

代码如下:

#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 函数中:
主要用于读取配置文件中的配置数据。

代码如下:

#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:对输入的字符串进行清洗,去掉不允许的字符,只保留字母、数字和特定的符号。

完整代码如下:

#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号与接收到图片后存储的文件夹位置。

代码如下:

# 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端函数

posted @ 2026-01-23 16:03  clnchanpin  阅读(2)  评论(0)    收藏  举报