Simple Boost TCP Server, example from the book "C++ Crash Course" - c++

I'm trying to understand the thing with std::enable_shared_from_this in case of TCP connections and I see it like when first connection is accepted in the serve func, object of the class Session is created and later invocations just create shared_ptr to the same object isn't it? If I get it well, I'm not sure is it completely correct to move everytime socket in serve? The below example is like original one from the book besides connections int I've added:
using namespace boost::asio;
int connections{};
struct Session : std::enable_shared_from_this<Session> {
explicit Session(ip::tcp::socket socket) : socket{ std::move(socket) } {}
void read() {
async_read_until(socket, dynamic_buffer(message), '\n',
[self=shared_from_this()] (boost::system::error_code ec,
std::size_t length) {
if(ec || self->message == "\n") {
std::cout<<"Ended connection as endline was sent\n" ;
return;
}
boost::algorithm::to_upper(self->message);
self->write();
});
}
void write() {
async_write(socket, buffer(message),
[self=shared_from_this()] (boost::system::error_code ec,
std::size_t length) {
if(ec) return;
self->message.clear();
self->read();
});
}
private:
ip::tcp::socket socket;
std::string message;
};
void serve(ip::tcp::acceptor& acceptor) {
acceptor.async_accept([&acceptor](boost::system::error_code ec,
ip::tcp::socket socket) {
serve(acceptor);
if (ec) return;
auto session = std::make_shared<Session>(std::move(socket));
std::cout<<"Connection established no "<<++connections<<"\n";
session->read();
});
}
int main(){
try{
io_context io_context;
ip::tcp::acceptor acceptor{ io_context,
ip::tcp::endpoint(ip::tcp::v4(), 1895)};
serve(acceptor);
io_context.run();
} catch (std::exception& e) {
std::cerr << e.what() << std::endl;
}
}

Socket is moved at each invocation of serve because it is a fresh socket for a newly established connection. Note that it is passed by value and unless moved to some long-living object (session in this case) it will be immediately destroyed after going out of scope, ending the connection.
"object of the class Session is created and later invocations just create shared_ptr to the same object isn't it" - nope, each make_shared invocation creates a new session object - one per connection. shared_from_this spawns pointer to the current object.

Related

Why in asio's example the tcp acceptor pattern uses shared_pointer model wrapping heap socket, while udp use stack socket?

