Building a SignalR C++ client using Visual Studio 2013, I am starting with the working sample code from NuGet Package Microsoft.AspNet.SignalR.Client.Cpp.v120.WinDesktop, source here
Reviewing the library source it seems to me the event handling processes are based on the Concurrency Runtime (pplx::task) which relies on C++11 features
void chat(const utility::string_t& name)
{
signalr::hub_connection connection{ U("https://testsite") };
auto proxy = connection.create_hub_proxy(U("ChatHub"));
proxy.on(U("broadcastMessage"), [](const web::json::value& m)
{
ucout << std::endl << m.at(0).as_string() << U(" wrote:") << m.at(1).as_string() << std::endl << U("Enter your message: ");
});
connection.start()
.then([proxy, name]()
{
for (;;)
{
utility::string_t message;
std::getline(ucin, message);
if (message == U(":q"))
{
break;
}
send_message(proxy, name, message);
}
})
.then([&connection]() // fine to capture by reference - we are blocking so it is guaranteed to be valid
{
return connection.stop();
})
.then([](pplx::task<void> stop_task)
{
try
{
stop_task.get();
ucout << U("connection stopped successfully") << std::endl;
}
catch (const std::exception &e)
{
ucout << U("exception when starting or stopping connection: ") << e.what() << std::endl;
}
}).get();
}
I want to eliminate the "user input" component; and instead quit loop when a particular "broadcastMessage" has been received.
If I replace the for loop with a sleep statement, the broadcastMessage event stops firing.
If I use the for loop without the getline, set bComplete to true when done, it works the way I want but causes high CPU usage (obviously)
for (;;)
{
if (bComplete) break;
}
Ideally I want connection to start, and then just wait until the broadcastMessage events signals to close the connection.
In addition the "chat" function shouldn't return until connection has closed.
I can see in your answer that you've already discovered Windows event objects; however, if you were looking for a C++11 platform-independent solution, consider std::condition_variable!
unsigned int accountAmount;
std::mutex mx;
std::condition_variable cv;
void depositMoney()
{
// go to the bank etc...
// wait in line...
{
std::unique_lock<std::mutex> lock(mx);
std::cout << "Depositing money" << std::endl;
accountAmount += 5000;
}
// Notify others we're finished
cv.notify_all();
}
void withdrawMoney()
{
std::unique_lock<std::mutex> lock(mx);
// Wait until we know the money is there
cv.wait(lock);
std::cout << "Withdrawing money" << std::endl;
accountAmount -= 2000;
}
int main()
{
accountAmount = 0;
std::thread deposit(&depositMoney);
std::thread withdraw(&withdrawMoney);
deposit.join();
withdraw.join();
std::cout << "All transactions processed. Final amount: " << accountAmount << std::endl;
return 0;
}
In this example we make two threads: one to deposit money into the account and one to withdraw money. Because it's possible for the thread to withdraw the money to run first, especially because there's more processing involved with depositMoney(), we need to wait until we know the money is there. We lock our thread before accessing the money, and then tell the condition_variable what we are waiting for. The condition_variable will unlock the thread, and once the money has been deposited and notify_all() is called we'll be re-awoken to finish processing our logic.
Note that it's possible to do the exact same using the Windows event objects. Instead of std::condition_variable::wait() and std::condition_variable::notify_all() you'd use SetEvent() and WaitForSingleObject(). This is platform-independent though.
I got this working using WinAPI WaitForSingleObject:
HANDLE hEvent;
void chat(const utility::string_t& name)
{
signalr::hub_connection connection{ U("https://testsite") };
auto proxy = connection.create_hub_proxy(U("ChatHub"));
proxy.on(U("broadcastMessage"), [](const web::json::value& m)
{
ucout << std::endl << m.at(0).as_string() << U(" wrote:") << m.at(1).as_string() << std::endl;
if (m.at(1).as_string() == L"quit")
{
SetEvent(hEvent);
}
});
hEvent = CreateEvent(0, TRUE, FALSE, 0);
connection.start()
.then([proxy, name]()
{
WaitForSingleObject(hEvent, INFINITE);
})
.then([&connection]() // fine to capture by reference - we are blocking so it is guaranteed to be valid
{
return connection.stop();
})
.then([](pplx::task<void> stop_task)
{
try
{
stop_task.get();
ucout << U("connection stopped successfully") << std::endl;
}
catch (const std::exception &e)
{
ucout << U("exception when starting or stopping connection: ") << e.what() << std::endl;
}`enter code here`
}).get();
}
Related
I have tried to create a server with asio, when i try to integrate a timer behind the event handler from client.
asio::io_context m_asioContext;
std::thread m_threadContext;
void print()
{
std::cout << "Hello, world!" << std::endl;
SendTimer();
}
void SendTimer()
{
asio::steady_timer timer(m_asioContext, asio::chrono::seconds(2));
timer.async_wait(boost::bind(&server_interface::print, this));
}
bool Start()
{
try
{
// Issue a task to the asio context - This is important
// as it will prime the context with "work", and stop it
// from exiting immediately. Since this is a server, we
// want it primed ready to handle clients trying to
// connect.
WaitForClientConnection();
std::cout << "[SERVER] Started!azazaz\n";
// Launch the asio context in its own thread
m_threadContext = std::thread([this]() { m_asioContext.run(); });
}
catch (std::exception& e)
{
// Something prohibited the server from listening
std::cerr << "[SERVER] Exception: " << e.what() << "\n";
return false;
}
std::cout << "[SERVER] Started!\n";
return true;
}
void Update(size_t nMaxMessages = -1, bool bWait = false)
{
if (bWait) m_qMessagesIn.wait();
// Process as many messages as you can up to the value
// specified
size_t nMessageCount = 0;
while (nMessageCount < nMaxMessages && !m_qMessagesIn.empty())
{
// Grab the front message
auto msg = m_qMessagesIn.pop_front();
// Pass to message handler
OnMessage(msg.remote, msg.msg);
nMessageCount++;
}
Update(nMaxMessages, bWait);
}
Server call
CustomServer server(60000);
server.Start();
asio::io_context io;
server.Update(-1, true);
It'seem that the timer could not run correctly. Just like the infinitive loop. I really newbie with asio. So I wonder how we could keep multi event with only a thread.
Thanks for your answer.
I'm new to C++ but so far most of the asio stuff has made sense. I am however stuggling to get my UDPServer working.
My question is possibly similar to: Trying to write UDP server class, io_context doesn't block
I think my UDPServer stops before work can be given to its io_context. However, I am issuing work to the context before calling io_context.run() so I don't understand why.
Of course, I am not entirely sure if I am even on the right track with the above statement and would appreciate some guidance. Here is my class:
template<typename message_T>
class UDPServer
{
public:
UDPServer(uint16_t port)
: m_socket(m_asioContext, asio::ip::udp::endpoint(asio::ip::udp::v4(), port))
{
m_port = port;
}
virtual ~UDPServer()
{
Stop();
}
public:
// Starts the server!
bool Start()
{
try
{
// Issue a task to the asio context
WaitForMessages();
m_threadContext = std::thread([this]() { m_asioContext.run(); });
}
catch (std::exception& e)
{
// Something prohibited the server from listening
std::cerr << "[SERVER # PORT " << m_port << "] Exception: " << e.what() << "\n";
return false;
}
std::cout << "[SERVER # PORT " << m_port << "] Started!\n";
return true;
}
// Stops the server!
void Stop()
{
// Request the context to close
m_asioContext.stop();
// Tidy up the context thread
if (m_threadContext.joinable()) m_threadContext.join();
// Inform someone, anybody, if they care...
std::cout << "[SERVER # PORT " << m_port << "] Stopped!\n";
}
void WaitForMessages()
{
m_socket.async_receive_from(asio::buffer(vBuffer.data(), vBuffer.size()), m_endpoint,
[this](std::error_code ec, std::size_t length)
{
if (!ec)
{
std::cout << "[SERVER # PORT " << m_port << "] Got " << length << " bytes \n Data: " << vBuffer.data() << "\n" << "Address: " << m_endpoint.address() << " Port: " << m_endpoint.port() << "\n" << "Data: " << m_endpoint.data() << "\n";
}
else
{
std::cerr << "[SERVER # PORT " << m_port << "] Exception: " << ec.message() << "\n";
return;
}
WaitForMessages();
}
);
}
void Send(message_T& msg, const asio::ip::udp::endpoint& ep)
{
asio::post(m_asioContext,
[this, msg, ep]()
{
// If the queue has a message in it, then we must
// assume that it is in the process of asynchronously being written.
bool bWritingMessage = !m_messagesOut.empty();
m_messagesOut.push_back(msg);
if (!bWritingMessage)
{
WriteMessage(ep);
}
}
);
}
private:
void WriteMessage(const asio::ip::udp::endpoint& ep)
{
m_socket.async_send_to(asio::buffer(&m_messagesOut.front(), sizeof(message_T)), ep,
[this, ep](std::error_code ec, std::size_t length)
{
if (!ec)
{
m_messagesOut.pop_front();
// If the queue is not empty, there are more messages to send, so
// make this happen by issuing the task to send the next header.
if (!m_messagesOut.empty())
{
WriteMessage(ep);
}
}
else
{
std::cout << "[SERVER # PORT " << m_port << "] Write Header Fail.\n";
m_socket.close();
}
});
}
void ReadMessage()
{
}
private:
uint16_t m_port = 0;
asio::ip::udp::endpoint m_endpoint;
std::vector<char> vBuffer = std::vector<char>(21);
protected:
TSQueue<message_T> m_messagesIn;
TSQueue<message_T> m_messagesOut;
Message<message_T> m_tempMessageBuf;
asio::io_context m_asioContext;
std::thread m_threadContext;
asio::ip::udp::socket m_socket;
};
}
Code is invoked in the main function for now:
enum class TestMsg {
Ping,
Join,
Leave
};
int main() {
Message<TestMsg> msg; // Message is a pretty basic struct that I'm not using yet. When I was, I was only receiving the first 4 bytes - which led me down this path of investigation
msg.id = TestMsg::Join;
msg << "hello";
UDPServer<Message<TestMsg>> server(60000);
}
When invoked the Server immediately exits before it gets chance to print "[SERVER] Started"
I'll try adding the work guard as the link post describes but I would still like to understand why the io_context is not being primed with work quick enough.
Update (Now I also read the question not just the code)
While in WaitForMessages you do start listening by calling the m_socket.async_receive_from function, as it is async, that function will return/unblock as soon as it has setup the listening. So as long as you don't actually have a client sending you something, you server has nothing do to. Only when it has received something the callback will be called, by a thread calling io_context::run. So you need the work guard so that your thread running run won't unblock right after start, but will block as long as the work guard is there.
Usually it is also combined with a try/while pattern if an exception gets thrown in a handler and you still want to move on with your server.
Also in the code you posted, you never actually call UDPServer::Start!
This was my first idea of an answer:
This is normal behavior of ASIO. The io_context::run function will return as soon as it has no work to do.
So to change the behaviour of the run function to block you have to use a boost::asio::executor_work_guard<boost::asio::io_context::executor_type> i.e. a so called work guard. Construct that object with a reference to your io_context and hold it i.e. don't let it destruct as long as you want to let the server run, i.e. do not want to let io_context::run return when there is not work.
So given
boost::asio::io_context io_context_;
boost::asio::executor_work_guard<boost::asio::io_context::executor_type> work_guard_;
you then could call
work_guard_{boost::asio::make_work_guard(io_context_)},
const auto thread_count{std::max<unsigned>(std::thread::hardware_concurrency(), 1)};
std::generate_n(std::back_inserter(this->io_run_threads_),
thread_count,
[this]() {
return std::thread{io_run_loop,
std::ref(this->io_context_), std::ref(this->error_handler_)};
});
void io_run_loop(boost::asio::io_context &context,
const std::function<void(std::exception &)> &error_handler) {
while (true) {
try {
context.run();
break;
} catch (std::exception &e) {
error_handler(e);
}
}
}
And then for server shutdown:
work_guard_.reset();
io_context_.stop();
std::for_each(this->io_run_threads_.begin(), this->io_run_threads_.end(), [](auto &thread) {
if (thread.joinable()) thread.join();
});
For a more graceful shutdown you can omit the stop call and rather close all sockets before.
Looks like you forgot to call server.Start();. Moreover, you will want to make the main thread wait for some amount of time, otherwise the destructor of Server will immediately cause Stop() to be called:
int main()
{
Message<TestMsg> msg;
msg.id = TestMsg::Join;
msg << "hello";
UDPServer<Message<TestMsg>> server(60000);
server.Start();
std::this_thread::sleep_for(30s);
}
Issues
There is a conceptual problem with the Send API.
It takes an endpoint on each call, but it only uses the one that starts the write call chain! This means that if you do
srv.Send(msg1, {mymachine, 60001});
srv.Send(msg1, {otherserver, 5517});
It is likely they both get sent to mymachine:60001.
How you treat the buffer received. Just using .data() blindly assumes that the data is NUL-terminated. Don't do that:
std::string const data(vBuffer.data(), length);
Also, you seem to have at some time been confused about data and printed m_endpoint.data() - your princess is in another castle.
In reality you probably want ways to extract the typed data. I'm leaving that as beyond the scope of this question for today.
Regardless you should clear the buffer before reuse, because you might be seeing old data in subsequent reads.
vBuffer.assign(vBuffer.size(), '\0');
This is most likely undefined behaviour:
asio::buffer(&m_messagesOut.front(), sizeof(message_T)), ep,
This is only valid if message_T is trivial and standard-layout ("POD" - Plain Old Data). The presence of operator<< strongly suggests that is not the case.
Instead, build a (sequence of) buffer(s) hat represents the message as raw bytes, e.g.
auto& msg = m_messagesOut.front();
msg.length = msg.body.size();
m_socket.async_send_to(
std::vector<asio::const_buffer>{
asio::buffer(&msg.id, sizeof(msg.id)),
asio::buffer(&msg.length, sizeof(msg.length)),
asio::buffer(msg.body),
},
// ...
Thread safe queues seem to be overkill since you have a single service thread; that is an implicit "strand" so you can post to it to have single-threaded semantics.
Here's a few adaptations to make it work so far (except the exercise-for-the-reader pointed out):
Live On Coliru
#include <boost/asio.hpp>
#include <iostream>
#include <deque>
#include <sstream>
// Library facilities
namespace asio = boost::asio;
using asio::ip::udp;
using boost::system::error_code;
using namespace std::chrono_literals;
/////////////////////////////////
// mock ups:
template <typename message_T> struct Message {
message_T id;
uint16_t length; // automatically filled on send, UDP packets are < 64k
std::string body;
template <typename T> friend Message& operator<<(Message& m, T const& v)
{
std::ostringstream oss;
oss << v;
m.body += oss.str();
//m.body += '\0'; // suggestion for easier message extraction
return m;
}
};
// Thread-safety can be replaced with the implicit strand of a single service
// thread
template <typename T> using TSQueue = std::deque<T>;
// end mock ups
/////////////////////////////////
template <typename message_T> class UDPServer {
public:
UDPServer(uint16_t port)
: m_socket(m_asioContext, udp::endpoint(udp::v4(), port))
{
m_port = port;
}
virtual ~UDPServer() { Stop(); }
public:
// Starts the server!
bool Start()
{
if (m_threadContext.joinable() && !m_asioContext.stopped())
return false;
try {
// Issue a task to the asio context
WaitForMessages();
m_threadContext = std::thread([this]() { m_asioContext.run(); });
} catch (std::exception const& e) {
// Something prohibited the server from listening
std::cerr << "[SERVER # PORT " << m_port
<< "] Exception: " << e.what() << "\n";
return false;
}
std::cout << "[SERVER # PORT " << m_port << "] Started!\n";
return true;
}
// Stops the server!
void Stop()
{
// Tell the context to stop processing
m_asioContext.stop();
// Tidy up the context thread
if (m_threadContext.joinable())
m_threadContext.join();
// Inform someone, anybody, if they care...
std::cout << "[SERVER # PORT " << m_port << "] Stopped!\n";
m_asioContext
.reset(); // required in case you want to reuse this Server object
}
void Send(message_T& msg, const udp::endpoint& ep)
{
asio::post(m_asioContext, [this, msg, ep]() {
// If the queue has a message in it, then we must
// assume that it is in the process of asynchronously being written.
bool bWritingMessage = !m_messagesOut.empty();
m_messagesOut.push_back(msg);
if (!bWritingMessage) {
WriteMessage(ep);
}
});
}
private:
void WaitForMessages() // assumed to be on-strand
{
vBuffer.assign(vBuffer.size(), '\0');
m_socket.async_receive_from(
asio::buffer(vBuffer.data(), vBuffer.size()), m_endpoint,
[this](std::error_code ec, std::size_t length) {
if (!ec) {
std::string const data(vBuffer.data(), length);
std::cout << "[SERVER # PORT " << m_port << "] Got "
<< length << " bytes \n Data: " << data << "\n"
<< "Address: " << m_endpoint.address()
<< " Port: " << m_endpoint.port() << "\n"
<< std::endl;
} else {
std::cerr << "[SERVER # PORT " << m_port
<< "] Exception: " << ec.message() << "\n";
return;
}
WaitForMessages();
});
}
void WriteMessage(const udp::endpoint& ep)
{
auto& msg = m_messagesOut.front();
msg.length = msg.body.size();
m_socket.async_send_to(
std::vector<asio::const_buffer>{
asio::buffer(&msg.id, sizeof(msg.id)),
asio::buffer(&msg.length, sizeof(msg.length)),
asio::buffer(msg.body),
},
ep, [this, ep](std::error_code ec, std::size_t length) {
if (!ec) {
m_messagesOut.pop_front();
// If the queue is not empty, there are more messages to
// send, so make this happen by issuing the task to send the
// next header.
if (!m_messagesOut.empty()) {
WriteMessage(ep);
}
} else {
std::cout << "[SERVER # PORT " << m_port
<< "] Write Header Fail.\n";
m_socket.close();
}
});
}
private:
uint16_t m_port = 0;
udp::endpoint m_endpoint;
std::vector<char> vBuffer = std::vector<char>(21);
protected:
TSQueue<message_T> m_messagesIn;
TSQueue<message_T> m_messagesOut;
Message<message_T> m_tempMessageBuf;
asio::io_context m_asioContext;
std::thread m_threadContext;
udp::socket m_socket;
};
enum class TestMsg {
Ping,
Join,
Leave
};
int main()
{
UDPServer<Message<TestMsg>> server(60'000);
if (server.Start()) {
std::this_thread::sleep_for(3s);
{
Message<TestMsg> msg;
msg.id = TestMsg::Join;
msg << "hello PI equals " << M_PI << " in this world";
server.Send(msg, {{}, 60'001});
}
std::this_thread::sleep_for(27s);
}
}
For some reason netcat doesn't work with UDP on Coliru, so here's a "live" demo:
You can see our netcat client messages arriving. You can see the message Sent to 60001 arriving in the tcpdump output.
Suppose I have a future object to run a process, and then in my main function I check if the process is timed out, in which case I want to end the program.
Consider the following template code:
//(include relevant libraries)
int main() {
std::future<int> future = std::async(std::launch::async, []() {
int result = uncertainFunctionCall();
return result;
});
std::future_status status = future.wait_for(std::chrono::milliseconds(50));
if (status == std::future_status::timeout) {
std::cout << "Timeout" << std::endl;
exit();
}
try {
std::cout << future.get() << std::endl;
std::cout << "Success" << std::endl;
}
catch(...) {
std::cout << "Exception Occurred" << std::endl;
exit();
}
return 0;
}
My question is, should there circumstances under which I need to do some cleaning up before calling the exit() function? For my use case, I only care about getting the value, but I don't want uncertainFunctionCall() to affect future executions of this program.
I want to create a server async with multi threads.
When I create a thread_group and waiting for some connections in asynchronous way. My program don't wait and termine immediatly.
void Server::configServer() {
_ip = boost::asio::ip::address_v4::from_string("127.0.0.1");
boost::asio::ip::tcp::resolver resolver(_io_service);
_endpoint = *resolver.resolve({tcp::v4(), _port});
std::cout << "Server address: " << _ip.to_string() << ":" << _port << std::endl;
_acceptor.close();
_acceptor.open(_endpoint.protocol());
_acceptor.set_option(boost::asio::ip::tcp::acceptor::reuse_address(true));
_acceptor.bind(_endpoint);
_acceptor.listen();
for (int i = 0; i < 8; ++i) {
_threads.create_thread(boost::bind(&boost::asio::io_service::run, &_io_service));
}
_threads.join_all();
std::cout << "Server is set up" << std::endl;
run();
}
void Server::run() {
Connection::pointer newConnection = Connection::create(_acceptor.get_io_service());
std::cout << "Server is running" << std::endl;
_acceptor.async_accept(newConnection->socket(),
boost::bind(&Server::handleAccept, this, newConnection,
boost::asio::placeholders::error));
}
void Server::handleAccept(Connection::pointer newConnection, const boost::system::error_code& error) {
if (!error) {
std::cout << "Reçu un client!" << std::endl;
newConnection->start();
run();
}
}
Can you tell me what am I doing wrong ?
run works as long as there are any pending tasks/handlers to be processed.
In your case you started run, then first async_ method was called. So run ends immediately due to no handlers to be called.
You should init some asynchronous task, then invoke run or use object called work guard. You didn't specify which version of Boost you use, but there are two options:
in olders io_service/io_context::work (ref)
current, executor_work_guard (ref)
In your class you can add executor_work_guard as additional member variable:
class Server {
boost::asio::io_context _io_service;
boost::asio::executor_work_guard<boost::asio::io_context::executor_type> guard;
Server() : ...., guard(boost::asio::make_work_guard(_io_service)) {
}
};
with this approach, run doesn't return even if there are no handlers to be processed.
I'm working on a project that involves a boost::beast websocket/http mixed server, which runs on top of boost::asio. I've heavily based my project off the advanced_server.cpp example source.
It works fine, but right now I'm attempting to add a feature that requires the sending of a message to all connected clients.
I'm not very familiar with boost::asio, but right now I can't see any way to have something like "broadcast" events (if that's even the correct term).
My naive approach would be to see if I can have the construction of websocket_session() attach something like an event listener, and the destructor detatch the listener. At that point, I could just fire the event, and have all the currently valid websocket sessions (to which the lifetime of websocket_session() is scoped) execute a callback.
There is https://stackoverflow.com/a/17029022/268006, which does more or less what I want by (ab)using a boost::asio::steady_timer, but that seems like a kind of horrible hack to accomplish something that should be pretty straightforward.
Basically, given a stateful boost::asio server, how can I do an operation on multiple connections?
First off: You can broadcast UDP, but that's not to connected clients. That's just... UDP.
Secondly, that link shows how to have a condition-variable (event)-like interface in Asio. That's only a tiny part of your problem. You forgot about the big picture: you need to know about the set of open connections, one way or the other:
e.g. keeping a container of session pointers (weak_ptr) to each connection
each connection subscribing to a signal slot (e.g. Boost Signals).
Option 1. is great for performance, option 2. is better for flexibility (decoupling the event source from subscribers, making it possible to have heterogenous subscribers, e.g. not from connections).
Because I think Option 1. is much simpler w.r.t to threading, better w.r.t. efficiency (you can e.g. serve all clients from one buffer without copying) and you probably don't need to doubly decouple the signal/slots, let me refer to an answer where I already showed as much for pure Asio (without Beast):
How to design proper release of a boost::asio socket or wrapper thereof
It shows the concept of a "connection pool" - which is essentially a thread-safe container of weak_ptr<connection> objects with some garbage collection logic.
Demonstration: Introducing Echo Server
After chatting about things I wanted to take the time to actually demonstrate the two approaches, so it's completely clear what I'm talking about.
First let's present a simple, run-of-the mill asynchronous TCP server with
with multiple concurrent connections
each connected session reads from the client line-by-line, and echoes the same back to the client
stops accepting after 3 seconds, and exits after the last client disconnects
master branch on github
#include <boost/asio.hpp>
#include <memory>
#include <list>
#include <iostream>
namespace ba = boost::asio;
using ba::ip::tcp;
using boost::system::error_code;
using namespace std::chrono_literals;
using namespace std::string_literals;
static bool s_verbose = false;
struct connection : std::enable_shared_from_this<connection> {
connection(ba::io_context& ioc) : _s(ioc) {}
void start() { read_loop(); }
void send(std::string msg, bool at_front = false) {
post(_s.get_io_service(), [=] { // _s.get_executor() for newest Asio
if (enqueue(std::move(msg), at_front))
write_loop();
});
}
private:
void do_echo() {
std::string line;
if (getline(std::istream(&_rx), line)) {
send(std::move(line) + '\n');
}
}
bool enqueue(std::string msg, bool at_front)
{ // returns true if need to start write loop
at_front &= !_tx.empty(); // no difference
if (at_front)
_tx.insert(std::next(begin(_tx)), std::move(msg));
else
_tx.push_back(std::move(msg));
return (_tx.size() == 1);
}
bool dequeue()
{ // returns true if more messages pending after dequeue
assert(!_tx.empty());
_tx.pop_front();
return !_tx.empty();
}
void write_loop() {
ba::async_write(_s, ba::buffer(_tx.front()), [this,self=shared_from_this()](error_code ec, size_t n) {
if (s_verbose) std::cout << "Tx: " << n << " bytes (" << ec.message() << ")" << std::endl;
if (!ec && dequeue()) write_loop();
});
}
void read_loop() {
ba::async_read_until(_s, _rx, "\n", [this,self=shared_from_this()](error_code ec, size_t n) {
if (s_verbose) std::cout << "Rx: " << n << " bytes (" << ec.message() << ")" << std::endl;
do_echo();
if (!ec)
read_loop();
});
}
friend struct server;
ba::streambuf _rx;
std::list<std::string> _tx;
tcp::socket _s;
};
struct server {
server(ba::io_context& ioc) : _ioc(ioc) {
_acc.bind({{}, 6767});
_acc.set_option(tcp::acceptor::reuse_address());
_acc.listen();
accept_loop();
}
void stop() {
_ioc.post([=] {
_acc.cancel();
_acc.close();
});
}
private:
void accept_loop() {
auto session = std::make_shared<connection>(_acc.get_io_context());
_acc.async_accept(session->_s, [this,session](error_code ec) {
auto ep = ec? tcp::endpoint{} : session->_s.remote_endpoint();
std::cout << "Accept from " << ep << " (" << ec.message() << ")" << std::endl;
session->start();
if (!ec)
accept_loop();
});
}
ba::io_context& _ioc;
tcp::acceptor _acc{_ioc, tcp::v4()};
};
int main(int argc, char** argv) {
s_verbose = argc>1 && argv[1] == "-v"s;
ba::io_context ioc;
server s(ioc);
std::thread th([&ioc] { ioc.run(); }); // todo exception handling
std::this_thread::sleep_for(3s);
s.stop(); // active connections will continue
th.join();
}
Approach 1. Adding Broadcast Messages
So, let's add "broadcast messages" that get sent to all active connections simultaneously. We add two:
one at each new connection (saying "Player ## has entered the game")
one that emulates a global "server event", like you described in the question). It gets triggered from within main:
std::this_thread::sleep_for(1s);
auto n = s.broadcast("random global event broadcast\n");
std::cout << "Global event broadcast reached " << n << " active connections\n";
Note how we do this by registering a weak pointer to each accepted connection and operating on each:
_acc.async_accept(session->_s, [this,session](error_code ec) {
auto ep = ec? tcp::endpoint{} : session->_s.remote_endpoint();
std::cout << "Accept from " << ep << " (" << ec.message() << ")" << std::endl;
if (!ec) {
auto n = reg_connection(session);
session->start();
accept_loop();
broadcast("player #" + std::to_string(n) + " has entered the game\n");
}
});
broadcast is also used directly from main and is simply:
size_t broadcast(std::string const& msg) {
return for_each_active([msg](connection& c) { c.send(msg, true); });
}
using-asio-post branch on github
#include <boost/asio.hpp>
#include <memory>
#include <list>
#include <iostream>
namespace ba = boost::asio;
using ba::ip::tcp;
using boost::system::error_code;
using namespace std::chrono_literals;
using namespace std::string_literals;
static bool s_verbose = false;
struct connection : std::enable_shared_from_this<connection> {
connection(ba::io_context& ioc) : _s(ioc) {}
void start() { read_loop(); }
void send(std::string msg, bool at_front = false) {
post(_s.get_io_service(), [=] { // _s.get_executor() for newest Asio
if (enqueue(std::move(msg), at_front))
write_loop();
});
}
private:
void do_echo() {
std::string line;
if (getline(std::istream(&_rx), line)) {
send(std::move(line) + '\n');
}
}
bool enqueue(std::string msg, bool at_front)
{ // returns true if need to start write loop
at_front &= !_tx.empty(); // no difference
if (at_front)
_tx.insert(std::next(begin(_tx)), std::move(msg));
else
_tx.push_back(std::move(msg));
return (_tx.size() == 1);
}
bool dequeue()
{ // returns true if more messages pending after dequeue
assert(!_tx.empty());
_tx.pop_front();
return !_tx.empty();
}
void write_loop() {
ba::async_write(_s, ba::buffer(_tx.front()), [this,self=shared_from_this()](error_code ec, size_t n) {
if (s_verbose) std::cout << "Tx: " << n << " bytes (" << ec.message() << ")" << std::endl;
if (!ec && dequeue()) write_loop();
});
}
void read_loop() {
ba::async_read_until(_s, _rx, "\n", [this,self=shared_from_this()](error_code ec, size_t n) {
if (s_verbose) std::cout << "Rx: " << n << " bytes (" << ec.message() << ")" << std::endl;
do_echo();
if (!ec)
read_loop();
});
}
friend struct server;
ba::streambuf _rx;
std::list<std::string> _tx;
tcp::socket _s;
};
struct server {
server(ba::io_context& ioc) : _ioc(ioc) {
_acc.bind({{}, 6767});
_acc.set_option(tcp::acceptor::reuse_address());
_acc.listen();
accept_loop();
}
void stop() {
_ioc.post([=] {
_acc.cancel();
_acc.close();
});
}
size_t broadcast(std::string const& msg) {
return for_each_active([msg](connection& c) { c.send(msg, true); });
}
private:
using connptr = std::shared_ptr<connection>;
using weakptr = std::weak_ptr<connection>;
std::mutex _mx;
std::vector<weakptr> _registered;
size_t reg_connection(weakptr wp) {
std::lock_guard<std::mutex> lk(_mx);
_registered.push_back(wp);
return _registered.size();
}
template <typename F>
size_t for_each_active(F f) {
std::vector<connptr> active;
{
std::lock_guard<std::mutex> lk(_mx);
for (auto& w : _registered)
if (auto c = w.lock())
active.push_back(c);
}
for (auto& c : active) {
std::cout << "(running action for " << c->_s.remote_endpoint() << ")" << std::endl;
f(*c);
}
return active.size();
}
void accept_loop() {
auto session = std::make_shared<connection>(_acc.get_io_context());
_acc.async_accept(session->_s, [this,session](error_code ec) {
auto ep = ec? tcp::endpoint{} : session->_s.remote_endpoint();
std::cout << "Accept from " << ep << " (" << ec.message() << ")" << std::endl;
if (!ec) {
auto n = reg_connection(session);
session->start();
accept_loop();
broadcast("player #" + std::to_string(n) + " has entered the game\n");
}
});
}
ba::io_context& _ioc;
tcp::acceptor _acc{_ioc, tcp::v4()};
};
int main(int argc, char** argv) {
s_verbose = argc>1 && argv[1] == "-v"s;
ba::io_context ioc;
server s(ioc);
std::thread th([&ioc] { ioc.run(); }); // todo exception handling
std::this_thread::sleep_for(1s);
auto n = s.broadcast("random global event broadcast\n");
std::cout << "Global event broadcast reached " << n << " active connections\n";
std::this_thread::sleep_for(2s);
s.stop(); // active connections will continue
th.join();
}
Approach 2: Those Broadcast But With Boost Signals2
The Signals approach is a fine example of Dependency Inversion.
Most salient notes:
signal slots get invoked on the thread invoking it ("raising the event")
the scoped_connection is there so subscriptions are *automatically removed when the connection is destructed
there's subtle difference in the wording of the console message from "reached # active connections" to "reached # active subscribers".
The difference is key to understanding the added flexibility: the signal owner/invoker does not know anything about the subscribers. That's the decoupling/dependency inversion we're talking about
using-signals2 branch on github
#include <boost/asio.hpp>
#include <memory>
#include <list>
#include <iostream>
#include <boost/signals2.hpp>
namespace ba = boost::asio;
using ba::ip::tcp;
using boost::system::error_code;
using namespace std::chrono_literals;
using namespace std::string_literals;
static bool s_verbose = false;
struct connection : std::enable_shared_from_this<connection> {
connection(ba::io_context& ioc) : _s(ioc) {}
void start() { read_loop(); }
void send(std::string msg, bool at_front = false) {
post(_s.get_io_service(), [=] { // _s.get_executor() for newest Asio
if (enqueue(std::move(msg), at_front))
write_loop();
});
}
private:
void do_echo() {
std::string line;
if (getline(std::istream(&_rx), line)) {
send(std::move(line) + '\n');
}
}
bool enqueue(std::string msg, bool at_front)
{ // returns true if need to start write loop
at_front &= !_tx.empty(); // no difference
if (at_front)
_tx.insert(std::next(begin(_tx)), std::move(msg));
else
_tx.push_back(std::move(msg));
return (_tx.size() == 1);
}
bool dequeue()
{ // returns true if more messages pending after dequeue
assert(!_tx.empty());
_tx.pop_front();
return !_tx.empty();
}
void write_loop() {
ba::async_write(_s, ba::buffer(_tx.front()), [this,self=shared_from_this()](error_code ec, size_t n) {
if (s_verbose) std::cout << "Tx: " << n << " bytes (" << ec.message() << ")" << std::endl;
if (!ec && dequeue()) write_loop();
});
}
void read_loop() {
ba::async_read_until(_s, _rx, "\n", [this,self=shared_from_this()](error_code ec, size_t n) {
if (s_verbose) std::cout << "Rx: " << n << " bytes (" << ec.message() << ")" << std::endl;
do_echo();
if (!ec)
read_loop();
});
}
friend struct server;
ba::streambuf _rx;
std::list<std::string> _tx;
tcp::socket _s;
boost::signals2::scoped_connection _subscription;
};
struct server {
server(ba::io_context& ioc) : _ioc(ioc) {
_acc.bind({{}, 6767});
_acc.set_option(tcp::acceptor::reuse_address());
_acc.listen();
accept_loop();
}
void stop() {
_ioc.post([=] {
_acc.cancel();
_acc.close();
});
}
size_t broadcast(std::string const& msg) {
_broadcast_event(msg);
return _broadcast_event.num_slots();
}
private:
boost::signals2::signal<void(std::string const& msg)> _broadcast_event;
size_t reg_connection(connection& c) {
c._subscription = _broadcast_event.connect(
[&c](std::string msg){ c.send(msg, true); }
);
return _broadcast_event.num_slots();
}
void accept_loop() {
auto session = std::make_shared<connection>(_acc.get_io_context());
_acc.async_accept(session->_s, [this,session](error_code ec) {
auto ep = ec? tcp::endpoint{} : session->_s.remote_endpoint();
std::cout << "Accept from " << ep << " (" << ec.message() << ")" << std::endl;
if (!ec) {
auto n = reg_connection(*session);
session->start();
accept_loop();
broadcast("player #" + std::to_string(n) + " has entered the game\n");
}
});
}
ba::io_context& _ioc;
tcp::acceptor _acc{_ioc, tcp::v4()};
};
int main(int argc, char** argv) {
s_verbose = argc>1 && argv[1] == "-v"s;
ba::io_context ioc;
server s(ioc);
std::thread th([&ioc] { ioc.run(); }); // todo exception handling
std::this_thread::sleep_for(1s);
auto n = s.broadcast("random global event broadcast\n");
std::cout << "Global event broadcast reached " << n << " active subscribers\n";
std::this_thread::sleep_for(2s);
s.stop(); // active connections will continue
th.join();
}
See the diff between Approach 1. and 2.: Compare View on github
A sample of the output when run against 3 concurrent clients with:
(for a in {1..3}; do netcat localhost 6767 < /etc/dictionaries-common/words > echoed.$a& sleep .1; done; time wait)
The answer from #sehe was amazing, so I'll be brief. Generally speaking, to implement an algorithm which operates on all active connections you must do the following:
Maintain a list of active connections. If this list is accessed by multiple threads, it will need synchronization (std::mutex). New connections should be inserted to the list, and when a connection is destroyed or becomes inactive it should be removed from the list.
To iterate the list, synchronization is required if the list is accessed by multiple threads (i.e. more than one thread calling asio::io_context::run, or if the list is also accessed from threads that are not calling asio::io_context::run)
During iteration, if the algorithm needs to inspect or modify the state of any connection, and that state can be changed by other threads, additional synchronization is needed. This includes any internal "queue" of messages that the connection object stores.
A simple way to synchronize a connection object is to use boost::asio::post to submit a function for execution on the connection object's context, which will be either an explicit strand (boost::asio::strand, as in the advanced server examples) or an implicit strand (what you get when only one thread calls io_context::run). The Approach 1 provided by #sehe uses post to synchronize in this fashion.
Another way to synchronize the connection object is to "stop the world." That means call io_context::stop, wait for all the threads to exit, and then you are guaranteed that no other threads are accessing the list of connections. Then you can read and write connection object state all you want. When you are finished with the list of connections, call io_context::restart and launch the threads which call io_context::run again. Stopping the io_context does not stop network activity, the kernel and network drivers still send and receive data from internal buffers. TCP/IP flow control will take care of things so the application still operates smoothly even though it becomes briefly unresponsive during the "stop the world." This approach can simplify things but depending on your particular application you will have to evaluate if it is right for you.
Hope this helps!
Thank you #sehe for the amazing answer. Still, I think there is a small but severe bug in the Approach 2. IMHO reg_connection should look like this:
size_t reg_connection(std::shared_ptr<connection> c) {
c->_subscription = _broadcast_event.connect(
[weak_c = std::weak_ptr<connection>(c)](std::string msg){
if(auto c = weak_c.lock())
c->send(msg, true);
}
);
return _broadcast_event.num_slots();
}
Otherwise you can end up with a race condition leading to a server crash. In case the connection instance is destroyed during the call to the lambda, the reference becomes invalid.
Similarly connection#send() should look like this, because otherwise this might be dead by the time the lambda is called:
void send(std::string msg, bool at_front = false) {
post(_s.get_io_service(),
[self=shared_from_this(), msg=std::move(msg), at_front] {
if (self->enqueue(std::move(msg), at_front))
self->write_loop();
});
}
PS: I would have posted this as a comment on #sehe's answer, but unfortunately I have not enough reputation.