Estoy tratando de encontrar el mejor enfoque para transferir algunos datos a través de la red. Esto es lo que espero lograr:

La aplicación ejecuta y calcula algunos datos:

int w = 5;
float x = 4.736;
std::string y = "Some String.";
std::vector<int> z;
z.push_back(1);
z.push_back(2);
z.push_back(3);

Luego la ponemos en un recipiente binario:

BinaryContainer Data;
Data.Write(w);
Data.Write(x);
Data.Write(y);
Data.Write(z);

Luego lo transferimos a través de la red:

SendData(Data.c_str());

Y léelo en el otro lado:

BinaryContainer ReceivedData(IncomingData);
int w = ReceivedData.Read();
float x = ReceivedData.Read();
std::string y = ReceivedData.Read();
std::vector<int> z = ReceivedData.Read();

El ejemplo anterior describe cómo debería funcionar la funcionalidad básica desde una perspectiva de alto nivel. He examinado muchas bibliotecas de serialización diferentes y ninguna parece encajar del todo bien. Me inclino por aprender a escribir la funcionalidad yo mismo.

Endianness no importa. La arquitectura que lee y escribe datos nunca será diferente. Solo necesitamos almacenar datos binarios dentro del contenedor. La aplicación de lectura y la aplicación de escritura son exclusivamente responsables de leer los datos en el mismo orden en que fueron escritos. Solo se necesitan escribir tipos básicos, no clases arbitrarias completas o punteros a las cosas. Lo más importante en general es que la velocidad a la que esto ocurre debe ser de máxima prioridad porque una vez que se formulan los datos, debemos escribirlos en el contenedor, transferirlos a través de la red y leerlos en el otro extremo lo más rápido posible.

La transmisión de red se está realizando actualmente utilizando la API WinSock RIO de bajo nivel y ya estamos trasladando los datos de la aplicación al cable lo más rápido posible. La latencia de transmisión a través del cable siempre será una tasa mucho más alta y variable. El punto en el que serializamos nuestros datos antes de la transmisión es el siguiente paso en la cadena para garantizar que estamos desperdiciando el menor tiempo posible antes de enviar nuestros datos al cable.

Los nuevos paquetes se recibirán muy rápidamente y, como tal, la capacidad de preasignar recursos sería beneficiosa. Por ejemplo:

Serializer DataHandler;
...
void NewIncomingPacket(const char* Data)
{
    DataHandler.Reset();
    DataHandler.Load(Data);
    int x = DataHandler.Read();
    float y = DataHandler.Read();
    ...
}

Estoy buscando aportes de expertos de la comunidad en qué dirección ir aquí.

c++
4
KKlouzal 4 dic. 2019 a las 13:40

2 respuestas

La mejor respuesta

Si no le importa el endianness y solo desea serializar tipos triviales, una simple memoria será la más rápida y segura. Simplemente ingrese / salga del búfer al serializar / deserializar.

#include <iostream>
#include <vector>
#include <cstring>
#include <cstdint>
#include <type_traits>
#include <cstddef>

template <std::size_t CapacityV>
struct BinaryContainer
{
    BinaryContainer() :
        m_write(0),
        m_read(0)
    {
    }

    template <typename T>
    void write(const std::vector<T>& vec)
    {
        static_assert(std::is_trivial_v<T>);

        // TODO: check if access is valid

        const std::size_t bytes = vec.size() * sizeof(T);
        std::memcpy(m_buffer + m_write, vec.data(), bytes);
        m_write += bytes;
    }

    template <typename T>
    void write(T value)
    {
        static_assert(std::is_trivial_v<T>);

        // TODO: check if access is valid

        const std::size_t bytes = sizeof(T);
        std::memcpy(m_buffer + m_write, &value, bytes);
        m_write += bytes;
    }

    template <typename T>
    std::vector<T> read(std::size_t count)
    {
        static_assert(std::is_trivial_v<T>);

        // TODO: check if access is valid

        std::vector<T> result;
        result.resize(count);

        const std::size_t bytes = count * sizeof(T);
        std::memcpy(result.data(), m_buffer + m_read, bytes);
        m_read += bytes;

        return result;
    }

    template <typename T>
    T read()
    {
        static_assert(std::is_trivial_v<T>);

        // TODO: check if access is valid

        T result;

        const std::size_t bytes = sizeof(T);
        std::memcpy(&result, m_buffer + m_read, bytes);
        m_read += bytes;

        return result;
    }

    const char* data() const
    {
        return m_buffer;
    }

    std::size_t size() const
    {
        return m_write;
    }

private:
    std::size_t m_write;
    std::size_t m_read;
    char m_buffer[CapacityV]; // or a dynamically sized equivalent
};

int main()
{

    BinaryContainer<1024> cont;

    {
        std::vector<std::uint32_t> values = {1, 2, 3, 4, 5};
        // probably want to make serializing size part of the vector serializer
        cont.write(values.size());
        cont.write(values);
    }

    {
        auto size = cont.read<std::vector<std::uint32_t>::size_type>();
        auto values = cont.read<std::uint32_t>(size);

        for (auto val : values) std::cout << val << ' ';
    }
}

Demostración: http://coliru.stacked-crooked.com/a/4d176a41666dbad1

1
Sopel 4 dic. 2019 a las 12:28

He escrito en serio, una biblioteca de C ++ rápida de solo encabezado que debería hacer lo que quieras :-)

Proporciona un serializador y un deserializador.

Los datos serializados son portables en diferentes arquitecturas y endianness. Sin dependencias externas.

    seriously::Packer<1024> packer;     // a 1024 byte serialization buffer

    int32_t value1 = 83656;
    bool value2 = true;
    int16_t value3 = -2345;
    std::string value4("only an example");
    double value5 = -6.736;
    std::vector<int64_t> value6;

    value6.push_back(42);
    value6.push_back(11);
    value6.push_back(93);

    packer << value1 << value2 << value3 << value4 << value5 << value6;

    std::cout << "packed size: " << packer.size() << std::endl;
    // packer.data() contains the serialized data

    int32_t restored1;
    bool restored2;
    int16_t restored3;
    std::string restored4;
    double restored5 = -6.736;
    std::vector<int64_t> restored6;

    packer >> restored1 >> restored2 >> restored3 >> restored4 >> restored5 >> restored6;

    std::cout << "unpacked: " << restored1 << " " << (restored2 ? "t" : "f") << " " << restored3 << " " << restored4 << " " << restored5 << std::endl;

    std::vector<int64_t>::const_iterator it;
    for (it = restored6.begin(); it != restored6.end(); it++) {
        std::cout << *it << std::endl;
    }
1
Marco Pantaleoni 4 dic. 2019 a las 12:44