c++ Sending struct over network

2019-02-20 16:57发布

问题:

I'm working with Intel SGX which has predefined structures. I need to send these structures over a network connection which is operated by using boost::asio. The structure that needs to be send has the following format:

typedef struct _ra_samp_request_header_t{
    uint8_t  type;     /* set to one of ra_msg_type_t*/
    uint32_t size;     /*size of request body*/
    uint8_t  align[3];
    uint8_t body[];
} ra_samp_request_header_t;

For the sending and receiving, the methods async_write and async_async_read_some are used

boost::asio::async_write(socket_, boost::asio::buffer(data_, max_length),
                                          boost::bind(&Session::handle_write, this,
                                          boost::asio::placeholders::error));

socket_.async_read_some(boost::asio::buffer(data_, max_length),
                            boost::bind(&Session::handle_read, this,
                            boost::asio::placeholders::error,
                            boost::asio::placeholders::bytes_transferred));

whereas data_ is defined as

enum { max_length = 1024 };
char data_[max_length];

My first approach was to transform the single structure elements into strings and store them in a vector<string> which is then further transformed into char* whereas each element is separated by \n.

But when assembling the received char* on the receiver side back to the original structure I run into some troubles.

Is this really the way this should be done or is there a nicer more sufficient way of transfering the structure

回答1:

Do you need it to be portable?

If not:

  1. simplistic approach
  2. using Boost Serialization

If it needs to be portable

  1. complicate the simplistic approach with ntohl and htonl calls etc.
  2. use Boost Serialization with EOS Portable Archives

1. simplistic approach

Just send the struct as POD data (assuming it is actually POD, which given the code in your question is a fair assumption as the struct is clearly not C++).

A simple sample that uses synchronous calls on 2 threads (listener and client) shows how the server sends a packet to the client which the client receives correctly.

Notes:

  • using async calls is a trivial change (change write and read into async_write and async_write, which just makes control flow a bit less legible unless using coroutines)
  • I showed how I'd use malloc/free in a (exceptio) safe manner in C++11. You may want to make a simple Rule-Of-Zero wrapper instead in your codebase.

Live On Coliru

#include <boost/asio.hpp>
#include <cstring>

namespace ba = boost::asio;
using ba::ip::tcp;

typedef struct _ra_samp_request_header_t{
    uint8_t  type;     /* set to one of ra_msg_type_t*/
    uint32_t size;     /*size of request body*/
    uint8_t  align[3];
    uint8_t  body[];
} ra_samp_request_header_t;

#include <iostream>
#include <thread>
#include <memory>

int main() {
    auto unique_ra_header = [](uint32_t body_size) {
        static_assert(std::is_pod<ra_samp_request_header_t>(), "not pod");

        auto* raw = static_cast<ra_samp_request_header_t*>(::malloc(sizeof(ra_samp_request_header_t)+body_size));
        new (raw) ra_samp_request_header_t { 2, body_size, {0} };
        return std::unique_ptr<ra_samp_request_header_t, decltype(&std::free)>(raw, std::free);
    };

    auto const& body = "There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which looks reasonable.";

    auto sample = unique_ra_header(sizeof(body));
    std::strncpy(reinterpret_cast<char*>(+sample->body), body, sizeof(body));

    ba::io_service svc;
    ra_samp_request_header_t const& packet = *sample;
    auto listener = std::thread([&] {
        try {
            tcp::acceptor a(svc, tcp::endpoint { {}, 6767 });
            tcp::socket s(svc);
            a.accept(s);

            std::cout << "listener: Accepted: " << s.remote_endpoint() << "\n";
            auto written = ba::write(s, ba::buffer(&packet, sizeof(packet) + packet.size));
            std::cout << "listener: Written: " << written << "\n";
        } catch(std::exception const& e) {
            std::cerr << "listener: " << e.what() << "\n";
        }
    });

    std::this_thread::sleep_for(std::chrono::milliseconds(10)); // make sure listener is ready

    auto client = std::thread([&] {
        try {
            tcp::socket s(svc);
            s.connect(tcp::endpoint { {}, 6767 });

            // this is to avoid the output to get intermingled, only
            std::this_thread::sleep_for(std::chrono::milliseconds(200));

            std::cout << "client: Connected: " << s.remote_endpoint() << "\n";

            enum { max_length = 1024 };
            auto packet_p = unique_ra_header(max_length); // slight over allocation for simplicity
            boost::system::error_code ec;
            auto received = ba::read(s, ba::buffer(packet_p.get(), max_length), ec); 

            // we expect only eof since the message received is likely not max_length
            if (ec != ba::error::eof) ba::detail::throw_error(ec);

            std::cout << "client: Received: " << received << "\n";
            (std::cout << "client: Payload: ").write(reinterpret_cast<char const*>(packet_p->body), packet_p->size) << "\n";
        } catch(std::exception const& e) {
            std::cerr << "client: " << e.what() << "\n";
        }
    });

    client.join();
    listener.join();
}

