簡體   English   中英

C++ UDP 在線程中運行的服務器 io_context 在工作開始前退出

[英]C++ UDP Server io_context running in thread exits before work can start

我是 C++ 的新手,但到目前為止,大多數 asio 內容都是有意義的。 然而,我正在努力讓我的 UDPServer 工作。

我的問題可能類似於: Trying to write UDP server class, io_context doesn't block

我認為我的 UDPServer 在可以將工作交給它的 io_context 之前就停止了。 但是,我在調用 io_context.run() 之前向上下文發布工作,所以我不明白為什么。

當然,我不完全確定我的上述陳述是否正確,希望得到一些指導。 這是我的 class:

template<typename message_T>
    class UDPServer
    {
    public:
        UDPServer(uint16_t port)
            : m_socket(m_asioContext, asio::ip::udp::endpoint(asio::ip::udp::v4(), port))
        {
            m_port = port;
        }

        virtual ~UDPServer()
        {
            Stop();
        }



    public:

        // Starts the server!
        bool Start()
        {
            try
            {
                // Issue a task to the asio context
                WaitForMessages();

                m_threadContext = std::thread([this]() { m_asioContext.run(); });
            }
            catch (std::exception& e)
            {
                // Something prohibited the server from listening
                std::cerr << "[SERVER @ PORT " << m_port << "] Exception: " << e.what() << "\n";
                return false;
            }

            std::cout << "[SERVER @ PORT " << m_port << "] Started!\n";
            return true;
        }

        // Stops the server!
        void Stop()
        {
            // Request the context to close
            m_asioContext.stop();

            // Tidy up the context thread
            if (m_threadContext.joinable()) m_threadContext.join();

            // Inform someone, anybody, if they care...
            std::cout << "[SERVER @ PORT " << m_port << "] Stopped!\n";
        }


        void WaitForMessages()
        {

            m_socket.async_receive_from(asio::buffer(vBuffer.data(), vBuffer.size()), m_endpoint,
                [this](std::error_code ec, std::size_t length)
                {
                    if (!ec)
                    {
                        
                        std::cout << "[SERVER @ PORT " << m_port << "] Got " << length << " bytes \n Data: " << vBuffer.data() << "\n" << "Address: " << m_endpoint.address() << " Port: " << m_endpoint.port() << "\n" << "Data: " << m_endpoint.data() << "\n";

                    }
                    else
                    {
                        std::cerr << "[SERVER @ PORT " << m_port << "] Exception: " << ec.message() << "\n";
                        return;
                    }

                    WaitForMessages();
                }
            );
        }

        void Send(message_T& msg, const asio::ip::udp::endpoint& ep)
        {
            asio::post(m_asioContext,
                [this, msg, ep]()
                {
                    // If the queue has a message in it, then we must
                    // assume that it is in the process of asynchronously being written.
                    
                    bool bWritingMessage = !m_messagesOut.empty();
                    m_messagesOut.push_back(msg);
                    if (!bWritingMessage)
                    {
                        WriteMessage(ep);
                    }
                }
            );
        }

    private:

        void WriteMessage(const asio::ip::udp::endpoint& ep)
        {

            m_socket.async_send_to(asio::buffer(&m_messagesOut.front(), sizeof(message_T)), ep,
                [this, ep](std::error_code ec, std::size_t length)
                {

                    if (!ec)
                    {

                        m_messagesOut.pop_front();

                        // If the queue is not empty, there are more messages to send, so
                        // make this happen by issuing the task to send the next header.
                        if (!m_messagesOut.empty())
                        {
                            WriteMessage(ep);
                        }
                        
                    }
                    else
                    {
                        std::cout << "[SERVER @ PORT " << m_port << "] Write Header Fail.\n";
                        m_socket.close();
                    }
                });
        }

        void ReadMessage()
        {
        }

    private:
        
        uint16_t m_port = 0;
        asio::ip::udp::endpoint m_endpoint;
        std::vector<char> vBuffer = std::vector<char>(21);


    protected:
        TSQueue<message_T> m_messagesIn;
        TSQueue<message_T> m_messagesOut;
        Message<message_T> m_tempMessageBuf;
        asio::io_context m_asioContext;
        std::thread m_threadContext;
        asio::ip::udp::socket m_socket;
    };
}

現在在主 function 中調用代碼:

enum class TestMsg {
    Ping,
    Join,
    Leave
};

int main() {
    Message<TestMsg> msg; // Message is a pretty basic struct that I'm not using yet. When I was, I was only receiving the first 4 bytes - which led me down this path of investigation
    msg.id = TestMsg::Join;
    msg << "hello";

    UDPServer<Message<TestMsg>> server(60000);
}

調用時,服務器會在有機會打印“[SERVER] Started”之前立即退出

在此處輸入圖像描述

我將嘗試按照鏈接帖子的描述添加工作守衛,但我仍然想了解為什么 io_context 沒有足夠快地准備好工作。