Source code:
https://think-async.com/Asio/asio-1.18.0/doc/asio/tutorial/tutdaytime7/src.html
tcp_server shows an intention to use socket on the heap, wrapped by a type called tcp_connection.
class tcp_server
{
private:
void start_accept()
{
tcp_connection::pointer new_connection =
tcp_connection::create(io_context_);
acceptor_.async_accept(new_connection->socket(),
boost::bind(&tcp_server::handle_accept, this, new_connection,
asio::placeholders::error));
}
void handle_accept(tcp_connection::pointer new_connection,
const asio::error_code& error)
{
if (!error)
{
new_connection->start();
}
start_accept();
}
...
socket heap objects are managed by enable_shared_from_this aka shared_ptr
class tcp_connection
: public boost::enable_shared_from_this<tcp_connection>
{
public:
typedef boost::shared_ptr<tcp_connection> pointer;
static pointer create(asio::io_context& io_context)
{
return pointer(new tcp_connection(io_context));
}
tcp::socket& socket()
{
return socket_;
}
void start()
{
message_ = make_daytime_string();
asio::async_write(socket_, asio::buffer(message_),
boost::bind(&tcp_connection::handle_write, shared_from_this()));
}
...
While udp server just use member socket.
class udp_server
{
public:
udp_server(asio::io_context& io_context)
: socket_(io_context, udp::endpoint(udp::v4(), 13))
{
start_receive();
}
private:
void start_receive()
{
socket_.async_receive_from(
asio::buffer(recv_buffer_), remote_endpoint_,
boost::bind(&udp_server::handle_receive, this,
asio::placeholders::error));
}
void handle_receive(const asio::error_code& error)
{
if (!error)
{
boost::shared_ptr<std::string> message(
new std::string(make_daytime_string()));
socket_.async_send_to(asio::buffer(*message), remote_endpoint_,
boost::bind(&udp_server::handle_send, this, message));
start_receive();
}
}
void handle_send(boost::shared_ptr<std::string> /*message*/)
{
}
udp::socket socket_;
udp::endpoint remote_endpoint_;
boost::array<char, 1> recv_buffer_;
};
My question is why tcp acceptor and udp socket examples choose different approaches?
The shared pointer is there to manage the lifetime of the connection.
In the case of TCP it is more common to have multiple connections which leads to a situation where you will likely have multiple connections instances with unrelated lifetimes.
UDP is connection-less and many times is used for one-shot messages.
In fact you can come up with scenarios where you'd use shared pointers with UDP (e.g. with "logical connections", such as streaming audio over UDP).
Also, conversely you CAN solve the lifetime puzzle differently (regardless of TCP/UDP). For example here I used a std::list (for reference stability) judicious single-threaded access to it: How to pass a boost asio tcp socket to a thread for sending heartbeat to client or server ¹
¹ I previously compared that to a shared_ptr approach in this answer: How to eliminate crashes when destroying boost::asio entities on fly?, which might interest you as well
Only to show different approaches ...
So considering the context, you may use one or another solution.

Boost 1.70 io_service deprecation

I'm trying to migrate some old code from using io_service to io_context for the basic tcp acceptor, but am running into issues when switching get_io_service() to get_executor().context() results in the following error:
cannot convert ‘boost::asio::execution_context’ to ‘boost::asio::io_context&’
This is the listener:
ImageServerListener::ImageServerListener(boost::asio::io_context& io)
{
_acceptor = new boost::asio::ip::tcp::acceptor(io, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), sConfig.net.imageServerPort));
StartAccept();
}
ImageServerListener::~ImageServerListener()
{
delete _acceptor;
}
void ImageServerListener::StartAccept()
{
std::shared_ptr<ImageServerConnection> connection = ImageServerConnection::create(_acceptor->get_executor().context());
_acceptor->async_accept(connection->socket(), std::bind(&ImageServerListener::HandleAccept, this, connection));
}
void ImageServerListener::HandleAccept(std::shared_ptr<ImageServerConnection> connection)
{
connection->Process();
StartAccept();
}
What would have to be changed in order to return an io_context instead of an execution_context?
You will want to focus on executors rather than contexts.
Passing around executors is cheap, they are copyable, as opposed to contexts.
Also, it abstracts away (polymorphism) the type of execution context that the executor is attached to, so you don't need to bother.
However, the static type of the executor is not fixed. This means that the typical way to accept one is by template argument:
struct MyThing {
template <typename Executor>
explicit MyThing(Executor ex)
: m_socket(ex)
{ }
void do_stuff(std::string caption) {
post(m_socket.get_executor(),
[=] { std::cout << ("Doing stuff " + caption + "\n") << std::flush; });
}
// ...
private:
tcp::socket m_socket;
};
Now you employ it in many ways without changes:
Live On Coliru
int main() {
boost::asio::thread_pool pool;
MyThing a(pool.get_executor());
MyThing b(make_strand(pool));
a.do_stuff("Pool a");
b.do_stuff("Pool b");
boost::asio::io_context ioc;
MyThing c(ioc.get_executor());
MyThing d(make_strand(ioc));
c.do_stuff("IO c");
d.do_stuff("IO d");
pool.join();
ioc.run();
}
Which will print something like
Doing stuff Pool a
Doing stuff Pool b
Doing stuff IO c
Doing stuff IO d
Type Erasure
As you have probably surmised, there's type erasure inside m_socket that stores the executor. If you want to do the same, you can use
boost::asio::any_io_executor ex;
ex = m_socket.get_executor();

How to make async_write_some() data argument in the buffer dynamic with C++ boost::asio?

