如何设计boost :: asio套接字或其包装的正确释放

时间:2017-04-05 18:41:51

标签: c++ tcp boost-asio

我几次尝试使用boost :: asio创建自己的简单异步TCP服务器几年后没有触及它。

我能找到的最新示例列表是: http://www.boost.org/doc/libs/1_54_0/doc/html/boost_asio/tutorial/tutdaytime3/src.html

我在这个示例列表中遇到的问题是(我觉得)它欺骗并且通过使tcp_connection成为shared_ptr来欺骗它,这样它就不用担心每个连接的生命周期管理了。 (我认为)他们这样做是为了简洁,因为它是一个小教程,但这个解决方案不是现实世界。

如果您想在定时器或类似的东西上向每个客户端发送消息,该怎么办?在任何真实世界的非平凡服务器中都需要一组客户端连接。

我担心每个连接的生命周期管理。我认为自然要做的是在tcp_server中保留一些tcp_connection对象或指向它们的指针。从OnConnect回调添加到该集合并从该集合中删除OnDisconnect。

请注意,OnDisconnect很可能是从实际的Disconnect方法调用的,而在发生错误的情况下,该方法将从OnReceive回调或OnSend回调中调用。

嗯,这就是问题所在。

考虑一下我们有一个看起来像这样的callstack:

tcp_connection::~tcp_connection
tcp_server::OnDisconnect
tcp_connection::OnDisconnect
tcp_connection::Disconnect
tcp_connection::OnReceive

这会在调用堆栈展开时导致错误,并且我们正在一个已经调用了析构函数的对象中执行代码......我想,对吧?

我想每个做服务器编程的人都会以某种方式遇到这种情况。处理它的策略是什么?

我希望这个解释足够好。如果没有让我知道,我将创建自己的源列表,但它将非常大。

编辑: 相关

Memory management in asynchronous C++ code

IMO不是一个可以接受的答案,依赖于在接听电话上使用shared_ptr作弊,仅此而已,并不是现实世界。如果服务器想要每5分钟对所有客户说“嗨”怎么办?某种集合是必要的。如果你在多个线程上调用io_service.run怎么办?

我也在询问提升邮件列表: http://boost.2283326.n4.nabble.com/How-to-design-proper-release-of-a-boost-asio-socket-or-wrapper-thereof-td4693442.html

4 个答案:

答案 0 :(得分:3)

虽然其他人的回答与此答案的后半部分相似,但似乎是我能找到的最完整的答案,来自在Boost Mailing列表上提出同样的问题。

http://boost.2283326.n4.nabble.com/How-to-design-proper-release-of-a-boost-asio-socket-or-wrapper-thereof-td4693442.html

我将在此总结,以便将来从搜索到达的人员提供帮助。

有两个选项

1)关闭套接字以取消任何未完成的io,然后在io_service上发布断开连接逻辑的回调,并在套接字断开连接时调用服务器类。然后它可以安全地释放连接。只要只有一个线程调用了io_service :: run,那么在进行回调时,其他异步操作就已经解决了。但是,如果有多个线程调用了io_service :: run,那么这是不安全的。

2)正如其他人在他们的回答中指出的那样,使用shared_ptr来管理连接生命周期,使用优秀的io操作来保持它们的存活是可行的。我们可以将一个集合weak_ptr用于连接,以便在需要时访问它们。后者是关于该主题的其他帖子中遗漏的一小撮,这让我很困惑。

答案 1 :(得分:2)

就像我说的那样,我没有看到使用智能指针是“作弊,欺骗大”。我也不认为你的评价“他们为了简洁而这样做”有用。

以下是我们代码库中略有编辑的摘录¹,它举例说明了如何使用shared_ptrs排除跟踪连接。

它只显示服务器端的事物,

  • connection.hpp中一个非常简单的connection对象;这使用enable_shared_from_this

  • 只是固定大小connection_pool(我们也动态调整池的大小,因此锁定原语)。请注意我们如何对所有活动连接执行操作。

    所以你很容易写出类似这样的内容写给所有客户端,比如定时器:

    _pool.for_each_active([] (auto const& conn) {
        send_message(conn, hello_world_packet);
    });
    
  • 示例listener,展示了它与connection_pool(它有一个关闭所有连接的示例方法)的关系