更新(現在我還閱讀了問題,而不僅僅是代碼)在WaitForMessages中,您確實通過從 function 調用m_socket.async_receive_from開始監聽,因為它是異步的,function 將在設置監聽后立即返回/解鎖。 因此,只要您實際上沒有客戶端向您發送內容,您的服務器就無能為力。 只有當它收到一些東西時,回調才會被調用io_context::run的線程調用。 所以你需要工作守衛,這樣你的線程運行run就不會在啟動后立即解除阻塞,而是只要工作守衛在那里就會阻塞。 如果在處理程序中拋出異常並且您仍想繼續使用您的服務器,通常它還會與 try/while 模式結合使用。

同樣在您發布的代碼中,您實際上從未調用UDPServer::Start


這是我對答案的第一個想法:

這是 ASIO 的正常行為。 io_context::run function 將在沒有工作要做時立即返回。

因此,要更改run function 的行為以阻止您必須使用boost::asio::executor_work_guard<boost::asio::io_context::executor_type>即所謂的工作守衛。 使用對您的 io_context 的引用構造io_context並保持它,即不要讓它破壞,只要您想讓服務器運行,即不想讓io_context::run在沒有工作時返回。

所以給出

    boost::asio::io_context io_context_;
    boost::asio::executor_work_guard<boost::asio::io_context::executor_type> work_guard_;

然后你可以打電話

      work_guard_{boost::asio::make_work_guard(io_context_)},

const auto thread_count{std::max<unsigned>(std::thread::hardware_concurrency(), 1)};

std::generate_n(std::back_inserter(this->io_run_threads_),
                thread_count,
                [this]() {
                    return std::thread{io_run_loop,
                                       std::ref(this->io_context_), std::ref(this->error_handler_)};
                });

void io_run_loop(boost::asio::io_context &context,
                                    const std::function<void(std::exception &)> &error_handler) {
    while (true) {
        try {
            context.run();
            break;
        } catch (std::exception &e) {
            error_handler(e);
        }
    }

}

然后關閉服務器:

work_guard_.reset();
io_context_.stop();
std::for_each(this->io_run_threads_.begin(), this->io_run_threads_.end(), [](auto &thread) {
    if (thread.joinable()) thread.join();
});

為了更優雅地關閉,您可以省略stop調用,而是關閉之前的所有 sockets。

看起來你忘了打電話給server.Start(); . 此外,您需要讓主線程等待一段時間,否則Server的析構函數將立即導致Stop()被調用:

int main()
{
    Message<TestMsg> msg;
    msg.id = TestMsg::Join;
    msg << "hello";

    UDPServer<Message<TestMsg>> server(60000);
    server.Start();

    std::this_thread::sleep_for(30s);
}

