×

Search anything:

Creating UPD Asynchronous Client Server in C++ using Boost.Asio

Binary Tree book by OpenGenus

Open-Source Internship opportunity by OpenGenus for programmers. Apply now.

A UDP (User Datagram Protocol) connection is a lightweight, connectionless communication protocol that operates without establishing a direct connection between sender and receiver. It's commonly used for tasks where speed and efficiency are prioritized over reliability, such as real-time streaming or gaming. Boost.Asio is a C++ library that provides asynchronous I/O and networking functionalities.

It can be utilized to create UDP connections in a straightforward and efficient manner, handling tasks like sending and receiving UDP datagrams, managing timeouts, and handling errors, making it a valuable tool for building networked applications with UDP.

Table of contents:

  1. What is a UDP Connection ?
  2. How Boost.Asio will be used ?
  3. Creating UDP Client
  4. Creating UDP Server
  5. Creating a Async UDP Server
  6. Output Response & Execution
  7. Conclusion

1. What is a UDP Connection ?

User Datagram Protocol (UDP) is a core member of the Internet Protocol Suite, used for transmitting data over a network. Unlike its counterpart, TCP (Transmission Control Protocol), UDP is connectionless, meaning it does not establish a dedicated end-to-end connection before data is exchanged. This fundamental characteristic shapes its primary features and use cases. Here's a detailed look at UDP and its characteristics:

1. Connectionless Protocol

UDP does not perform a handshake to establish a connection before data transmission. Packets (also known as datagrams) are sent without prior arrangement or negotiation, leading to a simpler and faster process.This approach allows for quicker data transmission, as the overhead of establishing and maintaining a connection is eliminated.

2. Unreliable Delivery

There is no mechanism for acknowledging the receipt of data. Senders cannot confirm if their packets reach the intended destination.Lost packets are not automatically retransmitted. This might result in data loss.

3. No Congestion Control

UDP does not adjust its data transfer rate based on network conditions, which can lead to congestion and packet loss, especially in busy networks.

4. Stateless

Each UDP transaction is independent of the previous one. The protocol does not keep track of the connection state, making it suitable for simple query-response protocols like DNS lookups.

Use Cases

Given its characteristics, UDP is well-suited for applications where speed and efficiency are paramount, and where the occasional loss of packets is acceptable or can be managed by the application layer. This includes:

  • Real-time applications (VoIP, online gaming, live broadcasts)
  • DNS queries
  • Streaming media
  • Time-sensitive applications (e.g., some IoT devices)

Trade-offs

Choosing UDP over TCP or other protocols involves trade-offs. While UDP offers speed and efficiency, applications must either tolerate or implement their own mechanisms for dealing with its lack of reliability, ordering, and congestion control. This might involve adding sequence numbers to packets, implementing time-outs for responses, or developing custom error-checking and recovery procedures.

2. How Boost.Asio will be used ?

Just as Boost.Asio provides comprehensive support for TCP/IP networking, it also offers robust functionalities for working with UDP (User Datagram Protocol). Here's a breakdown of the key classes and utilities within the boost::asio::ip::udp namespace, which facilitate the development of UDP-based client and server applications:

  1. boost::asio::ip::udp::socket: This class represents a socket for UDP communication. It is used to send and receive datagrams over UDP. The socket can operate in both synchronous and asynchronous modes, allowing for flexible implementation of network interactions.

  2. boost::asio::ip::udp::endpoint: Similar to its TCP counterpart, this class represents a network endpoint in a UDP context, encapsulating an IP address and a port number. It is used to specify the target for datagram sends or the binding for local reception.

  3. boost::asio::ip::udp::resolver: This class is used for resolving domain names and service names to udp::endpoint objects. It translates human-readable hostnames and service names into the IP addresses and port numbers required for UDP communication.

  4. boost::asio::ip::udp: Beyond being a namespace, boost::asio::ip::udp is also used as a protocol type specifier. When creating sockets or performing operations that require specifying the protocol, this type is used to indicate that UDP should be used.

These components of the boost::asio::ip::udp namespace provide a powerful and flexible toolkit for implementing UDP-based communication. They allow for the construction of both simple and complex networked applications, from basic datagram send/receive tasks to advanced asynchronous networking solutions. By leveraging Boost.Asio's support for asynchronous operations, developers can create efficient and scalable UDP applications, suitable for scenarios where low-latency or high-throughput communication is essential, such as in video streaming, gaming, or real-time data collection systems.

3. Creating a UDP Client

