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.
I would like to implement a Boost Asio pattern using a thread for GUI and a worker thread for some socket IO.
The worker thread will use boost::asio::io_service to manage a socket client. All operations on sockets will be performed by the worker thread only.
The GUI thread needs to send and receive messages from the worker thread.
I can't exactly figure how to implement this pattern using Boost Asio.
I've already implemented the socket communication in the standard Asio way (I call io_service.run() from the worker thread and I use async_read_some/async_send). I don't need strands because io_service.run() is called from the worker thread only.
Now I'm trying to add the cross thread message queue. How I can I implement it?
Should I run the io_service from the GUI thread too?
Or should I just use strands with post to post messages from the GUI thread to the worker thread (without calling io_service.run() or io_service.poll_one() from the GUI thread), and use the operating system's GUI message loop to post messages from the worker thread to the GUI thread?
If I need to call io_service.run() or io_service.poll_one() from the GUI thread too, do I need to use strands on the socket operations, since the io_service is shared between two threads?
EDIT: to clarify my question, I would like to do whatever I can, to implement the message queue, using Boost Asio, relying on other libraries only if Boost Asio can't do the job.
Message passing is fairly generic. There are various ways to approach the problem, and the solution will likely be dependent on the desired behavioral details. For example, blocking or non-blocking, controlling memory allocation, context, etc.
Boost.Lockfree provides thread-safe lock-free non-blocking queues for singe/multi consumer/producers. It tends to lend itself fairly nicely to event loops, where it is not ideal for the consumer to be blocked, waiting for the producer to signal a synchronization construct.
boost::lockfree::queue<message_type> worker_message_queue;
void send_worker_message(const message_type& message)
{
// Add message to worker message queue.
worker_message_queue.push(message);
// Add work to worker_io_service that will process the queue.
worker_io_service.post(&process_message);
}
void process_message()
{
message_type message;
// If the message was not retrieved, then return early.
if (!worker_message_queue.pop(message)) return;
...
}
Alternatively, Boost.Asio's io_service can function as a queue. The message just needs to be bound to the specified handler.
void send_worker_message(const message_type& message)
{
// Add work to worker_io_service that will process the message.
worker_io_service.post(boost::bind(&process_message, message));
}
void process_message(message_type& message)
{
...
}
This comment suggest that the desire is more than message passing. It sounds as though the end goal is to allow one thread to cause another thread to invoke arbitrary functions.
If this is the case, then consider:
Using Boost.Signals2 for a managed signals and slots implementation. This allows arbitrary functions to register with a signal.
Using Boost.Asio's io_service to setup signal emissions. If the GUI thread and worker thread each have their own io_service, then the worker thread can post a handler into the GUI thread's io_service that will emit a signal. In the GUI thread's main loop, it will poll the io_service, emit the signal, and cause slots to be invoked from within the GUI thread's context.
Here is complete example where two threads pass a message (as an unsigned int) to one another, as well as causing arbitrary functions to be invoked within another thread.
#include <iostream>
#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <boost/signals2.hpp>
#include <boost/thread.hpp>
/// #brief io_service dedicated to gui.
boost::asio::io_service gui_service;
/// #brief io_service dedicated to worker.
boost::asio::io_service worker_service;
/// #brief work to keep gui_service from stopping prematurely.
boost::optional<boost::asio::io_service::work> gui_work;
/// #brief hello slot.
void hello(int x)
{
std::cout << "hello with " << x << " from thread " <<
boost::this_thread::get_id() << std::endl;
}
/// #brief world slot.
void world(int x)
{
std::cout << "world with " << x << " from thread " <<
boost::this_thread::get_id() << std::endl;
}
/// #brief Type for signals.
typedef boost::signals2::signal<void (int)> signal_type;
void emit_then_notify_gui(signal_type& signal, unsigned int x);
/// #brief Emit signals then message worker.
void emit_then_notify_worker(signal_type& signal, unsigned int x)
{
// Emit signal, causing registered slots to run within this thread.
signal(x);
// If x has been exhausted, then cause gui service to run out of work.
if (!x)
{
gui_work = boost::none;
}
// Otherwise, post work into worker service.
else
{
std::cout << "GUI thread: " << boost::this_thread::get_id() <<
" scheduling other thread to emit signals" << std::endl;
worker_service.post(boost::bind(
&emit_then_notify_gui,
boost::ref(signal), --x));
}
}
/// #brief Emit signals then message worker.
void emit_then_notify_gui(signal_type& signal, unsigned int x)
{
// Emit signal, causing registered slots to run within this thread.
signal(x);
// If x has been exhausted, then cause gui service to run out of work.
if (!x)
{
gui_work = boost::none;
}
// Otherwise, post more work into gui.
else
{
std::cout << "Worker thread: " << boost::this_thread::get_id() <<
" scheduling other thread to emit signals" << std::endl;
gui_service.post(boost::bind(
&emit_then_notify_worker,
boost::ref(signal), --x));
}
}
void worker_main()
{
std::cout << "Worker thread: " << boost::this_thread::get_id() << std::endl;
worker_service.run();
}
int main()
{
signal_type signal;
// Connect slots to signal.
signal.connect(&hello);
signal.connect(&world);
boost::optional<boost::asio::io_service::work> worker_work(
boost::ref(worker_service));
gui_work = boost::in_place(boost::ref(gui_service));
std::cout << "GUI thread: " << boost::this_thread::get_id() << std::endl;
// Spawn off worker thread.
boost::thread worker_thread(&worker_main);
// Add work to worker.
worker_service.post(boost::bind(
&emit_then_notify_gui,
boost::ref(signal), 3));
// Mocked up GUI main loop.
while (!gui_service.stopped())
{
// Do other GUI actions.
// Perform message processing.
gui_service.poll_one();
}
// Cleanup.
worker_work = boost::none;
worker_thread.join();
}
And its output:
GUI thread: b7f2f6d0
Worker thread: b7f2eb90
hello with 3 from thread b7f2eb90
world with 3 from thread b7f2eb90
Worker thread: b7f2eb90 scheduling other thread to emit signals
hello with 2 from thread b7f2f6d0
world with 2 from thread b7f2f6d0
GUI thread: b7f2f6d0 scheduling other thread to emit signals
hello with 1 from thread b7f2eb90
world with 1 from thread b7f2eb90
Worker thread: b7f2eb90 scheduling other thread to emit signals
hello with 0 from thread b7f2f6d0
world with 0 from thread b7f2f6d0
If you have only one worker, then it's rather easy.
ASIO's handlers are executed by the thread(s) that are calling io_service.run(). In your case, that means that only one thread, the worker one, can execute callback handler. So you need not to worry about thread safety here.
Your GUI thread, assuming that it has access to one's socket, can call boost::asio::async_write() without problem. The callback handler, however, will be executed in the worker thread.
From my experience (admitedly limited), I used this pattern:
The business logic thread (could be your GUI thread) can schedule a write to one of its client easily, by calling boost::asio::async_write(): the worker thread will take care of it.
The worker thread start some boost::asio::async_read(), and could be building "business logic packet". What I mean here, is that it construct meaningfull message (could be a subclass of a custom class Packet or Event or w/e you what) from raw data.
When the worker thread has enough data to build such a message, it does, and then enqueue it to a thread-safe queue that the GUI thread will be pulling.
The GUI (or business logic) thread then process the message.
Let me know if its not clear / if I can be of more help.
The way that I exchange messages between 2+ threads is to use a container like a queue and store them in there and then use an event to notify the worker thread to wake up and process them. Here is an example:
void SSLSocket::SendToServer(const int bytesInMsg, Byte* pBuf)
{
// This method creates a msg object and saves it in the SendMsgQ object.
//
Message* pMsg = Message::GetMsg(this, bytesInMsg, pBuf);
SendMsgQ.Push(pMsg);
// Signal the send worker thread to wake up and send the msg to the server.
SetEvent(hEvent);
}
In the header file:
std::queue<Message*> SendMsgQueue; // Queue of msgs to send to the server.
The above code is for Microsoft VC++. You might have to use a different class or methods if your development environment is different. But, the idea should be the same.
Edit - More Complete Code Example
#include "StdAfx.h"
#include "SSLSocket.h"
boost::shared_ptr< boost::asio::io_service > SSLSocket::IOService;
bool SSLSocket::LobbySocketOpen = false;
SSLSocket* SSLSocket::pSSLLobby = 0;
int SSLSocket::StaticInit = 0;
Callback SSLSocket::CallbackFunction;
BufferManagement SSLSocket::BufMang;
volatile bool SSLSocket::ReqAlive = true;
Logger SSLSocket::Log;
HANDLE SSLSocket::hEvent;
bool SSLSocket::DisplayInHex;
ConcurrentMsgQueue SSLSocket::SendMsgQ;
bool SSLSocket::RcvThreadCreated = 0;
BufferManagement* Message::pBufMang;
bool SSLSocket::ShuttingDown = false;
std::vector<SSLSocket *> SocketList;
SSLSocket::SSLSocket(const bool logToFile, const bool logToConsole, const bool displayInHex,
const LogLevel levelOfLog, const string& logFileName, const int bufMangLen) : pSocket(0)
{
// SSLSocket Constructor.
// If the static members have not been intialized yet, then initialize them.
LockCode = new Lock();
if (!StaticInit)
{
SocketList.push_back(this);
DisplayInHex = displayInHex;
BufMang.Init(bufMangLen);
Message::SetBufMang(&BufMang);
// This constructor enables logging according to the vars passed in.
Log.Init(logToFile, logToConsole, levelOfLog, logFileName);
StaticInit = 1;
hEvent = CreateEvent(NULL, false, false, NULL);
// Define the ASIO IO service object.
// IOService = new boost::shared_ptr<boost::asio::io_service>(new boost::asio::io_service);
boost::shared_ptr<boost::asio::io_service> IOServ(new boost::asio::io_service);
IOService = IOServ;
pSSLLobby = this;
}
}
SSLSocket::~SSLSocket(void)
{
if (pSocket)
delete pSocket;
if (--StaticInit == 0)
CloseHandle(hEvent);
}
void SSLSocket::Connect(SSLSocket* psSLS, const string& serverPath, string& port)
{
// Connects to the server.
// serverPath - specifies the path to the server. Can be either an ip address or url.
// port - port server is listening on.
//
try
{
LockCode->Acquire(); // Single thread the code.
// If the user has tried to connect before, then make sure everything is clean before trying to do so again.
if (pSocket)
{
delete pSocket;
pSocket = 0;
}
// If serverPath is a URL, then resolve the address.
if ((serverPath[0] < '0') || (serverPath[0] > '9')) // Assumes that the first char of the server path is not a number when resolving to an ip addr.
{
// Create the resolver and query objects to resolve the host name in serverPath to an ip address.
boost::asio::ip::tcp::resolver resolver(*IOService);
boost::asio::ip::tcp::resolver::query query(serverPath, port);
boost::asio::ip::tcp::resolver::iterator EndpointIterator = resolver.resolve(query);
// Set up an SSL context.
boost::asio::ssl::context ctx(*IOService, boost::asio::ssl::context::tlsv1_client);
// Specify to not verify the server certificiate right now.
ctx.set_verify_mode(boost::asio::ssl::context::verify_none);
// Init the socket object used to initially communicate with the server.
pSocket = new boost::asio::ssl::stream<boost::asio::ip::tcp::socket>(*IOService, ctx);
//
// The thread we are on now, is most likely the user interface thread. Create a thread to handle all incoming socket work messages.
// Only one thread is created to handle the socket I/O reading and another thread is created to handle writing.
if (!RcvThreadCreated)
{
WorkerThreads.create_thread(boost::bind(&SSLSocket::RcvWorkerThread, this));
RcvThreadCreated = true;
WorkerThreads.create_thread(boost::bind(&SSLSocket::SendWorkerThread, this));
}
// Try to connect to the server. Note - add timeout logic at some point.
boost::asio::async_connect(pSocket->lowest_layer(), EndpointIterator,
boost::bind(&SSLSocket::HandleConnect, this, boost::asio::placeholders::error));
}
else
{
// serverPath is an ip address, so try to connect using that.
//
stringstream ss1;
boost::system::error_code EC;
ss1 << "SSLSocket::Connect: Preparing to connect to game server " << serverPath << " : " << port << ".\n";
Log.LogString(ss1.str(), LogInfo);
// Create an endpoint with the specified ip address.
const boost::asio::ip::address IP(boost::asio::ip::address::from_string(serverPath));
int iport = atoi(port.c_str());
const boost::asio::ip::tcp::endpoint EP(IP, iport);
// Set up an SSL context.
boost::asio::ssl::context ctx(*IOService, boost::asio::ssl::context::tlsv1_client);
// Specify to not verify the server certificiate right now.
ctx.set_verify_mode(boost::asio::ssl::context::verify_none);
// Init the socket object used to initially communicate with the server.
pSocket = new boost::asio::ssl::stream<boost::asio::ip::tcp::socket>(*IOService, ctx);
//
// Try to connect to the server. Note - add timeout logic at some point.
pSocket->next_layer().connect(EP, EC);
if (EC)
{
// Log an error. This worker thread should exit gracefully after this.
stringstream ss;
ss << "SSLSocket::Connect: connect failed to " << sClientIp << " : " << uiClientPort << ". Error: " << EC.message() + ".\n";
Log.LogString(ss.str(), LogError);
}
stringstream ss;
ss << "SSLSocket::Connect: Calling HandleConnect for game server " << serverPath << " : " << port << ".\n";
Log.LogString(ss.str(), LogInfo);
HandleConnect(EC);
}
}
catch (std::exception& e)
{
stringstream ss;
ss << "SSLSocket::Connect: threw an error - " << e.what() << ".\n";
Log.LogString(ss.str(), LogError);
Stop();
}
LockCode->Release();
}
void SSLSocket::SendToServer(const int bytesInMsg, Byte* pBuf)
{
// This method creates a msg object and saves it in the SendMsgQ object.
// sends the number of bytes specified by bytesInMsg in pBuf to the server.
//
Message* pMsg = Message::GetMsg(this, bytesInMsg, pBuf);
SendMsgQ.Push(pMsg);
// Signal the send worker thread to wake up and send the msg to the server.
SetEvent(hEvent);
}
void SSLSocket::SendWorkerThread(SSLSocket* psSLS)
{
// This thread method gets called to process the messages to be sent to the server.
//
// Since this has to be a static method, call a method on the class to handle server requests.
psSLS->ProcessSendRequests();
}
void SSLSocket::ProcessSendRequests()
{
// This method handles sending msgs to the server.
//
std::stringstream ss;
DWORD WaitResult;
Log.LogString("SSLSocket::ProcessSendRequests: Worker thread " + Logger::NumberToString(boost::this_thread::get_id()) + " started.\n", LogInfo);
// Loop until the user quits, or an error of some sort is thrown.
try
{
do
{
// If there are one or more msgs that need to be sent to a server, then send them out.
if (SendMsgQ.Count() > 0)
{
Message* pMsg = SendMsgQ.Front();
SSLSocket* pSSL = pMsg->pSSL;
SendMsgQ.Pop();
const Byte* pBuf = pMsg->pBuf;
const int BytesInMsg = pMsg->BytesInMsg;
boost::system::error_code Error;
LockCode->Acquire(); // Single thread the code.
try
{
boost::asio::async_write(*pSSL->pSocket, boost::asio::buffer(pBuf, BytesInMsg), boost::bind(&SSLSocket::HandleWrite, this,
boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred));
}
catch (std::exception& e)
{
stringstream ss;
ss << "SSLSocket::ProcessSendRequests: threw an error - " << e.what() << ".\n";
Log.LogString(ss.str(), LogError);
}
ss.str(std::string());
ss << "SSLSocket::ProcessSendRequests: # bytes sent = " << BytesInMsg << "\n";
Log.LogString(ss.str(), LogDebug2);
Log.LogBuf(pBuf, BytesInMsg, DisplayInHex, LogDebug3);
LockCode->Release();
}
else
{
// Nothing to send, so go into a wait state.
WaitResult = WaitForSingleObject(hEvent, INFINITE);
if (WaitResult != 0L)
{
Log.LogString("SSLSocket::ProcessSendRequests: WaitForSingleObject event error. Code = " + Logger::NumberToString(GetLastError()) + ". \n", LogError);
}
}
} while (ReqAlive);
Log.LogString("SSLSocket::ProcessSendRequests: Worker thread " + Logger::NumberToString(boost::this_thread::get_id()) + " done.\n", LogInfo);
}
catch (std::exception& e)
{
stringstream ss;
ss << "SSLSocket::ProcessSendRequests: threw an error - " << e.what() << ".\n";
Log.LogString(ss.str(), LogError);
Stop();
}
}
void SSLSocket::HandleWrite(const boost::system::error_code& error, size_t bytesTransferred)
{
// This method is called after a msg has been written out to the socket. Nothing to do really since reading is handled by the HandleRead method.
//
std::stringstream ss;
try
{
if (error)
{
ss << "SSLSocket::HandleWrite: failed - " << error.message() << ".\n";
Log.LogString(ss.str(), LogError);
Stop();
}
}
catch (std::exception& e)
{
stringstream ss;
ss << "SSLSocket::HandleHandshake: threw an error - " << e.what() << ".\n";
Log.LogString(ss.str(), LogError);
Stop();
}
}
void SSLSocket::RcvWorkerThread(SSLSocket* psSLS)
{
// This is the method that gets called when the receive thread is created by this class.
// This thread method focuses on processing messages received from the server.
//
// Since this has to be a static method, call an instance method on the class to handle server requests.
psSLS->InitAsynchIO();
}
void SSLSocket::InitAsynchIO()
{
// This method is responsible for initiating asynch i/o.
boost::system::error_code Err;
string s;
stringstream ss;
//
try
{
ss << "SSLSocket::InitAsynchIO: Worker thread - " << Logger::NumberToString(boost::this_thread::get_id()) << " started.\n";
Log.LogString(ss.str(), LogInfo);
// Enable the handlers for asynch i/o. The thread will hang here until the stop method has been called or an error occurs.
// Add a work object so the thread will be dedicated to handling asynch i/o.
boost::asio::io_service::work work(*IOService);
IOService->run();
Log.LogString("SSLSocket::InitAsynchIO: receive worker thread done.\n", LogInfo);
}
catch (std::exception& e)
{
stringstream ss;
ss << "SSLSocket::InitAsynchIO: threw an error - " << e.what() << ".\n";
Log.LogString(ss.str(), LogError);
Stop();
}
}
void SSLSocket::HandleConnect(const boost::system::error_code& error)
{
// This method is called asynchronously when the server has responded to the connect request.
std::stringstream ss;
try
{
if (!error)
{
LockCode->Acquire(); // Single thread the code.
pSocket->async_handshake(boost::asio::ssl::stream_base::client,
boost::bind(&SSLSocket::HandleHandshake, this, boost::asio::placeholders::error));
LockCode->Release();
ss << "SSLSocket::HandleConnect: From worker thread " << Logger::NumberToString(boost::this_thread::get_id()) << ".\n";
Log.LogString(ss.str(), LogInfo);
}
else
{
// Log an error. This worker thread should exit gracefully after this.
ss << "SSLSocket::HandleConnect: connect failed. Error: " << error.message() + ".\n";
Log.LogString(ss.str(), LogError);
Stop();
}
}
catch (std::exception& e)
{
stringstream ss;
ss << "SSLSocket::InitAsynchIO: threw an error - " << e.what() << ".\n";
Log.LogString(ss.str(), LogError);
Stop();
}
}
void SSLSocket::HandleHandshake(const boost::system::error_code& error)
{
// This method is called asynchronously when the server has responded to the handshake request.
std::stringstream ss;
try
{
if (!error)
{
// Try to send the first message that the server is expecting. This msg tells the server we want to connect.
//
unsigned char Msg[5] = {0x17, 0x00, 0x00, 0x00, 0x06};
boost::system::error_code Err;
//
if (pSSLLobby == this)
LobbySocketOpen = true;
sClientIp = pSocket->lowest_layer().remote_endpoint().address().to_string();
uiClientPort = pSocket->lowest_layer().remote_endpoint().port();
ReqAlive = true;
LockCode->Acquire(); // Single thread the code.
int Count = boost::asio::write(*pSocket, boost::asio::buffer(Msg), boost::asio::transfer_exactly(5), Err);
if (Err)
{
ss << "SSLSocket::HandleHandshake: write failed - " << error.message() << ".\n";
Log.LogString(ss.str(), LogInfo);
}
HandleFirstWrite(Err, Count);
LockCode->Release();
ss.str("");
ss << "SSLSocket::HandleHandshake: From worker thread " << boost::this_thread::get_id() << ".\n";
}
else
{
ss << "SSLSocket::HandleHandshake: failed - " << error.message() << ".\n";
IOService->stop();
}
Log.LogString(ss.str(), LogInfo);
}
catch (std::exception& e)
{
stringstream ss;
ss << "SSLSocket::HandleHandshake: threw an error - " << e.what() << ".\n";
Log.LogString(ss.str(), LogError);
Stop();
}
}
void SSLSocket::HandleFirstWrite(const boost::system::error_code& error, size_t bytesTransferred)
{
// This method is called after a msg has been written out to the socket. This method is only called from HandleHandShake.
std::stringstream ss;
try
{
if (!error)
{
// Notify the UI that we are now connected. Create a 6 byte msg for this.
pDataBuf = BufMang.GetPtr(6);
BYTE* p = pDataBuf;
// Create msg type 500
*p = 244;
*++p = 1;
CallbackFunction(this, 2, (void*)pDataBuf);
// Get the 1st 4 bytes of the next msg, which is always the length of the msg.
pDataBuf = BufMang.GetPtr(MsgLenBytes);
try
{
boost::asio::async_read(*pSocket, boost::asio::buffer(pDataBuf, MsgLenBytes), boost::bind(&SSLSocket::HandleRead, this,
boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred));
}
catch (std::exception& e)
{
stringstream ss;
ss << "SSLSocket::HandleFirstWrite: threw an error - " << e.what() << ".\n";
Log.LogString(ss.str(), LogError);
Stop();
}
}
else
{
ss << "SSLSocket::HandleFirstWrite: failed - " << error.message() << ".\n";
Log.LogString(ss.str(), LogError);
Stop();
}
}
catch (std::exception& e)
{
stringstream ss;
ss << "SSLSocket::HandleFirstWrite: threw an error - " << e.what() << ".\n";
Log.LogString(ss.str(), LogError);
Stop();
}
}
void SSLSocket::HandleRead(const boost::system::error_code& error, size_t bytesTransferred)
{
// This method is called to process an incoming message.
//
std::stringstream ss;
int ByteCount;
try
{
// ss << "SSLSocket::HandleRead: From worker thread " << boost::this_thread::get_id() << ".\n";
// Log.LogString(ss.str(), LogInfo);
// Set to exit this thread if the user is done.
if (!ReqAlive)
{
// IOService->stop();
return;
}
if (!error)
{
// Get the number of bytes in the message.
if (bytesTransferred == 4)
{
ByteCount = BytesToInt(pDataBuf);
}
else
{
// Call the C# callback method that will handle the message.
ss << "SSLSocket::HandleRead: From worker thread " << boost::this_thread::get_id() << "; # bytes transferred = " << bytesTransferred << ".\n";
Log.LogString(ss.str(), LogDebug2);
if (bytesTransferred > 0)
{
Log.LogBuf(pDataBuf, (int)bytesTransferred, true, LogDebug3);
Log.LogString("SSLSocket::HandleRead: sending msg to the C# client.\n\n", LogDebug2);
CallbackFunction(this, bytesTransferred, (void*)pDataBuf);
}
else
{
// # of bytes transferred = 0. Don't do anything.
bytesTransferred = 0; // For debugging.
}
// Prepare to read in the next message length.
ByteCount = MsgLenBytes;
}
pDataBuf = BufMang.GetPtr(ByteCount);
boost::system::error_code Err;
try
{
boost::asio::async_read(*pSocket, boost::asio::buffer(pDataBuf, ByteCount), boost::bind(&SSLSocket::HandleRead,
this, boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred));
}
catch (std::exception& e)
{
stringstream ss;
ss << "SSLSocket::HandleRead: threw this error - " << e.what() << ".\n";
Log.LogString(ss.str(), LogError);
}
}
else
{
Log.LogString("SSLSocket::HandleRead failed: " + error.message() + "\n", LogError);
Stop();
}
}
catch (std::exception& e)
{
stringstream ss;
ss << "SSLSocket::HandleRead: threw an error - " << e.what() << ".\n";
Log.LogString(ss.str(), LogError);
Stop();
}
}
void SSLSocket::Stop()
{
// This method calls the shutdown method on the socket in order to stop reads or writes that might be going on. If this is not done, then an exception will be thrown
// when it comes time to delete this object.
//
boost::system::error_code EC;
try
{
// This method can be called from the handler as well. So once the ShuttingDown flag is set, don't go throught the same code again.
if (ShuttingDown)
return;
LockCode->Acquire(); // Single thread the code.
if (!ShuttingDown)
{
ShuttingDown = true;
pSocket->next_layer().cancel();
pSocket->shutdown(EC);
if (EC)
{
stringstream ss;
ss << "SSLSocket::Stop: socket shutdown error - " << EC.message() << ".\n";
}
else
{
pSocket->next_layer().close();
}
delete pSocket;
pSocket = 0;
ReqAlive = false;
SetEvent(hEvent);
IOService->stop();
LobbySocketOpen = false;
WorkerThreads.join_all();
}
LockCode->Release();
delete LockCode;
LockCode = 0;
}
catch (std::exception& e)
{
stringstream ss;
ss << "SSLSocket::Stop: threw an error - " << e.what() << ".\n";
Log.LogString(ss.str(), LogError);
Stop();
}
}
So, in answer to your question about whether you have to use a queue or not. In your comment to Xaqq, you said "I need to exchange messages between the two threads." So using a container like a queue is how messages can be passed to another thread for processing. If you don't like the STL containers, Boost does have some. As far as I know, there is no Boost ASIO internal container that can be accessed. Storing and passing the messages around is something you have to do in your code.
One last note about the call to io_service::run. It will only block while there is work to do. See this link. In my example code above, a work item is added to the io_service object before the run method is called, so it will block indefinitely - which is what I want. If I really wanted only one thread, then what I might do is set up the worker thread to call the run method with a work object so that it would block indefinitely. This would handle all asynchronous I/O coming from and going to the server. Inside the class, I would write an interface method or two so that the gui can send data to the server. These methods could use the async write .vs. the synch write method and thus would return right away - so your gui won't block long. You would need to write a HandleWrite method. My code does not do much with it - just logs an error if one occurs.
I want simply to have a chat client that is asynchronously listening from a socket and from a boost::asio::posix::stream_descriptor assigned to stdin.
If I run this code in a single threaded application, all works fine.
If I call io_service.run() from 2 or more threads, the asynchronous operations from stdin, never go fine, but async readings from the socket are still executed.
here is the code:
MasterClient::MasterClient(boost::asio::io_service& io_service,boost::asio::ip::tcp::resolver::iterator iter, string nickName)
:it(iter),chatNick(nickName)
{
this->fdIn_ = str_ptr(new boost::asio::posix::stream_descriptor(io_service,::dup(STDIN_FILENO)));
this->dirServer_ = new(connectedPeer);
this->dirServer_->sock = socket_ptr(new boost::asio::ip::tcp::socket(this->io_service_));
boost::asio::async_connect(*(this->dirServer_->sock), this->it,
boost::bind(&MasterClient::connectionHandler, this,
boost::asio::placeholders::error));
}
main:
int main(int argc, const char * argv[])
{
boost::asio::io_service io_service(2);
boost::asio::io_service::work work(io_service);
boost::asio::ip::tcp::resolver resolver(io_service);
boost::asio::ip::tcp::resolver::query query(argv[1], argv[2]);
boost::asio::ip::tcp::resolver::iterator iterator = resolver.resolve(query);
string nick;
cout << "Inserire un nickname per la sessione di chat: " << flush;
getline(cin,nick);
MasterClient cli(io_service,iterator,nick);
cli.run();
}
and the MasterClient::run()
void MasterClient::run()
{
// Create a pool of threads to run all of the io_services.
std::vector<boost::shared_ptr<boost::thread> > threads;
boost::asio::io_service::work work(this->io_service_);
for (std::size_t i = 0; i < 1; ++i)
{
boost::shared_ptr<boost::thread> thread(new boost::thread(boost::bind(&boost::asio::io_service::run, &io_service_)));
threads.push_back(thread);
}
// Wait for all threads in the pool to exit.
for (std::size_t i = 0; i < threads.size(); ++i)
threads[i]->join();
}
here i call the async readings:
void MasterClient::clientDelegate()
{
if(this->connectToServer())
{
this->mainMenu();
boost::asio::async_read_until(*fdIn_, inBuff_, '\n',
boost::bind(&MasterClient::fdInMenuHandler,
this,
boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred));
(*(this->dirServer_->sock)).async_read_some(boost::asio::buffer(this->buff_),
boost::bind(&MasterClient::serverHandler,
this,
boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred,
this->dirServer_->sock));
this->io_service_.post(boost::bind(&MasterClient::printer,this));
}else
{
if(this->isDebugging)
cout << "Error in ClientDelegate." << endl;
}
if(this->isDebugging)
cout << "ClientDelegate END" << endl;
}
connection handler, where the clientDelegate gets called:
void MasterClient::connectionHandler(const boost::system::error_code& error)
{
cout << "connected" << endl;
try
{
if(error)
throw boost::system::system_error(error);
else
{
this->dirServer_->endpoint = boost::asio::ip::tcp::endpoint((*(this->dirServer_->sock)).remote_endpoint());
this->clientDelegate();
}
}catch(const boost::system::system_error& e)
{
cerr << "Boost Exception in ConnectHandler ---> " << e.what() << endl;
this->io_service_.stop();
}
}
What am i doing wrong?
According to the documentation of boost::asio::posix::stream_descriptor, it's unsafe to use the same instance from multiple threads. In multithreading situations, typically the handlers are wrapped into a strand for serialisation.
In your case I don't see the point of using multiple threads for the same client connection.