Boost::Asio :服务器代码仅在某些时候导致 SEGFAULT,似乎与 io_contexts 的破坏有关

Boost::Asio : Server code causes SEGFAULT only some of the time, seemingly related to the destruction of io_contexts

我正在尝试使用 boost asio 制作一个相当简单的客户端-服务器程序。服务器class实现如下:

template<class RequestHandler, class RequestClass>
class Server {
public:
    typedef std::map<std::string, RequestHandler> CommandMap;

    Server(short port, CommandMap commands, RequestClass *request_class_inst)
            : acceptor_(io_context_, tcp::endpoint(tcp::v4(), port))
            , commands_(std::move(commands))
            , request_class_inst_(request_class_inst)
    {
        DoAccept();
    }

    ~Server()
    {
    }

    void Run()
    {
        io_context_.run();
    }

    void RunInBackground()
    {
        std::thread t( [this]{ Run(); });
        t.detach();
    }

    void Kill()
    {
        acceptor_.close();
    }

private:
    boost::asio::io_context io_context_;
    tcp::acceptor acceptor_;
    CommandMap commands_;
    RequestClass *request_class_inst_;

    void DoAccept()
    {
        acceptor_.async_accept(
                [this](boost::system::error_code ec, tcp::socket socket) {
                  if (!ec)
                      std::make_shared<Session<RequestHandler, RequestClass>>
                              (std::move(socket), commands_, request_class_inst_)->Run();
                  DoAccept();
                });
    }
};

除了服务器class,我实现了一个基本的客户端class这样:

class Client {
public:
    /**
     * Constructor, initializes JSON parser and serializer.
     */
    Client()
        : reader_((new Json::CharReaderBuilder)->newCharReader())
    {}

    Json::Value MakeRequest(const std::string &ip_addr, unsigned short port,
                            const Json::Value &request)
    {
        boost::asio::io_context io_context;

        std::string serialized_req = Json::writeString(writer_, request);
        tcp::socket s(io_context);
        tcp::resolver resolver(io_context);
        s.connect({ boost::asio::ip::address::from_string(ip_addr), port });
        boost::asio::write(s, boost::asio::buffer(serialized_req));
        s.shutdown(tcp::socket::shutdown_send);

        error_code ec;
        char reply[2048];
        size_t reply_length = boost::asio::read(s, boost::asio::buffer(reply),
                                                ec);

        std::cout << std::string(reply).substr(0, reply_length) << std::endl;

        Json::Value json_resp;
        JSONCPP_STRING parse_err;
        std::string resp_str(reply);
        if (reader_->parse(resp_str.c_str(), resp_str.c_str() + resp_str.length(),
                           &json_resp, &parse_err))
            return json_resp;

        throw std::runtime_error("Error parsing response.");
    }

    bool IsAlive(const std::string &ip_addr, unsigned short port)
    {
        boost::asio::io_context io_context;
        tcp::socket s(io_context);
        tcp::resolver resolver(io_context);
        try {
            s.connect({boost::asio::ip::address::from_string(ip_addr), port});
        } catch(const boost::wrapexcept<boost::system::system_error> &err) {
            s.close();
            return false;
        }

        s.close();
        return true;
    }

private:
    /// Reads JSON.
    const std::unique_ptr<Json::CharReader> reader_;
    /// Writes JSON.
    Json::StreamWriterBuilder writer_;
};

我已经实现了一个小例子来测试Client::IsAlive:

int main()
{
    auto *request_inst = new RequestClass(1);
    std::map<std::string, RequestClassMethod> commands {
            {"ADD_1", std::mem_fn(&RequestClass::add_n)},
            {"SUB_1", std::mem_fn(&RequestClass::sub_n)}
    };
    Server<RequestClassMethod, RequestClass> s1(5000, commands, request_inst);

    s1.RunInBackground();
    std::vector<Client*> clients(6, new Client());

    s1.Kill();
    // Should output "0" to console.
    std::cout << clients.at(1)->IsAlive("127.0.0.1", 5000);

    return 0;
}

但是,当我尝试 运行 时,输出会有所不同。大约一半的时间,我收到正确的值并且程序以代码 0 退出,但在其他情况下,程序将:(1) 在输出 0 到控制台之前以代码 139 (SEGFAULT) 退出, (2) 将 0 输出到控制台,随后以代码 139 退出,(3) 将 0 输出到控制台,然后挂起,或者 (4) 在向控制台写入任何内容之前挂起。

