【问题标题】:Boost ASIO: Send message to all connected clientsBoost ASIO:向所有连接的客户端发送消息
【发布时间】:2018-03-20 21:19:17
【问题描述】:

我正在处理一个涉及boost::beast websocket/http 混合服务器的项目,该服务器在boost::asio 之上运行。我的项目很大程度上基于 advanced_server.cpp 示例源。

它工作正常,但现在我正在尝试添加一个需要向所有连接的客户端发送消息的功能。

我对@9​​87654326@ 不是很熟悉,但现在我看不出有任何方法可以进行“广播”事件(如果这甚至是正确的术语)。

我天真的方法是看看我是否可以让websocket_session() 的构造附加事件侦听器之类的东西,而析构函数则分离侦听器。那时,我可以触发事件,并让所有当前有效的 websocket 会话(websocket_session() 的生命周期被限定)执行回调。

有https://stackoverflow.com/a/17029022/268006,它通过(ab)使用boost::asio::steady_timer 或多或少地完成了我想要的,但这似乎是一种可怕的黑客攻击来完成应该非常简单的事情。

基本上,给定一个有状态的boost::asio 服务器,我如何对多个连接进行操作?

【问题讨论】:

    标签: c++ boost boost-asio


    【解决方案1】:

    首先:您可以广播 UDP,但这不适用于已连接的客户端。那只是... UDP。

    其次,该链接显示了如何在 Asio 中拥有类似条件变量(事件)的接口。这只是你问题的一小部分。您忘记了大局:您需要以一种或另一种方式了解打开的连接集:

    1. 例如为每个连接保留一个包含会话指针 (weak_ptr) 的容器
    2. 每个连接订阅一个信号槽(例如Boost Signals)。

    选项 1. 非常适合性能,选项 2. 更适合灵活性(将事件源与订阅者分离,从而可以拥有异构订阅者,例如不来自连接)。

    因为我认为选项 1. 相对于线程来说更简单,更好的 w.r.t.效率(例如,您可以从一个缓冲区为所有客户端提供服务而无需复制)并且您可能不需要双重解耦信号/插槽,让我参考一个答案,我已经为纯 Asio(没有 Beast)展示了同样多的内容:

    它展示了“连接池”的概念——它本质上是weak_ptr<connection> 对象的线程安全容器,带有一些垃圾收集逻辑。

    演示:介绍 Echo 服务器

    在chatting about things 之后,我想花时间实际演示这两种方法,所以我在说什么就完全清楚了。

    首先让我们展示一个简单的、普通的异步 TCP 服务器与

    • 具有多个并发连接
    • 每个连接的会话从客户端逐行读取,并将相同的内容回显给客户端
    • 3 秒后停止接受,最后一个客户端断开连接后退出

    master branch on github

    #include <boost/asio.hpp>
    #include <memory>
    #include <list>
    #include <iostream>
    
    namespace ba = boost::asio;
    using ba::ip::tcp;
    using boost::system::error_code;
    using namespace std::chrono_literals;
    using namespace std::string_literals;
    
    static bool s_verbose = false;
    
    struct connection : std::enable_shared_from_this<connection> {
        connection(ba::io_context& ioc) : _s(ioc) {}
    
        void start() { read_loop(); }
        void send(std::string msg, bool at_front = false) {
            post(_s.get_io_service(), [=] { // _s.get_executor() for newest Asio
                if (enqueue(std::move(msg), at_front))
                    write_loop();
            });
        }
    
      private:
        void do_echo() {
            std::string line;
            if (getline(std::istream(&_rx), line)) {
                send(std::move(line) + '\n');
            }
        }
    
        bool enqueue(std::string msg, bool at_front)
        { // returns true if need to start write loop
            at_front &= !_tx.empty(); // no difference
            if (at_front)
                _tx.insert(std::next(begin(_tx)), std::move(msg));
            else
                _tx.push_back(std::move(msg));
    
            return (_tx.size() == 1);
        }
        bool dequeue()
        { // returns true if more messages pending after dequeue
            assert(!_tx.empty());
            _tx.pop_front();
            return !_tx.empty();
        }
    
        void write_loop() {
            ba::async_write(_s, ba::buffer(_tx.front()), [this,self=shared_from_this()](error_code ec, size_t n) {
                    if (s_verbose) std::cout << "Tx: " << n << " bytes (" << ec.message() << ")" << std::endl;
                    if (!ec && dequeue()) write_loop();
                });
        }
    
        void read_loop() {
            ba::async_read_until(_s, _rx, "\n", [this,self=shared_from_this()](error_code ec, size_t n) {
                    if (s_verbose) std::cout << "Rx: " << n << " bytes (" << ec.message() << ")" << std::endl;
                    do_echo();
                    if (!ec)
                        read_loop();
                });
        }
    
        friend struct server;
        ba::streambuf          _rx;
        std::list<std::string> _tx;
        tcp::socket            _s;
    };
    
    struct server {
        server(ba::io_context& ioc) : _ioc(ioc) {
            _acc.bind({{}, 6767});
            _acc.set_option(tcp::acceptor::reuse_address());
            _acc.listen();
            accept_loop();
        }
    
        void stop() {
            _ioc.post([=] {
                    _acc.cancel();
                    _acc.close();
                });
        }
    
      private:
        void accept_loop() {
            auto session = std::make_shared<connection>(_acc.get_io_context());
            _acc.async_accept(session->_s, [this,session](error_code ec) {
                 auto ep = ec? tcp::endpoint{} : session->_s.remote_endpoint();
                 std::cout << "Accept from " << ep << " (" << ec.message() << ")" << std::endl;
    
                 session->start();
                 if (!ec)
                     accept_loop();
            });
        }
    
        ba::io_context& _ioc;
        tcp::acceptor _acc{_ioc, tcp::v4()};
    };
    
    int main(int argc, char** argv) {
        s_verbose = argc>1 && argv[1] == "-v"s;
    
        ba::io_context ioc;
    
        server s(ioc);
    
        std::thread th([&ioc] { ioc.run(); }); // todo exception handling
    
        std::this_thread::sleep_for(3s);
        s.stop(); // active connections will continue
    
        th.join();
    }
    

    方法一、添加广播消息

    所以,让我们添加同时发送到所有活动连接的“广播消息”。我们添加两个:

    • 每个新连接一个(说“玩家 ## 已进入游戏”)
    • 一个模拟全局“服务器事件”的事件,就像您在问题中描述的那样)。它从 main 内部触发:

      std::this_thread::sleep_for(1s);
      
      auto n = s.broadcast("random global event broadcast\n");
      std::cout << "Global event broadcast reached " << n << " active connections\n";
      

    注意我们如何通过注册一个指向每个接受的连接的弱指针并在每个连接上进行操作来做到这一点:

        _acc.async_accept(session->_s, [this,session](error_code ec) {
             auto ep = ec? tcp::endpoint{} : session->_s.remote_endpoint();
             std::cout << "Accept from " << ep << " (" << ec.message() << ")" << std::endl;
    
             if (!ec) {
                 auto n = reg_connection(session);
    
                 session->start();
                 accept_loop();
    
                 broadcast("player #" + std::to_string(n) + " has entered the game\n");
             }
    
        });
    

    broadcast 也直接从main 使用,很简单:

    size_t broadcast(std::string const& msg) {
        return for_each_active([msg](connection& c) { c.send(msg, true); });
    }
    

    using-asio-post branch on github

    #include <boost/asio.hpp>
    #include <memory>
    #include <list>
    #include <iostream>
    
    namespace ba = boost::asio;
    using ba::ip::tcp;
    using boost::system::error_code;
    using namespace std::chrono_literals;
    using namespace std::string_literals;
    
    static bool s_verbose = false;
    
    struct connection : std::enable_shared_from_this<connection> {
        connection(ba::io_context& ioc) : _s(ioc) {}
    
        void start() { read_loop(); }
        void send(std::string msg, bool at_front = false) {
            post(_s.get_io_service(), [=] { // _s.get_executor() for newest Asio
                if (enqueue(std::move(msg), at_front))
                    write_loop();
            });
        }
    
      private:
        void do_echo() {
            std::string line;
            if (getline(std::istream(&_rx), line)) {
                send(std::move(line) + '\n');
            }
        }
    
        bool enqueue(std::string msg, bool at_front)
        { // returns true if need to start write loop
            at_front &= !_tx.empty(); // no difference
            if (at_front)
                _tx.insert(std::next(begin(_tx)), std::move(msg));
            else
                _tx.push_back(std::move(msg));
    
            return (_tx.size() == 1);
        }
        bool dequeue()
        { // returns true if more messages pending after dequeue
            assert(!_tx.empty());
            _tx.pop_front();
            return !_tx.empty();
        }
    
        void write_loop() {
            ba::async_write(_s, ba::buffer(_tx.front()), [this,self=shared_from_this()](error_code ec, size_t n) {
                    if (s_verbose) std::cout << "Tx: " << n << " bytes (" << ec.message() << ")" << std::endl;
                    if (!ec && dequeue()) write_loop();
                });
        }
    
        void read_loop() {
            ba::async_read_until(_s, _rx, "\n", [this,self=shared_from_this()](error_code ec, size_t n) {
                    if (s_verbose) std::cout << "Rx: " << n << " bytes (" << ec.message() << ")" << std::endl;
                    do_echo();
                    if (!ec)
                        read_loop();
                });
        }
    
        friend struct server;
        ba::streambuf          _rx;
        std::list<std::string> _tx;
        tcp::socket            _s;
    };
    
    struct server {
        server(ba::io_context& ioc) : _ioc(ioc) {
            _acc.bind({{}, 6767});
            _acc.set_option(tcp::acceptor::reuse_address());
            _acc.listen();
            accept_loop();
        }
    
        void stop() {
            _ioc.post([=] {
                    _acc.cancel();
                    _acc.close();
                });
        }
    
        size_t broadcast(std::string const& msg) {
            return for_each_active([msg](connection& c) { c.send(msg, true); });
        }
    
      private:
        using connptr = std::shared_ptr<connection>;
        using weakptr = std::weak_ptr<connection>;
    
        std::mutex _mx;
        std::vector<weakptr> _registered;
    
        size_t reg_connection(weakptr wp) {
            std::lock_guard<std::mutex> lk(_mx);
            _registered.push_back(wp);
            return _registered.size();
        }
    
        template <typename F>
        size_t for_each_active(F f) {
            std::vector<connptr> active;
            {
                std::lock_guard<std::mutex> lk(_mx);
                for (auto& w : _registered)
                    if (auto c = w.lock())
                        active.push_back(c);
            }
    
            for (auto& c : active) {
                std::cout << "(running action for " << c->_s.remote_endpoint() << ")" << std::endl;
                f(*c);
            }
    
            return active.size();
        }
    
        void accept_loop() {
            auto session = std::make_shared<connection>(_acc.get_io_context());
            _acc.async_accept(session->_s, [this,session](error_code ec) {
                 auto ep = ec? tcp::endpoint{} : session->_s.remote_endpoint();
                 std::cout << "Accept from " << ep << " (" << ec.message() << ")" << std::endl;
    
                 if (!ec) {
                     auto n = reg_connection(session);
    
                     session->start();
                     accept_loop();
    
                     broadcast("player #" + std::to_string(n) + " has entered the game\n");
                 }
    
            });
        }
    
        ba::io_context& _ioc;
        tcp::acceptor _acc{_ioc, tcp::v4()};
    };
    
    int main(int argc, char** argv) {
        s_verbose = argc>1 && argv[1] == "-v"s;
    
        ba::io_context ioc;
    
        server s(ioc);
    
        std::thread th([&ioc] { ioc.run(); }); // todo exception handling
    
        std::this_thread::sleep_for(1s);
    
        auto n = s.broadcast("random global event broadcast\n");
        std::cout << "Global event broadcast reached " << n << " active connections\n";
    
        std::this_thread::sleep_for(2s);
        s.stop(); // active connections will continue
    
        th.join();
    }
    

    方法 2:那些广播但使用 Boost Signals2

    信号方法是Dependency Inversion 的一个很好的例子。

    最突出的注释:

    • 在调用它的线程上调用信号槽(“引发事件”)
    • scoped_connection 存在,因此当connection 被破坏时订阅会*自动删除
    • subtle difference in the wording of the console message 从“达到 # 个活动连接”到“达到 # 个活动 订阅者”。

    区别是理解增加的灵活性的关键:信号所有者/调用者对订阅者一无所知。这就是我们所说的解耦/依赖倒置

    using-signals2 branch on github

    #include <boost/asio.hpp>
    #include <memory>
    #include <list>
    #include <iostream>
    #include <boost/signals2.hpp>
    
    namespace ba = boost::asio;
    using ba::ip::tcp;
    using boost::system::error_code;
    using namespace std::chrono_literals;
    using namespace std::string_literals;
    
    static bool s_verbose = false;
    
    struct connection : std::enable_shared_from_this<connection> {
        connection(ba::io_context& ioc) : _s(ioc) {}
    
        void start() { read_loop(); }
        void send(std::string msg, bool at_front = false) {
            post(_s.get_io_service(), [=] { // _s.get_executor() for newest Asio
                if (enqueue(std::move(msg), at_front))
                    write_loop();
            });
        }
    
      private:
        void do_echo() {
            std::string line;
            if (getline(std::istream(&_rx), line)) {
                send(std::move(line) + '\n');
            }
        }
    
        bool enqueue(std::string msg, bool at_front)
        { // returns true if need to start write loop
            at_front &= !_tx.empty(); // no difference
            if (at_front)
                _tx.insert(std::next(begin(_tx)), std::move(msg));
            else
                _tx.push_back(std::move(msg));
    
            return (_tx.size() == 1);
        }
        bool dequeue()
        { // returns true if more messages pending after dequeue
            assert(!_tx.empty());
            _tx.pop_front();
            return !_tx.empty();
        }
    
        void write_loop() {
            ba::async_write(_s, ba::buffer(_tx.front()), [this,self=shared_from_this()](error_code ec, size_t n) {
                    if (s_verbose) std::cout << "Tx: " << n << " bytes (" << ec.message() << ")" << std::endl;
                    if (!ec && dequeue()) write_loop();
                });
        }
    
        void read_loop() {
            ba::async_read_until(_s, _rx, "\n", [this,self=shared_from_this()](error_code ec, size_t n) {
                    if (s_verbose) std::cout << "Rx: " << n << " bytes (" << ec.message() << ")" << std::endl;
                    do_echo();
                    if (!ec)
                        read_loop();
                });
        }
    
        friend struct server;
        ba::streambuf          _rx;
        std::list<std::string> _tx;
        tcp::socket            _s;
    
        boost::signals2::scoped_connection _subscription;
    };
    
    struct server {
        server(ba::io_context& ioc) : _ioc(ioc) {
            _acc.bind({{}, 6767});
            _acc.set_option(tcp::acceptor::reuse_address());
            _acc.listen();
            accept_loop();
        }
    
        void stop() {
            _ioc.post([=] {
                    _acc.cancel();
                    _acc.close();
                });
        }
    
        size_t broadcast(std::string const& msg) {
            _broadcast_event(msg);
            return _broadcast_event.num_slots();
        }
    
      private:
        boost::signals2::signal<void(std::string const& msg)> _broadcast_event;
    
        size_t reg_connection(connection& c) {
            c._subscription = _broadcast_event.connect(
                    [&c](std::string msg){ c.send(msg, true); }
                );
    
            return _broadcast_event.num_slots();
        }
    
        void accept_loop() {
            auto session = std::make_shared<connection>(_acc.get_io_context());
            _acc.async_accept(session->_s, [this,session](error_code ec) {
                 auto ep = ec? tcp::endpoint{} : session->_s.remote_endpoint();
                 std::cout << "Accept from " << ep << " (" << ec.message() << ")" << std::endl;
    
                 if (!ec) {
                     auto n = reg_connection(*session);
    
                     session->start();
                     accept_loop();
    
                     broadcast("player #" + std::to_string(n) + " has entered the game\n");
                 }
    
            });
        }
    
        ba::io_context& _ioc;
        tcp::acceptor _acc{_ioc, tcp::v4()};
    };
    
    int main(int argc, char** argv) {
        s_verbose = argc>1 && argv[1] == "-v"s;
    
        ba::io_context ioc;
    
        server s(ioc);
    
        std::thread th([&ioc] { ioc.run(); }); // todo exception handling
    
        std::this_thread::sleep_for(1s);
    
        auto n = s.broadcast("random global event broadcast\n");
        std::cout << "Global event broadcast reached " << n << " active subscribers\n";
    
        std::this_thread::sleep_for(2s);
        s.stop(); // active connections will continue
    
        th.join();
    }
    

    查看方法 1. 和 2. 之间的差异:Compare View on github

    针对 3 个并发客户端运行时的输出示例:

    (for a in {1..3}; do netcat localhost 6767 < /etc/dictionaries-common/words > echoed.$a& sleep .1; done; time wait)
    

    【讨论】:

    • > You forgot about the big picture: you need to know about the set of open connections, one way or the other: 我没有。我已经有一些每个连接的状态,我可以在它的构造上附加到事件源,并在它的破坏时分离。这将连接状态与侦听器联系起来,并重复使用 asio 循环也必须为此知道所有客户端的事实。
    • 嗯,这是“选项 2”。然后。我只是这么说,因为使用link 的“模拟条件变量”确实没有多大意义。事实上,你说过这件事:“有stackoverflow.com/a/17029022/268006,它或多或少地做了我想要的” - 这就是为什么我说你忘记了大局。 [仅仅因为在线程并发世界中你会使用条件变量进行同步,并不意味着在 Asio 中模拟它“或多或少地做了你想要的”。]
    • 所以回到答案,我认为您基本上说您更喜欢“选项 2”是否正确。方法[所以你不需要明确的连接跟踪]?还有什么我们可以澄清的吗?
    • 但重点是我不需要同步,我只需要在某个时候为每个侦听器调用一个函数(输出缓冲区是一个共享指针,它也避免了复制)。我确实需要明确的连接跟踪,但将其推入已经运行的 asio 系统对我来说比自己实现它更有意义。
    • 我不确定。大多数情况下,我认为我只是对 boost 有点恼火。我实际上结束了使用计时器黑客,它似乎工作正常。我真的希望有一种提升原生的方式将事件/订阅者作为 asio 的一部分(我只是在文档中错过了,不知何故),显然没有。
    【解决方案2】:

    @sehe 的回答非常棒,所以我会很简短。一般来说,要实现对所有活动连接进行操作的算法,您必须执行以下操作:

    • 维护活动连接列表。如果这个列表被多个线程访问,它将需要同步(std::mutex)。应将新连接插入到列表中,当连接被破坏或变为非活动状态时,应将其从列表中删除。

    • 要迭代列表,如果列表被多个线程访问(即多个线程调用asio::io_context::run,或者如果列表也从不调用asio::io_context::run的线程访问),则需要同步

    • 在迭代期间,如果算法需要检查或修改任何连接的状态,并且该状态可以被其他线程更改,则需要额外的同步。这包括连接对象存储的任何内部消息“队列”。

    • 同步连接对象的一种简单方法是使用boost::asio::post 提交一个函数以在连接对象的上下文中执行,这将是显式链(boost::asio::strand,如高级服务器示例中所示) ) 或隐式链(当只有一个线程调用 io_context::run 时得到的结果)。 @sehe 提供的方法 1 使用 post 以这种方式进行同步。

    • 同步连接对象的另一种方法是“停止世界”。这意味着调用io_context::stop,等待所有线程退出,然后保证没有其他线程正在访问连接列表。然后,您可以随心所欲地读取和写入连接对象状态。完成连接列表后,调用io_context::restart 并再次启动调用io_context::run 的线程。停止io_context 不会停止网络活动,内核和网络驱动程序仍会从内部缓冲区发送和接收数据。 TCP/IP 流控制会处理这些事情,因此应用程序仍然可以顺利运行,即使它在“停止世界”期间变得短暂无响应。这种方法可以简化事情,但根据您的特定应用程序,您必须评估它是否适合您。

    希望这会有所帮助!

    【讨论】:

    • 我最终使用了boost::signal2 和boost::asio::post 的组合。一些sn-ps here
    • 是的,@sehe 在他的回答中表现得非常可笑。
    【解决方案3】:

    感谢@sehe 的精彩回答。不过,我认为方法 2 中存在一个小而严重的错误。恕我直言 reg_connection 应该如下所示:

    size_t reg_connection(std::shared_ptr<connection> c) {
        c->_subscription = _broadcast_event.connect(
            [weak_c = std::weak_ptr<connection>(c)](std::string msg){ 
                if(auto c = weak_c.lock())
                    c->send(msg, true); 
            }
        );
        return _broadcast_event.num_slots();
    }
    

    否则,您可能会遇到导致服务器崩溃的竞争条件。如果在调用 lambda 期间连接实例被破坏,则引用将变为无效。

    connection#send() 应该看起来像这样,否则 this 可能在调用 lambda 时已经死了:

        void send(std::string msg, bool at_front = false) {
          post(_s.get_io_service(),
            [self=shared_from_this(), msg=std::move(msg), at_front] { 
              if (self->enqueue(std::move(msg), at_front))
                  self->write_loop();
            });
        }
    

    PS:我会将此作为对@sehe 答案的评论发布,但不幸的是我没有足够的声誉。

    【讨论】:

    猜你喜欢
    • 2019-08-04
    • 1970-01-01
    • 2014-09-18
    • 2017-07-01
    • 2018-05-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-04-29
    相关资源
    最近更新 更多