[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:
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