Basic sender & receiverΒΆ

This basic example shows a score-c 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
79
80
81
82
83
84
85
86
87
88
89
90
91
// 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 <stdio.h>
#include <stdint.h>
#include <stdlib.h>

#include <scorec/scorec.h>

// This callback function will be executed once when the send queue gets empty
void on_queue_empty_callback(void* context)
{
    // Here we stop the io service
    score_io_service_t io = (score_io_service_t) context;
    score_io_service_stop(io);
    printf("IO service stopped\n");
}

int main()
{
    // Create the io_service object
    score_io_service_t io = score_new_io_service();

    // Create the score sender object
    score_sender_t sender = score_new_object_sender(io);

    // Create an error code object
    score_error_code_t error_code = score_new_error_code();

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

    // If an error occurred while adding remote,
    // display an error message and exit
    if (score_error_code_value(error_code) != 0)
    {
        printf("Could not add remote. Error: %s",
               score_error_code_message(error_code));
    }
    else
    {
        // 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, e.g. 100kBytes of data:
        uint32_t data_size = 100000;
        uint8_t* data = (uint8_t*) malloc(data_size);

        // Add the data to the sender
        score_write_data(sender, data, data_size);

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

        // Ensure that the EOT message is transmitted by flushing internal buffers
        score_flush(sender);

        // When the queue gets empty, we will stop the io service
        score_set_on_send_queue_threshold_callback(
            sender, 0, on_queue_empty_callback, io);

        // Run the event loop of the io_service
        score_io_service_run(io);

        printf("Sent %d bytes\n", data_size);

        // Clean up the data buffer
        free(data);
    }

    // Store the program return value before deleting the error code
    int ret = score_error_code_value(error_code);

    // Delete the error code object
    score_delete_error_code(error_code);
    // Delete the score sender object
    score_delete_sender(sender);
    // Finally, delete the io_service object
    score_delete_io_service(io);

    return ret;
}

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 score_write_data. After this, we send a small end-of-transmission message that the receiver recognizes and can act upon. Here this message is just a single byte with value ‘zero’. Finally, we call score_flush to make sure that everything buffered inside the sender will be queued for transmission.

We also set the callback that will be executed when the sender transmission queue is emptied. This is a normal C function where we can also pass an arbitrary value using the context parameter. If this callback is set before any data is written to the sender (and thus the send queue is empty when it is set), the callback will be called immediately.

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 function.

The final step is cleaning up all resources with the appropriate deleters. This is very important in C that does not provide automatic memory management.

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
67
68
69
70
71
72
73
74
75
76
// 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 <stdio.h>
#include <stdint.h>
#include <stdlib.h>

#include <scorec/scorec.h>

uint32_t total_bytes_received = 0;

// This callback function will be executed when some data is received.
void read_data_callback(uint8_t* data, uint32_t size, void* context)
{
    // We print how many bytes were received so far
    total_bytes_received += size;
    printf("Total bytes received: %d\n", total_bytes_received);

    // We use a single-byte message with value '0' as the end-of-transmission
    // message.
    if (size == 1 && data[0] == 0)
    {
        // Shut down the io_service, as no more data will come
        score_io_service_t io = (score_io_service_t) context;
        score_io_service_stop(io);
        printf("IO service stopped\n");
    }
}

int main()
{
    // Create the io_service object
    score_io_service_t io = score_new_io_service();

    // Create the score receiver object
    score_receiver_t receiver = score_new_receiver(io);

    // Create an error code object
    score_error_code_t error_code = score_new_error_code();

    // 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
    score_bind(receiver, "224.0.0.251", 7891, error_code);

    // If an error occurred while binding, display an error message and exit
    if (score_error_code_value(error_code) != 0)
    {
        printf("Could not bind score receiver. Error: %s",
               score_error_code_message(error_code));
    }
    else
    {
        // Set the callback to be used when data is received
        score_set_data_ready_callback(receiver, read_data_callback, io);

        // Run the event loop of the io_service
        score_io_service_run(io);
    }

    // Store the program return value before deleting the error code
    int ret = score_error_code_value(error_code);

    // Delete the error code object
    score_delete_error_code(error_code);
    // Delete the score receiver object
    score_delete_receiver(receiver);
    // Finally, delete the io_service object
    score_delete_io_service(io);

    return ret;
}

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_callback function just prints the size of the received block. 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. the read_data_callback function will be called once when the full block is available). If we send multiple blocks, then this callback function would be invoked for each block.