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 Host文件夹下函数介绍

在这里插入图片描述

  1. host_main.cpp:
    用于快速测试的文件demo。
  2. host_main_test.cpp:
    根据用户需求进行功能设计的文件,其中对于封装类别的使用参考了host_main.cpp中的函数。
  3. ImageServer.h:
    ImageServer类别的头文件。
  4. ImageServer.cpp:
    ImageServer类别的源文件。

2.1.1 ImageServer.h

  1. ImageServer(uint16_t port, std::filesystem::path base_dir);
    构造函数:保存监听端口和图片保存的根目录。
  2. bool start()
    创建监听 socket、绑定到 port_、开始 listen,然后启动 accept_thread_ 在线程中跑 acceptLoop(),正式对外提供服务。
  3. void stop();
    把 running_ 置为 false,关闭 listen_fd_,并等待 accept_thread_ 结束,用来优雅关闭服务器。
  4. ~ImageServer();
    析构时确保服务器被停止、资源被释放(比如还在跑的线程、打开的 fd 等)。
  5. void acceptLoop();
    在一个循环里调用 accept 接受客户端连接,当有新连接时得到 cfd,再调用 handleClient(cfd) 去处理;循环条件通常检查 running_,其中会开启一个新线程处理handleClient(cfd)。
  6. void handleClient(int cfd);
    针对单个客户端连接:按你在协议里定义的 MsgHeader → mode → 文件名 → 文件内容,一路 recv 解析并写入磁盘,最后关闭 cfd。

代码如下:

#pragma once
#include <atomic>
#include <cstdint>
#include <filesystem>
#include <string>
#include <thread>

class ImageServer
{
public:
    ImageServer(uint16_t port, std::filesystem::path base_dir);
    bool start();
    void stop();
    ~ImageServer();

private:
    void acceptLoop();
    void handleClient(int cfd);
    std::filesystem::path uniquePathIfExists(const std::filesystem::path &target);

private:
    uint16_t port_;
    std::filesystem::path base_dir_;
    int listen_fd_{-1};
    std::atomic<bool> running_{false};
    std::thread accept_thread_;
};

2.1.2 ImageServer.cpp

ImageServer.h中函数的具体实现:

#include "ImageServer.h"
#include "ImageTransferCommon.h"

#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <unistd.h>

#include <unordered_set> // 顶部别忘了加这个头
#include <cerrno>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <vector>

namespace fs = std::filesystem;
using namespace itx;

/**
 * @brief 构造函数
 *
 * 初始化服务器监听端口和图片保存的基础目录。
 *
 * @param port      服务器监听的 TCP 端口
 * @param base_dir  所有接收图片的根目录
 */
ImageServer::ImageServer(uint16_t port, fs::path base_dir)
    : port_(port), base_dir_(std::move(base_dir)) {}

/**
 * @brief 启动图片服务器
 *
 * 1. 确保基础目录存在(如不存在则创建);
 * 2. 创建监听 socket,设置 SO_REUSEADDR;
 * 3. 绑定到指定端口并调用 listen 开始监听;
 * 4. 将 running_ 置为 true,并启动后台线程执行 acceptLoop()。
 *
 * @return true  启动成功
 * @return false 启动失败(错误会输出到标准错误)
 */
bool ImageServer::start()
{
    std::error_code ec;
    fs::create_directories(base_dir_, ec);

    listen_fd_ = ::socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd_ < 0)
    {
        perror("socket");
        return false;
    }

    int on = 1;
    ::setsockopt(listen_fd_, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port = htons(port_);

    if (::bind(listen_fd_, (sockaddr *)&addr, sizeof(addr)) < 0)
    {
        perror("bind");
        ::close(listen_fd_);
        return false;
    }
    if (::listen(listen_fd_, 128) < 0)
    {
        perror("listen");
        ::close(listen_fd_);
        return false;
    }

    running_ = true;
    // 开一个后台线程不断 accept 新连接
    accept_thread_ = std::thread(&ImageServer::acceptLoop, this);
    return true;
}

/**
 * @brief 停止服务器
 *
 * 将 running_ 置为 false,
 * 关闭监听 socket,唤醒并结束 acceptLoop 线程,
 * 然后 join 接受线程,完成资源回收。
 */
