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
Do you need it to be portable?
If not:
- simplistic approach
- using Boost Serialization
If it needs to be portable
- complicate the simplistic approach with
ntohl
and htonl
calls etc.
- 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.