Sending and receiving messages ============================== Sending ------- Sending messages is done with the :cpp:class:`netio3::NetioSender` class. First you instantiate a :cpp:class:`netio3::NetioSender` object, then open a connection to the :cpp:class:`netio3::EndPointAddress` you want to communicate with. Once the connection is established, you can call one of the :cpp:func:`netio3::NetioSender::send_data` methods. .. code-block:: cpp const NetioSenderConfig sender_config{ .backend_type = NetworkType::ASYNCMSG, .backend_mode = NetworkMode::TCP, .buffersize = 1024, .nbuffers = 10 }; NetioSender sender (sender_config, evloop); The :cpp:class:`netio3::NetioSender` is configured with a :cpp:struct:`netio3::NetioSenderConfig` object which is passed to the constructor along with the event loop (see :cpp:class:`netio3::BaseEventLoop`). .. code-block:: cpp std::atomic_bool connection_cb = false; bool connection_ready = false; sender.set_on_connection_established ([&](const EndPointAddress&) { connection_ready = true; connection_cb.store(true); connection_cb.notify_all(); }); bool connection_failed = false; sender.set_on_connection_refused ([&](const EndPointAddress&) { connection_failed = true; connection_cb.store(true); connection_cb.notify_all(); }); const EndPointAddress remoteEp{"127.0.0.1", 1337}; auto& connection = sender.open_connection(remoteEp); // Wait for connection to establish connection_cb.wait(false); if (connection_failed) { // Tidy up and exit or throw } After constructing the :cpp:class:`netio3::NetioSender`, the next step is to open a connection to the destination network endpoint. The :cpp:func:`netio3::NetioSender::open_connection` method returns a :cpp:class:`netio3::NetioSender::Connection` object immediately but the connection is established asynchronously so may still fail. If you want to be informed of a successful connection, you can set a callback to be called on connection established. There is also a callback for connection refused. Here we set the connection callbacks to set a flag that we can wait for before attempting to send our message. .. code-block:: cpp std::string text{"Hello world"}; auto message = std::span( reinterpret_cast(text.data()), text.size()); const uint64_t tag=0x1234abcdfeedface; status = sender.send_data(connection, tag, message); if (status != NetioStatus::OK) { // Something went wrong The message is represented as a :cpp:class:`std::span` of unsigned 8-bit values. The :cpp:class:`netio3::NetioSender` class provides several methods for sending data. The main two options are :cpp:func:`netio3::NetioSender::send_data` which sends the data immediately and :cpp:func:`netio3::NetioSender::buffered_send_data` which will accumulate multiple packets in a buffer and send either when the buffer is full or a time-out is reached. In the example above, the 'immediate' version is used, sending a single span of data. Note that there are send_data methods that take the endpoint address of an opened connection as the first argument so you don't have to keep track of the connection between the open and the send but it saves a map lookup to use the connection object. All :cpp:func:`netio3::NetioSender::send_data` methods include a `tag` in their parameter lists. This tag is used by the publish/subscribe mechanism but can be any arbitrary value in a simple send/receive set up. If you need to be notified of the successful completion of the send, you can set an on send completed callback (see :cpp:func:`netio3::NetioSender::set_on_send_completed`). Choosing a send_data method ^^^^^^^^^^^^^^^^^^^^^^^^^^^ There are 3 modes of communication, 'send', 'buffered_send' and 'zero_copy_send' each with a group of methods :cpp:func:`netio3::NetioSender::send_data`, :cpp:func:`netio3::NetioSender::buffered_send_data` and :cpp:func:`netio3::NetioSender::zero_copy_send_data`. Although in all cases the data is sent asynchronously, in the first 2 the data are copied into a local intermediate buffer and so the memory passed to the send_data method may be reused or deallocated immediately. In the zero-copy case, the memory containing the data being sent must remain valid until the user receives a callback indicating that the send is complete. The plain send methods send a single message straight away. The data will be copied into a buffer so the message memory may be reused as soon as the method returns even though the data will be sent asynchronously. .. doxygenfunction:: netio3::NetioSender::send_data(const EndPointAddress&, uint64_t, std::span, uint8_t _status) :no-link: :outline: The buffered send methods accumulate several messages into one buffer before sending, either when the buffer is full or the time since the first message was inserted reaches a predefined limit. If you want to force the sending of the buffer at any point, you may call the :cpp:func:`netio3::NetioSender::flush_buffer` method. .. doxygenfunction:: netio3::NetioSender::buffered_send_data(const EndPointAddress&, uint64_t, std::span, uint8_t _status) :no-link: :outline: The zero-copy methods initiate the sending of data immediately without a copy. The completion of the sending is asynchronous so the memory the containing the data must not be modified until the send is complete. To track usage of the transfer, the zero-copy send methods accept a key parameter which is then returned by the send complete callback when the transfer is complete. .. doxygenfunction:: netio3::NetioSender::zero_copy_send_data(const EndPointAddress&, std::uint64_t, std::span, uint8_t _status, std::uint64_t) :no-link: :outline: .. _receiving: Receiving messages ------------------ To receive messages, you first need to instantiate a :cpp:class:`NetioReceiver` object and set it's "on_data" callback to handle your messages. Then you need to set it to listen on a port. .. code-block:: cpp const NetioReceiverConfig rec_config { .backend_type = NetworkType::ASYNCMSG, .backend_mode = NetworkMode::TCP, .thread_safety = ThreadSafetyModel::SAFE }; NetioReceiver receiver(rec_config, evloop); std::atomic_uint nmessages_received = 0; receiver.set_on_data_cb([&](uint64_t tag, std::span payload, uint8_t status){ const std::string msg(payload.begin(),payload.end()); std::cout << std::format( "Received message with tag {:x}, size {}, status {}, message <{}>\n", tag, payload.size(), status, msg); nmessages_received++; }); ConnectionParametersRecv connection_pars{.buf_size=512, .num_buf=4}; EndPointAddress local_ep{"127.0.0.1", 1337}; receiver.listen(local_ep, connection_pars); The main work of the receiver is implemented in the "on_data" callback. There are two options for the callback, one just receiving a single message at a time (set via the :cpp:func:`netio3::NetioReceiver::set_on_data_cb` method) and the other receiving a complete buffer of possibly multiple messages (set via the :cpp:func:`netio3::NetioReceiver::set_on_buffer_cb` method). In this example we use the :cpp:func:`netio3::NetioReceiver::set_on_data_cb` method to set a callback (:cpp:type:`netio3::CbMessageReceived`) to just print a summary and count the messages. After setting the data receiving callback we have to set the receiver to listen on a network port for incoming connections. The receiver needs to be told how many buffers to allocate for receiving and how large they need to be. This is done via a :cpp:struct:`netio3::ConnectionParametersRecv` object. .. LocalWords: cpp func code-block const NetioSenderConfig struct .. LocalWords: NetioSender bool EndPointAddress NetioStatus .. LocalWords: NetioReceiver doxygenfunction