我不确定是什么导致了这些错误。我希望它与 Server::io_context_ 的销毁和 Server::Kill 的实现有关。这与我将 Server::io_context_ 作为数据成员存储的方式有关吗?

下面显示了一个最小的可重现示例:

#define BOOST_ASIO_HAS_MOVE

#include <cstdlib>
#include <iostream>
#include <memory>
#include <utility>
#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <json/json.h>

using boost::asio::ip::tcp;
using boost::system::error_code;
/// NOTE: This class exists exclusively for unit testing.
class RequestClass {
public:
    /**
     * Initialize class with value n to add sub from input values.
     *
     * @param n Value to add/sub from input values.
     */
    explicit RequestClass(int n) : n_(n) {}

    /// Value to add/sub from
    int n_;

    /**
     * Add n to value in JSON request.
     *
     * @param request JSON request with field "value".
     * @return JSON response containing modified field "value" = [original_value] + n.
     */
    [[nodiscard]] Json::Value add_n(const Json::Value &request) const
    {
        Json::Value resp;
        resp["SUCCESS"] = true;

        // If value is present in request, return value + 1, else return error.
        if (request.get("VALUE", NULL) != NULL) {
            resp["VALUE"] = request["VALUE"].asInt() + this->n_;
        } else {
            resp["SUCCESS"] = false;
            resp["ERRORS"] = "Invalid value.";
        }
        return resp;
    }

    /**
     * Sun n from value in JSON request.
     *
     * @param request JSON request with field "value".
     * @return JSON response containing modified field "value" = [original_value] - n.
     */
    [[nodiscard]] Json::Value sub_n(const Json::Value &request) const
    {
        Json::Value resp, value;
        resp["SUCCESS"] = true;

        // If value is present in request, return value + 1, else return error.
        if (request.get("VALUE", NULL) != NULL) {
            resp["VALUE"] = request["VALUE"].asInt() - this->n_;
        } else {
            resp["SUCCESS"] = false;
            resp["ERRORS"] = "Invalid value.";
        }
        return resp;
    }
};

typedef std::function<Json::Value(RequestClass, const Json::Value &)> RequestClassMethod;

template<class RequestHandler, class RequestClass>
class Session :
    public std::enable_shared_from_this<Session<RequestHandler,
        RequestClass>>
{
public:
    typedef std::map<std::string, RequestHandler> CommandMap;

    Session(tcp::socket socket, CommandMap commands,
                   RequestClass *request_class_inst)
            : socket_(std::move(socket))
            , commands_(std::move(commands))
            , request_class_inst_(request_class_inst)
            , reader_((new Json::CharReaderBuilder)->newCharReader())
    {}

    void Run()
    {
        DoRead();
    }

    void Kill()
    {
        continue_ = false;
    }

private:
    tcp::socket socket_;
    RequestClass *request_class_inst_;
    CommandMap commands_;
    /// Reads JSON.
    const std::unique_ptr<Json::CharReader> reader_;
    /// Writes JSON.
    Json::StreamWriterBuilder writer_;
    bool continue_ = true;
    char data_[2048];
    std::string resp_;

    void DoRead()
    {
        auto self(this->shared_from_this());
        socket_.async_read_some(boost::asio::buffer(data_),
                                [this, self](error_code ec, std::size_t length)
                                {
                                  if (!ec)
                                      DoWrite(length);
                                });
    }

    void DoWrite(std::size_t length)
    {
        JSONCPP_STRING parse_err;
        Json::Value json_req, json_resp;
        std::string client_req_str(data_);

        if (reader_->parse(client_req_str.c_str(),
                           client_req_str.c_str() +
                           client_req_str.length(),
                           &json_req, &parse_err))
        {
            try {
                // Get JSON response.
                json_resp = ProcessRequest(json_req);
                json_resp["SUCCESS"] = true;
            } catch (const std::exception &ex) {
                // If json parsing failed.
                json_resp["SUCCESS"] = false;
                json_resp["ERRORS"] = std::string(ex.what());
            }
        } else {
            // If json parsing failed.
            json_resp["SUCCESS"] = false;
            json_resp["ERRORS"] = std::string(parse_err);
        }

        resp_ = Json::writeString(writer_, json_resp);

        auto self(this->shared_from_this());
        boost::asio::async_write(socket_,
                                 boost::asio::buffer(resp_),
                                 [this, self]
                                 (boost::system::error_code ec,
                                  std::size_t bytes_xfered) {
                                    if (!ec)     DoRead();
                                 });
    }

    Json::Value ProcessRequest(Json::Value request)
    {
        Json::Value response;
        std::string command = request["COMMAND"].asString();


        // If command is not valid, give a response with an error.
        if(commands_.find(command) == commands_.end()) {
            response["SUCCESS"] = false;
            response["ERRORS"] = "Invalid command.";
        }
            // Otherwise, run the relevant handler.
        else {
            RequestHandler handler = commands_.at(command);
            response = handler(*request_class_inst_, request);
        }

        return response;
    }

};




