接受者拒绝只为 -O0 构建打开套接字
Acceptor refusing to open socket only for -O0 build
亲爱的 Whosebug 社区,您好,
(编辑:单文件版本在最后)
我只在某些编译设置中遇到问题(这表明某种 UB,但我决定将此问题标记为 boost-asio,因为它可能涉及有关 asio 细节的知识)。请注意,我使用的是当前 git 版本的独立 asio 库。
首先让我放一些片段 - 完整的代码在这里:
https://github.com/paulhilbert/magellan
(请注意,CMakeLists.txt 非常老套,可能需要修复以防您想要编译它)。
我在 examples/echo_server.cpp:
中有一个测试回显服务器(tcp,异步)
[snip...]
int main (int argc, char const* argv[]) {
try {
if (argc != 2) {
std::cerr << "Usage: echo_server <port>\n";
return 1;
}
asio::io_context io_context;
magellan::server server;
server.accept<echo_session>(io_context, 9003);
io_context.run();
} catch (std::exception& e) {
std::cerr << e.what() << "\n";
}
}
显然有趣的部分是 server
和 echo_session
classes;但是后者似乎可以正常工作,所以我将把 server
class 放在这里。 include/server.hpp:
#ifndef MAGELLAN_SERVER_HPP_
#define MAGELLAN_SERVER_HPP_
#include "session.hpp"
namespace magellan {
class server {
public:
[snip typedefs...]
public:
server();
virtual ~server();
template <typename Session>
void accept(asio::io_context& io_context, short port);
[snip comments...]
};
} // magellan
#include "server.ipp"
#endif /* MAGELLAN_SERVER_HPP_ */
... 和 include/server.ipp:
#include <iostream>
namespace magellan {
template <typename Session>
inline void
server::accept(asio::io_context& io_context, short port) {
using asio::ip::tcp;
asio::spawn(io_context, [&](asio::yield_context yield) {
tcp::acceptor acceptor(io_context, tcp::endpoint(tcp::v4(), port));
for (;;) {
asio::error_code ec;
tcp::socket socket(io_context);
acceptor.async_accept(socket, yield[ec]);
if (!ec) {
auto session = std::make_shared<Session>(std::move(socket));
session->start();
}
}
});
}
[snip comments...]
} // magellan
现在有趣的是 async_accept
打开一个 tcp 套接字 iff 我用 -O1、-O2 和 -O3 编译,而不是 -O0。我通过以下方式检查过:
> ss -a | grep 9003
296:tcp LISTEN 0 128 *:9003 *:*
当使用 -O0 编译时,永远不会打开套接字。我还通过 io_context::work 实例检查了该服务是否仍然 运行。
我最好的猜测(诚然缺乏信心)是 boost 协程与 -O0 做了一些不同的事情。还值得一提的是,如果我在 server.ipp 中使用注释掉的代码,它也不会打开套接字(无论编译设置如何):
template <typename Session>
inline void
server::accept(asio::io_context& io_context, short port) {
using asio::ip::tcp;
accept<Session>(io_context, port, [](tcp::socket s) {
return std::make_shared<Session>(std::move(s));
});
}
template <typename Session, typename Func>
void
server::accept(asio::io_context& io_context, short port, Func&& factory) {
using asio::ip::tcp;
asio::spawn(io_context, [&](asio::yield_context yield) {
tcp::acceptor acceptor(io_context, tcp::endpoint(tcp::v4(), port));
for (;;) {
asio::error_code ec;
tcp::socket socket(io_context);
acceptor.async_accept(socket, yield[ec]);
if (!ec) {
auto session = factory(std::move(socket));
session->start();
} else {
std::cout << "failed accept" << "\n";
}
}
});
}
这是我最初的问题,我将其归因于复制而不是移动的套接字,直到我遇到 -O0 标志问题。
此时我很迷茫,因为我不知道如何调试这些异步进程,但我仍然相信我的问题的实际答案在某种程度上会令人尴尬;)
希望你能给我一个提示。
最好的,
理查德
编辑:
这是一个压缩的单文件版本:
#include <iostream>
#include <asio.hpp>
#include <asio/spawn.hpp>
using asio::ip::tcp;
namespace magellan {
class session : public std::enable_shared_from_this<session> {
public:
session(asio::ip::tcp::socket socket) : socket_(std::move(socket)), strand_(socket_.get_io_context()) {
}
template <typename Func>
void
async_do(Func&& f) {
auto self(shared_from_this());
asio::spawn(strand_, [this, self, f](asio::yield_context yield) {
try {
f(std::ref(socket_), std::ref(yield));
} catch (std::exception& e) {
socket_.close();
}
});
}
void start() {
async_do([this] (tcp::socket& s, asio::yield_context& yc) {
perform(s, yc);
});
}
protected:
virtual void perform(asio::ip::tcp::socket& s, asio::yield_context&) {
s.close();
}
protected:
asio::ip::tcp::socket socket_;
asio::io_context::strand strand_;
};
class server {
public:
typedef std::shared_ptr<server> ptr;
typedef std::weak_ptr<server> wptr;
typedef std::shared_ptr<const server> const_ptr;
typedef std::weak_ptr<const server> const_wptr;
public:
server() {}
virtual ~server() {}
template <typename Session>
void
accept(asio::io_context& io_context, short port) {
using asio::ip::tcp;
asio::spawn(io_context, [&](asio::yield_context yield) {
tcp::acceptor acceptor(io_context, tcp::endpoint(tcp::v4(), port));
for (;;) {
asio::error_code ec;
tcp::socket socket(io_context);
acceptor.async_accept(socket, yield[ec]);
if (!ec) {
auto session = std::make_shared<Session>(std::move(socket));
session->start();
}
}
});
}
};
} // magellan
class echo_session : public magellan::session {
public:
typedef std::shared_ptr<echo_session> ptr;
typedef std::weak_ptr<echo_session> wptr;
typedef std::shared_ptr<const echo_session> const_ptr;
typedef std::weak_ptr<const echo_session> const_wptr;
public:
echo_session(tcp::socket socket)
: magellan::session(std::move(socket)) {}
virtual ~echo_session() {}
protected:
void perform(asio::ip::tcp::socket& s, asio::yield_context& yc) {
char data[128];
for (;;) {
std::size_t n = s.async_read_some(asio::buffer(data), yc);
asio::async_write(s, asio::buffer(data, n), yc);
}
}
};
int main (int argc, char const* argv[]) {
try {
if (argc != 2) {
std::cerr << "Usage: echo_server <port>\n";
return 1;
}
asio::io_context io_context;
magellan::server server;
server.accept<echo_session>(io_context, 9003);
io_context.run();
} catch (std::exception& e) {
std::cerr << e.what() << "\n";
}
}
存在一种竞争条件,可能导致访问悬空引用,从而调用未定义的行为。 lambda 捕获列表通过引用捕获自动变量 port
和 io_service
。但是,port
的生命周期可能会在它用于构建 acceptor
之前结束。在这种情况下,未定义的行为很可能会导致程序绑定到随机端口,但它可能会以其他方式失败。
void server::accept(asio::io_context& io_context, short port)
{
using asio::ip::tcp;
asio::spawn(io_context, [&](asio::yield_context yield)
{
tcp::acceptor acceptor(io_context,
tcp::endpoint(tcp::v4(), port));
// ~~~~ lifetime may have ended
...
}
}
要解决此问题,请在 lambda 捕获中按值捕获 port
。变化:
[&](boost::asio::yield_context yield) { ... }
至:
[port, &io_service](boost::asio::yield_context yield) { ... }
以下示例基于原始代码,并且可以(有时)demonstrate竞争条件:
#include <iostream>
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
class server
{
public:
void accept(boost::asio::io_service& io_service, short port)
{
using boost::asio::ip::tcp;
std::cout << "port in accept: " << port << std::endl;
boost::asio::spawn(io_service, [&](boost::asio::yield_context yield)
{
std::cout << "port in coroutine: " << port << std::endl;
tcp::acceptor acceptor(io_service, tcp::endpoint(tcp::v4(), port));
assert(acceptor.is_open());
std::cout << "open on port " << acceptor.local_endpoint() << std::endl;
tcp::socket socket(io_service);
acceptor.async_accept(socket, yield);
});
}
};
int main ()
{
try
{
boost::asio::io_service io_service;
server server;
server.accept(io_service, 12345);
std::cout << "running io_service" << std::endl;
io_service.run();
assert(false);
}
catch (std::exception& e)
{
std::cerr << e.what() << std::endl;
}
}
一个运行的输出:
port in accept: 12345
running io_service
port in coroutine: 0
open on port 0.0.0.0:58424
从输出中可以看出,port
的预期值在 server::accept()
内,但在协程内,悬空引用导致 port
的值为 0
,导致接受器绑定到随机端口。
亲爱的 Whosebug 社区,您好,
(编辑:单文件版本在最后)
我只在某些编译设置中遇到问题(这表明某种 UB,但我决定将此问题标记为 boost-asio,因为它可能涉及有关 asio 细节的知识)。请注意,我使用的是当前 git 版本的独立 asio 库。
首先让我放一些片段 - 完整的代码在这里: https://github.com/paulhilbert/magellan (请注意,CMakeLists.txt 非常老套,可能需要修复以防您想要编译它)。
我在 examples/echo_server.cpp:
中有一个测试回显服务器(tcp,异步)[snip...]
int main (int argc, char const* argv[]) {
try {
if (argc != 2) {
std::cerr << "Usage: echo_server <port>\n";
return 1;
}
asio::io_context io_context;
magellan::server server;
server.accept<echo_session>(io_context, 9003);
io_context.run();
} catch (std::exception& e) {
std::cerr << e.what() << "\n";
}
}
显然有趣的部分是 server
和 echo_session
classes;但是后者似乎可以正常工作,所以我将把 server
class 放在这里。 include/server.hpp:
#ifndef MAGELLAN_SERVER_HPP_
#define MAGELLAN_SERVER_HPP_
#include "session.hpp"
namespace magellan {
class server {
public:
[snip typedefs...]
public:
server();
virtual ~server();
template <typename Session>
void accept(asio::io_context& io_context, short port);
[snip comments...]
};
} // magellan
#include "server.ipp"
#endif /* MAGELLAN_SERVER_HPP_ */
... 和 include/server.ipp:
#include <iostream>
namespace magellan {
template <typename Session>
inline void
server::accept(asio::io_context& io_context, short port) {
using asio::ip::tcp;
asio::spawn(io_context, [&](asio::yield_context yield) {
tcp::acceptor acceptor(io_context, tcp::endpoint(tcp::v4(), port));
for (;;) {
asio::error_code ec;
tcp::socket socket(io_context);
acceptor.async_accept(socket, yield[ec]);
if (!ec) {
auto session = std::make_shared<Session>(std::move(socket));
session->start();
}
}
});
}
[snip comments...]
} // magellan
现在有趣的是 async_accept
打开一个 tcp 套接字 iff 我用 -O1、-O2 和 -O3 编译,而不是 -O0。我通过以下方式检查过:
> ss -a | grep 9003
296:tcp LISTEN 0 128 *:9003 *:*
当使用 -O0 编译时,永远不会打开套接字。我还通过 io_context::work 实例检查了该服务是否仍然 运行。
我最好的猜测(诚然缺乏信心)是 boost 协程与 -O0 做了一些不同的事情。还值得一提的是,如果我在 server.ipp 中使用注释掉的代码,它也不会打开套接字(无论编译设置如何):
template <typename Session>
inline void
server::accept(asio::io_context& io_context, short port) {
using asio::ip::tcp;
accept<Session>(io_context, port, [](tcp::socket s) {
return std::make_shared<Session>(std::move(s));
});
}
template <typename Session, typename Func>
void
server::accept(asio::io_context& io_context, short port, Func&& factory) {
using asio::ip::tcp;
asio::spawn(io_context, [&](asio::yield_context yield) {
tcp::acceptor acceptor(io_context, tcp::endpoint(tcp::v4(), port));
for (;;) {
asio::error_code ec;
tcp::socket socket(io_context);
acceptor.async_accept(socket, yield[ec]);
if (!ec) {
auto session = factory(std::move(socket));
session->start();
} else {
std::cout << "failed accept" << "\n";
}
}
});
}
这是我最初的问题,我将其归因于复制而不是移动的套接字,直到我遇到 -O0 标志问题。
此时我很迷茫,因为我不知道如何调试这些异步进程,但我仍然相信我的问题的实际答案在某种程度上会令人尴尬;)
希望你能给我一个提示。
最好的, 理查德
编辑:
这是一个压缩的单文件版本:
#include <iostream>
#include <asio.hpp>
#include <asio/spawn.hpp>
using asio::ip::tcp;
namespace magellan {
class session : public std::enable_shared_from_this<session> {
public:
session(asio::ip::tcp::socket socket) : socket_(std::move(socket)), strand_(socket_.get_io_context()) {
}
template <typename Func>
void
async_do(Func&& f) {
auto self(shared_from_this());
asio::spawn(strand_, [this, self, f](asio::yield_context yield) {
try {
f(std::ref(socket_), std::ref(yield));
} catch (std::exception& e) {
socket_.close();
}
});
}
void start() {
async_do([this] (tcp::socket& s, asio::yield_context& yc) {
perform(s, yc);
});
}
protected:
virtual void perform(asio::ip::tcp::socket& s, asio::yield_context&) {
s.close();
}
protected:
asio::ip::tcp::socket socket_;
asio::io_context::strand strand_;
};
class server {
public:
typedef std::shared_ptr<server> ptr;
typedef std::weak_ptr<server> wptr;
typedef std::shared_ptr<const server> const_ptr;
typedef std::weak_ptr<const server> const_wptr;
public:
server() {}
virtual ~server() {}
template <typename Session>
void
accept(asio::io_context& io_context, short port) {
using asio::ip::tcp;
asio::spawn(io_context, [&](asio::yield_context yield) {
tcp::acceptor acceptor(io_context, tcp::endpoint(tcp::v4(), port));
for (;;) {
asio::error_code ec;
tcp::socket socket(io_context);
acceptor.async_accept(socket, yield[ec]);
if (!ec) {
auto session = std::make_shared<Session>(std::move(socket));
session->start();
}
}
});
}
};
} // magellan
class echo_session : public magellan::session {
public:
typedef std::shared_ptr<echo_session> ptr;
typedef std::weak_ptr<echo_session> wptr;
typedef std::shared_ptr<const echo_session> const_ptr;
typedef std::weak_ptr<const echo_session> const_wptr;
public:
echo_session(tcp::socket socket)
: magellan::session(std::move(socket)) {}
virtual ~echo_session() {}
protected:
void perform(asio::ip::tcp::socket& s, asio::yield_context& yc) {
char data[128];
for (;;) {
std::size_t n = s.async_read_some(asio::buffer(data), yc);
asio::async_write(s, asio::buffer(data, n), yc);
}
}
};
int main (int argc, char const* argv[]) {
try {
if (argc != 2) {
std::cerr << "Usage: echo_server <port>\n";
return 1;
}
asio::io_context io_context;
magellan::server server;
server.accept<echo_session>(io_context, 9003);
io_context.run();
} catch (std::exception& e) {
std::cerr << e.what() << "\n";
}
}
存在一种竞争条件,可能导致访问悬空引用,从而调用未定义的行为。 lambda 捕获列表通过引用捕获自动变量 port
和 io_service
。但是,port
的生命周期可能会在它用于构建 acceptor
之前结束。在这种情况下,未定义的行为很可能会导致程序绑定到随机端口,但它可能会以其他方式失败。
void server::accept(asio::io_context& io_context, short port)
{
using asio::ip::tcp;
asio::spawn(io_context, [&](asio::yield_context yield)
{
tcp::acceptor acceptor(io_context,
tcp::endpoint(tcp::v4(), port));
// ~~~~ lifetime may have ended
...
}
}
要解决此问题,请在 lambda 捕获中按值捕获 port
。变化:
[&](boost::asio::yield_context yield) { ... }
至:
[port, &io_service](boost::asio::yield_context yield) { ... }
以下示例基于原始代码,并且可以(有时)demonstrate竞争条件:
#include <iostream>
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
class server
{
public:
void accept(boost::asio::io_service& io_service, short port)
{
using boost::asio::ip::tcp;
std::cout << "port in accept: " << port << std::endl;
boost::asio::spawn(io_service, [&](boost::asio::yield_context yield)
{
std::cout << "port in coroutine: " << port << std::endl;
tcp::acceptor acceptor(io_service, tcp::endpoint(tcp::v4(), port));
assert(acceptor.is_open());
std::cout << "open on port " << acceptor.local_endpoint() << std::endl;
tcp::socket socket(io_service);
acceptor.async_accept(socket, yield);
});
}
};
int main ()
{
try
{
boost::asio::io_service io_service;
server server;
server.accept(io_service, 12345);
std::cout << "running io_service" << std::endl;
io_service.run();
assert(false);
}
catch (std::exception& e)
{
std::cerr << e.what() << std::endl;
}
}
一个运行的输出:
port in accept: 12345
running io_service
port in coroutine: 0
open on port 0.0.0.0:58424
从输出中可以看出,port
的预期值在 server::accept()
内,但在协程内,悬空引用导致 port
的值为 0
,导致接受器绑定到随机端口。