TCP chat server
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_write
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, _1),
[&, weak = std::weak_ptr(client)]
{
if(auto shared = weak.lock(); shared && clients.erase(shared))
{
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