代码清单

  • connection.hpp

    #pragma once
    
    #include "xxx/net/rpc/protocol.hpp"
    #include "log.hpp"
    #include "stats_filer.hpp"
    #include <memory>
    
    namespace xxx { namespace net { namespace rpc {
    
        struct connection : std::enable_shared_from_this<connection>, protected LogSource {
            typedef std::shared_ptr<connection> ptr;
    
          private:
            friend struct io;
            friend struct listener;
    
            boost::asio::io_service& _svc;
            protocol::socket _socket;
            protocol::endpoint _ep;
            protocol::endpoint _peer;
          public:
    
            connection(boost::asio::io_service& svc, protocol::endpoint ep)
                : LogSource("rpc::connection"),
                  _svc(svc),
                  _socket(svc),
                  _ep(ep)
            {}
    
            void init() {
                _socket.set_option(protocol::no_delay(true));
                _peer = _socket.remote_endpoint();
                g_stats_filer_p->inc_value("asio." + _ep.address().to_string() + ".sockets_accepted");
                debug() << "New connection from " << _peer;
            }
    
            protocol::endpoint endpoint() const { return _ep;     } 
            protocol::endpoint peer() const     { return _peer;   } 
            protocol::socket&  socket()         { return _socket; } 
    
            // TODO encapsulation
            int handle() {
                return _socket.native_handle();
            }
    
            bool valid() const { return _socket.is_open(); }
    
            void cancel() {
                _svc.post([this] { _socket.cancel(); }); 
            }
    
            using shutdown_type = boost::asio::ip::tcp::socket::shutdown_type;
            void shutdown(shutdown_type what = shutdown_type::shutdown_both) {
                _svc.post([=] { _socket.shutdown(what); }); 
            }
    
            ~connection() {
                g_stats_filer_p->inc_value("asio." + _ep.address().to_string() + ".sockets_disconnected");
            }
        };
    
    } } }
    
  • connection_pool.hpp

    #pragma once
    
    #include <mutex>
    #include "xxx/threads/null_mutex.hpp"
    #include "xxx/net/rpc/connection.hpp"
    #include "stats_filer.hpp"
    #include "log.hpp"
    
    namespace xxx { namespace net { namespace rpc {
    
        // not thread-safe by default, but pass e.g. std::mutex for `Mutex` if you need it
        template <typename Ptr = xxx::net::rpc::connection::ptr, typename Mutex = xxx::threads::null_mutex>
        struct basic_connection_pool : LogSource {
            using WeakPtr = std::weak_ptr<typename Ptr::element_type>;
    
            basic_connection_pool(std::string name = "connection_pool", size_t size)
                : LogSource(std::move(name)), _pool(size) 
            { }
    
            bool try_insert(Ptr const& conn) {
                std::lock_guard<Mutex> lk(_mx);
    
                auto slot = std::find_if(_pool.begin(), _pool.end(), std::mem_fn(&WeakPtr::expired));
    
                if (slot == _pool.end()) {
                    g_stats_filer_p->inc_value("asio." + conn->endpoint().address().to_string() + ".connections_dropped");
                    error() << "dropping connection from " << conn->peer() << ": connection pool (" << _pool.size() << ") saturated";
                    return false;
                }
    
                *slot = conn;
                return true;
            }
    
            template <typename F>
            void for_each_active(F action) {
                auto locked = [=] {
                    using namespace std;
                    lock_guard<Mutex> lk(_mx);
                    vector<Ptr> locked(_pool.size());
                    transform(_pool.begin(), _pool.end(), locked.begin(), mem_fn(&WeakPtr::lock));
                    return locked;
                }();
    
                for (auto const& p : locked)
                    if (p) action(p);
            }
    
            constexpr static bool synchronizing() {
                return not std::is_same<xxx::threads::null_mutex, Mutex>();
            }
    
          private:
            void dump_stats(LogSource::LogTx tx) const {
                // lock is assumed!
                size_t empty = 0, busy = 0, idle = 0;
    
                for (auto& p : _pool) {
                    switch (p.use_count()) {
                        case 0:  empty++; break;
                        case 1:  idle++;  break;
                        default: busy++;  break;
                    }
                }
    
                tx << "usage empty:" << empty << " busy:" << busy << " idle:" << idle;
            }
    
            Mutex _mx;
            std::vector<WeakPtr> _pool;
        };
    
        // TODO FIXME use null_mutex once growing is no longer required AND if
        // en-pooling still only happens from the single IO thread (XXX-2535)
        using server_connection_pool = basic_connection_pool<xxx::net::rpc::connection::ptr, std::mutex>;
    
    } } }
    
  • listener.hpp

    #pragma once
    
    #include "xxx/threads/null_mutex.hpp"
    #include <mutex>
    #include "xxx/net/rpc/connection_pool.hpp"
    #include "xxx/net/rpc/io_operations.hpp"
    
    namespace xxx { namespace net { namespace rpc {
    
        struct listener : std::enable_shared_from_this<listener>, LogSource {
            typedef std::shared_ptr<listener> ptr;
    
            protocol::acceptor _acceptor;
            protocol::endpoint _ep;
    
            listener(boost::asio::io_service& svc, protocol::endpoint ep, server_connection_pool& pool) 
                : LogSource("rpc::listener"), _acceptor(svc), _ep(ep), _pool(pool)
            {
                _acceptor.open(ep.protocol());
    
                _acceptor.set_option(protocol::acceptor::reuse_address(true));
                _acceptor.set_option(protocol::no_delay(true));
                ::fcntl(_acceptor.native(), F_SETFD, FD_CLOEXEC); // FIXME use non-racy socket factory?
                _acceptor.bind(ep);
    
                _acceptor.listen(32);
            }
    
            void accept_loop(std::function<void(connection::ptr conn)> on_accept) {
    
                auto self = shared_from_this();
                auto conn = std::make_shared<xxx::net::rpc::connection>(_acceptor.get_io_service(), _ep);
    
                _acceptor.async_accept(conn->_socket, [this,self,conn,on_accept](boost::system::error_code ec) {
                    if (ec) {
                        auto tx = ec == boost::asio::error::operation_aborted? debug() : warn();
                        tx << "failed accept " << ec.message();
                    } else {
                        ::fcntl(conn->_socket.native(), F_SETFD, FD_CLOEXEC); // FIXME use non-racy socket factory?
    
                        if (_pool.try_insert(conn)) {
                            on_accept(conn);
                        }
    
                        self->accept_loop(on_accept);
                    }
                });
            }
    
            void close() {
                _acceptor.cancel();
                _acceptor.close();
    
                _acceptor.get_io_service().post([=] {
                    _pool.for_each_active([] (auto const& sp) {
                        sp->shutdown(connection::shutdown_type::shutdown_both);
                        sp->cancel();
                    });
                });
    
                debug() << "shutdown";
            }
    
            ~listener() {
            }
    
          private:
            server_connection_pool& _pool;
        };
    
    } } }
    