問題

  1. Send API 存在一個概念性問題。它在每次調用時都需要一個端點,但它只使用啟動寫入調用鏈的端點! 這意味着如果你這樣做

     srv.Send(msg1, {mymachine, 60001}); srv.Send(msg1, {otherserver, 5517});

    它們很可能都被發送到 mymachine:60001。

  2. 你如何對待收到的緩沖區。 只是盲目地使用.data()假設數據是 NUL 終止的。 不要那樣做:

     std::string const data(vBuffer.data(), length);

    此外,您似乎有時對數據和打印m_endpoint.data()感到困惑 - 您的公主在另一座城堡中。

    實際上,您可能想要提取類型化數據的方法。 我將其保留在今天這個問題的 scope 之外。

  3. 無論如何,您應該在重用之前清除緩沖區,因為您可能會在后續讀取中看到舊數據。

     vBuffer.assign(vBuffer.size(), '\0');
  4. 這很可能是未定義的行為

     asio::buffer(&m_messagesOut.front(), sizeof(message_T)), ep,

    這僅在message_T是微不足道且標准布局(“POD” - 普通舊數據)時有效。 operator<<的存在強烈表明情況並非如此。

    相反,構建一個(序列)緩沖區帽子將消息表示為原始字節,例如

    auto& msg = m_messagesOut.front(); msg.length = msg.body.size(); m_socket.async_send_to( std::vector<asio::const_buffer>{ asio::buffer(&msg.id, sizeof(msg.id)), asio::buffer(&msg.length, sizeof(msg.length)), asio::buffer(msg.body), }, //...
  5. 線程安全隊列似乎有點矯枉過正,因為您只有一個服務線程; 那是一個隱式的“鏈”,因此您可以將其發布到它以具有單線程語義。

到目前為止,這里有一些改編以使其工作(除了讀者練習指出的):

生活在 Coliru

#include <boost/asio.hpp>
#include <iostream>
#include <deque>
#include <sstream>

// Library facilities
namespace asio = boost::asio;
using asio::ip::udp;
using boost::system::error_code;
using namespace std::chrono_literals;

/////////////////////////////////
// mock ups:
template <typename message_T> struct Message {
    message_T   id;
    uint16_t    length; // automatically filled on send, UDP packets are < 64k
    std::string body;

    template <typename T> friend Message& operator<<(Message& m, T const& v)
    {
        std::ostringstream oss;
        oss << v;
        m.body += oss.str();
        //m.body += '\0'; // suggestion for easier message extraction

        return m;
    }
};

// Thread-safety can be replaced with the implicit strand of a single service
// thread
template <typename T> using TSQueue = std::deque<T>;
// end mock ups
/////////////////////////////////

template <typename message_T> class UDPServer {
  public:
    UDPServer(uint16_t port)
        : m_socket(m_asioContext, udp::endpoint(udp::v4(), port))
    {
        m_port = port;
    }

    virtual ~UDPServer() { Stop(); }

  public:
    // Starts the server!
    bool Start()
    {
        if (m_threadContext.joinable() && !m_asioContext.stopped())
            return false;

        try {
            // Issue a task to the asio context
            WaitForMessages();

            m_threadContext = std::thread([this]() { m_asioContext.run(); });
        } catch (std::exception const& e) {
            // Something prohibited the server from listening
            std::cerr << "[SERVER @ PORT " << m_port
                      << "] Exception: " << e.what() << "\n";
            return false;
        }
        std::cout << "[SERVER @ PORT " << m_port << "] Started!\n";
        return true;
    }

    // Stops the server!
    void Stop()
    {
        // Tell the context to stop processing
        m_asioContext.stop();

        // Tidy up the context thread
        if (m_threadContext.joinable())
            m_threadContext.join();

        // Inform someone, anybody, if they care...
        std::cout << "[SERVER @ PORT " << m_port << "] Stopped!\n";

        m_asioContext
            .reset(); // required in case you want to reuse this Server object
    }

    void Send(message_T& msg, const udp::endpoint& ep)
    {
        asio::post(m_asioContext, [this, msg, ep]() {
            // If the queue has a message in it, then we must
            // assume that it is in the process of asynchronously being written.

            bool bWritingMessage = !m_messagesOut.empty();
            m_messagesOut.push_back(msg);
            if (!bWritingMessage) {
                WriteMessage(ep);
            }
        });
    }

  private:
    void WaitForMessages() // assumed to be on-strand
    {
        vBuffer.assign(vBuffer.size(), '\0');
        m_socket.async_receive_from(
            asio::buffer(vBuffer.data(), vBuffer.size()), m_endpoint,
            [this](std::error_code ec, std::size_t length) {
                if (!ec) {
                    std::string const data(vBuffer.data(), length);

                    std::cout << "[SERVER @ PORT " << m_port << "] Got "
                              << length << " bytes \n Data: " << data << "\n"
                              << "Address: " << m_endpoint.address()
                              << " Port: " << m_endpoint.port() << "\n"
                              << std::endl;
                } else {
                    std::cerr << "[SERVER @ PORT " << m_port
                              << "] Exception: " << ec.message() << "\n";
                    return;
                }

                WaitForMessages();
            });
    }

    void WriteMessage(const udp::endpoint& ep)
    {
        auto& msg = m_messagesOut.front();
        msg.length = msg.body.size();

        m_socket.async_send_to(
            std::vector<asio::const_buffer>{
                asio::buffer(&msg.id, sizeof(msg.id)),
                asio::buffer(&msg.length, sizeof(msg.length)),
                asio::buffer(msg.body),
            },
            ep, [this, ep](std::error_code ec, std::size_t length) {
                if (!ec) {
                    m_messagesOut.pop_front();

                    // If the queue is not empty, there are more messages to
                    // send, so make this happen by issuing the task to send the
                    // next header.
                    if (!m_messagesOut.empty()) {
                        WriteMessage(ep);
                    }

                } else {
                    std::cout << "[SERVER @ PORT " << m_port
                              << "] Write Header Fail.\n";
                    m_socket.close();
                }
            });
    }

  private:
    uint16_t          m_port = 0;
    udp::endpoint     m_endpoint;
    std::vector<char> vBuffer = std::vector<char>(21);

  protected:
    TSQueue<message_T> m_messagesIn;
    TSQueue<message_T> m_messagesOut;
    Message<message_T> m_tempMessageBuf;

    asio::io_context m_asioContext;
    std::thread      m_threadContext;
    udp::socket      m_socket;
};

enum class TestMsg {
    Ping,
    Join,
    Leave
};

int main()
{
    UDPServer<Message<TestMsg>> server(60'000);
    if (server.Start()) {
        std::this_thread::sleep_for(3s);

        {
            Message<TestMsg> msg;
            msg.id = TestMsg::Join;
            msg << "hello PI equals " << M_PI  << " in this world";

            server.Send(msg, {{}, 60'001});
        }

        std::this_thread::sleep_for(27s);
    }
}

出於某種原因,.netcat 不適用於 Coliru 上的 UDP,所以這里有一個“實時”演示:

在此處輸入圖像描述

您可以看到我們的 .netcat 客戶端消息到達。 您可以在 tcpdump output 中看到發送到 60001 的消息。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM