C++小白实习日记——Pollnet,Efvi,UDP,数据类型转换(上)

发布于:2024-12-18 ⋅ 阅读:(81) ⋅ 点赞:(0)

上周主要是熟悉了一下公司内部一些自定义结构体对应的数据类型,要求:读取文件,将文件中数据转化为定义的结构体中的数据类型,按照时间进行排序,用UDP发送数据;在另一台服务器上接收数据,按照定义好的数据结构存储传输过来的UDP包

一,发送端设计

1,读取文件并转换成指定数据结构

包括但不限于,读取文件,获取表头,打印表头,之前的同事留下的代码将表头标签用列表存储,我做了一个字段到索引的映射,就是读取第一行作为表头

    const std::string _csvPath = "文件.csv";

    // 检查文件是否存在
    if (!fileExists(_csvPath)) {
        std::cerr << "CSV file does not exist: " << _csvPath << std::endl;
        return false;
    }

    // 初始化 CsvReader
    CsvReader reader;
    try {
        reader.load_from_file(_csvPath.c_str());
    } catch (const std::exception& e) {
        std::cerr << "Error loading CSV file: " << e.what() << std::endl;
        return false;
    }

    // 获取表头信息
    std::vector<std::string> header;
    if (!reader.next_row(header)) {
        std::cerr << "CSV file is empty or cannot read the header row." << std::endl;
        return false;
    }

    // 打印表头内容
    std::cout << "Table Header: ";
    for (const auto& field : header) {
        std::cout << field << " ";
    }
    std::cout << std::endl;

    // 存储字段的有序顺序和映射
    std::vector<std::string> orderedFields;
    std::unordered_map<std::string, int> fieldMap;

    // 构建字段映射
    for (size_t i = 0; i < header.size(); ++i) {
        orderedFields.push_back(header[i]);
        fieldMap[header[i]] = static_cast<int>(i);
    }

    // 打印映射结果
    printFieldMap(orderedFields, fieldMap);

其中读取文件以及读表用了一个封装了的类CSVReader

// CSV 文件解析类
class CsvReader {
public:
    CsvReader() = default;

    void load_from_file(const char* filepath) {
        file_.open(filepath);
        if (!file_.is_open()) {
            throw std::runtime_error("Failed to open CSV file.");
        }
    }

    bool next_row(std::vector<std::string>& row) {
        row.clear();
        std::string line;
        if (std::getline(file_, line)) {
            std::stringstream line_stream(line);
            std::string cell;
            while (std::getline(line_stream, cell, ',')) {
                row.push_back(cell);
            }
            return true;
        }
        return false;
    }

private:
    std::ifstream file_;
};

这个类依赖于 std::ifstream。std::ifstream是 C++ 标准库中的一个输入流类,用于从文件读取数据。std::getline是标准中的函数。

  • next_row 方法每次被调用时会从文件中读取一行数据(std::getline(file_, line))。如果成功读取到一行数据,函数会继续处理这行数据。

  • 使用 std::stringstream 来解析这一行数据,通过 std::getline(line_stream, cell, ',') 按照逗号(,) 将行数据分割成单独的单元(单元格),然后将每个单元(cell)加入到 row 向量中。

字段映射主要是用std::vector<std::string> orderedFields; std::unordered_map<std::string, int> fieldMap;

  • orderedFields.push_back(header[i]) 将每个字段名称按顺序存储到 orderedFields 中,这样 orderedFields 就是一个字段名的有序集合。

  • fieldMap[header[i]] = static_cast<int>(i) 将每个字段名称与其索引(即列的位置)存储到 fieldMap 映射中,这样 fieldMap 就是一个字段名到索引值的映射。

其中fieldMap是无序的,但是他里面的键值对有序号。

打印键值对:

// 打印字段映射
void printFieldMap(const std::vector<std::string>& orderedFields, const std::unordered_map<std::string, int>& fieldMap) {
    std::cout << "Field Map (Ordered):" << std::endl;
    for (const auto& field : orderedFields) {
        std::cout << "  " << field << " -> " << fieldMap.at(field) << std::endl;
    }
}

at(field)std::unordered_map 类的成员函数,它的作用是 根据键(field)查找并返回对应的值,如果找不到该键,它会抛出一个 std::out_of_range 异常。

2,转换数据类型

std::vector<PSILEV2API::PSILev2MarketDataField> marketDataList; // 用于存储所有行的 MarketData
    // 解析数据行
    PSILEV2API::PSILev2MarketDataField marketData{};
    // 读取数据行
    std::vector<std::string> row;
    while (reader.next_row(row)) {
        std::cout << "Row: ";
        for (const auto &value: row) {
            std::cout << value << " ";
        }
        std::cout << std::endl;
        for (const auto &field: orderedFields) {
            auto it = fieldMap.find(field);
            if (it == fieldMap.end() || it->second >= static_cast<int>(row.size())) {
                continue;
            }
            std::string Field = field;
            Field.erase(std::remove(Field.begin(), Field.end(), '\"'), Field.end()); // 去掉引号
            Field.erase(0, Field.find_first_not_of(" \t\n\r"));
            Field.erase(Field.find_last_not_of(" \t\n\r") + 1);

            if (Field == "datetime") {
                std::string datetime = row[it->second];
                marketData.DataTimeStamp = convertToTimeStamp(datetime);
                std::cout << "DataTimeStamp: " << marketData.DataTimeStamp << std::endl;
            } 
        }
        marketDataList.push_back(marketData); // 将每一行的 MarketData 对象存入列表
    }

