[C++] A Boost Asio Server-Client Example

You can find this example here on GitHub.

It's been a while since I started my first server-client project and I thought it's worth an article to provide an example. Let's create a simple project with boost.asio which builds two executables where one represents the server and the other the client.

Server

In the context of this server we'll have the server class which (obviously) represents the server and a session. A session is basically the connection to a client. When a client connects to a server, the server will then create a session. Let's take a look at the server class:

class server
{
public:
    // we need an io_context and a port where we listen to 
    server(boost::asio::io_context& io_context, short port) 
    : m_acceptor(io_context, tcp::endpoint(tcp::v4(), port)) {
        // now we call do_accept() where we wait for clients
        do_accept();
    }
private:
    void do_accept() {
        // this is an async accept which means the lambda function is 
        // executed, when a client connects
        m_acceptor.async_accept([this](boost::system::error_code ec, tcp::socket socket) {
            if (!ec) {
                // let's see where we created our session
                std::cout << "creating session on: " 
                    << socket.remote_endpoint().address().to_string() 
                    << ":" << socket.remote_endpoint().port() << '\n';
                // create a session where we immediately call the run function
                // note: the socket is passed to the lambda here
                std::make_shared<session>(std::move(socket))->run();
            } else {
                std::cout << "error: " << ec.message() << std::endl;
            }
            // since we want multiple clients to connnect, wait for the next one by calling do_accept()
            do_accept();
        });
    }
private: 
    tcp::acceptor m_acceptor;
};

For now, that's enough to have a running server. The io_context which we need to construct the server will be created in our main. An io_context represents the state with all plattform specific calls for our I/O operations. Further information you can find on boosts documentation. And now, create the corresponding session:

// this was created as shared ptr and we need later `this`
// therefore we need to inherit from enable_shared_from_this
class session : public std::enable_shared_from_this<session>
{
public:
    // our sesseion holds the socket
    session(tcp::socket socket)  
    : m_socket(std::move(socket)) { }
    
    // and run was already called in our server, where we just wait for requests
    void run() {
        wait_for_request();
    }
private:
    void wait_for_request() {
        // since we capture `this` in the callback, we need to call shared_from_this()
        auto self(shared_from_this());
        // and now call the lambda once data arrives
        // we read a string until the null termination character
        boost::asio::async_read_until(m_socket, m_buffer, "\0", 
        [this, self](boost::system::error_code ec, std::size_t /*length*/)
        {
            // if there was no error, everything went well and for this demo
            // we print the data to stdout and wait for the next request
            if (!ec)  {
                std::string data{
                    std::istreambuf_iterator<char>(&m_buffer), 
                    std::istreambuf_iterator<char>() 
                };
                // we just print the data, you can here call other api's 
                // or whatever the server needs to do with the received data
                std::cout << data << std::endl;
                wait_for_request();
            } else {
                std::cout << "error: " << ec << std::endl;;
            }
        });
    }
private:
    tcp::socket m_socket;
    boost::asio::streambuf m_buffer;
};

Some notes to the session:

auto self(shared_from_this()):
This might be confusing in the beginning, but we're passing a callback to async_read_until. This callback is at any point in time (when data arrives) executed and basically we aren't in the scope of wait_for_request any more. We need a valid reference to this (this post can help for further information).

Databuffer m_buffer:
Also we need to hold m_buffer as member, because we need to store the data somewhere. If you would create a local variable in wait_for_request it wouldn't be there when the lambda is called. When we keep the buffer as member we still have it.

 

And now we can start our server in the main. For this example I shutdown the server with ctrl+c. Consider a proper shutdown mechanism on real applications.

int main(int argc, char* argv[])
{
    // here we create the io_context
    boost::asio::io_context io_context;
    // we'll just use an arbitrary port here 
    server s(io_context, 25000);
    // and we run until our server is alive
    io_context.run();

    return 0;
}
 

Client

The client now is fairly easy, compared to the server. We'll use a short client which connects, sends data and disconnects in a main:

int main(int argc, char* argv[])
{
    using boost::asio::ip::tcp;
    boost::asio::io_context io_context;
    
    // we need a socket and a resolver
    tcp::socket socket(io_context);
    tcp::resolver resolver(io_context);
    
    // now we can use connect(..)
    boost::asio::connect(socket, resolver.resolve("127.0.0.1", "25000"));
    
    // and use write(..) to send some data which is here just a string
    std::string data{"some client data ..."};
    auto result = boost::asio::write(socket, boost::asio::buffer(data));
    
    // the result represents the size of the sent data
    std::cout << "data sent: " << data.length() << '/' << result << std::endl;

    // and close the connection now
    boost::system::error_code ec;
    socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
    socket.close();

    return 0;
}
 

And now start the server and send some data from the client:

On the left side we have the server which received the data from the client on the right. As far as I know, the displayed error means that the client closed the socket.

 

Conclusion

You can find this example here on GitHub.

And that's it, a pretty simple server-client example to get started with boost asio. From here on you can include protobuffer for example to define data, which you want to send between server and clients and you can implement actual logic of what to do with the data on the server side.

But for now, thats it here.

I hope that helps.

Best Thomas

Previous
Previous

Windows Docker Container for MSVC Builds with Conan in Gitlab

Next
Next

[C++] Static, Dynamic Polymorphism, CRTP and C++20’s Concepts