A synchronous UDP client is a part of a distributed application that complies with the following statements:
A typical synchronous UDP client works according to the following algorithm:
This recipe demonstrates how to implement a synchronous UDP client application with Boost.Asio.
The following code sample demonstrates a possible implementation of a synchronous UDP client application with Boost.Asio. It is assumed that the client uses UDP protocol with the underlying IPv4 protocol for communication:
#include <boost/asio.hpp> #include <iostream> using namespace boost; class SyncUDPClient { public: SyncUDPClient() : m_sock(m_ios) { m_sock.open(asio::ip::udp::v4()); } std::string emulateLongComputationOp( unsigned int duration_sec, const std::string& raw_ip_address, unsigned short port_num) { std::string request = "EMULATE_LONG_COMP_OP " + std::to_string(duration_sec) + " "; asio::ip::udp::endpoint ep( asio::ip::address::from_string(raw_ip_address), port_num); sendRequest(ep, request); return receiveResponse(ep); }; private: void sendRequest(const asio::ip::udp::endpoint& ep, const std::string& request) { m_sock.send_to(asio::buffer(request), ep); } std::string receiveResponse(asio::ip::udp::endpoint& ep) { char response[6]; std::size_t bytes_recieved = m_sock.receive_from(asio::buffer(response), ep); m_sock.shutdown(asio::ip::udp::socket::shutdown_both); return std::string(response, bytes_recieved); } private: asio::io_service m_ios; asio::ip::udp::socket m_sock; }; int main() { const std::string server1_raw_ip_address = "127.0.0.1"; const unsigned short server1_port_num = 3333; const std::string server2_raw_ip_address = "192.168.1.10"; const unsigned short server2_port_num = 3334; try { SyncUDPClient client; std::cout << "Sending request to the server #1 ... " << std::endl; std::string response = client.emulateLongComputationOp(10, server1_raw_ip_address, server1_port_num); std::cout << "Response from the server #1 received: " << response << std::endl; std::cout << "Sending request to the server #2... " << std::endl; response = client.emulateLongComputationOp(10, server2_raw_ip_address, server2_port_num); std::cout << "Response from the server #2 received: " << response << std::endl; } catch (system::system_error &e) { std::cout << "Error occured! Error code = " << e.code() << ". Message: " << e.what(); return e.code().value(); } return 0; }
The sample consists of two main components—the SyncUDPClient
class and the application entry point function main()
that uses the SyncUDPClient
class to communicate with two server applications. Let's consider each component separately.
The SyncUDPClient
class is the key component in the sample. It implements the server communication functionality and provides access to it for the user.
The class has two private members as follows:
The socket object m_sock
is instantiated and opened in the class's constructor. Because the client is intended to use IPv4 protocol, we pass the object returned by the asio::ip::udp::v4()
static method to the socket's open()
method to designate the socket to use IPv4 protocol.
Because the SyncUDPClient
class implements communication over UDP protocol, which is a connectionless protocol, a single object of this class can be used to communicate with multiple servers. The interface of the class consists of a single method—emulateLongComputationOp()
. This method can be used to communicate with the server just after the object of the SyncUDPClient
class is instantiated. The following is the prototype of the method:
std::string emulateLongComputationOp( unsigned int duration_sec, const std::string& raw_ip_address, unsigned short port_num)
Besides the duration_sec
argument that represents a request parameter, the method accepts the server IP-address and the protocol port number. This method may be called multiple times to communicate with different servers.
The method begins with preparing a request string according to the application layer protocol and creating an endpoint object designating the target server application. Then, the request string and the endpoint object are passed to the class's private method sendRequest()
, which sends the request message to the specified server. When the request is sent and the sendRequest()
method returns, the receiveResponse()
method is called to receive a response from the server.
When the response is received, the receiveResponse()
method returns the string containing the response. In turn, the emulateLongComputationOp()
method returns the response to its caller. The sendRequest()
method uses the socket object's send_to()
method to send the request message to a particular server. Let's have a look at the declaration of this method:
template <typename ConstBufferSequence> std::size_t send_to(const ConstBufferSequence& buffers, const endpoint_type& destination)
The method accepts a buffer containing the request and an endpoint designating the server to which the content of the buffer should be sent as arguments and blocks until the whole buffer is sent, or an error occurs. Note that, if the method returns without an error, it only means that the request has been sent and does not mean that the request has been received by the server. UDP protocol doesn't guarantee message delivery and it provides no means to check whether the datagram has been successfully received on the server-side or got lost somewhere on its way to the server.
Having sent the request, now we want to receive the response from the server. This is the purpose of the receiveResponse()
method of the SyncUDPClient
class. This method begins with allocating a buffer that will hold the response message. We choose the size of the buffer such that it can fit the largest message that the server may send according to the application layer protocol. This message is an ERROR
string that consists of six ASCII symbols, which is therefore 6 bytes long; hence is the size of our buffer - 6 bytes. Because the buffer is small enough, we allocate it on the stack.
To read the response data arriving from the server, we use the socket object's receive_from()
method. Here is the prototype of the method:
template <typename MutableBufferSequence> std::size_t receive_from(const MutableBufferSequence& buffers, endpoint_type& sender_endpoint)
This method copies a datagram that came from the server designated by the sender_endpoint
object to the buffer specified by the buffers
argument.
There are two things to note about socket object's receive_from()
method. The first thing is that this method is synchronous and it blocks the thread of execution until the datagram arrives from the specified server. If the datagram never arrives (for example, gets lost somewhere on its way to the client), the method will never unblock and the whole application will hang. The second thing is that if the size of the datagram that arrives from the server is larger than the size of the supplied buffer, the method will fail.
After the response is received, the std::string
object is created, initialized with a response string, and returned to the caller—the emulateLongComputationOp()
method. This in turn returns the response to its caller—the main()
function.
The SyncUDPClient
class does not contain error handling-related code. That's is because it uses only those overloads of Boost.Asio functions and objects' methods that throw exceptions in case of failure. It is assumed that the user of the class is responsible for catching and handling the exceptions.
In this function, we use the SyncUDPClient
class in order to communicate with two server applications. Firstly, we obtain the IP-addresses and the port numbers of the target server applications. Then, we instantiate the object of the SyncUDPClient
class and call the object's emulateLongComputationOp()
method twice to synchronously consume the same service from two different servers.
3.15.186.79