Search code examples
c++boostboost-asio

how to create boost::asio server that listen to two different ports


I'm using boost 1.75.0 and I'm trying to update my server so he can listen to two different ports at the same time let's say IP 127.0.0.1 port 6500 and port 6600. do I need to hold in the Server two sockets? this is my server

#include <boost/asio/io_service.hpp>
#include <boost/asio.hpp>
#include <queue>
#include "Session.h"
using boost::asio::ip::tcp;

class Server
{
 public:

  Server(boost::asio::io_service &io_service, short port)
      : acceptor_(io_service, tcp::endpoint(tcp::v4(), port)),
        socket_(io_service)
  {
    do_accept();
  }

 private:
  void do_accept(){
    acceptor_.async_accept(socket_,
                           [this](boost::system::error_code ec) {
                             if (!ec) {
                               std::cout << "accept connection\n";

                               std::make_shared<Session>(std::move(socket_))->start();
                             }

                             do_accept();
                           });
  }


  tcp::acceptor acceptor_;
  tcp::socket socket_;

};

this is my Session class

#include <boost/asio/io_service.hpp>
#include <boost/asio.hpp>
#include "message.h"
#include <fstream>
#include <boost/bind/bind.hpp>

namespace {
    using boost::asio::ip::tcp;
    auto constexpr log_active=true;
    using boost::system::error_code;
    using namespace std::chrono_literals;
    using namespace boost::posix_time;
};


class Session
    : public std::enable_shared_from_this<Session>
{
 public:
  Session(tcp::socket socket)
      : socket_(std::move(socket)) ,
  {
  }
  ~Session()= default;

  void start();
  void do_write_alive();


 private:
  void do_read_header();
  void do_read_body();
  void do_write();
  using Strand = boost::asio::strand<tcp::socket::executor_type>;
  using Timer  = boost::asio::steady_timer;

  tcp::socket socket_{strand_};
  Strand      strand_{make_strand(socket_.get_executor())};
  Timer       recv_deadline_{strand_};
  Timer       send_deadline_{strand_};
  enum { max_length = 1024 };
  char data_[max_length];
};

I didn't include the implementation of the Session class only the constructor because there not relevant.


Solution

  • You need two acceptors.

    You can do without the socket_ instance, removing the duplication as well.

    Here's a minimal refactor that runs 10 listeners, stored in a deque (for reference stability):

    class Server {
      public:
        Server(boost::asio::io_service& svc, uint16_t base_port) {
            for (uint16_t port = base_port; port < base_port + 10; ++port) {
                auto& a = acceptors_.emplace_back(svc, tcp::endpoint{{}, port});
                a.listen();
                accept_loop(a);
            }
        }
    
        void stop() {
            for (auto& a: acceptors_)
                a.cancel();
        }
    
      private:
        static void accept_loop(tcp::acceptor& a) {
            a.async_accept(a.get_executor(), [&a](error_code ec, tcp::socket&& s) {
                if (!ec) {
                    std::cout << "accepted " << s.remote_endpoint() << " on :" << a.local_endpoint().port() << std::endl;
                    std::make_shared<Session>(std::move(s))->start();
    
                    accept_loop(a);
                } else {
                    std::cout << "exit accept_loop " << ec.message() << std::endl;
                }
            });
        }
    
        std::deque<tcp::acceptor> acceptors_;
    };
    

    Use io_context and executors

    You can modernize this some more by using the executor model:

    Live On Coliru

    #include <boost/asio.hpp>
    #include <boost/asio/io_service.hpp>
    #include <boost/bind/bind.hpp>
    #include <fstream>
    #include <deque>
    
    namespace {
        using boost::asio::ip::tcp;
        //auto constexpr log_active = true;
        using boost::system::error_code;
        using namespace std::chrono_literals;
        //using namespace boost::posix_time;
    } // namespace
    
    class Session : public std::enable_shared_from_this<Session> {
      public:
        Session(tcp::socket socket) : socket_(std::move(socket)) {}
        ~Session() = default;
    
        void start() {}
        void do_write_alive() {}
    
      private:
        void do_read_header() {}
        void do_read_body() {}
        void do_write() {}
    
        using Strand = boost::asio::strand<tcp::socket::executor_type>;
        using Timer  = boost::asio::steady_timer;
    
        tcp::socket socket_{strand_};
        Strand      strand_{make_strand(socket_.get_executor())};
        Timer       recv_deadline_{strand_};
        Timer       send_deadline_{strand_};
        enum { max_length = 1024 };
        char data_[max_length];
    };
    
    #include <boost/asio.hpp>
    #include <boost/asio/io_service.hpp>
    #include <queue>
    #include <iostream>
    using boost::asio::ip::tcp;
    
    class Server {
      public:
        template <typename Ex>
        Server(Ex executor, uint16_t base_port) {
            for (uint16_t port = base_port; port < base_port + 10; ++port) {
                auto& a = acceptors_.emplace_back(executor, tcp::endpoint{{}, port});
                a.listen();
                accept_loop(a);
            }
        }
    
        void stop() {
            for (auto& a: acceptors_)
                a.cancel();
        }
    
      private:
        static void accept_loop(tcp::acceptor& a) {
            a.async_accept(a.get_executor(), [&a](error_code ec, tcp::socket&& s) {
                if (!ec) {
                    std::cout << "accepted " << s.remote_endpoint() << " on :" << a.local_endpoint().port() << std::endl;
                    std::make_shared<Session>(std::move(s))->start();
    
                    accept_loop(a);
                } else {
                    std::cout << "exit accept_loop " << ec.message() << std::endl;
                }
            });
        }
    
        std::deque<tcp::acceptor> acceptors_;
    };
    
    int main() {
        boost::asio::io_context context;
        Server s(context.get_executor(), 7878);
    
        context.run_for(10s);
    
        s.stop();
        context.run();
    }
    

    Note also that the accept-loop can now be canceled. With a sample client run of e.g.

    (for p in {7878..7887}; do (sleep 1.$RANDOM; nc localhost $p <<<"HELLO")& done; wait)
    

    Prints e.g.

    accepted 127.0.0.1:37958 on :7880
    accepted 127.0.0.1:45100 on :7885
    accepted 127.0.0.1:42414 on :7883
    accepted 127.0.0.1:49988 on :7886
    accepted 127.0.0.1:44898 on :7878
    accepted 127.0.0.1:43536 on :7879
    accepted 127.0.0.1:35350 on :7882
    accepted 127.0.0.1:40158 on :7887
    accepted 127.0.0.1:53022 on :7884
    accepted 127.0.0.1:39020 on :7881
    exit accept_loop Operation canceled
    exit accept_loop Operation canceled
    exit accept_loop Operation canceled
    exit accept_loop Operation canceled
    exit accept_loop Operation canceled
    exit accept_loop Operation canceled
    exit accept_loop Operation canceled
    exit accept_loop Operation canceled
    exit accept_loop Operation canceled
    exit accept_loop Operation canceled