template<class RequestHandler, class RequestClass>
class Server {
public:
    typedef std::map<std::string, RequestHandler> CommandMap;

    Server(short port, CommandMap commands, RequestClass *request_class_inst)
            : acceptor_(io_context_, tcp::endpoint(tcp::v4(), port))
            , commands_(std::move(commands))
            , request_class_inst_(request_class_inst)
    {
        DoAccept();
    }

    ~Server()
    {
    }

    void Run()
    {
        io_context_.run();
    }

    void RunInBackground()
    {
        std::thread t( [this]{ Run(); });
        t.detach();
    }

    void Kill()
    {
        acceptor_.close();
    }

private:
    boost::asio::io_context io_context_;
    tcp::acceptor acceptor_;
    CommandMap commands_;
    RequestClass *request_class_inst_;

    void DoAccept()
    {
        acceptor_.async_accept(
                [this](boost::system::error_code ec, tcp::socket socket) {
                  if (!ec)
                      std::make_shared<Session<RequestHandler, RequestClass>>
                              (std::move(socket), commands_, request_class_inst_)->Run();
                  DoAccept();
                });
    }
};


class Client {
public:
    /**
     * Constructor, initializes JSON parser and serializer.
     */
    Client()
        : reader_((new Json::CharReaderBuilder)->newCharReader())
    {}

    Json::Value MakeRequest(const std::string &ip_addr, unsigned short port,
                            const Json::Value &request)
    {
        boost::asio::io_context io_context;

        std::string serialized_req = Json::writeString(writer_, request);
        tcp::socket s(io_context);
        tcp::resolver resolver(io_context);
        s.connect({ boost::asio::ip::address::from_string(ip_addr), port });
        boost::asio::write(s, boost::asio::buffer(serialized_req));
        s.shutdown(tcp::socket::shutdown_send);

        error_code ec;
        char reply[2048];
        size_t reply_length = boost::asio::read(s, boost::asio::buffer(reply),
                                                ec);

        std::cout << std::string(reply).substr(0, reply_length) << std::endl;

        Json::Value json_resp;
        JSONCPP_STRING parse_err;
        std::string resp_str(reply);
        if (reader_->parse(resp_str.c_str(), resp_str.c_str() + resp_str.length(),
                           &json_resp, &parse_err))
            return json_resp;

        throw std::runtime_error("Error parsing response.");
    }

    bool IsAlive(const std::string &ip_addr, unsigned short port)
    {
        boost::asio::io_context io_context;
        tcp::socket s(io_context);
        tcp::resolver resolver(io_context);
        try {
            s.connect({boost::asio::ip::address::from_string(ip_addr), port});
        } catch(const boost::wrapexcept<boost::system::system_error> &err) {
            s.close();
            return false;
        }

        s.close();
        return true;
    }

private:
    /// Reads JSON.
    const std::unique_ptr<Json::CharReader> reader_;
    /// Writes JSON.
    Json::StreamWriterBuilder writer_;
};



int main()
{
    auto *request_inst = new RequestClass(1);
    std::map<std::string, RequestClassMethod> commands {
            {"ADD_1", std::mem_fn(&RequestClass::add_n)},
            {"SUB_1", std::mem_fn(&RequestClass::sub_n)}
    };
    Server<RequestClassMethod, RequestClass> s1(5000, commands, request_inst);

    s1.RunInBackground();

    std::vector<Client*> clients(6, new Client());

    Json::Value sub_one_req;
    sub_one_req["COMMAND"] = "SUB_1";
    sub_one_req["VALUE"] = 1;

    s1.Kill();
    std::cout << clients.at(1)->IsAlive("127.0.0.1", 5000);

    return 0;
}