I've been making a simple server using boost::asio. I've used Boost's docs to create basics and ran into a problem:
I want a client to be able to send the server a command, so I want the server to be able to respond to that. I use async_write_some() and async_read_some(), as shown in the docs.
I use m_Data member to store whatever a client sent to the server, but I don't know how to make that async_write_some() send a certain thing.
Connection.hpp
using namespace boost::asio;
class Connection : public boost::enable_shared_from_this<Connection> {
private:
ip::tcp::socket m_Socket;
std::string m_Msg;
uint16_t m_MaxLen;
char* m_Data;
public:
Connection(boost::asio::io_service& _ioSrvc);
static boost::shared_ptr<Connection> Create(boost::asio::io_service& _ioSrvc);
ip::tcp::socket& Socket();
void Start();
void HandleRead(const boost::system::error_code& _err, size_t _bytes);
void HandleWrite(const boost::system::error_code& _err, size_t _bytes);
};
Connection.cpp
Connection::Connection(boost::asio::io_service& _ioSrvc)
: m_Socket(_ioSrvc), m_Msg("placeholder_message"), m_MaxLen(1024), m_Data(new char[m_MaxLen]) { }
boost::shared_ptr<Connection> Connection::Create(boost::asio::io_service& _ioSrvc) {
return boost::shared_ptr<Connection>(new Connection(_ioSrvc));
}
ip::tcp::socket& Connection::Socket() {
return m_Socket;
}
void Connection::Start() {
m_Socket.async_read_some(boost::asio::buffer(m_Data, m_MaxLen),
boost::bind(&Connection::HandleRead, shared_from_this(), boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred));
m_Socket.async_write_some(boost::asio::buffer(m_Msg, m_MaxLen),
boost::bind(&Connection::HandleWrite, shared_from_this(), boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred));
}
void Connection::HandleRead(const boost::system::error_code& _err, size_t _bytes) {
if (!_err) {
std::cout << m_Data << std::endl;
}
else {
std::cerr << "[ERROR]: " << _err.message() << std::endl;
m_Socket.close();
}
}
void Connection::HandleWrite(const boost::system::error_code& _err, size_t _bytes) {
if (!_err) {
std::cout << "!" << std::endl;
}
else {
std::cerr << "[ERROR]: " << _err.message() << std::endl;
m_Socket.close();
}
}
I know it takes a buffer, which takes a reference to a string, so I thought that changing the original string, which is m_Msg member, would result in server being able to dynamically change the response to whatever I wanted, but it didn't work, it looked like the buffer took a copy instead of a reference, though I'm sure I saw that std::string& arg in it. I tried to change the m_Msg member in HandleRead() method, using outside method and many stuff but nothing worked for me, the client would always end up receiving "placeholder_message", which m_Msg is set to in the constructor.
So basically, I want to send various data based on the received data and I don't know how to do that and I'm asking for help.
Thank you in advance!

C++ ASIO: Asynchronous sockets and threading

