Table of Contents
If you are new to boost::asio
, you can find a good introduction on boost.org.
This post describes an exercize built upon the asynchronous daytime server presented in the boost tutorial.
List of clients
First thing we are going to implement is a way to ask the server the list of clients currently connected. We need to keep track of incoming connections, therefore we add the current field to the server:
std::vector<std::unique_ptr<Connection>> Server::connections;
Since we use references to connection objects in other parts of the program, connections should not move from their position in memory. New connections may come in while other connections are still being handled. If the vector resizes, its objects might move. Hence why we allocate connections on the heap and use smart pointers as elements of the vector.
Once we have a field to store incoming connections, Server::accept()
can be improved. We do not need to create new connections all the time as we can recycle old connections that were closed.
std::unique_ptr<Connection>* new_con = nullptr;
for (auto& old_con : connections) {
if (!old_con->get_socket().is_open()) {
// Recycle old connection
new_con = &old_con;
break;
}
}
if (!new_con) {
// No old connections found, so we create a new one
new_con = &connections.emplace_back(std::make_unique<Connection>(ctx));
}
What happens if a consistent amount of clients try to connect to the server at the same time?
Communication protocol
When either the server or the client wants to wait for incoming packets, they use the following asynchronous function:
asio::async_read(
socket,
asio::buffer(&message, sizeof(message)),
std::bind(&Connection::handle_read, this, std::placeholders::_1)
);
This function will call handle_read
only when exactly sizeof(message)
bytes will be received. Data received will be stored into message
, an instance of Message
, which is defined like this:
enum class Command : uint16_t {
NOP, HI, LIST, STR, // and more...
};
struct Message {
Command command;
uint16_t version;
std::array<char, 28> data;
};
Here are a few examples of messages that can be created using this design:
This is a command message. It is used by a client to ask the server the list of all connected clients.
LIST (2 bytes) VERSION (2 bytes) - (28 bytes) This command sends a human readable message.
STR (2 bytes) VERSION (2 bytes) STR (28 bytes)
Server
When a new connection comes in, the server introduces itself with a HI
message. This message contains the name and the version of the server:
HI (2 bytes) | VERSION (2 bytes) | NAME (28 bytes) |
Then it waits for incoming packets via async_read
, previously mentioned. By reading data directly into message
, the handle_read
callback becomes straightforward:
void handle_read(const boost::system::error_code& error) {
// Error handling [...]
switch (message.command) {
case Command::NOP: break; // nothing to do
case Command::LIST: {
// @todo Send a message with the list of all clients
}
} // switch
// [...]
}
Client
The client should run two different threads:
- A network thread to handle incoming messages from the server.
- A main thread to read user input, update its logic, etc..
For this purpose, before the main loop, we spawn a thread to run the IO context.
auto con = Connection(io, endpoints);
auto thread = std::thread([&io] () { io.run(); });
while (true) {
// main loop
}