First we will explore the creation of a UDP Client. A Simple UDP client implemented using Boost.Asio in C++. It sends data to a server specified by its IP address and port number, then waits and prints the response received from the server. Here's the step-by-step explanation of the process followed :

  1. Create io_service Object: A boost::asio::io_service object is created, which provides core synchronous and asynchronous I/O functionality. This object is essential for handling I/O operations.

  2. Create a Resolver: A udp::resolver object is instantiated with the io_service object. The resolver's purpose is to resolve the hostname and service (port number) into one or more endpoint objects.

  3. Create a Query: A udp::resolver::query object is created with the UDP protocol version 4 (udp::v4()), the server's IP address , and the server's port number. This query specifies the criteria for the resolution process.

  4. Resolve the Query: The resolver object resolves the query into a list of endpoints. In our code we assume that at least one valid endpoint is returned and directly dereferences the result to obtain the receiver_endpoint.

  5. Create and Open a Socket: A udp::socket object is created with the io_service. The socket is then opened with the UDP protocol version 4. This socket will be used to send data to and receive data from the server.

  6. Send Data to Server: A single byte of data (initialized to 0) is sent to the receiver_endpoint using the socket.send_to() function. This sends a datagram to the server.

  7. Receive Response from Server: The client then prepares to receive a response from the server. A buffer recv_buf is created to hold the incoming data, and the socket.receive_from() function is called to receive data into this buffer. The function also fills in the sender_endpoint with the endpoint from which the data was received. This step blocks execution until data is received.

  8. Output the Response: The received data is written to the standard output using std::cout.write(), with the length of the data (len) specifying how much of the buffer to write.

The code for the same is as follows :

#include <iostream>
#include <boost/array.hpp>
#include <boost/asio.hpp>

using boost::asio::ip::udp;