that 节目上使用 ASAN (-fsanitize=addess)

false
=================================================================
==31232==ERROR: AddressSanitizer: heap-use-after-free on address 0x6110000002c0 at pc 0x561409ca2ea3 bp 0x7efcf
bbfdc60 sp 0x7efcfbbfdc50
READ of size 8 at 0x6110000002c0 thread T1

=================================================================
    #0 0x561409ca2ea2 in boost::asio::detail::epoll_reactor::run(long, boost::asio::detail::op_queue<boost::asi
o::detail::scheduler_operation>&) /home/sehe/custom/boost_1_76_0/boost/asio/detail/impl/epoll_reactor.ipp:504
==31232==ERROR: LeakSanitizer: detected memory leaks
    #1 0x561409cb442c in boost::asio::detail::scheduler::do_run_one(boost::asio::detail::conditionally_enabled_
mutex::scoped_lock&, boost::asio::detail::scheduler_thread_info&, boost::system::error_code const&) /home/sehe/
custom/boost_1_76_0/boost/asio/detail/impl/scheduler.ipp:470

Direct leak of 4 byte(s) in 1 object(s) allocated from:
    #0 0x7efd08fca717 in operator new(unsigned long) (/usr/lib/x86_64-linux-gnu/libasan.so.6+0xb4717)
    #2 0x561409cf2792 in boost::asio::detail::scheduler::run(boost::system::error_code&) /home/sehe/custom/boos
t_1_76_0/boost/asio/detail/impl/scheduler.ipp:204
    #1 0x561409bc62b5 in main /home/sehe/Projects/Whosebug/test.cpp:229

SUMMARY: AddressSanitizer: 4 byte(s) leaked in 1 allocation(s).

或另一个 运行:

它已经告诉您您需要知道的“一切”。巧合的是,这是我在之前的回答中提到的错误。要正常关机,您 必须 在线程上同步。分离它会永远毁掉你的机会。所以,我们不要分离它:

void RunInBackground()
{
    if (!t_.joinable()) {
        t_ = std::thread([this] { Run(); });
    }
}

As you can see, this is captured, so you can never allow the thread to run past the destruction of the Server object.

然后在析构函数中加入它:

~Server()
{
    if (t_.joinable()) {
        t_.join();
    }
}

现在,让我们彻底了解一下。我们有 两个 线程。他们共享对象。 io_context 是线程安全的,所以没关系。但是 tcp::acceptor 不是。也不会 request_class_inst_。您需要同步更多:

void Kill()
{
    post(io_context_, [this] { acceptor_.close(); });
}

现在,请注意这还不够! .close() 在接受器上导致 .cancel(),但这只会使完成处理程序被 error::operation_aborted 调用。因此,在这种情况下,您需要防止再次启动DoAccept

void DoAccept()
{
    acceptor_.async_accept(
        [this](boost::system::error_code ec, tcp::socket socket) {
            if (ec) {
                std::cout << "Accept loop: " << ec.message() << std::endl;
            } else {
                std::make_shared<Session<RequestHandler, RequestClass>>(
                    std::move(socket), commands_, request_class_inst_)
                    ->Run();
                DoAccept();
            }
        });
}

I took the liberty of aborting on /any/ error. Err on the safe side: you prefer processes to exit instead of being stuck in unresponsive state of high-CPU loops.

无论如何,您应该了解服务器 startup/shutdown 和您的测试客户端之间的竞争条件:

s1.RunInBackground();

// unspecified, race condition!
std::cout << "IsAlive(" << __LINE__ << "): " << clients.at(0).IsAlive("127.0.0.1", 5000) << std::endl;

sleep_for(10ms); // likely enough for acceptor to start

// true:
std::cout << "IsAlive(" << __LINE__ << "): " << clients.at(1).IsAlive("127.0.0.1", 5000) << std::endl;
std::cout << "MakeRequest: " << clients.at(2).MakeRequest(
                 "127.0.0.1", 5000, {{"COMMAND", "MUL_2"}, {"VALUE", "21"}})
          << std::endl;

s1.Kill();
// unspecified, race condition!
std::cout << "IsAlive(" << __LINE__ << "): " << clients.at(3).IsAlive("127.0.0.1", 5000) << std::endl;

