Basic sender & receiverΒΆ

This basic example shows a score sender and receiver application that are used to multicast a single buffer of data on a local network.

The complete sender example is shown below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// Copyright (c) 2016 Steinwurf ApS
// All Rights Reserved
//
// THIS IS UNPUBLISHED PROPRIETARY SOURCE CODE OF STEINWURF
// The copyright notice above does not evidence any
// actual or intended publication of such source code.

#include <cstdint>
#include <cstdlib>
#include <iostream>
#include <vector>
#include <algorithm>

#include <score/api/udp_sender.hpp>

int main()
{
    // Create the io_service object
    score::api::io_service io;

    // Create the score sender object with the object profile
    score::api::udp_sender sender(io, score::api::sender_profile::object);

    // Configure the destination address and port on the score sender.
    // Address can be any IPv4 address, including multicast and broadcast.
    std::error_code ec;
    sender.add_remote("224.0.0.251", 7891, ec);

    if (ec)
    {
        std::cerr << "Could not add remote. Error: " << ec.message()
                  << std::endl;
        return ec.value();
    }

    sender.set_error_message_callback([&io](
        std::string operation, std::error_code error)
    {
        std::cerr << "Error in " << operation << ": " << error.message()
                  << std::endl;

        // Stop if something went wrong
        io.stop();
    });

    // At this point the sender is ready to send data over the network.

    // For now, we just put in dummy data. Let us assume the data was loaded
    // from a small file:
    uint32_t data_size = 100000;
    std::vector<uint8_t> data_in(data_size);
    // Fill the data buffer with random bytes
    std::generate(data_in.begin(), data_in.end(), rand);

    // Add the data to the sender
    sender.write_data(data_in);

    // In this example, we use a single-byte message with value '0' as the
    // end-of-transmission message. The receiver does not expect more data
    // after receiving this same message.
    // Note that any unique message can be used for this purpose.
    std::vector<uint8_t> eot(1, 0);
    sender.write_data(eot);

    // Ensure that the EOT message is transmitted immediately
    sender.flush();

    // Set what to do when the sender transmission queue is empty.
    // We just stop the IO service immediately.
    sender.set_on_queue_empty_callback([&io]() { io.stop(); });

    // Run the event loop of the io_service
    io.run();

    std::cout << "Sent: " << sender.sent_bytes() << " bytes" << std::endl;

    return 0;
}

First we create an io_service and a sender object, then configure the destination address for the sender.

After the initialization, we allocate a small buffer that should be transmitted to the receiver(s). We could fill this block with some actual data (e.g. from a small file), but that is not relevant here. We write this data block to the sender with write_data. After this, we send a small 1-byte message using the same write_data function. This small 1-byte message is the end-of-transmission message. When the receiving application sees this message, it knows no data more will arrive, and it can shut down. The end-of-transmission message format is defined by the application, and can be anything. After this we set a callback to be executed when the sender’ transmission queue is empty and no more data is to be sent. Here, we stop the IO service in this callback.

The actual network operations start when we run the io_service (this is the event loop that drives the sender). The event loop will terminate when the sender finishes all transmissions, because we explicitly stop the io_service in our on_queue_empty_callback callback. This also means that receivers that have not received everything when this callback is executed will never finish transmission. This approach does not leave much time for repairing packet loss. A more advanced application may want to leave time for receivers to request and receive repair data before shutting down the event loop.

The code for the corresponding receiver application is shown below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// Copyright (c) 2017 Steinwurf ApS
// All Rights Reserved
//
// THIS IS UNPUBLISHED PROPRIETARY SOURCE CODE OF STEINWURF
// The copyright notice above does not evidence any
// actual or intended publication of such source code.

#include <cstdint>
#include <vector>
#include <string>
#include <iostream>

#include <score/api/udp_receiver.hpp>

int main()
{
    // Create the io_service object
    score::api::io_service io;

    // Create the score receiver object
    score::api::udp_receiver receiver(io);

    // Bind the score receiver to a specific address and port
    // The address can be a local IPv4 address or a multicast/broadcast address
    // It can also be "0.0.0.0" to listen on all local interfaces
    std::error_code ec;
    receiver.bind("224.0.0.251", 7891, ec);

    if (ec)
    {
        std::cerr << "Could not bind score receiver. Error: " << ec.message()
                  << std::endl;
        return ec.value();
    }

    receiver.set_error_message_callback([](
        std::string operation, std::error_code error)
    {
        std::cerr << "Error in " << operation << ": " << error.message()
                  << std::endl;
    });

    // This callback function will be executed when some data is received.
    // In this example, we just print how many bytes were received.
    auto read_data = [&io](std::vector<uint8_t>& data)
    {
        (void) data;
        std::cout << "Received: " << data.size() << " bytes." << std::endl;

        // We use a single-byte message with value '0' as the
        // end-of-transmission message.
        if (data.size() == 1 && data[0] == 0)
        {
            // Shut down the io_service, as no more data will come
            io.stop();
            std::cout << "IO service stopped." << std::endl;
        }
    };
    // Set the callback to be used when data is received
    receiver.set_data_ready_callback(read_data);

    // Run the event loop of the io_service
    io.run();

    return 0;
}

The initialization steps are very similar for the receiver, but we also set a callback function that will be executed when data is received. We could process the incoming data, but here the read_data lambda function just prints the size of the received block and checks if the received message is the defined end-of-transmission messsage. The receiver event loop is stopped when this message is received. Note that we have written a single block of data on the sender side, so the same block will be received in one go (i.e. read_data will be called once when the full block is available, and finally when the end-of-transmission message is received.). If we send multiple blocks, this callback function would be invoked for each block. The data messages are atomic in score, so no partial message will be passed to the read_data function.