int main()
{
    int argc = 3;
    char *argv[] = {"client", "127.0.0.1"  , "50000"};

    try
    {
        if (argc != 3)
        {
            std::cerr << "Usage: client <host>" << std::endl;
            return 1;
        }

        boost::asio::io_service io_service;

        udp::resolver resolver(io_service);

        // use an ip::udp::resolver object to find the correct remote endpoint to use based on the host and service names
        // The query is restricted to return only IPv4 endpoints by the ip::udp::v4() argument.
        udp::resolver::query query(udp::v4(), argv[1], argv[2]);

        // The ip::udp::resolver::resolve() function is guaranteed to return at least one endpoint in the list if it does not fail.
        // This means it is safe to dereference the return value directly.
        udp::endpoint receiver_endpoint = *resolver.resolve(query);

        // Since UDP is datagram-oriented, we will not be using a stream socket.
        // Create an ip::udp::socket and initiate contact with the remote endpoint.
        udp::socket socket(io_service);
        socket.open(udp::v4());
        boost::array<char, 1> send_buf = {{0}};
        socket.send_to(boost::asio::buffer(send_buf), receiver_endpoint);

        // Now we need to be ready to accept whatever the server sends back to us.
        // The endpoint on our side that receives the server's response will be initialised by ip::udp::socket::receive_from().

        boost::array<char, 128> recv_buf;
        udp::endpoint sender_endpoint;
        size_t len = socket.receive_from(
            boost::asio::buffer(recv_buf), sender_endpoint
        );
        std::cout.write(recv_buf.data(), len);
        
    }
    catch (std::exception &e)
    {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}

4. Creating a UDP Server

Now before making a Async UDP Server we will create a synchronous UDP Server which will only operate on one client at a time.

We will create a simple UDP daytime server using the Boost.Asio library. The server listens for incoming UDP datagrams on port 50000. Upon receiving a datagram, it sends back a response containing the current server time. Here's a detailed breakdown of the process:

  1. Daytime String Generation: The make_daytime_string function generates a string representing the current time. It uses the C standard library functions time and ctime to get the current time and convert it to a readable string format, respectively.

  2. Setup and Socket Initialization: The program will start by creating a boost::asio::io_service object, which is required to perform I/O operations. A UDP socket is then created and bound to the specified port (50000) on all interfaces. This is done by constructing a udp::socket object with io_service and a udp::endpoint specifying udp::v4() as the protocol and 50000 as the port.

  3. Infinite Loop for Listening: The server enters an infinite loop, where it listens for incoming datagrams. We will declares a boost::array of for receiving data and a udp::endpoint to store the sender's address. The server then calls socket.receive_from(), which blocks until a datagram is received. The received data is stored in recv_buf, and the sender's endpoint is saved in remote_endpoint. If there's an error during the receive operation (except for errors related to the message size), an exception is thrown.

  4. Sending Response: After receiving a datagram, the server generates the current daytime string using make_daytime_string(). It then sends this string back to the sender using socket.send_to(), targeting the remote_endpoint received earlier. Errors during sending are ignored (captured in ignored_error but not acted upon).

This server is a simple example of a stateless protocol implementation using UDP, where the server responds to each request without maintaining any session or connection state. It demonstrates the basics of socket programming with Boost.Asio, including socket creation, receiving and sending datagrams, and error handling.

#include <iostream>
#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <boost/array.hpp>
#include <string>
#include <ctime>

using boost::asio::ip::udp;

std::string make_daytime_string()
{
    using namespace std; // For time_t, time and ctime;
    time_t now = time(0);
    return ctime(&now);
}

int main()
{

    try
    {
        boost::asio::io_service io_service;
        udp::socket socket(io_service, udp::endpoint(udp::v4(), 50000));

        // Wait for a client to initiate contact with us.
        // The remote_endpoint object will be populated by ip::udp::socket::receive_from().

        for (;;)
        {
            boost::array<char, 1> recv_buf;
            udp::endpoint remote_endpoint;
            boost::system::error_code error;
            
            std::cout<<"Listening at : "<<socket.local_endpoint()<<std::endl;
            socket.receive_from(

                boost::asio::buffer(recv_buf),
                remote_endpoint,
                0,
                error
            );

            if (error && error != boost::asio::error::message_size)
            {

                throw boost::system::system_error(error);
            }

            // Determine what we are going to send back to the client.

            std::string message = make_daytime_string();

            // Send the response to the remote_endpoint.

            boost::system::error_code ignored_error;

            socket.send_to(
                boost::asio::buffer(message),
                remote_endpoint, 
                0, 
                ignored_error
            );
        }
    }

    catch (const std::exception &e)
    {
        std::cerr << e.what() << '\n';
    }

    return 0;
}

5. Creating a Async UDP Server

Now that we have created a simple synchornous UDP server we can take one step furthur to make it asynchornous.

Unlike the synchronous example provided earlier, this server handles client requests without blocking, allowing for efficient processing of I/O operations. The server listens on UDP port 50000 and sends back the current date and time when it receives any data from a client. Here's how it works, focusing on its asynchronous nature:

Main Components:

  • udp_server class: Encapsulates the server functionality, including socket initialization, asynchronous receive, and send operations.

  • start_receive() method: Initiates reception of data from clients in an asynchronous manner.

  • handle_receive() method: Callback function called by Boost.Asio when data is received. The function also starts the start_receive again to listen to new client ahead.

  • handle_send() method: Callback function called by Boost.Asio after data has been sent to a client.

Detailed Process:

The detailed process we will follow is as follows :

  1. Initialization: The udp_server class constructor initializes a UDP socket, binding it to port 50000 on all interfaces. It then calls start_receive() to begin listening for incoming packets.

  2. Asynchronous Receive: The start_receive() sets up the socket to asynchronously receive data. It uses socket_.async_receive_from(), specifying a buffer (recv_buffer_), an endpoint to store the sender's address (remote_endpoint_), and a callback function (handle_receive) to be invoked when data arrives or an error occurs.

  3. Handling Incoming Data: When data is received, or an error is detected (except for errors related to message size), handle_receive() is called. If there's no critical error, it creates a shared pointer to a string containing the current date and time, generated by make_daytime_string(). It then initiates an asynchronous send operation using socket_.async_send_to(), passing the message to be sent, the sender's endpoint, and a callback function (handle_send) for post-send operations.

  4. Post-Send Operation: The handle_send() method is a placeholder in this context. It's designed to be executed after the message has been sent, although it doesn't perform any action in this implementation. Its presence is necessary for error handling and for completing the asynchronous operation chain, but here it simply ensures that resources (like the message string) remain valid until the send operation is fully completed.

  5. Looping Back: After handling a received message (and initiating its response), start_receive() is called again to reset the server to listen for the next incoming datagram. This creates a continuous loop, allowing the server to handle multiple successive requests asynchronously.

  6. main Function: Creates an io_service object required by Boost.Asio to perform asynchronous operations. Instantiates the udp_server, passing it the io_service. Calls io_service.run(), which starts the asynchronous operation processing loop. This call will block and only return when all asynchronous operations are complete (which, in this server's case, effectively never happens because it continuously loops to listen for more data).

Asynchronous Nature:

The asynchronous behavior is achieved through the use of non-blocking calls (async_receive_from and async_send_to) and callback functions (handle_receive and handle_send). This design allows the server to efficiently manage resources and handle multiple client requests concurrently without the need for multi-threading or manual polling of socket states. The io_service object coordinates the completion of asynchronous operations and invokes the appropriate handlers when actions are completed or when events occur.

#include <iostream>
#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <boost/array.hpp>
#include <string>
#include <ctime>

using boost::asio::ip::udp;

std::string make_daytime_string()
{
    using namespace std; // For time_t, time and ctime;
    time_t now = time(0);
    return ctime(&now);
}

class udp_server
{

    udp::socket socket_;
    udp::endpoint remote_endpoint_;
    boost::array<char, 1> recv_buffer_;

    // start to receive asynchronously
    void start_receive()
    {
        std::cout<<"Started to receive : "<<socket_.local_endpoint()<<std::endl;
        socket_.async_receive_from(
            boost::asio::buffer(recv_buffer_),
            remote_endpoint_,
            boost::bind(&udp_server::handle_receive, this, boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred));
    }

    void handle_receive(const boost::system::error_code &error, std::size_t /*bytes_trasfered*/)
    {

        // if no error has occurred
        if (!error || error == boost::asio::error::message_size)
        {

            boost::shared_ptr<std::string> message(new std::string(make_daytime_string()));

            socket_.async_send_to(
                boost::asio::buffer(*message),
                remote_endpoint_,
                boost::bind(
                    &udp_server::handle_send, this, message, boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred));

            start_receive();
        }
    }

    void handle_send(boost::shared_ptr<std::string> /*message*/,
                     const boost::system::error_code & /*error*/,
                     std::size_t /*bytes_transferred*/)
    {
    }

public:
    udp_server(boost::asio::io_service &io_service) : socket_(io_service, udp::endpoint(udp::v4(), 50000))
    {
        start_receive();
    }
};

int main()
{

    try
    {
       
        boost::asio::io_service io_service;
        udp_server server(io_service);
        io_service.run();
    }
    catch (const std::exception &e)
    {
        std::cerr << e.what() << '\n';
    }

    return 0;
}

6. Output Response & Execution

To execute the above client server application , first start the server , it will print :

Started to receive : 0.0.0.0:50000

Now our server is ready to respond to the client , if we run the client , the following message is received from the server which is daytime string :

Sun Mar  3 11:07:11 2024

The again the server will start listening for the new client :

Started to receive : 0.0.0.0:50000

7. Conclusion

In conclusion, the User Datagram Protocol (UDP) offers a lightweight and efficient communication protocol, particularly suitable for scenarios where speed and simplicity are prioritized over reliability. Unlike TCP, UDP does not establish a direct connection between sender and receiver and operates in a connectionless manner, making it ideal for real-time applications like online gaming, streaming media, and VoIP.

Boost.Asio, a C++ library, provides comprehensive support for both UDP and TCP networking, offering asynchronous I/O functionalities that simplify the development of networked applications. With Boost.Asio, developers can create UDP connections easily, handling tasks such as sending and receiving datagrams, managing timeouts, and handling errors efficiently.

By leveraging Boost.Asio's asynchronous capabilities, developers can design high-performance networked applications that can handle multiple client requests concurrently without the need for complex threading models. As demonstrated in the provided examples, Boost.Asio enables the creation of both synchronous and asynchronous UDP servers and clients, facilitating the development of a wide range of networked applications.

In summary, the combination of UDP and Boost.Asio provides a powerful toolkit for building efficient and scalable networked applications, allowing developers to meet the demands of modern real-time communication and streaming services while maintaining simplicity and speed.

Key Takeaways (Creating UDP Asynchronous Client-Server in C++ using Boost.Asio)

  • UDP (User Datagram Protocol) offers a lightweight, connectionless communication protocol suitable for real-time applications prioritizing speed and efficiency.
  • Boost.Asio provides robust support for UDP networking in C++, offering asynchronous I/O functionalities for creating efficient networked applications.
  • Creating a UDP client involves initializing an `io_service`, resolving the server's endpoint, opening a socket, sending data, and receiving a response.
  • Developing a synchronous UDP server includes socket initialization, listening for incoming datagrams, handling received data, and sending responses back to clients.
  • Implementing an asynchronous UDP server with Boost.Asio involves creating a class encapsulating server functionality, starting asynchronous receive operations, handling received data, and sending responses asynchronously.
  • Boost.Asio's asynchronous capabilities allow for efficient management of resources and handling of multiple client requests concurrently without the need for complex threading models.
Creating UPD Asynchronous Client Server in C++ using Boost.Asio
Share this