void ImageServer::stop()
{
    running_ = false;
    if (listen_fd_ >= 0)
    {
        ::shutdown(listen_fd_, SHUT_RDWR);
        ::close(listen_fd_);
        listen_fd_ = -1;
    }
    if (accept_thread_.joinable())
    {
        accept_thread_.join();
    }
}

/**
 * @brief 析构函数
 *
 * 确保对象销毁前服务器已经被优雅关闭。
 */
ImageServer::~ImageServer() { stop(); }

/**
 * @brief 接受连接的主循环(在独立线程中运行)
 *
 * 在 running_ 为 true 时不断调用 accept() 等待客户端连接,
 * 每有一个新连接就启动一个分离线程调用 handleClient() 处理,
 * 自身只负责“接客”,不阻塞在单个客户端逻辑上。
 */
void ImageServer::acceptLoop()
{
    std::cout << "Server listening on port " << port_
              << ", base dir: " << base_dir_ << "\n";
    while (running_)
    {
        sockaddr_in cli{};
        socklen_t len = sizeof(cli);
        int cfd = ::accept(listen_fd_, (sockaddr *)&cli, &len);
        if (cfd < 0)
        {
            if (errno == EINTR)
                continue;
            if (!running_)
                break;
            perror("accept");
            continue;
        }
        // 每个客户端连接交给一个 detached 线程处理
        std::thread(&ImageServer::handleClient, this, cfd).detach();
    }
}

/**
 * @brief 若 target 已存在,则在同目录下生成一个不重名的新路径
 *
 * 例如 target = "a/b/img.png"
 * 若存在,则尝试 "img_1.png"、"img_2.png" ... 直到找到不存在的文件名。
 *
 * @param target 期望的目标文件路径
 * @return fs::path 可用的、不与已存在文件冲突的路径
 */
fs::path ImageServer::uniquePathIfExists(const fs::path &target)
{
    if (!fs::exists(target))
        return target;
    fs::path dir = target.parent_path();
    std::string stem = target.stem().string();
    std::string ext = target.extension().string();
    for (int i = 1; i < 1000000; ++i)
    {
        fs::path cand = dir / (stem + "_" + std::to_string(i) + ext);
        if (!fs::exists(cand))
            return cand;
    }
    return target; // 兜底:实在找不到就返回原路径(极端情况)
}

/**
 * @brief 检查是否为允许的 mode
 *
 * 服务器只接受限定的 8 种 mode,
 * 防止客户端随意传入未约定的业务模式。
 *
 * @param m 客户端传来的 mode 字符串
 * @return true  mode 合法
 * @return false mode 不在允许列表中
 */
static bool isAllowedMode(const std::string &m)
{
    static const std::unordered_set<std::string> modes = {
        "mode1_picture1", "mode1_picture2", "mode1_picture3", "mode1_picture4",
        "mode2_dataset1", "mode2_dataset2", "mode3_dataset1", "mode3_dataset2"};
    return modes.count(m) > 0;
}

/**
 * @brief 处理单个客户端连接(收图并落盘)
 *
 * 协议流程:
 *  1. 读取并校验 MsgHeader(magic、version、长度、文件大小);
 *  2. 按 header 中的长度读取 mode 字符串和文件名;
 *  3. 检查 mode 是否在允许列表中;
 *  4. 对 mode 和文件名进行 sanitize,组合出保存目录和文件路径;
 *  5. 如果文件重名则调用 uniquePathIfExists() 生成不冲突的名字;
 *  6. 按 file_size 从 socket 中循环读取文件数据并写入磁盘;
 *  7. 关闭连接并打印保存结果。
 *
 * @param cfd 已经 accept 得到的客户端 socket 描述符
 */