Prints

g++ -std=gnu++11 -Os -Wall -pedantic main.cpp -pthread -lboost_system && ./a.out
listener: Accepted: 127.0.0.1:42914
listener: Written: 645
client: Connected: 127.0.0.1:6767
client: Received: 645
client: Payload: There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which looks reasonable.

1b. simplistic with wrapper

Because for Boost Serialization it would be convenient to have such a wrapper anyways, let's rewrite that using such a "Rule Of Zero" wrapper:

Live On Coliru

namespace mywrappers {
    struct ra_samp_request_header {
        enum { max_length = 1024 };

        // Rule Of Zero - https://rmf.io/cxx11/rule-of-zero
        ra_samp_request_header(uint32_t body_size = max_length) : _p(create(body_size)) {}

        ::ra_samp_request_header_t const& get() const { return *_p; };
        ::ra_samp_request_header_t&       get()       { return *_p; };

      private:
        static_assert(std::is_pod<::ra_samp_request_header_t>(), "not pod");
        using Ptr = std::unique_ptr<::ra_samp_request_header_t, decltype(&std::free)>;
        Ptr _p;

        static Ptr create(uint32_t body_size) {
            auto* raw = static_cast<::ra_samp_request_header_t*>(::malloc(sizeof(::ra_samp_request_header_t)+body_size));
            new (raw) ::ra_samp_request_header_t { 2, body_size, {0} };
            return Ptr(raw, std::free);
        };
    };
}

2. using Boost Serialization

Without much ado, here's a simplistic way to implement serialization in-class for that wrapper:

friend class boost::serialization::access;

template<typename Ar>
void save(Ar& ar, unsigned /*version*/) const {
    ar & _p->type
       & _p->size
       & boost::serialization::make_array(_p->body, _p->size);
}

template<typename Ar>
void load(Ar& ar, unsigned /*version*/) {
    uint8_t  type = 0;
    uint32_t size = 0;
    ar & type & size;

    auto tmp = create(size);
    *tmp = ::ra_samp_request_header_t { type, size, {0} };

    ar & boost::serialization::make_array(tmp->body, tmp->size);

    // if no exceptions, swap it out
    _p = std::move(tmp);
}

BOOST_SERIALIZATION_SPLIT_MEMBER()

Which then simplifies the test driver to this - using streambuf:

auto listener = std::thread([&] {
    try {
        tcp::acceptor a(svc, tcp::endpoint { {}, 6767 });
        tcp::socket s(svc);
        a.accept(s);

        std::cout << "listener: Accepted: " << s.remote_endpoint() << "\n";

        ba::streambuf sb;
        {
            std::ostream os(&sb);
            boost::archive::binary_oarchive oa(os);
            oa << sample;
        }

        auto written = ba::write(s, sb);
        std::cout << "listener: Written: " << written << "\n";
    } catch(std::exception const& e) {
        std::cerr << "listener: " << e.what() << "\n";
    }
});

std::this_thread::sleep_for(std::chrono::milliseconds(10)); // make sure listener is ready

auto client = std::thread([&] {
    try {
        tcp::socket s(svc);
        s.connect(tcp::endpoint { {}, 6767 });

        // this is to avoid the output to get intermingled, only
        std::this_thread::sleep_for(std::chrono::milliseconds(200));

        std::cout << "client: Connected: " << s.remote_endpoint() << "\n";

        mywrappers::ra_samp_request_header packet;
        boost::system::error_code ec;

        ba::streambuf sb;
        auto received = ba::read(s, sb, ec); 

        // we expect only eof since the message received is likely not max_length
        if (ec != ba::error::eof) ba::detail::throw_error(ec);

        std::cout << "client: Received: " << received << "\n";

        {
            std::istream is(&sb);
            boost::archive::binary_iarchive ia(is);
            ia >> packet;
        }

        (std::cout << "client: Payload: ").write(reinterpret_cast<char const*>(packet.get().body), packet.get().size) << "\n";
    } catch(std::exception const& e) {
        std::cerr << "client: " << e.what() << "\n";
    }
});

All other code is unchanged from the above, see it Live On Coliru. Output unchanged, except packet sizes grew to 683 on my 64-bit machine using Boost 1.62.

3. complicate the simplistic approach

I'm not in the mood to demo this. It feels like being a C programmer instead of a C++ programmer. Of course there are clever ways to avoid writing the endian-ness twiddling etc. For a modern approach see e.g.

  • I can't find the sample talk/blog post about using Fusion to generate struct portable serialization for your classes (might add later)
  • https://www.youtube.com/watch?v=zvfPK4ot9uA

4. use EAS Portable Archive

Is a simple drop-in exercise using the code of 3.