I have the following simple coroutine-based server:
class Server
{
private:
boost::asio::io_service Service;
boost::asio::ip::tcp::acceptor Acceptor;
boost::asio::ip::tcp::socket Socket;
private:
void Accept(boost::asio::yield_context Yield);
void Write(boost::asio::yield_context Yield);
public:
Server(): Acceptor(Service), Socket(Service) {}
void Open(unsigned short PortNum);
void Run();
void Stop();
};
void Server::Accept(boost::asio::yield_context Yield)
{
boost::system::error_code ec;
for (;;)
{
Socket.close();
Acceptor.async_accept(Socket,Yield[ec]);
spawn(Yield,std::bind(&Server::Write,this,Yield[ec]));
}
}
void Server::Write(boost::asio::yield_context Yield)
{
char InBuffer[1024]= {};
std::size_t Size;
boost::system::error_code ec;
double Data= 6.66;
for (;;)
{
boost::asio::streambuf OutBuffer;
std::ostream os(&OutBuffer);
Size= Socket.async_read_some(boost::asio::buffer(InBuffer),Yield[ec]);
if (ec)
break;
os.write(reinterpret_cast<const char *>(&Data),sizeof(double));
Socket.async_write_some(OutBuffer.data(),Yield[ec]);
if (ec)
break;
}
}
void Server::Open(unsigned short PortNum)
{
Acceptor.open(boost::asio::ip::tcp::v4());
Acceptor.bind({{},PortNum});
Acceptor.listen();
}
void Server::Run()
{
spawn(Service,std::bind(&Server::Accept,this,std::placeholders::_1));
Service.run();
}
void Server::Stop()
{
Service.stop();
}
I want to run this server on a thread and stop it cleanly when the main program is about to finish:
int main()
{
Server s;
s.Open(1024);
std::thread Thread(&Server::Run,&s);
Sleep(10'000);
s.Stop();
Thread.join();
}
Unfortunately, if there is a connected socket, when I call Stop an exception boost::coroutines::detail::forced_unwind is thrown.
I have also tried creating an explicit strand and dispatching a Socket.close() before stopping with the same result.
Is there something wrong with this approach?
I’m having trouble trying to stop gracefully a similar server ( stackoverflow.com/questions/50833730/…). – metalfox 4 hours ago
Here's a minimal change that shows how to handle
an Exit command that closes a session
a Shutdown command that closes the server (so it stops accepting connections and terminates after the last session exits)
Live On Coliru
#include <boost/asio.hpp>
#include <iostream>
using boost::asio::ip::tcp;
using boost::system::error_code;
using boost::asio::streambuf;
int main() {
boost::asio::io_service svc;
tcp::acceptor a(svc);
a.open(tcp::v4());
a.set_option(tcp::acceptor::reuse_address(true));
a.bind({{}, 6767}); // bind to port 6767 on localhost
a.listen(5);
using session = std::shared_ptr<tcp::socket>;
std::function<void()> do_accept;
std::function<void(session)> do_session;
do_session = [&](session s) {
// do a read
auto buf = std::make_shared<boost::asio::streambuf>();
async_read_until(*s, *buf, "\n", [&,s,buf](error_code ec, size_t /*bytes*/) {
if (ec)
std::cerr << "read failed: " << ec.message() << "\n";
else {
std::istream is(buf.get());
std::string line;
while (getline(is, line)) // FIXME being sloppy with partially read lines
{
async_write(*s, boost::asio::buffer("Ack\n", 4), [&,s,buf](error_code ec, size_t) {
if (ec) std::cerr << "write failed: " << ec.message() << "\n";
});
if (line == "Exit") {
std::cout << "Exit received\n";
return;
}
if (line == "Shutdown") {
std::cout << "Server shutdown requested\n";
a.close();
return;
}
}
do_session(s); // full duplex, can read while writing, using a second buffer
}
});
};
do_accept = [&] {
auto s = std::make_shared<session::element_type>(svc);
a.async_accept(*s, [&,s](error_code ec) {
if (ec)
std::cerr << "accept failed: " << ec.message() << "\n";
else {
do_session(s);
do_accept(); // accept the next
}
});
};
do_accept(); // kick-off
svc.run(); // wait for shutdown (Ctrl-C or failure)
}
Note the sample sessions
echo -en "hello world\nExit\n" | netcat 127.0.0.1 6767
echo -en "hello world\nShutdown\n" | netcat 127.0.0.1 6767
Printing
Ack
Ack
Ack
Ack
Exit received
Server shutdown requested
accept failed: Operation canceled
A Terminate Command
If you want a "Terminate" command that actively closes all open sessions and shuts down the server, you'll have to
keep a list of sessions
or use signal
You can see code for both approaches here: Boost ASIO: Send message to all connected clients
The simplest way to integrate with the current sample:
Live On Coliru
#include <boost/asio.hpp>
#include <iostream>
#include <list>
using boost::asio::ip::tcp;
using boost::system::error_code;
using boost::asio::streambuf;
int main() {
boost::asio::io_service svc;
tcp::acceptor a(svc);
a.open(tcp::v4());
a.set_option(tcp::acceptor::reuse_address(true));
a.bind({{}, 6767}); // bind to port 6767 on localhost
a.listen(5);
using session = std::shared_ptr<tcp::socket>;
using sessref = std::weak_ptr<tcp::socket>;
std::function<void()> do_accept;
std::function<void(session)> do_session;
std::list<sessref> session_list;
auto garbage_collect_sessions = [&session_list] {
session_list.remove_if(std::mem_fn(&sessref::expired));
};
do_session = [&](session s) {
// do a read
auto buf = std::make_shared<boost::asio::streambuf>();
async_read_until(*s, *buf, "\n", [&,s,buf](error_code ec, size_t /*bytes*/) {
if (ec)
std::cerr << "read failed: " << ec.message() << "\n";
else {
std::istream is(buf.get());
std::string line;
while (getline(is, line)) // FIXME being sloppy with partially read lines
{
async_write(*s, boost::asio::buffer("Ack\n", 4), [&,s,buf](error_code ec, size_t) {
if (ec) std::cerr << "write failed: " << ec.message() << "\n";
});
if (line == "Exit") {
std::cout << "Exit received\n";
return;
}
if (line == "Shutdown") {
std::cout << "Server shutdown requested\n";
a.close();
return;
}
if (line == "Terminate") {
std::cout << "Server termination requested\n";
a.close();
for (auto wp : session_list) {
if (auto session = wp.lock())
session->close();
}
return;
}
}
do_session(s); // full duplex, can read while writing, using a second buffer
}
});
};
do_accept = [&] {
auto s = std::make_shared<session::element_type>(svc);
a.async_accept(*s, [&,s](error_code ec) {
if (ec)
std::cerr << "accept failed: " << ec.message() << "\n";
else {
garbage_collect_sessions();
session_list.push_back(s);
do_session(s);
do_accept(); // accept the next
}
});
};
do_accept(); // kick-off
svc.run(); // wait for shutdown (Ctrl-C or failure)
}
Which obvioiusly uses a session_list to implement the "Terminate" command:
if (line == "Terminate") {
std::cout << "Server termination requested\n";
a.close();
for (auto wp : session_list) {
if (auto session = wp.lock())
session->close();
}
return;
}
Related
when I build it, and running server and then run client, that appear a error
error code = 2, error message = End of file
when I code synchronous tcp server it's work ok;
thanks
full client code
#include <boost/predef.h> // Tools to identify the os
#ifdef BOOST_OS_WINDOWS
#define _WIN32_WINNT 0x0501
#if _WIN32_WINNT <= 0x0502
#define BOOST_ASIO_DISABLE_TOCP
#define BOOST_ASIO_ENABLE_CANCELIO
#endif
#endif
#include <boost/asio.hpp>
#include <mutex>
#include <thread>
#include <memory>
#include <iostream>
#include <map>
using namespace boost;
typedef void(*Callback) (unsigned int request_id, const std::string& response, const system::error_code& ec);
struct Session{
Session(asio::io_service& ios, const std::string& raw_ip_address, unsigned short port_num, const std::string& request, unsigned int id, Callback callback) : m_sock(ios), m_ep(asio::ip::address::from_string(raw_ip_address),port_num), m_request(request), m_id(id), m_callback(callback), m_was_cancelled(false) {}
asio::ip::tcp::socket m_sock;
asio::ip::tcp::endpoint m_ep; // Remote endpoint
std::string m_request;
// streambuf where the response will be stored.
asio::streambuf m_response_buf;
std::string m_response; // Response represented as a string
system::error_code m_ec;
unsigned int m_id;
Callback m_callback;
bool m_was_cancelled;
std::mutex m_cancel_guard;
};
class AsyncTCPClient : public boost::asio::noncopyable {
public:
AsyncTCPClient(){
m_work.reset(new boost::asio::io_service::work(m_ios));
m_thread.reset(new std::thread([this](){
m_ios.run();
}));
}
void emulateLongComputationOp( unsigned int duration_sec, const std::string& raw_ip_address, unsigned short port_num, Callback callback, unsigned int request_id){
std::string request = "EMULATE_LONG_CALC_OP " + std::to_string(duration_sec) + "\n";
std::cout << "Request: " << request << std::endl;
std::shared_ptr<Session> session = std::shared_ptr<Session> (new Session(m_ios, raw_ip_address, port_num, request, request_id, callback));
session->m_sock.open(session->m_ep.protocol());
// active sessions list can be accessed from multiple thread, we guard it with a mutex to avoid data coruption
std::unique_lock<std::mutex> lock(m_active_sessions_guard);
m_active_sessions[request_id] = session;
lock.unlock();
session->m_sock.async_connect(session->m_ep, [this, session](const system::error_code& ec) {
if (ec.value() != 0) {
session->m_ec = ec;
onRequestComplete(session);
return;
}
std::unique_lock<std::mutex> cancel_lock(session->m_cancel_guard);
if (session->m_was_cancelled) {
onRequestComplete(session);
return;
}
asio::async_write(session->m_sock, asio::buffer(session->m_request), [this, session](const boost::system::error_code &ec, std::size_t bytes_transferred) {
if (ec.value() != 0) {
session->m_ec = ec;
onRequestComplete(session);
return;
}
std::unique_lock<std::mutex> cancel_lock(session->m_cancel_guard);
if (session->m_was_cancelled) {
onRequestComplete(session);
return;
}
asio::async_read_until(session->m_sock, session->m_response_buf, '\n',
[this, session](const boost::system::error_code &ec,
std::size_t bytes_transferred) {
if (ec.value() != 0) {
session->m_ec = ec;
} else {
std::istream strm(&session->m_response_buf);
std::getline(strm, session->m_response);
}
onRequestComplete(session);
});
});
});
};
// Cancels the request
void cancelRequest(unsigned int request_id){
std::unique_lock<std::mutex> lock(m_active_sessions_guard);
auto it = m_active_sessions.find(request_id);
if(it != m_active_sessions.end()){
std::unique_lock<std::mutex> cancel_lock(it->second->m_cancel_guard);
it->second->m_was_cancelled = true;
it->second->m_sock.cancel();
}
}
void close(){
// Destroy work object
m_work.reset(NULL);
// wait for the I/O thread tot exit
m_thread->join();
}
private:
void onRequestComplete(std::shared_ptr<Session> session){
// shutting down the connection, we don't care about the error code if function failed
boost::system::error_code ignored_ec;
session->m_sock.shutdown(asio::ip::tcp::socket::shutdown_both, ignored_ec);
// remove session from the map of active sessions
std::unique_lock<std::mutex> lock(m_active_sessions_guard);
auto it = m_active_sessions.find(session->m_id);
if(it != m_active_sessions.end()){
m_active_sessions.erase(it);
}
lock.unlock();
boost::system::error_code ec;
if(session->m_ec.value() == 0 && session->m_was_cancelled){
ec = asio::error::operation_aborted;
}else{
ec = session->m_ec;
}
session->m_callback(session->m_id, session->m_response, ec);
};
private:
asio::io_service m_ios;
std::map<int, std::shared_ptr<Session>> m_active_sessions;
std::mutex m_active_sessions_guard;
std::unique_ptr<boost::asio::io_service::work> m_work;
std::unique_ptr<std::thread> m_thread;
};
void handler(unsigned int request_id, const std::string& response, const system::error_code& ec){
if(ec.value() == 0){
std::cout << "Request #" << request_id << " has completed. Reponse: "<< response << std::endl;
}else if(ec == asio::error::operation_aborted){
std::cout << "Request #" << request_id << " has been cancelled by the user. " << std::endl;
}else{
std::cout << "Request #" << request_id << " failed! Error code = " << ec.value() << ". Error Message = " << ec.message() << std::endl;
}
return;
}
int main(){
try{
AsyncTCPClient client;
// emulate the user's behavior
client.emulateLongComputationOp(10, "127.0.0.1", 3333, handler, 1);
std::this_thread::sleep_for(std::chrono::seconds(60));
// another request with id 2
client.emulateLongComputationOp(11, "127.0.0.1", 3334, handler, 2);
// cancel request 1
client.cancelRequest(1);
std::this_thread::sleep_for(std::chrono::seconds(6));
// another request with id 3
client.emulateLongComputationOp(12, "127.0.0.1", 3335, handler, 3);
std::this_thread::sleep_for(std::chrono::seconds(15));
// exit the application
client.close();
}
catch(system::system_error &e){
std::cout << "Error occured! Error code = " << e.code() << ". Message: " << e.what();
return e.code().value();
}
return 0;
}
full server code
#include <boost/asio.hpp>
#include <thread>
#include <atomic>
#include <memory>
#include <iostream>
using namespace boost;
class Service {
public:
Service(std::shared_ptr<asio::ip::tcp::socket> sock) : m_sock(sock) {}
void StartHandling() {
asio::async_read_until(*m_sock.get(), m_request, '\n', [this](const boost::system::error_code& ec, std::size_t bytes_transferred){
onRequestReceived(ec, bytes_transferred);
});
std::istream is(&m_request);
std::string line;
std::getline(is, line);
std::cout << "m_request: " << line << std::endl;
}
private:
void onRequestReceived(const boost::system::error_code& ec, std::size_t bytes_transfered){
std::cout << "ec.value : " << ec.value() << std::endl;
if (ec.value() != 0){
std::cout << "Error occurred! Error code = " << ec.value() << ".Message: " << ec.message();
onFinish();
return;
}
// Process the request
asio::async_write(*m_sock.get(), asio::buffer(m_response), [this](const boost::system::error_code& ec, std::size_t bytes_transferred){
onResponseSent(ec, bytes_transferred);
});
}
void onResponseSent(const boost::system::error_code& ec, std::size_t bytes_transferred){
if(ec.value() != 0){
std::cout << "Error occurred! Error code = " << ec.value() << ". Message: " << ec.message();
}
onFinish();
}
// cleanup
void onFinish(){
delete this;
}
std::string ProcessingRequest(asio::streambuf& request){
// parse the request, process it and prepare the request
// Emulating CPU-consuming operations
int i = 0;
while (i != 1000){
i++;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
std::string response = "Response\n";
return response;
}
std::shared_ptr<asio::ip::tcp::socket> m_sock;
std::string m_response;
asio::streambuf m_request;
};
class Acceptor {
public:
Acceptor(asio::io_service& ios, unsigned short port_num) : m_ios(ios), m_acceptor(m_ios, asio::ip::tcp::endpoint(asio::ip::address_v4::any(), port_num)), m_isStopped(
false) {}
// Start accepting incoming connection request.
void Start(){
m_acceptor.listen();
InitAccept();
}
void Stop() {
m_isStopped.store(true);
}
private:
void InitAccept() {
std::shared_ptr<asio::ip::tcp::socket> sock(new asio::ip::tcp::socket(m_ios));
m_acceptor.async_accept(*sock.get(), [this, sock](const boost::system::error_code& error){
onAccept(error, sock);
});
}
void onAccept(const boost::system::error_code& ec, std::shared_ptr<asio::ip::tcp::socket> sock){
if(ec.value() == 0){
(new Service(sock))->StartHandling();
}else{
std::cout << "Error occurred! Error code = " << ec.value() << ". Message: " << ec.message();
}
// Init next accept operation if acceptor has not been stopped yet
if(!m_isStopped.load()){
InitAccept();
}else{
// free resources
m_acceptor.close();
}
}
private:
asio::io_service& m_ios;
asio::ip::tcp::acceptor m_acceptor;
std::atomic<bool> m_isStopped;
};
class Server{
public:
Server() {
m_work.reset(new asio::io_service::work(m_ios));
}
// Start the server
void Start(unsigned short port_num, unsigned int thread_pool_size){
assert(thread_pool_size > 0);
// Create and start Acceptor
acc.reset(new Acceptor(m_ios, port_num));
acc->Start();
// Create specified number of thread and add them to the pool
for(unsigned int i = 0; i < thread_pool_size; i++){
std::cout << "Thread " << i << " Running !";
std::unique_ptr<std::thread> th(new std::thread([this](){
m_ios.run();
}));
m_thread_pool.push_back(std::move(th));
}
}
// Stop the Server
void Stop(){
acc->Stop();
m_ios.stop();
for(auto& th : m_thread_pool){
th->join();
}
}
private:
asio::io_service m_ios;
std::unique_ptr<asio::io_service::work> m_work;
std::unique_ptr<Acceptor> acc;
std::vector<std::unique_ptr<std::thread>> m_thread_pool;
};
const unsigned int DEFAULT_THREAD_POOL_SIZE = 2;
int main(){
unsigned short port_num = 3333;
try{
Server srv;
unsigned int thread_pool_size = std::thread::hardware_concurrency() * 2;
if (thread_pool_size == 0){
thread_pool_size = DEFAULT_THREAD_POOL_SIZE;
}
srv.Start(port_num, thread_pool_size);
std::this_thread::sleep_for(std::chrono::seconds(60));
srv.Stop();
}
catch(system::system_error &e){
std::cout << "Error occurred! Error code = " << e.code() << ". Message: " << e.what();
}
return 0;
}
The server closes the connection after sending the (empty) response. That leads to EOF on the client, naturally. Just handle it.
There's loads of code smells
delete this; is an abomination, just make Service shared_from_this.
No need to use shared_ptrs other than that
When you use smart pointers, use them. Don't "convert to raw pointer" just to dereference (so *m_socket instead of *m_socket.get()).
In fact, there should be no need to use new, delete or get() in your code
You are accessing the m_request immediately after async_read_until which is too early,
it is a data race (so Undefined Behaviour)
it doesn't get the request, because async_read_until didn't complete yet.
So move that code into onRequestReceived at a minimum
It's pretty unnecessary to use an istream to read the line from the request when you already have bytes_transferred. I'd suggest
if (bytes_transferred) {
std::string line(m_request.data().data(), bytes_transferred - 1);
m_request.consume(bytes_transferred);
std::cout << "request: " << line << std::endl;
}
Or even:
std::cout << "request: ";
std::cout.write(asio::buffer_cast<char const*>(m_request.data()),
bytes_transferred - 1);
m_request.consume(bytes_transferred);
Or, if you indeed wanted to show the entire m_request, simply
std::cout << "m_request: " << &m_request << std::endl;
Note that read_until may read more than just including the delimiter; for your safety you might want to validate that no other data is trailing, or process it as well
Never switch on error_code::value(), that loses the error category, which is essential to interpret error codes.
Why unique_ptr for each thread? Just a deque<thread>:
while (thread_pool_size--)
m_thread_pool.emplace_back([this] { m_ios.run(); });
But see Should the exception thrown by boost::asio::io_service::run() be caught?
Why unique_ptr for acceptor?
Why a separate class for acceptor? It's not like the server allows more than 1
why a vector of threads anyways? Prefer boost::thread_group
why a manual thread pool? Prefer asio::thread_pool - which already uses the hardware_concurrency if available
In terms of review, the TCPAsyncClient looks like an attempt to implement async_result protocol. It misses the mark on many points. So I'll just point to something like how do i return the response back to caller asynchronously using a final callback dispatched from on_read handler? or How do I make this HTTPS connection persistent in Beast?. They have pretty similar interfaces (perhaps except for the cancellation, if I remember correctly).
Fixed/Return Demo
Here's the completed sample. It includes request parsing, so the server waits the actual amount of time requested.
I scaled all the times down 10x so it can complete online.
Client and server are in single source. Starting with:
./sotest&
./sotest client
wait
Completes both in 6 seconds (see screengrab below)
Live On Coliru
#include <boost/asio.hpp>
#include <boost/spirit/home/x3.hpp> // for request parsing
#include <iomanip>
#include <iostream>
#include <map>
#include <mutex>
#include <thread>
namespace asio = boost::asio;
using asio::ip::tcp;
using boost::system::error_code;
using namespace std::chrono_literals;
using std::this_thread::sleep_for;
/////// server //////////////////////////////////////////////////////////
struct Service : std::enable_shared_from_this<Service> {
Service(tcp::socket sock) : m_sock(std::move(sock)) {}
void StartHandling() {
async_read_until(
m_sock, asio::dynamic_buffer(m_request), '\n',
[this, self = shared_from_this()](error_code ec, size_t bytes) {
onRequestReceived(ec, bytes);
});
}
private:
void onRequestReceived(error_code ec, size_t /*bytes*/) {
std::cout << "onRequestReceived: " << ec.message() << std::endl;
if (ec)
return;
// Process the request
m_response = ProcessingRequest(m_request);
async_write(
m_sock, asio::buffer(m_response),
[this, self = shared_from_this()](error_code ec, size_t bytes) {
onResponseSent(ec, bytes);
});
}
void onResponseSent(error_code ec, size_t /*bytes*/) {
std::cout << "onResponseSent: " << ec.message() << std::endl;
}
std::string static ProcessingRequest(std::string request) {
std::cout << "request: " << request << std::endl;
// parse the request, process it and prepare the response
namespace x3 = boost::spirit::x3;
double value;
if (parse(request.begin(), request.end(),
"EMULATE_LONG_CALC_OP " >> x3::double_ >> "s" >> x3::eol >> x3::eoi,
value)) //
{
// Emulating time-consuming operation
sleep_for(1.0s * value);
return "Waited " + std::to_string(value) + "s\n";
}
return "Unknown request\n";
}
tcp::socket m_sock;
std::string m_request, m_response;
};
struct Server {
Server(asio::any_io_executor ex, uint16_t port_num)
: m_acceptor{ex, {{}, port_num}} {
m_acceptor.listen();
accept_loop();
}
void Stop() { m_acceptor.cancel(); }
private:
void accept_loop() {
m_acceptor.async_accept([this](error_code ec, tcp::socket sock) {
std::cout << "OnAccept: " << ec.message() << std::endl;
if (!ec) {
std::make_shared<Service>(std::move(sock))->StartHandling();
accept_loop();
}
//m_acceptor.close();
});
}
tcp::acceptor m_acceptor;
};
void server(uint16_t port) try {
asio::thread_pool io;
Server srv{io.get_executor(), port};
sleep_for(6s);
srv.Stop();
io.join();
} catch (std::exception const& e) {
std::cout << "Exception: " << e.what() << std::endl;
}
/////// client //////////////////////////////////////////////////////////
struct RequestOp : public std::enable_shared_from_this<RequestOp> {
using Callback = std::function<void( //
unsigned /*request_id*/, std::string_view /*response*/, error_code)>;
RequestOp(asio::any_io_executor ex, const std::string& raw_ip_address,
uint16_t port_num, std::string request, unsigned id,
Callback callback)
: m_ep(asio::ip::address::from_string(raw_ip_address), port_num)
, m_sock(ex, m_ep.protocol())
, m_request(std::move(request))
, m_id(id)
, m_callback(callback) {}
void Run() {
// assumed on logical strand
m_sock.async_connect(
m_ep, [this, self = shared_from_this()](error_code ec) {
if ((m_ec = ec) || m_was_cancelled)
return onComplete();
asio::async_write(m_sock, asio::buffer(m_request),
[this, self = shared_from_this()](
error_code ec, size_t /*bytes*/) {
onRequestWritten(ec);
});
});
}
void Cancel() {
m_was_cancelled = true;
dispatch(m_sock.get_executor(), [self=shared_from_this()]{ self->doCancel(); });
}
private:
void doCancel() {
m_sock.cancel();
}
void onRequestWritten(error_code ec) {
if ((m_ec = ec) || m_was_cancelled)
return onComplete();
asio::async_read_until(
m_sock, asio::dynamic_buffer(m_response), '\n',
[this, self = shared_from_this()](error_code ec, size_t bytes) {
onResponseReceived(ec, bytes);
});
}
void onResponseReceived(error_code ec, size_t /*bytes*/) {
if ((m_ec = ec) || m_was_cancelled)
return onComplete();
if (!m_response.empty())
m_response.resize(m_response.size() - 1); // drop '\n'
onComplete();
}
void onComplete() {
// shutting down the connection, we don't care about the error code
// if function failed
error_code ignored_ec;
m_sock.shutdown(tcp::socket::shutdown_both, ignored_ec);
if(!m_ec && m_was_cancelled){
m_ec = asio::error::operation_aborted;
}
m_callback(m_id, m_response, m_ec);
}
tcp::endpoint m_ep; // Remote endpoint
tcp::socket m_sock;
std::string m_request;
std::string m_response; // Response represented as a string
error_code m_ec;
unsigned m_id;
Callback m_callback;
std::atomic_bool m_was_cancelled{false};
};
class AsyncTCPClient {
public:
AsyncTCPClient(asio::any_io_executor ex) : m_executor(ex) {}
using Duration = std::chrono::steady_clock::duration;
size_t emulateLongCalcOp(Duration delay, std::string const& raw_ip_address,
uint16_t port_num, RequestOp::Callback callback) {
auto request =
"EMULATE_LONG_CALC_OP " + std::to_string(delay / 1.0s) + "s\n";
std::cout << "Request: " << request << std::flush;
auto const request_id = m_nextId++;
auto session = std::make_shared<RequestOp>(
make_strand(m_executor), //
raw_ip_address, port_num, request, request_id, callback);
{
// active sessions list can be accessed from multiple thread, we
// guard it with a mutex to avoid data coruption
std::unique_lock lock(m_active_sessions_guard);
auto [_,ok] = m_pending_ops.emplace(request_id, session);
assert(ok); // duplicate request_id?
// optionally: garbage collect completed sessions
std::erase_if(m_pending_ops,
[](auto& kv) { return kv.second.expired(); });
};
session->Run();
return request_id;
}
// Cancels the request
void cancelRequest(unsigned request_id) {
std::unique_lock lock(m_active_sessions_guard);
if (auto session = m_pending_ops[request_id].lock())
session->Cancel();
}
private:
using PendingOp = std::weak_ptr<RequestOp>;
asio::any_io_executor m_executor;
std::mutex m_active_sessions_guard;
size_t m_nextId = 1;
std::map<int, PendingOp> m_pending_ops;
};
void handler(unsigned request_id, std::string_view response, error_code ec) {
std::cout << "Request #" << request_id << " ";
if (!ec.failed())
std::cout << "Response: " << std::quoted(response) << std::endl;
else if (ec == asio::error::operation_aborted)
std::cout << "Cancelled" << std::endl;
else
std::cout << ec.message() << std::endl;
}
void client(uint16_t port) try {
asio::thread_pool io;
{
AsyncTCPClient client(io.get_executor());
auto id1 = client.emulateLongCalcOp(4s, "127.0.0.1", port, handler);
auto id2 = client.emulateLongCalcOp(1100ms, "127.0.0.1", port, handler);
auto id3 = client.emulateLongCalcOp(3500ms, "127.0.0.1", port, handler);
// cancel request 1
sleep_for(3s);
client.cancelRequest(id1);
sleep_for(1200ms);
client.cancelRequest(id2); // no effect, already completed
client.cancelRequest(id3); // no effect, already completed
// exit the application
}
io.join();
} catch (std::exception const& e) {
std::cout << "Exception: " << e.what() << std::endl;
}
/////// main //////////////////////////////////////////////////////////
int main(int argc, char**) {
if (argc > 1)
client(3333);
else
server(3333);
}
Prints client:
Request: EMULATE_LONG_CALC_OP 4.000000s
Request: EMULATE_LONG_CALC_OP 1.100000s
Request: EMULATE_LONG_CALC_OP 3.500000s
Request #2 Response: "Waited 1.100000s"
Request #1 Cancelled
Request #3 Response: "Waited 3.500000s"
Prints server:
OnAccept: Success
OnAccept: Success
onRequestReceived: Success
request: EMULATE_LONG_CALC_OP 1.100000s
onRequestReceived: Success
request: EMULATE_LONG_CALC_OP 4.000000s
OnAccept: Success
onRequestReceived: Success
request: EMULATE_LONG_CALC_OP 3.500000s
onResponseSent: Success
onResponseSent: Success
onResponseSent: Success
OnAccept: Operation canceled
I'm trying to write server with boost::asio but I want that the boost::asio::async_read operation to be with time out if no data is coming, but i can figure how to do it.
this is my code so far
void do_read_header() {
auto self(shared_from_this());
std::cout << "do_read_header\n";
boost::asio::async_read(
socket_, boost::asio::buffer(res.data(), res.header_length),
[this, self](boost::system::error_code ec,
std::size_t length) {
if (!ec && res.decode_header()) {
do_read_body();
}
});
do_write();
}
void do_read_body() {
auto self(shared_from_this());
Message msg;
std::cout << "do_read_body\n";
boost::asio::async_read(
socket_, boost::asio::buffer(res.body(), res.body_length()),
[this, self](boost::system::error_code ec,
std::size_t length) {
if (!length) {
return;
}
if (!ec) {
try {
std::cout << "read " << res.body() << "\n";
request_queue_.send(res.body(), res.body_length(),
0);
} catch (const std::exception& ex) {
std::cout << ex.what() << "\n";
}
} else {
if (ec) {
std::cerr << "read error:" << ec.value()
<< " message: " << ec.message() << "\n";
}
socket_.close();
}
do_read_header();
});
}
void start() {
post(strand_, [this, self = shared_from_this()] {
do_read_header();
do_write();
});
}
class Server {
public:
Server(boost::asio::io_service& io_service, short port)
: acceptor_(io_service, tcp::endpoint(tcp::v4(), port)),
socket_(io_service) {
do_accept();
}
private:
void do_accept() {
acceptor_.async_accept(
socket_, [this](boost::system::error_code ec) {
if (!ec) {
std::cout << "accept connection\n";
std::make_shared<Session>(std::move(socket_))
->start();
}
do_accept();
});
}
tcp::acceptor acceptor_;
tcp::socket socket_;
};
You can add a deadline timer that cancels the IO operation. You can observe the cancellation because the completion will be called with error::operation_aborted.
deadline_.expires_from_now(1s);
deadline_.async_wait([self, this] (error_code ec) {
if (!ec) socket_.cancel();
});
I spent about 45 minutes making the rest of your code self-contained:
in this example I'll assume that we
want to wait for max 5s for a new header to arrive (so after a new session was started or until the next request arrives on the same session)
after which the fullbody must be received within 1s
Note also that we avoid closing the socket - that's done in the session's destructor. It's better to shutdown gracefully.
Live Demo
#include <boost/asio.hpp>
#include <boost/endian/arithmetic.hpp>
#include <boost/interprocess/ipc/message_queue.hpp>
#include <iomanip>
#include <iostream>
using namespace std::chrono_literals;
namespace bip = boost::interprocess;
using boost::asio::ip::tcp;
using boost::system::error_code;
using Queue = boost::interprocess::message_queue;
static constexpr auto MAX_MESG_LEN = 100;
static constexpr auto MAX_MESGS = 10;
struct Message {
using Len = boost::endian::big_uint32_t;
struct header_t {
Len len;
};
static const auto header_length = sizeof(header_t);
std::array<char, MAX_MESG_LEN + header_length> buf;
char const* data() const { return buf.data(); }
char* data() { return buf.data(); }
char const* body() const { return data() + header_length; }
char* body() { return data() + header_length; }
static_assert(std::is_standard_layout_v<header_t> and
std::is_trivial_v<header_t>);
Len body_length() const { return std::min(h().len, max_body_length()); }
Len max_body_length() const { return buf.max_size() - header_length; }
bool decode_header() { return h().len <= max_body_length(); }
bool set_body(std::string_view value) {
assert(value.length() <= max_body_length());
h().len = value.length();
std::copy_n(value.begin(), body_length(), body());
return (value.length() == body_length()); // not truncated
}
private:
header_t& h() { return *reinterpret_cast<header_t*>(data()); }
header_t const& h() const { return *reinterpret_cast<header_t const*>(data()); }
};
struct Session : std::enable_shared_from_this<Session> {
Session(tcp::socket&& s) : socket_(std::move(s)) {}
void start() {
post(strand_,
[ this, self = shared_from_this() ] { do_read_header(); });
}
private:
using Strand = boost::asio::strand<tcp::socket::executor_type>;
using Timer = boost::asio::steady_timer;
tcp::socket socket_{strand_};
Strand strand_{make_strand(socket_.get_executor())};
Message res;
Queue request_queue_{bip::open_or_create, "SendQueue", MAX_MESGS, MAX_MESG_LEN};
Timer recv_deadline_{strand_};
void do_read_header() {
auto self(shared_from_this());
std::cout << "do_read_header: " << res.header_length << std::endl;
recv_deadline_.expires_from_now(5s);
recv_deadline_.async_wait([ self, this ](error_code ec) {
if (!ec) {
std::cerr << "header timeout" << std::endl;
socket_.cancel();
}
});
boost::asio::async_read(
socket_, boost::asio::buffer(res.data(), res.header_length),
[ this, self ](error_code ec, size_t /*length*/) {
std::cerr << "header: " << ec.message() << std::endl;
recv_deadline_.cancel();
if (!ec && res.decode_header()) {
do_read_body();
} else {
socket_.shutdown(tcp::socket::shutdown_both);
}
});
}
void do_read_body() {
auto self(shared_from_this());
// Message msg;
std::cout << "do_read_body: " << res.body_length() << std::endl;
recv_deadline_.expires_from_now(1s);
recv_deadline_.async_wait([self, this] (error_code ec) {
if (!ec) {
std::cerr << "body timeout" << std::endl;
socket_.cancel();
}
});
boost::asio::async_read(
socket_,
boost::asio::buffer(res.body(), res.body_length()),
boost::asio::transfer_exactly(res.body_length()),
[ this, self ](error_code ec, std::size_t length) {
std::cerr << "body: " << ec.message() << std::endl;
recv_deadline_.cancel();
if (!ec) {
try {
// Not safe to print unless NUL-terminated, see e.g.
// https://stackoverflow.com/questions/66278813/boost-deadline-timer-causes-stack-buffer-overflow/66279497#66279497
if (length)
request_queue_.send(res.body(), res.body_length(), 0);
} catch (const std::exception& ex) {
std::cout << ex.what() << std::endl;
}
do_read_header();
} else {
socket_.shutdown(tcp::socket::shutdown_both);
}
});
}
};
class Server {
public:
Server(boost::asio::io_service& io_service, short port)
: acceptor_(io_service, tcp::endpoint(tcp::v4(), port)),
socket_(io_service) {
do_accept();
}
private:
void do_accept() {
acceptor_.async_accept(socket_, [ this ](error_code ec) {
std::cerr << "async_accept: " << ec.message() << std::endl;
if (!ec) {
std::cerr << "session: " << socket_.remote_endpoint() << std::endl;
std::make_shared<Session>(std::move(socket_))->start();
}
do_accept();
});
}
tcp::acceptor acceptor_;
tcp::socket socket_;
};
int main(int argc, char**) {
Queue queue{bip::open_or_create, "SendQueue", MAX_MESGS, MAX_MESG_LEN}; // ensure it exists
if (argc == 1) {
boost::asio::io_context ioc;
Server s(ioc, 8989);
ioc.run_for(10s);
} else {
while (true) {
using Buf = std::array<char, MAX_MESG_LEN>;
Buf buf;
unsigned prio;
size_t n;
queue.receive(buf.data(), buf.size(), n, prio);
std::cout << "Received: " << std::quoted(std::string_view(buf.data(), n)) << std::endl;
}
}
}
Testable with
./sotest
In another terminal:
./sotest consumer
And somewhere else e.g. some requests that don't timeout:
for msg in '0000 0000' '0000 0001 31' '0000 000c 6865 6c6c 6f20 776f 726c 640a'
do
xxd -r -p <<< "$msg" |
netcat localhost 8989 -w 1
done
Or, multi-request on single session, then session times out (-w 6 exceeds 5s):
msg='0000 0000 0000 0001 31 0000 000c 6865 6c6c 6f20 776f 726c 640a'; xxd -r -p <<< "$msg"| netcat localhost 8989 -w 6
I had to solve a similar issue when implementing the Serial Port wrapper and this is the simplified version of the code:
// Possible outcome of a read. Set by callback
enum class ReadResult
{
ResultInProgress,
ResultSuccess,
ResultError,
ResultTimeout
};
std::streamsize read(char* s, std::streamsize n, boost::posix_time::time_duration timeout)
{
boost::asio::io_service io;
boost::asio::serial_port port(io);
// result is atomic to avoid race condition
std::atomic<ReadResult> result(ReadResult::ResultInProgress);
std::streamsize bytesTransferred = 0;
// Create async timer that fires after duration
boost::asio::deadline_timer timer(io);
timer.expires_from_now(timeout);
timer.async_wait(boost::bind([&result](const boost::system::error_code& error){
// If there wasn't any error and reading is still in progress, set result as timeout
if (!error && result == ReadResult::ResultInProgress)
result = ReadResult::ResultTimeout;
},boost::asio::placeholders::error));
// Read asynchronously
port.async_read_some(boost::asio::buffer(s, n), boost::bind([&result,&bytesTransferred](const boost::system::error_code& error,
const size_t transferred){
// If there wasn't any error on read finish set result as sucess else as error
if (!error){
result = ReadResult::ResultSuccess;
bytesTransferred = transferred;
return;
}
result = ReadResult::ResultError;
},boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred));
// Run loop until timeout, error or success occurs
for (;;)
{
io.run_one();
switch (result)
{
case ReadResult::ResultSuccess:
// Success, cancel timer and return amount of bytes read
timer.cancel();
return bytesTransferred;
case ReadResult::ResultTimeout:
// Timeout occured, cancel read and throw exception
port.cancel();
throw(TimeoutException("Timeout expired"));
case ReadResult::ResultError:
// Error occured, cancel read and timer and throw exception
port.cancel();
timer.cancel();
throw(std::ios_base::failure("Error while reading"));
default:
//if result is still in progress remain in the loop
break;
}
}
}
So basically, what you have to do is :
initialize timer with io_service
call async_wait on a timer with a callback function that sets result timeout flag
call async_read on your socket with a callback that sets result success flag
loop until result is no longer "InProgress", note that io.run_one() in loop is important
handle result the result
you could use it for any asynchronous function
I am writing a client/server program in boost TCP in which I want to send a HEARTBEAT message to the client every 2 seconds for which I am trying to create a new thread by which I can send it easily but unable to solve it. I am creating thread using boost::thread t(hearbeatSender,sock); this. but giving lots of errors. I also use bind to bind function name with the socket but not resolved the error.
void process(boost::asio::ip::tcp::socket & sock);
std::string read_data(boost::asio::ip::tcp::socket & sock);
void write_data(boost::asio::ip::tcp::socket & sock,std::string);
void hearbeatSender(boost::asio::ip::tcp::socket & sock);
int main()
{
unsigned short port_num = 3333;
boost::asio::ip::tcp::endpoint ep(boost::asio::ip::address_v4::any(), port_num);
boost::asio::io_service io;
try
{
boost::asio::ip::tcp::acceptor acceptor(io, ep.protocol());
acceptor.bind(ep);
acceptor.listen();
boost::asio::ip::tcp::socket sock(io);
acceptor.accept(sock);
boost::thread t(hearbeatSender,sock);
process(sock);
t.join();
}
catch (boost::system::system_error &e)
{
std::cout << "Error occured! Error code = " << e.code()
<< ". Message: " << e.what();
return e.code().value();
}
return 0;
}
void process(boost::asio::ip::tcp::socket & sock)
{
while(1){
std::string data = read_data(sock);
std::cout<<"Client's request is: "<<data<<std::endl;
write_data(sock,data);
}
}
std::string read_data(boost::asio::ip::tcp::socket & sock)
{
boost::asio::streambuf buf;
boost::asio::read_until(sock, buf, "\n");
std::string data = boost::asio::buffer_cast<const char*>(buf.data());
return data;
}
void write_data(boost::asio::ip::tcp::socket & sock,std::string data)
{
boost::system::error_code error;
std::string msg;
int ch = data[0]-'0';
switch(ch)
{
case 1: msg = "Case 1\n"; break;
case 2: msg = "Case 2\n"; break;
case 3: msg = "Case 3\n"; break;
case 4: msg = "Case 4\n"; break;
default: msg = "Case default\n"; break;
}
boost::asio::write( sock, boost::asio::buffer(msg+ "\n"), error );
if( !error ) {
std::cout << "Server sent hello message!" << std::endl;
}
else {
std::cout << "send failed: " << error.message() << std::endl;
}
}
void hearbeatSender(boost::asio::ip::tcp::socket & sock)
{
boost::system::error_code error;
std::string msg = "HEARTBEAT";
while(1)
{
sleep(2);
std::cout<<msg<<std::endl;
boost::asio::write( sock, boost::asio::buffer(msg+ "\n"), error );
if( !error ) {
std::cout << "Server sent HEARTBEAT message!" << std::endl;
}
else {
std::cout << "send failed: " << error.message() << std::endl;
}
}
}
This is a server-side code for responding to the message of the client and sending heartbeat to the client. This is a synchronous TCP server.
Instead of this:
boost::asio::ip::tcp::socket sock(io);
acceptor.accept(sock);
boost::thread t(hearbeatSender,sock);
this:
auto sock = acceptor.accept();
std::thread t([&sock]() {
hearbeatSender(sock);
});
And instead of sleep, just used std::this_thread::sleep for compiling universally.
Here's the complete program that compiles and runs
#include <boost/asio.hpp>
#include <iostream>
void process(boost::asio::ip::tcp::socket& sock);
std::string read_data(boost::asio::ip::tcp::socket& sock);
void write_data(boost::asio::ip::tcp::socket& sock, std::string);
void hearbeatSender(boost::asio::ip::tcp::socket& sock);
int main()
{
unsigned short port_num = 3333;
boost::asio::ip::tcp::endpoint ep(boost::asio::ip::address_v4::any(), port_num);
boost::asio::io_service io;
try
{
boost::asio::ip::tcp::acceptor acceptor(io, ep.protocol());
acceptor.bind(ep);
acceptor.listen();
auto sock = acceptor.accept();
std::thread t([&sock]() {
hearbeatSender(sock);
});
process(sock);
t.join();
}
catch (boost::system::system_error& e)
{
std::cout << "Error occured! Error code = " << e.code()
<< ". Message: " << e.what();
return e.code().value();
}
return 0;
}
void process(boost::asio::ip::tcp::socket& sock)
{
while (1) {
std::string data = read_data(sock);
std::cout << "Client's request is: " << data << std::endl;
write_data(sock, data);
}
}
std::string read_data(boost::asio::ip::tcp::socket& sock)
{
boost::asio::streambuf buf;
boost::asio::read_until(sock, buf, "\n");
std::string data = boost::asio::buffer_cast<const char*>(buf.data());
return data;
}
void write_data(boost::asio::ip::tcp::socket& sock, std::string data)
{
boost::system::error_code error;
std::string msg;
int ch = data[0] - '0';
switch (ch)
{
case 1: msg = "Case 1\n"; break;
case 2: msg = "Case 2\n"; break;
case 3: msg = "Case 3\n"; break;
case 4: msg = "Case 4\n"; break;
default: msg = "Case default\n"; break;
}
boost::asio::write(sock, boost::asio::buffer(msg + "\n"), error);
if (!error) {
std::cout << "Server sent hello message!" << std::endl;
}
else {
std::cout << "send failed: " << error.message() << std::endl;
}
}
void hearbeatSender(boost::asio::ip::tcp::socket& sock)
{
boost::system::error_code error;
std::string msg = "HEARTBEAT";
while (1)
{
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << msg << std::endl;
boost::asio::write(sock, boost::asio::buffer(msg + "\n"), error);
if (!error) {
std::cout << "Server sent HEARTBEAT message!" << std::endl;
}
else {
std::cout << "send failed: " << error.message() << std::endl;
}
}
}
Instead of this:
try
{
boost::asio::ip::tcp::acceptor acceptor(io, ep.protocol());
acceptor.bind(ep);
acceptor.listen();
auto sock = acceptor.accept();
std::thread t([&sock]() {
hearbeatSender(sock);
});
process(sock);
t.join();
}
Use it:
try{
boost::asio::ip::tcp::acceptor acceptor(io, ep.protocol());
acceptor.bind(ep);
acceptor.listen();
boost::asio::ip::tcp::socket sock(io);
acceptor.accept(sock);
std::thread t([&sock]() {
hearbeatSender(sock);
});
process(sock);
t.join();
}
and also include header files:
#include <thread>
#include <chrono>
(Optional) you can also use this_thread::sleep_for instead of sleep()
std::this_thread::sleep_for(std::chrono::seconds(10));
The problem of passing a socket to the thread is solved.
Now, for conversing a HEARTBEAT between a client and a server. Complete code can be checked from here:
Client code HEARTBEAT transfer in every 5 seconds
Server code for giving response to the client
It's more than a little weird to use a heartbeat... "sender" thread with async IO.
What's more, there is no synchronization on the socket object, so that's a data race which is Undefined Behavior.
Finally, this is unsafe:
std::string data = boost::asio::buffer_cast<const char*>(buf.data());
It assumes that the data() will be NUL-terminated (which isn't true).
Typical, Single Threaded ASIO
You would not spawn threads for timers, but use e.g. boost::asio::deadline_timer or boost::asio::highresolution_timer. It can wait asynchronously, so you can do other tasks on the IO service until it expires.
Similarly you can do the request/response reading/writing asynchronously. The only "complicating" factor is that asynchronous calls don't complete before returning, so you have to make sure the buffers live long enough (they should not be a local variable).
Now, you already have a logical "unit" of lifetime that practically JUMPS out of the code at you:
That just screams to be rewritten as
struct LifeTimeUnit {
boost::asio::ip::tcp::socket sock;
void process();
std::string read_data();
void write_data(std::string);
void hearbeatSender(sock);
};
Of course LifeTimeUnit is a funny name, so let's think of a better one: Session seems meaningful!
Now that we have a unit of lifetime, it can handsomely contain other things like buffers and the timer:
struct Session {
Session(tcp::socket&& s) : sock(std::move(s)) {}
void start() {
hb_wait();
req_loop();
}
void cancel() {
hbtimer.cancel();
sock.cancel(); // or shutdown() e.g.
}
private:
bool checked(error_code ec, std::string const& msg = "error") {
if (ec) {
std::clog << msg << ": " << ec.message() << "\n";
cancel();
}
return !ec.failed();;
}
void req_loop(error_code ec = {}) {
if (!checked(ec, "req_loop")) {
async_read_until(sock, buf, "\n",
[this](error_code ec, size_t xfr) { on_request(ec, xfr); });
}
}
void on_request(error_code ec, size_t n) {
if (checked(ec, "on_request")) {
request.resize(n);
buf.sgetn(request.data(), n);
response = "Case " + std::to_string(request.at(0) - '0') + "\n";
async_write(sock, buffer(response),
[this](error_code ec, size_t) { req_loop(ec); });
}
}
void hb_wait(error_code ec = {}) {
if (checked(ec, "hb_wait")) {
hbtimer.expires_from_now(2s);
hbtimer.async_wait([this](error_code ec) { hb_send(ec); });
}
}
void hb_send(error_code ec) {
if (checked(ec, "hb_send")) {
async_write(sock, buffer(hbmsg), [this](error_code ec, size_t) { hb_wait(ec); });
}
}
tcp::socket sock;
boost::asio::high_resolution_timer hbtimer { sock.get_executor() };
const std::string hbmsg = "HEARTBEAT\n";
boost::asio::streambuf buf;
std::string request, response;
};
The only public things are start() (actually we don't have a need for cancel() for now, but you know).
The main program can be much un-altered:
tcp::acceptor acceptor(io, tcp::v4());
acceptor.bind({{}, 3333});
acceptor.listen();
tcp::socket sock(io);
acceptor.accept(sock);
Session sess(std::move(sock));
sess.start(); // does both request loop and the heartbeat
io.run();
No more threads, perfect asynchrony! Using bash and netcat to test:
while sleep 4; do printf "%d request\n" {1..10}; done | netcat localhost 3333
Prints:
host 3333
HEARTBEAT
Case 1
Case 2
Case 3
Case 4
Case 5
Case 6
Case 7
Case 8
Case 9
Case 1
HEARTBEAT
HEARTBEAT
HEARTBEAT
Case 1
Case 2
Case 3
Case 4
Case 5
Case 6
Case 7
Case 8
Case 9
Case 1
^C
After stopping the client, the server exits with
on_request: End of file
hb_send: Operation canceled
Single-Thread / Multi-Session
A big advantage is that now you can accept multiple clients on a single server thread. In fact, thousands of them concurrently without a problem.
int main() {
boost::asio::thread_pool io(1);
try {
tcp::acceptor acceptor(io, tcp::v4());
acceptor.bind({{}, 3333});
acceptor.listen();
std::list<Session> sessions;
while (true) {
tcp::socket sock(io);
acceptor.accept(sock);
auto& sess = sessions.emplace_back(std::move(sock));
sess.start(); // does both request loop and the heartbeat
sessions.remove_if([](Session& s) { return !s.is_active(); });
}
io.join();
} catch (boost::system::system_error& e) {
std::cout << "Error occured! Error code = " << e.code() << ". Message: " << e.code().message() << "\n";
return e.code().value();
}
}
Note how we subtly changed our execution-context to a singleton thread pool.
This means we still run all sessions on a single thread, but that's a different thread than running main(), meaning we can continue to accept connections.
To avoid ever-increasing sessions list, we weed out the inactive ones using a trivially implemented is_active() property.
Note that we can ALMOST force a shutdown by doing
for (auto& sess: sessions)
sess.cancel();
That's ALMOST, because it requires posting the cancel operations on the pool thread:
for (auto& sess: sessions)
post(io, [&sess] { sess.cancel(); });
This is to avoid racing with any tasks on the IO pool
Since only the main thread ever touches sessions there is no need for locking.
Live On Coliru
Testing with
for a in 3 2 1; do (sleep $a; echo "$a request" | nc 127.0.0.1 3333)& done; time wait
Prints:
Case 1
Case 2
Case 3
HEARTBEAT
HEARTBEAT
...
Multi-Threading For The Win?
Now we could add multi-threading. The changes are mild:
we want to associate the socket with a strand (see Why do I need strand per connection when using boost::asio?)
note we already use sock's executor to run the timer
We have to take extra precautions to make all of the public interface in Session thread-safe:
post actions from start() and cancel() on the strand
make the active flag atomic_bool
next up, we simply increase the number of threads in the pool from 1 to, say 10
Note, in practice it rarely makes sense to use more threads than logical cores. Also, in this simple example everything is IO bound, so a single thread probably already serves as well. This is just for demonstration
Live On Coliru
boost::asio::thread_pool io(10);
try {
tcp::acceptor acceptor(io, tcp::v4());
acceptor.set_option(tcp::acceptor::reuse_address(true));
acceptor.bind({{}, 3333});
acceptor.listen();
std::list<Session> sessions;
while (true) {
tcp::socket sock(make_strand(io)); // NOTE STRAND!
// ...
// ...
io.join();
And the changes in Session:
void start() {
active = true;
post(sock.get_executor(), [this]{
hb_wait();
req_loop();
});
}
void cancel() {
post(sock.get_executor(), [this]{
hbtimer.cancel();
sock.cancel(); // or shutdown() e.g.
active = false;
});
}
// ....
std::atomic_bool active {false};
}
I have the following RESTServer implemented using boost::beast. The way the server is started is using
void http_server(tcp::acceptor& acceptor, tcp::socket& socket).
Logically acceptor and socket should logically belong to http_connection class,instead of as a separate function outside. What is the reason it is implemented like this?
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/asio.hpp>
#include <chrono>
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <memory>
#include <string>
namespace beast = boost::beast; // from <boost/beast.hpp>
namespace http = beast::http; // from <boost/beast/http.hpp>
namespace net = boost::asio; // from <boost/asio.hpp>
using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp>
namespace my_program_state
{
std::size_t
request_count()
{
static std::size_t count = 0;
return ++count;
}
std::time_t
now()
{
return std::time(0);
}
}
class http_connection : public std::enable_shared_from_this<http_connection>
{
public:
http_connection(tcp::socket socket)
: socket_(std::move(socket))
{
}
// Initiate the asynchronous operations associated with the connection.
void
start()
{
read_request();
check_deadline();
}
private:
// The socket for the currently connected client.
tcp::socket socket_;
// The buffer for performing reads.
beast::flat_buffer buffer_{8192};
// The request message.
http::request<http::dynamic_body> request_;
// The response message.
http::response<http::dynamic_body> response_;
// The timer for putting a deadline on connection processing.
net::steady_timer deadline_{
socket_.get_executor(), std::chrono::seconds(60)};
// Asynchronously receive a complete request message.
void
read_request()
{
auto self = shared_from_this();
http::async_read(
socket_,
buffer_,
request_,
[self](beast::error_code ec,
std::size_t bytes_transferred)
{
boost::ignore_unused(bytes_transferred);
if(!ec)
self->process_request();
});
}
// Determine what needs to be done with the request message.
void
process_request()
{
response_.version(request_.version());
response_.keep_alive(false);
switch(request_.method())
{
case http::verb::get:
response_.result(http::status::ok);
response_.set(http::field::server, "Beast");
create_response();
break;
default:
// We return responses indicating an error if
// we do not recognize the request method.
response_.result(http::status::bad_request);
response_.set(http::field::content_type, "text/plain");
beast::ostream(response_.body())
<< "Invalid request-method '"
<< std::string(request_.method_string())
<< "'";
break;
}
write_response();
}
// Construct a response message based on the program state.
void
create_response()
{
if(request_.target() == "/count")
{
response_.set(http::field::content_type, "text/html");
beast::ostream(response_.body())
<< "<html>\n"
<< "<head><title>Request count</title></head>\n"
<< "<body>\n"
<< "<h1>Request count</h1>\n"
<< "<p>There have been "
<< my_program_state::request_count()
<< " requests so far.</p>\n"
<< "</body>\n"
<< "</html>\n";
}
else if(request_.target() == "/time")
{
response_.set(http::field::content_type, "text/html");
beast::ostream(response_.body())
<< "<html>\n"
<< "<head><title>Current time</title></head>\n"
<< "<body>\n"
<< "<h1>Current time</h1>\n"
<< "<p>The current time is "
<< my_program_state::now()
<< " seconds since the epoch.</p>\n"
<< "</body>\n"
<< "</html>\n";
}
else
{
response_.result(http::status::not_found);
response_.set(http::field::content_type, "text/plain");
beast::ostream(response_.body()) << "File not found\r\n";
}
}
// Asynchronously transmit the response message.
void
write_response()
{
auto self = shared_from_this();
response_.set(http::field::content_length, response_.body().size());
http::async_write(
socket_,
response_,
[self](beast::error_code ec, std::size_t)
{
self->socket_.shutdown(tcp::socket::shutdown_send, ec);
self->deadline_.cancel();
});
}
// Check whether we have spent enough time on this connection.
void
check_deadline()
{
auto self = shared_from_this();
deadline_.async_wait(
[self](beast::error_code ec)
{
if(!ec)
{
// Close socket to cancel any outstanding operation.
self->socket_.close(ec);
}
});
}
};
// "Loop" forever accepting new connections.
void
http_server(tcp::acceptor& acceptor, tcp::socket& socket)
{
acceptor.async_accept(socket,
[&](beast::error_code ec)
{
if(!ec)
std::make_shared<http_connection>(std::move(socket))->start();
http_server(acceptor, socket);
});
}
int
main(int argc, char* argv[])
{
try
{
// Check command line arguments.
if(argc != 3)
{
std::cerr << "Usage: " << argv[0] << " <address> <port>\n";
std::cerr << " For IPv4, try:\n";
std::cerr << " receiver 0.0.0.0 80\n";
std::cerr << " For IPv6, try:\n";
std::cerr << " receiver 0::0 80\n";
return EXIT_FAILURE;
}
auto const address = net::ip::make_address(argv[1]);
unsigned short port = static_cast<unsigned short>(std::atoi(argv[2]));
net::io_context ioc{1};
tcp::acceptor acceptor{ioc, {address, port}};
tcp::socket socket{ioc};
http_server(acceptor, socket);
ioc.run();
}
catch(std::exception const& e)
{
std::cerr << "Error: " << e.what() << std::endl;
return EXIT_FAILURE;
}
}
One reason would be the author wanted to separate the logic.
Moreover i think it would complicate the procedure to create new sessions if you would move the listener code into the client session.
The acceptor and socket object should stay independent, you may reference it in your client and use it if you want but since these a more "global" and unique objects, it should stay outside of the session. Instead it can be also put into a separate class.
Roughly speaking the acceptor should just listen for incoming connection attempts from remote hosts and create the sessions accordingly.
I have the following code, trying to code an asynchronous client.
The problem is that in main(), the Client gets deleted in the try-catch block, because execution leaves the scope.
I've tried to come up with a solution to this problem, like adding a while(true), but I don't like this approach. Also, I don't prefer a getchar().
Due to the asynchronous nature of the calls, both connect() and loop() returns immediately.
How can I fix this?
#include <iostream>
#include <thread>
#include <string>
#include <boost\asio.hpp>
#include <Windows.h>
#define DELIM "\r\n"
using namespace boost;
class Client {
public:
Client(const std::string& raw_ip_address, unsigned short port_num) :
m_ep(asio::ip::address::from_string(raw_ip_address), port_num), m_sock(m_ios)
{
m_work.reset(new asio::io_service::work(m_ios));
m_thread.reset(new std::thread([this]() {
m_ios.run();
}));
m_sock.open(m_ep.protocol());
}
void connect()
{
m_sock.async_connect(m_ep, [this](const system::error_code& ec)
{
if (ec != 0) {
std::cout << "async_connect() error: " << ec.message() << " (" << ec.value() << ") " << std::endl;
return;
}
std::cout << "Connection to server has been established." << std::endl;
});
}
void loop()
{
std::thread t = std::thread([this]()
{
recv();
});
t.join();
}
void recv()
{
asio::async_read_until(m_sock, buf, DELIM, [this](const system::error_code& ec, std::size_t bytes_transferred)
{
if (ec != 0) {
std::cout << "async_read_until() error: " << ec.message() << " (" << ec.value() << ") " << std::endl;
return;
}
std::istream is(&buf);
std::string req;
std::getline(is, req, '\r');
is.get(); // discard newline
std::cout << "Received: " << req << std::endl;
if (req == "alive") {
recv();
}
else if (req == "close") {
close();
return;
}
else {
send(req + DELIM);
}
});
}
void send(std::string resp)
{
auto s = std::make_shared<std::string>(resp);
asio::async_write(m_sock, asio::buffer(*s), [this, s](const system::error_code& ec, std::size_t bytes_transferred)
{
if (ec != 0) {
std::cout << "async_write() error: " << ec.message() << " (" << ec.value() << ") " << std::endl;
return;
}
else {
recv();
}
});
}
void close()
{
m_sock.close();
m_work.reset();
m_thread->join();
}
private:
asio::io_service m_ios;
asio::ip::tcp::endpoint m_ep;
asio::ip::tcp::socket m_sock;
std::unique_ptr<asio::io_service::work> m_work;
std::unique_ptr<std::thread> m_thread;
asio::streambuf buf;
};
int main()
{
const std::string raw_ip_address = "127.0.0.1";
const unsigned short port_num = 8001;
try {
Client client(raw_ip_address, port_num);
client.connect();
client.loop();
}
catch (system::system_error &err) {
std::cout << "main() error: " << err.what() << " (" << err.code() << ") " << std::endl;
return err.code().value();
}
return 0;
}
You've not really understood how asio works. Typically in the main thread(s) you will call io_service::run() (which will handle all the asynchronous events.)
To ensure the lifetime of the Client, use a shared_ptr<> and ensure this shared pointer is used in the handlers. For example..
io_service service;
{
// Create the client - outside of this scope, asio will manage
// the life time of the client
auto client = make_shared<Client>(service);
client->connect(); // setup the connect operation..
}
// Now run the io service event loop - this will block until there are no more
// events to handle
service.run();
Now you need to refactor your Client code:
class Client : public std::enable_shared_from_this<Client> {
Client(io_service& service): socket_(service) ...
{ }
void connect() {
// By copying the shared ptr to the lambda, the life time of
// Client is guaranteed
socket_.async_connect(endpoint_, [self = this->shared_from_this()](auto ec)
{
if (ec) {
return;
}
// Read
self->read(self);
});
}
void read(shared_ptr<Client> self) {
// By copying the shared ptr to the lambda, the life time of
// Client is guaranteed
asio::async_read_until(socket_, buffer_, DELIM, [self](auto ec, auto size)
{
if (ec) {
return;
}
// Handle the data
// Setup the next read operation
self->read(self)
});
}
};
You have a thread for the read operation - which is not necessary. That will register one async read operation and return immediately. You need to register a new read operation to continue reading the socket (as I've sketched out..)
You can post any function to io_service via post(Handler)
http://www.boost.org/doc/libs/1_60_0/doc/html/boost_asio/reference/io_service/post.html
Then in the main() do something like:
while (!exit) {
io_service.run_one();
}
Or call io_service::run_one or io_service::run in the main()