接收方拒绝仅为- 0构建打开套接字

Acceptor refusing to open socket only for -O0 build

本文关键字:构建 套接字 方拒绝 拒绝      更新时间:2023-10-16

大家好,

(编辑:单文件版本在末尾)

我只在某些编译设置中有问题(这表明某种UB,但我决定将这个问题标记为boost-asio,因为它可能涉及有关asio细节的知识)。注意,我使用的是当前git版本的独立asio库。

首先让我删除一些片段-完整的代码在这里:https://github.com/paulhilbert/magellan(请注意,CMakeLists.txt非常粗糙,可能需要修复,以防您想要编译它)。

我有一个测试echo服务器(tcp, async)在examples/echo_server.cpp:

[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";
    }
}

显然,serverecho_session类是有趣的部分;然而,后者似乎工作正常,所以我要放弃server类在这里。包括/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套接字,如果我用-O1, -O2和-O3编译,而不是用- 0。我通过:

> ss -a | grep 9003
296:tcp    LISTEN     0      128     *:9003                  *:*  

当使用- 0编译时,套接字永远不会打开。我还通过io_context::work实例检查了该服务是否仍在运行。

我最好的猜测(诚然缺乏信心)是boost协程与- 0做了一些不同的事情。值得一提的是,如果我在服务器中使用注释掉的代码。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";
            }
        }
    });
}

这是我最初的问题,我将其归因于复制的套接字而不是移动的套接字,直到我到达- 0标志问题。

我在这一点上相当迷失,因为我不知道如何调试这些异步进程,但我仍然有信心对我的问题的实际答案将在某种程度上令人尴尬;)

希望你能给我点提示。

最好的,理查德。

编辑:

以下是一个压缩的单文件版本:

#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捕获列表通过引用捕获自动变量portio_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-capture中按值捕获port。变化:

[&](boost::asio::yield_context yield) { ... }

:

[port, &io_service](boost::asio::yield_context yield) { ... }

下面的示例基于原始代码,可以(有时)演示竞争条件:

#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

从输出中可以看出,portserver::accept()中具有预期的值,但是在协程中,悬空引用导致port的值为0,从而导致受体绑定到一个随机端口。