我几次尝试使用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怎么办?
答案 0 :(得分:3)
虽然其他人的回答与此答案的后半部分相似,但似乎是我能找到的最完整的答案,来自在Boost Mailing列表上提出同样的问题。
我将在此总结,以便将来从搜索到达的人员提供帮助。
有两个选项
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_ptr
和weak_ptr
的组合来管理连接生存期。可以在此处找到使用shared_ptr
&{39}和weak_ptr
组合的HTTP服务器:via-httplib。
HTTP服务器构建在一个异步TCP服务器上,该服务器使用一组(shared_ptr
)连接,在连接上创建并在断开连接时销毁。
答案 3 :(得分:0)
asio解决&#34;删除问题的方式&#34;其中有出色的异步方法是将每个启用异步的对象拆分为3个类,例如:
每个io_loop有一个服务(请参阅use_service<>
)。该服务为服务器创建一个impl,现在是一个句柄类。
这分隔了句柄的生命周期和实现的生命周期。
现在,在句柄的析构函数中,可以将消息(通过服务)发送到impl以取消所有未完成的IO。
如果需要,句柄的析构函数可以自由地等待那些io调用排队(例如,如果将服务器的工作委托给后台io循环或线程池)。
以这种方式实现所有启用io_service的对象已成为习惯,因为它使aiso编码变得非常简单。