TCP chat server

227

Since you're already know where things do come from, I'll start to aliasing names to make them shorter.

In this lesson we will review a very simple chat server. The server doesn't support user nicknames, colors, or any other user-associated data — that makes it a bit simpler.

In the previous lesson we discussed all new things in details that you'll find in this server. So, I'll comment this lesson's server very briefly. You'll find a complete soucre code at the end of the lesson. Download it, compile it, look how does it work. Try to understand how everything does work by yourself based on what did you learn so far. After all, you need to learn how to understand the code.

Prerequisites:

#include <boost/asio.hpp>
#include <optional>
#include <queue>
#include <unordered_set>

namespace io = boost::asio;
using tcp = io::ip::tcp;
using error_code = boost::system::error_code;

using message_handler = std::function<void (std::string)>;
using error_handler = std::function<void ()>;

Everything should be obvious so far. The main function looks exactly as the one from the previous server example (except the namespace aliasing):

int main()
{
    io::io_context io_context;
    server srv(io_context, 15001);
    srv.async_accept();
    io_context.run();
    return 0;
}

The session and the server classes are a bit bigger this time, and you already know half of the code, so we'll discuss key points only.

Let's start from the session key points. The start function now accept event handlers.

void session::start(message_handler&& on_message, error_handler&& on_error)
{
    this->on_message = std::move(on_message);
    this->on_error = std::move(on_error);
    async_read();
}

The post function enqueues a message addressed to the client and starts sending the message in case it isn't currently sending the previous message:

void session::post(std::string const& message)
{
    bool idle = outgoing.empty();
    outgoing.push(message);

    if(idle)
    {
        async_write();
    }
}

async_read and async_write completion handlers are now splitted away from their async_ session functions. async_read function reads a data from the remote client into the streambuf. Session's async_read function sends the front message from the message queue to the remote client.

void session::async_read()
{
    io::async_read_until(socket, streambuf, "\n", [self = shared_from_this()] (error_code error, std::size_t bytes_transferred)
    {
        self->on_read(error, bytes_transferred);
    });
}

void session::async_write()
{
    io::async_write(socket, io::buffer(outgoing.front()), [self = shared_from_this()] (error_code error, std::size_t bytes_transferred)
    {
        self->on_write(error, bytes_transferred);
    });
}

Session's read handler formats a message from the client, passes it into the message handler, and starts receiving the next message right away. It also does error handling now:

void session::on_read(error_code error, std::size_t bytes_transferred)
{
    if(!error)
    {
        std::stringstream message;
        message << socket.remote_endpoint(error) << ": " << std::istream(&streambuf).rdbuf();
        streambuf.consume(bytes_transferred);
        on_message(message.str());
        async_read();
    }
    else
    {
        socket.close(error);
        on_error();
    }
}

The write handler pops the front message from the queue, and if the queue is still not empty, then it repeat sending messages right away. It also handle errors:

void session::on_write(error_code error, std::size_t bytes_transferred)
{
    if(!error)
    {
        outgoing.pop();

        if(!outgoing.empty())
        {
            async_write();
        }
    }
    else
    {
        socket.close(error);
        on_error();
    }
}

Session data members:

tcp::socket socket; // Client's socket
io::streambuf streambuf; // Streambuf for incoming data
std::queue<std::string> outgoing; // The queue of outgoing messages
message_handler on_message; // Message handler
error_handler on_error; // Error handler

And some server code snippets. Let's start from the data section:

io::io_context& io_context;
tcp::acceptor acceptor;
std::optional<tcp::socket> socket;
std::unordered_set<session::pointer> clients; // A set of connected clients

post function broadcasts a message to all clients connected. This function is also used as a client's message handler (see below).

void server::post(std::string const& message)
{
    for(auto& client : clients)
    {
        client->post(message);
    }
}

async_accept function greets a newcomer personally and introduces him to the others. It also implements a client's error handler which deletes the session from the clients set and notifies the others about the client's quit:

void async_accept()
{
    socket.emplace(io_context);

    acceptor.async_accept(*socket, [&] (error_code error)
    {
        auto client = std::make_shared<session>(std::move(*socket));
        client->post("Welcome to chat\n\r");
        post("We have a newcomer\n\r");
        clients.insert(client);
        client->start
        (
            std::bind(&server::post, this, std::placeholders::_1),
            [&, client]
            {
                if(clients.erase(client))
                {
                    post("We are one less\n\r");
                }
            }
        );

        async_accept();
    });
}

Now, let's run the server as well as several telnet clients:

./server
telnet localhost 15001

Welcome to chat

After launching the second telnet client, the first one should see:

telnet localhost 15001

Welcome to chat
We have a newcomer

Launch one more client, write something into the chat and press enter:

telnet localhost 15001

Welcome to chat
Hello guys

The others should see something like that:

telnet localhost 15001

Welcome to chat
We have a newcomer
We have a newcomer
127.0.0.1:47235: Hello guys

Download full source code

Share this page:

Learning plan

The first simple asynchronous TCP server
How to handle Boost.Asio errors
There are several new things we should learn before jumping into a bigger example of a server
8. TCP chat server
A bigger example of a server where you'll need to apply everything you've learned so far
Principles you should take into consideration during the development of your applications
How to keep io_context::run running even when there is no work to do
How execute a regular code within io_context::run polling loop