逐行读取数据,将数据中的field与想要的标签做对比,符合要求后将其转化为需要的结构体中的数据,添加到结构体中,然后将转化后的结构体逐行push_back到marketData的列表中。

主要分为三部分:1)检查表头字段是不是在无序索引filedMap中

for (const auto &field: orderedFields)

这一行是一个范围基 for 循环,用来遍历 orderedFields 中的每一个 field(字段名称)。orderedFields 是一个存储字段名称(如 "datetime", "price" 等)的 std::vector<std::string>

auto it = fieldMap.find(field)

这一行在 fieldMap 中查找当前字段(field)的索引。

  • fieldMap 是一个 std::unordered_map<std::string, int>,用来存储字段名称到索引的映射关系。

  • fieldMap.find(field) 会返回一个迭代器 it,指向字段名称 fieldfieldMap 中的元素。如果找到了该字段名,它会返回对应的键值对的迭代器;如果没有找到,它会返回 fieldMap.end()

if (it == fieldMap.end() || it->second >= static_cast<int>(row.size())) { continue; }

这部分是一个条件判断,用来检查是否可以有效地从 row 中提取数据。

  • it == fieldMap.end():表示在 fieldMap 中找不到当前字段名称 field。如果找不到字段名,则跳过当前字段的处理。

  • it->second >= static_cast<int>(row.size())it->secondfieldMap 中该字段对应的索引(字段的位置)。row.size() 是当前行的列数。如果该索引超出了当前行的列数(即字段位置无效),则跳过当前字段的处理。

  • continue:如果满足以上任一条件,continue 会跳过当前循环的剩余部分,继续处理下一个字段。

2)将表头字段的前后空格,斜杠等去掉

Field.erase(std::remove(Field.begin(), Field.end(), '\"'), Field.end());
  • std::remove 是一个标准库算法,它会在范围 [Field.begin(), Field.end()) 内删除所有的字符 '\"',即所有的双引号字符(" ")。但是,它并不会真正从容器中删除元素,而是将这些元素“移动”到容器的末尾,返回一个新的迭代器,指向新容器的有效元素区域的末尾。

    例如,给定字符串 "\"example\""std::remove 会将所有的双引号字符移到字符串的末尾,并返回一个新的“有效范围”。

  • Field.erase 将这个新的范围以外的部分(即所有的双引号)实际删除,最后得到的字符串将不再包含双引号。

    例如,输入 "\"example\"",经过这行代码后,Field 会变成 "example"

Field.erase(0, Field.find_first_not_of(" \t\n\r"));
  • Field.find_first_not_of(" \t\n\r"):这个函数查找字符串中第一个不是空格、制表符(Tab)、换行符(\n)、回车符(\r)的字符的位置。

    • 如果 Field 开头有空格、Tab、换行符或回车符,它会返回第一个非空白字符的位置。

    • 如果没有找到空白字符(即字符串没有前导空格),则返回字符串的起始位置(0)。

  • Field.erase(0, ...):这一行的作用是删除 Field 字符串中的前导空白字符。它会从位置 0 开始删除,删除的长度是从 0find_first_not_of 找到的位置,实际上就是删除了所有前导的空格、制表符等。

    例如,如果 Field" example", 则 Field.find_first_not_of(" \t\n\r") 返回 3,表示第一个非空白字符的位置是 'e'。然后,erase 会删除前面的 3 个空格,Field 变成 "example"

 Field.erase(Field.find_last_not_of(" \t\n\r") + 1);
  • Field.find_last_not_of(" \t\n\r"):这个函数查找字符串中最后一个不是空格、制表符、换行符或回车符的字符的位置。

    • 如果 Field 末尾有空白字符,它会返回最后一个非空白字符的位置。

    • 如果没有找到空白字符(即字符串没有尾随空格),则返回字符串的最后一个字符的位置。

  • Field.erase(... + 1):这行代码的作用是删除 Field 字符串末尾的空白字符。通过 find_last_not_of 找到最后一个非空白字符的位置,并删除从该位置到字符串末尾的所有字符,实际上就是去掉尾部的空白字符。

    例如,如果 Field"example ", 则 Field.find_last_not_of(" \t\n\r") 返回 7,表示最后一个非空白字符的位置是 'e'。然后,erase 会删除从位置 8 开始到字符串末尾的空格,Field 变成 "example"

3)判断如果当前表头符合转换要求,将数据赋值给std::string,然后转换数据类型

