【Socket消息传递详细版本】(4) 嵌入式设备间Socket通信传输图片 Host端函数
用于快速测试的文件demo。根据用户需求进行功能设计的文件,其中对于封装类别的使用参考了host_main.cpp中的函数。ImageServer类别的头文件。ImageServer类别的源文件。本章节讲解了嵌入式设备间Socket通信传输图片 Host端函数。
·
文章目录
1 概要
博主最近因为工程需求,需要在两个嵌入式设备之间传输图片,具体功能如下描述:
硬件资源:
①米联客安路F3P-CZ02-FPSoc(FPGA) (HOST端)
②rk3568 (Client端)
满足功能:
①rk3568可以将消息包通过Socket接口发送给FPGA
②FPGA通过解包消息报,读取图片数据,并存入相应的文件夹中
2 代码文件结构
整个工程代码分为三个结构,Client,Host,Common,其中:
- Client文件夹内存放的为与Client端相关的函数
- Host文件夹内存放的为与Host端相关的函数
- Common文件夹内存放的为公共配置函数及部分工具包
2.1 Host文件夹下函数介绍

- host_main.cpp:
用于快速测试的文件demo。 - host_main_test.cpp:
根据用户需求进行功能设计的文件,其中对于封装类别的使用参考了host_main.cpp中的函数。 - ImageServer.h:
ImageServer类别的头文件。 - ImageServer.cpp:
ImageServer类别的源文件。
2.1.1 ImageServer.h
- ImageServer(uint16_t port, std::filesystem::path base_dir);
构造函数:保存监听端口和图片保存的根目录。 - bool start()
创建监听 socket、绑定到 port_、开始 listen,然后启动 accept_thread_ 在线程中跑 acceptLoop(),正式对外提供服务。 - void stop();
把 running_ 置为 false,关闭 listen_fd_,并等待 accept_thread_ 结束,用来优雅关闭服务器。 - ~ImageServer();
析构时确保服务器被停止、资源被释放(比如还在跑的线程、打开的 fd 等)。 - void acceptLoop();
在一个循环里调用 accept 接受客户端连接,当有新连接时得到 cfd,再调用 handleClient(cfd) 去处理;循环条件通常检查 running_,其中会开启一个新线程处理handleClient(cfd)。 - 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端函数
更多推荐

所有评论(0)