My application is based on the asio chat example and consists of a client and a server:
- Client: Connect to the server, receive requests and respond to it
- Server: Has a QT GUI (main thread) and a network service (separate thread) listening for connections, sending requests to particular clients and interprets the response from/in the GUI
I want to achieve this in an asynchronous way to avoid a seperate thread for each client connection.
In my QT window, I have one io_service instance and one instance of my network service:
io_service_ = new asio::io_service();
asio::ip::tcp::endpoint endpoint(asio::ip::tcp::v4(), "1234");
service_ = new Service(*io_service_, endpoint, this);
asio::io_service* ioServicePointer = io_service_;
t = std::thread{ [ioServicePointer](){ ioServicePointer->run(); } };
I want to be able to send data to one client, like this:
service_->send_message(selectedClient.id, msg);
And I am receiving and handling the responses via the observer pattern (the window implements the IStreamListener interface)
Service.cpp:
#include "Service.h"
#include "Stream.h"
void Service::runAcceptor()
{
acceptor_.async_accept(socket_,
[this](asio::error_code ec)
{
if (!ec)
{
std::make_shared<Stream>(std::move(socket_), &streams_)->start();
}
runAcceptor();
});
}
void Service::send_message(std::string streamID, chat_message& msg)
{
io_service_.post(
[this, msg, streamID]()
{
auto stream = streams_.getStreamByID(streamID);
stream->deliver(msg);
});
}
Stream.cpp:
#include "Stream.h"
#include <iostream>
#include "../chat_message.h"
Stream::Stream(asio::ip::tcp::socket socket, StreamCollection* streams)
: socket_(std::move(socket))
{
streams_ = streams; // keep a reference to the streamCollection
// retrieve endpoint ip
asio::ip::tcp::endpoint remote_ep = socket_.remote_endpoint();
asio::ip::address remote_ad = remote_ep.address();
this->ip_ = remote_ad.to_string();
}
void Stream::start()
{
streams_->join(shared_from_this());
readHeader();
}
void Stream::deliver(const chat_message& msg)
{
bool write_in_progress = !write_msgs_.empty();
write_msgs_.push_back(msg);
if (!write_in_progress)
{
write();
}
}
std::string Stream::getName()
{
return name_;
}
std::string Stream::getIP()
{
return ip_;
}
void Stream::RegisterListener(IStreamListener *l)
{
m_listeners.insert(l);
}
void Stream::UnregisterListener(IStreamListener *l)
{
std::set<IStreamListener *>::const_iterator iter = m_listeners.find(l);
if (iter != m_listeners.end())
{
m_listeners.erase(iter);
}
else {
std::cerr << "Could not unregister the specified listener object as it is not registered." << std::endl;
}
}
void Stream::readHeader()
{
auto self(shared_from_this());
asio::async_read(socket_,
asio::buffer(read_msg_.data(), chat_message::header_length),
[this, self](asio::error_code ec, std::size_t /*length*/)
{
if (!ec && read_msg_.decode_header())
{
readBody();
}
else if (ec == asio::error::eof || ec == asio::error::connection_reset)
{
std::for_each(m_listeners.begin(), m_listeners.end(), [&](IStreamListener *l) {l->onStreamDisconnecting(this->id()); });
streams_->die(shared_from_this());
}
else
{
std::cerr << "Exception: " << ec.message();
}
});
}
void Stream::readBody()
{
auto self(shared_from_this());
asio::async_read(socket_,
asio::buffer(read_msg_.body(), read_msg_.body_length()),
[this, self](asio::error_code ec, std::size_t /*length*/)
{
if (!ec)
{
// notify the listener (GUI) that a response has arrived and pass a reference to it
auto msg = std::make_shared<chat_message>(std::move(read_msg_));
std::for_each(m_listeners.begin(), m_listeners.end(), [&](IStreamListener *l) {l->onMessageReceived(msg); });
readHeader();
}
else
{
streams_->die(shared_from_this());
}
});
}
void Stream::write()
{
auto self(shared_from_this());
asio::async_write(socket_,
asio::buffer(write_msgs_.front().data(),
write_msgs_.front().length()),
[this, self](asio::error_code ec, std::size_t /*length*/)
{
if (!ec)
{
write_msgs_.pop_front();
if (!write_msgs_.empty())
{
write();
}
}
else
{
streams_->die(shared_from_this());
}
});
}
Interfaces
class IStream
{
public:
/// Unique stream identifier
typedef void* TId;
virtual TId id() const
{
return (TId)(this);
}
virtual ~IStream() {}
virtual void deliver(const chat_message& msg) = 0;
virtual std::string getName() = 0;
virtual std::string getIP() = 0;
/// observer pattern
virtual void RegisterListener(IStreamListener *l) = 0;
virtual void UnregisterListener(IStreamListener *l) = 0;
};
class IStreamListener
{
public:
virtual void onStreamDisconnecting(IStream::TId streamId) = 0;
virtual void onMessageReceived(std::shared_ptr<chat_message> msg) = 0;
};
/*
streamCollection / service delegates
*/
class IStreamCollectionListener
{
public:
virtual void onStreamDied(IStream::TId streamId) = 0;
virtual void onStreamCreated(std::shared_ptr<IStream> stream) = 0;
};
StreamCollection is basically a set of IStreams:
class StreamCollection
{
public:
void join(stream_ptr stream)
{
streams_.insert(stream);
std::for_each(m_listeners.begin(), m_listeners.end(), [&](IStreamCollectionListener *l) {l->onStreamCreated(stream); });
}
// more events and observer pattern inplementation
First of all: The code works as intended so far.
My question:
Is this the way ASIO is supposed to be used for asynchronous programming? I'm especially unsure about the Service::send_message method and the use of io_service.post. What is it's purpose in my case? It did work too when I just called async_write, without wrapping it in the io_service.post call.
Am I running into problems with this approach?
Asio is designed to be a tookit rather than a framework. As such, there are various ways to successfully use it. Separating the GUI and network threads, and using asynchronous I/O for scalability can be a good idea.
Delegating work to the io_service within a public API, such as Service::send_message(), has the following consequences:
decouples the caller's thread from the thread(s) servicing the io_service. For example, if Stream::write() performs a time consuming cryptographic function, the caller thread (GUI) would not be impacted.
it provides thread-safety. The io_service is thread-safe; however socket is not thread-safe. Additionally, other objects may not be thread safe, such as write_msgs_. Asio guarantees that handlers will only be invoked from within threads running the io_servce. Consequently, if only one thread is running the io_service, then there is no possibility for concurrency and both socket_ and write_msgs_ will be accessed in a thread-safe manner. Asio refers to this as an implicit strand. If more than one thread is processing the io_service, then one may need to use an explicit strand to provide thread safety. See this answer for more details on strands.
Additional Asio considerations:
Observers are invoked within handlers, and handlers are running within the network thread. If any observer takes a long time to complete, such as having to synchronize with various shared objects touched by the GUI thread, then it could create poor responsiveness across other operations. Consider using a queue to broker events between the observer and subject components. For instance, one could use another io_service as a queue, that is being ran by its own thread, and post into it:
auto msg = std::make_shared<chat_message>(std::move(read_msg_));
for (auto l: m_listeners)
dispatch_io_service.post([=](){ l->onMessageReceived(msg); });
Verify that the container type for write_msgs_ does not invalidate iterators, pointers and references to existing elements on push_back() and other elements for pop_front(). For instance, using std::list or std::dequeue would be safe, but a std::vector may invalidate references to existing elements on push_back.
StreamCollection::die() may be called multiple times for a single Stream. This function should either be idempotent or handle the side effects appropriately.
On failure for a given Stream, its listeners are informed of a disconnect only in one path: failing to read a header with an error of asio::error::eof or asio::error::connection_reset. Other paths do not invoke IStreamListener.onStreamDisconnecting():
the header is read, but decoding failed. In this particular case, the entire read chain will stop without informing other components. The only indication that a problem has occurred is a print statement to std::cerr.
when there is a failure reading the body.

boost::asio::async_read 100% CPU usage on simple example

In boost::asio standard examples after async_accept() the socket object is moving to the session object (which handles all async_read() calls) by initializing it as following:
std::make_shared<session>(std::move(socket_))->start();
And when constructing a session it's moving again (isn't it reduntantly?):
session(tcp::socket socket)
: socket_(std::move(socket))
Then reading from a client is done as following:
boost::asio::async_read(socket_, ...
And all goes well. But when I trying to make async_read() not from the session object but directly from the async_accept() and use it's socket object, CPU is raising to 100% immediately after client connects. Why?
#include <boost/asio.hpp>
using boost::asio::ip::tcp;
class Server
{
public:
Server(boost::asio::io_service& io_service,
const tcp::endpoint& endpoint)
: acceptor_(io_service, endpoint),
socket_(io_service)
{
do_accept();
}
private:
void do_accept()
{
acceptor_.async_accept(socket_,
[this](boost::system::error_code ec)
{
if (!ec) {
char* buf = new char[5];
boost::asio::async_read(socket_,
boost::asio::buffer(buf, 5),
[this, buf](boost::system::error_code ec, std::size_t)
{
if (!ec) {
std::cout.write(buf, 5);
std::cout << std::endl;
}
delete[] buf;
});
}
do_accept();
});
}
tcp::acceptor acceptor_;
tcp::socket socket_;
};
int main(int argc, char* argv[])
{
int port = 22222;
boost::asio::io_service io_service;
tcp::endpoint endpoint(tcp::v4(), port);
new Server(io_service, endpoint);
io_service.run();
}
Boost 1.49
EDIT
Thanks for the answers! I ended up by moving socket_ before using it:
tcp::socket *socket = new tcp::socket(std::move(socket_));
Also the same problem is discussed at Repeated std::move on an boost::asio socket object in C++11
If the peer socket passed to basic_socket_acceptor::async_accept() is not open, then it will be opened during the async_accept() operation. Otherwise, if the peer is already open, then handler will be posted into the io_service for invocation with an error code of boost::asio::error::already_open. Hence, the posted code causes a tight asynchronous call chain to form:
The async_accept() operation is invoked the first time, causing socket_ to be opened.
The async_accept() handler invokes do_accept(), initiating an async_accept() operation.
socket_ is already open, causing the async_accept() operation to post its handler into the io_service with an error of boost::asio::error::already_open.
The asynchronous call chain starts back at step 2.
This behavior is not observed in the official examples because the socket's move operator causes the the moved-from object to be in the same state as if it was constructed using basic_stream_socket(io_service&) constructor. Thus, the moved-from object is in a closed state, and ready for accepting.
You're using the single socket_ in all places, so when a connection is accepted, your handler calls do_accept() again which is using the same socket_, then it's accepted again and again...
You probably need to always use a new socket like below:
void do_accept()
{
boost::shared_ptr<tcp::socket> psocket(new tcp::socket(io_service));
acceptor_.async_accept(*psocket, boost::bind(&Server::handleAccept, this, psocket, _1));
}
void handleAccept(boost::shared_ptr<tcp::socket> psocket, const boost::system::error_code& ec)
{
if (!ec) {
char* buf = new char[5];
boost::asio::async_read(
*psocket,
boost::asio::buffer(buf, 5),
[this, buf](boost::system::error_code ec, std::size_t)
{
if (!ec) {
std::cout.write(buf, 5);
std::cout << std::endl;
}
delete[] buf;
});
}
do_accept();
}