sleep_for(10ms); // likely enough for acceptor to be closed
// false:
std::cout << "IsAlive(" << __LINE__ << "): " << clients.at(4).IsAlive("127.0.0.1", 5000) << std::endl;

版画

IsAlive(240): true
IsAlive(245): true
MakeRequest: {"SUCCESS":false,"ERRORS":"not an int64"}
{"SUCCESS":false,"ERRORS":"not an int64"}
IsAlive(252): CLOSING
Accept loop: Operation canceled
THREAD EXIT
false
IsAlive(256): false

完整列表

请注意,这也修复了 RequestClass 实例不必要的泄漏。你已经假设了复制能力(因为你在不同的地方按值传递它)。

另请注意,在 MakeRequest 中,我们现在不再吞下除 EOF 之外的任何错误。

像上次一样,我使用 Boost Json 来简化并使示例独立于 Whosebug。

Address sanitizer (ASan) 和 UBSan 保持沉默。生活是美好的。

Live On Coliru

#include <boost/asio.hpp>
#include <boost/json.hpp>
#include <boost/json/src.hpp>
#include <iostream>
#include <deque>

using boost::asio::ip::tcp;
using boost::system::error_code;
namespace json = boost::json;
using Value    = json::object;

using namespace std::chrono_literals;
static auto sleep_for(auto delay) { return std::this_thread::sleep_for(delay); }

/// NOTE: This class exists exclusively for unit testing.
struct RequestClass {
    int n_;

    Value add_n(Value const& request) const { return impl(std::plus<>{}, request); }
    Value sub_n(Value const& request) const { return impl(std::minus<>{}, request); }
    Value mul_n(Value const& request) const { return impl(std::multiplies<>{}, request); }
    Value div_n(Value const& request) const { return impl(std::divides<>{}, request); }

  private:
    template <typename Op> Value impl(Op op, Value const& req) const {
        return (req.contains("VALUE"))
            ? Value{{"VALUE", op(req.at("VALUE").as_int64(), n_)},
                    {"SUCCESS", true}}
            : Value{{"ERRORS", "Invalid value."}, {"SUCCESS", false}};
    }
};

using RequestClassMethod =
    std::function<Value(RequestClass const&, Value const&)>;

template <class RequestHandler, class RequestClass>
class Session
    : public std::enable_shared_from_this<
          Session<RequestHandler, RequestClass>> {
  public:
    using CommandMap = std::map<std::string, RequestHandler>;

    Session(tcp::socket socket, CommandMap commands,
            RequestClass request_class_inst)
        : socket_(std::move(socket))
        , commands_(std::move(commands))
        , request_class_inst_(std::move(request_class_inst))
    {
    }

    void Run()  { DoRead(); }
    void Kill() { continue_ = false; }

  private:
    tcp::socket  socket_;
    CommandMap   commands_;
    RequestClass request_class_inst_;
    bool         continue_ = true;
    char         data_[2048];
    std::string  resp_;

    void DoRead()
    {
        socket_.async_read_some(
            boost::asio::buffer(data_),
            [this, self = this->shared_from_this()](error_code ec, std::size_t length) {
                if (!ec) {
                    DoWrite(length);
                }
            });
    }

    void DoWrite(std::size_t length)
    {
        Value json_resp;

        try {
            auto json_req = json::parse({data_, length}).as_object();
            json_resp = ProcessRequest(json_req);
            json_resp["SUCCESS"] = true;
        } catch (std::exception const& ex) {
            json_resp = {{"SUCCESS", false}, {"ERRORS", ex.what()}};
        }

        resp_ = json::serialize(json_resp);

        boost::asio::async_write(socket_, boost::asio::buffer(resp_),
             [this, self = this->shared_from_this()](
                 error_code ec, size_t bytes_xfered) {
                 if (!ec)
                     DoRead();
             });
    }

    Value ProcessRequest(Value request)
    {
        auto command = request.contains("COMMAND")
            ? request["COMMAND"].as_string() //
            : "";
        std::string cmdstr(command.data(), command.size());

        // If command is not valid, give a response with an error.
        return commands_.contains(cmdstr)
            ? commands_.at(cmdstr)(request_class_inst_, request)
            : Value{{"SUCCESS", false}, {"ERRORS", "Invalid command."}};
    }
};