¹下载为gist https://gist.github.com/sehe/979af25b8ac4fd77e73cdf1da37ab4c2

答案 2 :(得分:1)

连接生命周期是boost::asio的基本问题。从经验来看,我可以向你保证,错误导致&#34;未定义的行为&#34; ......

asio示例使用shared_ptr来确保连接保持活动状态,同时asio::io_service中的处理程序可能非常出色。请注意,即使在单个线程中,asio::io_service也与应用程序代码异步运行,请参阅CppCon 2016: Michael Caisse "Asynchronous IO with Boost.Asio"以获得精确机制的精彩描述。

shared_ptr使连接的生命周期能够由shared_ptr实例计数控制。恕我直言,这不是&#34;欺骗和欺骗大&#34 ;;但是对复杂问题的优雅解决方案。

但是,我同意你的看法,仅使用shared_ptr来控制连接生命周期并不是一个完整的解决方案,因为它可能导致资源泄漏。

在我的回答中:Boost async_* functions and shared_ptr's,我提议使用shared_ptrweak_ptr的组合来管理连接生存期。可以在此处找到使用shared_ptr&{39}和weak_ptr组合的HTTP服务器:via-httplib

HTTP服务器构建在一个异步TCP服务器上,该服务器使用一组(shared_ptr)连接,在连接上创建并在断开连接时销毁。

答案 3 :(得分:0)

asio解决&#34;删除问题的方式&#34;其中有出色的异步方法是将每个启用异步的对象拆分为3个类,例如:

  • 服务器
  • server_service
  • server_impl

每个io_loop有一个服务(请参阅use_service<>)。该服务为服务器创建一个impl,现在是一个句柄类。

这分隔了句柄的生命周期和实现的生命周期。

现在,在句柄的析构函数中,可以将消息(通过服务)发送到impl以取消所有未完成的IO。

如果需要,句柄的析构函数可以自由地等待那些io调用排队(例如,如果将服务器的工作委托给后台io循环或线程池)。

以这种方式实现所有启用io_service的对象已成为习惯,因为它使aiso编码变得非常简单。