PSILEV2API::TTORATstpTimeStampType convertToTimeStamp(const std::string& datetime) {
    // 查找时间部分(假设格式为 "YYYY/MM/DD HH:MM:SS")
    size_t time_pos = datetime.find(' ');
    if (time_pos == std::string::npos) {
        throw std::invalid_argument("Invalid datetime format");
    }

    // 提取时间部分
    std::string time_part = datetime.substr(time_pos + 1);

    // 分割时、分、秒
    int hours, minutes, seconds;
    char delimiter;
    std::istringstream time_stream(time_part);
    time_stream >> hours >> delimiter >> minutes >> delimiter >> seconds;

    if (time_stream.fail()) {
        throw std::invalid_argument("Invalid time format");
    }

    // 转换为毫秒格式的整数:HHMMSS000
    return (hours * 10000000) + (minutes * 100000) + (seconds * 1000);
}

这里用了std::istringstream是一个标准库中的流类

  • 流的输入:当使用 >> 操作符时,std::istringstream 会逐个字符地读取 time_part 字符串,直到它成功地将数据解析为一个变量的类型。例如,当它试图将 "14" 解析为 hours 时,它成功地解析了数字 14。

  • 分隔符的“消耗”:流会自动跳过空格或其他分隔符(例如冒号)。这些分隔符并不直接存储在变量中,而是被“消耗”掉,用来区分不同的数据段。

  • 多个变量解析:可以通过连续使用 >> 操作符来解析多个值,每个操作符都会从流中提取出下一个数据,直到整个字符串被解析完或者遇到错误。

3,UDP发送

// 创建EfviUdpSender实例并初始化
        EfviUdpSender udpSender;
        udpSender.init("enp8s0f1", "10.100.100.162", 12345, "10.100.100.161", 12346);
        // 遍历 marketDataList,序列化并发送每一行数据
        for (size_t i = 0; i < marketDataList.size(); ++i) {
            const auto &marketData = marketDataList[i];

            // 序列化数据
            size_t dataSize = sizeof(marketData);
            std::vector<char> serializedData(dataSize);
            std::memcpy(serializedData.data(), &marketData, dataSize);

            // 打印字节流的十六进制表示
            for (size_t j = 0; j < dataSize; ++j) {
                printf("%02X ", static_cast<unsigned char>(serializedData[j]));
            }
            printf("\n");

            // 通过 UDP 发送数据包
            if (!udpSender.write(serializedData.data(), dataSize)) {
                std::cerr << "Failed to send market data for row " << i << std::endl;
                continue;
            }

            std::cout << "Market data for row " << i << " sent via UDP!" << std::endl;

            // 控制发送频率(例如每秒发送一次)
            std::this_thread::sleep_for(std::chrono::seconds(1));

 先初始化EfviUdpSender udpSender,其中

  • "enp8s0f1":网络接口的名称,表示通过该网络接口进行数据发送。enp8s0f1 是一个 Linux 下的网络接口名称。

  • "10.100.100.162", 12345:目标 IP 地址和端口号,表示将数据发送到该地址和端口。

  • "10.100.100.161", 12346:源 IP 地址和端口号,表示从该地址和端口发送数据。

用for循环遍历marketDataList并序列化数据。

1. std::memcpy 函数

std::memcpy 是 C++ 标准库中的一个函数,定义在 <cstring> 头文件中。它用于将内存区域的内容复制到另一个内存区域。函数原型如下:

void* memcpy(void* dest, const void* src, std::size_t count);
  • dest:目标内存地址,即要复制到的位置。

  • src:源内存地址,即要从哪里复制数据。

  • count:要复制的字节数。

在 C++ 中,结构体和类对象的数据通常以二进制格式存储。直接通过 std::memcpy 进行内存拷贝是一种高效的方法,可以避免逐个字段的序列化。尤其是在网络通信中,发送和接收的数据通常是字节流,而不是结构化数据。因此,使用 std::memcpy 将结构体数据转换为字节流,并直接发送是一种常见的做法

2. static_cast强制转换

  • 这里使用 static_castserializedData[j] 强制转换为 unsigned char 类型。这样做的原因是 printf 格式化输出时,默认处理的 char 类型(尤其是有符号 char)可能会导致负数问题,因为 char 可以是有符号的,取值范围为 -128127

  • 强制转换为 unsigned char 类型可以确保输出为 0 到 255 的有效字节值

3."%02X "

  • printf 用于格式化输出,第一个参数 "%02X " 是格式字符串,告诉 printf 如何输出数据:

    • %02X:以十六进制格式打印整数,且每个数字至少占 2 个字符宽度。如果数字是 1 位(比如 0x9),则会在前面补充 0,使其变成 09

    • X 表示输出大写的十六进制字母(A-F)。

    • 02 表示输出的最小宽度为 2 个字符,如果一个字节的值小于 16(即 0x0 到 0xF),则会在前面补充一个零以满足 2 位宽度。

    • " 用于在输出中打印空格,确保每个字节后面都有一个空格,使输出更加易读。

    所以,如果 serializedData[j] 的值是 255,那么输出将是 FF,如果它是 9,输出将是 09


网站公告

今日签到

点亮在社区的每一天
去签到