template <class RequestHandler, class RequestClass> class Server {
  public:
    using CommandMap = std::map<std::string, RequestHandler>;

    Server(uint16_t port, CommandMap commands, RequestClass request_class_inst)
        : acceptor_(io_context_, tcp::endpoint(tcp::v4(), port))
        , commands_(std::move(commands))
        , request_class_inst_(std::move(request_class_inst))
    {
        DoAccept();
    }

    ~Server()
    {
        if (t_.joinable()) {
            t_.join();
        }
        assert(not t_.joinable());
    }

    void Run()
    {
        io_context_.run();
    }

    void RunInBackground()
    {
        if (!t_.joinable()) {
            t_ = std::thread([this] {
                Run();
                std::cout << "THREAD EXIT" << std::endl;
            });
        }
    }

    void Kill()
    {
        post(io_context_, [this] {
            std::cout << "CLOSING" << std::endl;
            acceptor_.close(); // causes .cancel() as well
        });
    }

  private:
    boost::asio::io_context io_context_;
    tcp::acceptor           acceptor_;
    CommandMap              commands_;
    RequestClass            request_class_inst_;
    std::thread             t_;

    void DoAccept()
    {
        acceptor_.async_accept(
            [this](boost::system::error_code ec, tcp::socket socket) {
                if (ec) {
                    std::cout << "Accept loop: " << ec.message() << std::endl;
                } else {
                    std::make_shared<Session<RequestHandler, RequestClass>>(
                        std::move(socket), commands_, request_class_inst_)
                        ->Run();
                    DoAccept();
                }
            });
    }
};

class Client {
  public:
    /**
     * Constructor, initializes JSON parser and serializer.
     */
    Client() {}

    Value MakeRequest(std::string const& ip_addr, uint16_t port,
                      Value const& request)
    {
        boost::asio::io_context io_context;

        std::string   serialized_req = serialize(request);
        tcp::socket   s(io_context);

        s.connect({boost::asio::ip::address::from_string(ip_addr), port});
        boost::asio::write(s, boost::asio::buffer(serialized_req));

        s.shutdown(tcp::socket::shutdown_send);

        char       reply[2048];
        error_code ec;
        size_t     reply_length = read(s, boost::asio::buffer(reply), ec);

        if (ec && ec != boost::asio::error::eof) {
            throw boost::system::system_error(ec);
        }

        // safe method:
        std::string_view resp_str(reply, reply_length);

        Value res = json::parse({reply, reply_length}).as_object();
        std::cout << res << std::endl;

        return res;
    }

    bool IsAlive(std::string const& ip_addr, unsigned short port)
    {
        boost::asio::io_context io_context;
        tcp::socket             s(io_context);
        error_code              ec;
        s.connect({boost::asio::ip::address::from_string(ip_addr), port}, ec);
        return not ec.failed();
    }
};

int main()
{
    std::cout << std::boolalpha;
    std::deque<Client> clients(6);

    Server<RequestClassMethod, RequestClass> s1(
        5000,
        {
            {"ADD_2", std::mem_fn(&RequestClass::add_n)},
            {"SUB_2", std::mem_fn(&RequestClass::sub_n)},
            {"MUL_2", std::mem_fn(&RequestClass::mul_n)},
            {"DIV_2", std::mem_fn(&RequestClass::div_n)},
        },
        RequestClass{1});

    s1.RunInBackground();

    // unspecified, race condition!
    std::cout << "IsAlive(" << __LINE__ << "): " << clients.at(0).IsAlive("127.0.0.1", 5000) << std::endl;

    sleep_for(10ms); // likely enough for acceptor to start

    // true:
    std::cout << "IsAlive(" << __LINE__ << "): " << clients.at(1).IsAlive("127.0.0.1", 5000) << std::endl;
    std::cout << "MakeRequest: " << clients.at(2).MakeRequest(
                     "127.0.0.1", 5000, {{"COMMAND", "MUL_2"}, {"VALUE", "21"}})
              << std::endl;

    s1.Kill();
    // unspecified, race condition!
    std::cout << "IsAlive(" << __LINE__ << "): " << clients.at(3).IsAlive("127.0.0.1", 5000) << std::endl;

    sleep_for(10ms); // likely enough for acceptor to be closed
    // false:
    std::cout << "IsAlive(" << __LINE__ << "): " << clients.at(4).IsAlive("127.0.0.1", 5000) << std::endl;

}