void ImageServer::handleClient(int cfd)
{
    // 1. 读取并验证头部
    MsgHeader h{};
    if (!recv_all(cfd, &h, sizeof(h)))
    {
        std::cerr << "recv header failed\n";
        ::close(cfd);
        return;
    }
    if (std::memcmp(h.magic, MAGIC, 4) != 0 || ntohs(h.version) != VERSION)
    {
        std::cerr << "bad magic/version\n";
        ::close(cfd);
        return;
    }

    uint16_t mode_len = ntohs(h.mode_len);
    uint16_t name_len = ntohs(h.name_len);
    uint64_t file_sz  = ntohll(h.file_size);

    if (mode_len == 0 || name_len == 0)
    {
        std::cerr << "invalid lengths\n";
        ::close(cfd);
        return;
    }

    // 2. 读取 mode 和文件名
    std::string mode(mode_len, '\0');
    std::string name(name_len, '\0');

    if (!recv_all(cfd, mode.data(), mode_len) ||
        !recv_all(cfd, name.data(), name_len))
    {
        std::cerr << "recv strings failed\n";
        ::close(cfd);
        return;
    }

    // 3. 只允许特定模式
    if (!isAllowedMode(mode))
    {
        std::cerr << "reject unknown mode: " << mode << "\n";
        ::close(cfd);
        return;
    }

    // 4. 对 mode 和文件名做安全过滤,避免目录穿越等问题
    std::string mode_safe = sanitize(mode);
    std::string name_safe = sanitize(name);

    // 保存路径:base_dir / "<mode>_folder" / 文件名
    std::filesystem::path dir = base_dir_ / (mode_safe + "_folder");
    std::error_code ec;
    fs::create_directories(dir, ec);

    // 5. 处理重名文件
    fs::path outfile = uniquePathIfExists(dir / name_safe);

    std::ofstream ofs(outfile, std::ios::binary);
    if (!ofs.is_open())
    {
        std::cerr << "open output failed: " << outfile << "\n";
        ::close(cfd);
        return;
    }

    // 6. 循环接收文件数据并写入磁盘
    const size_t BUF = 1 << 16;
    std::vector<char> buf(BUF);
    uint64_t remain = file_sz;

    while (remain > 0)
    {
        size_t chunk = static_cast<size_t>(std::min<uint64_t>(remain, BUF));
        if (!recv_all(cfd, buf.data(), chunk))
        {
            std::cerr << "recv file chunk failed\n";
            ::close(cfd);
            return;
        }
        ofs.write(buf.data(), chunk);
        remain -= chunk;
    }

    ofs.close();
    ::close(cfd);

    // 7. 打印保存结果
    std::cout << "Saved: mode=" << mode_safe
              << ", name=" << outfile.filename().string()
              << ", bytes=" << file_sz
              << ", path=" << outfile << "\n";
}

2.1.3 host_main.cpp

demo实例:

#include "ImageServer.h"
#include <condition_variable>
#include <filesystem>
#include <iostream>
#include <mutex>
#include <string>
#include "Config.h"

/*
使用方式:./host 5000 /data/images
*/

#define CONF_PATH "../Common/lan_image_transfer.conf"

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: host <port> <base_dir>\n";
        return 1;
    }
    uint16_t port = static_cast<uint16_t>(std::stoi(argv[1]));
    std::filesystem::path base = argv[2];

    AppConfig cfg;
    std::string err;
    if (!loadConfig(CONF_PATH, cfg, &err))
    {
        std::cerr << "Load config failed: " << err << "\n";
        return 1;
    }

    ImageServer server(cfg.port, cfg.host_ip);
    if (!server.start())
        return 1;

    std::cout << "Host running. Press Ctrl+C to exit.\n";
    std::mutex m;
    std::unique_lock<std::mutex> lk(m);
    std::condition_variable cv;
    cv.wait(lk); // 阻塞等待
    return 0;
}

2.1.4 host_main_test.cpp

博主根据demo实例按照自己功能实现完成的代码:

#include "ImageServer.h"
#include <condition_variable>
#include <filesystem>
#include <iostream>
#include <mutex>
#include <string>
#include "Config.h"

/*
使用方式:./host 5000 /data/images
*/

#define CONF_PATH "../../Common/lan_image_transfer.conf"

int main(int argc, char *argv[])
{
    AppConfig cfg;
    std::string err;
    if (!loadConfig(CONF_PATH, cfg, &err))
    {
        std::cerr << "Load config failed: " << err << "\n";
        return 1;
    }

    ImageServer server(cfg.port, cfg.base_dir);
    // if (!server.start())
    //     return 1;

    std::cout << "Host running. Press Ctrl+C to exit.\n";
    std::mutex m;
    std::unique_lock<std::mutex> lk(m);
    std::condition_variable cv;
    cv.wait(lk); // 阻塞等待
    return 0;
}

3 总结

本章节讲解了嵌入式设备间Socket通信传输图片 Host端函数。

4 其余章节

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

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