Search code examples
c++networkingboost-asiop2p

How to make working p2p connection using asio and c++?


I'm trying to learn something about networking and I decided to try to make a p2p terminal program, but I can't get the idea of how to do this correctly. The idea behind this code is that you make an object of class other then call connect and if the other end already did the same you get connected, else you wait for the other device to try to call connect. It just plainly does not work. What am I doing wrong?

class other
{
public:
    other(asio::io_context& _context, asio::error_code _ec, std::string _IP, int _port);
    void Connect();
    asio::ip::tcp::endpoint endpoint;
    asio::ip::tcp::socket socket;
    asio::ip::tcp::acceptor acceptor;
    bool isConnected();
private:
    void AwaitConnection();
    asio::io_context& context;
    asio::error_code& ec;
    bool connected;

};

other::other(asio::io_context& _context, asio::error_code _ec, std::string _IP, int _port) :
    context(_context),
    ec(_ec),
    endpoint(asio::ip::address::from_string(_IP), _port),
    socket(_context),
    acceptor(_context),
    connected(false)
{

}
void other::Connect()
{
    std::cout << "Looking for connection..." << std::endl;
    socket.connect(endpoint, ec);
    if (!ec)
    {
        std::cout << "Connected!" << std::endl;
        connected = true;
    }
    else
    {
        std::cout << "Connection not found: " << ec.message() << std::endl;
        acceptor.open(endpoint.protocol(), ec);
        acceptor.bind(endpoint, ec);
        acceptor.set_option(asio::ip::tcp::acceptor::reuse_address(true), ec);
        if (ec)
            std::cout << ec.message() << std::endl;
        socket.close();
        if (ec)
            std::cout << "Binding socket failed: " << ec.message() << std::endl;
        std::cout << "Waiting for connection..." << std::endl;
        AwaitConnection();
    }
}
void other::AwaitConnection()
{
    static std::function<void(const asio::error_code&)> acceptHandler =
        [this](const asio::error_code& error)
    {
        if (!error)
        {
            std::cout << "Connection has been found!" << std::endl;
            connected = true;
        }
        else
        {
            std::cout << "Connection failed: " << error.message() << std::endl;
            //AwaitConnection();
        }
    };
    acceptor.async_accept(socket, acceptHandler);
}```

Solution

  • If you enable compiler warnings, the compiler will tell you some of the things you are doing wrong (like binding ec reference to stack-local):

    , ec(_ec)
    

    That's just UB.

    You don't actually show the code actually using this, but it seems that you want to do the error-handling centrally. I'd use exceptions then.

    On the high level, TCP is client/server by nature. One side listens, the other one connects. Of course you can hide this by making both sides capable of acting in both roles, but the fact remains, one side is the server (listening, accepting) the other is the client (connecting).

    It's probably best if you make the distinction in your code. Otherwise you will easily end up in the situation where both ends are listening (e.g. because they start at the same time, and both tried to connect to an "other" instance listening, but failed, so they both start listening for incoming connections instead.

    If you want to make that "automatic" you might just always start a listener, and treat failure to listen as non-fatal (e.g. because the "other" instance that was already listening is running on the same machine).

    One key reason to make the distinction explicit is because you're now creating confusion with "hostnames". You use the same "_IP" value both to connect or to listen to. That will rarely make sense, unless you do already know ahead of time which role will be taken. So, why conflate the two roles?

    Finally, you don't show that/where you are running the io_context. Since you don't seem to require asynchronous IO, you might simplify.

    This would work:

    class Object {
      public:
        void Connect(std::string const& ip_address, uint16_t port) {
            std::cout << "Looking for connection..." << std::endl;
            socket_.connect({asio::ip::address::from_string(ip_address), port});
            std::cout << "Connected!" << std::endl;
            connected_ = true;
        }
    
        void Listen(uint16_t port) {
            acceptor_.open(tcp::v4());
            acceptor_.bind(tcp::endpoint{{}, port});
            acceptor_.set_option(tcp::acceptor::reuse_address(true));
            acceptor_.listen();
    
            socket_ = acceptor_.accept();
            std::cout << "Connection has been found!" << std::endl;
            connected_ = true;
        }
    
        bool isConnected() const { return connected_; }
    
      private:
        tcp::socket   socket_{asio::system_executor{}};
        tcp::acceptor acceptor_{asio::system_executor{}};
        bool          connected_ = false;
    };
    

    Adding minimal code to actually send/receive some messages:

    Live On Coliru

    #include <boost/asio.hpp>
    #include <iomanip>
    #include <iostream>
    
    namespace asio = boost::asio;
    using tcp = asio::ip::tcp;
    
    class Object {
      public:
        void Connect(std::string const& ip_address, uint16_t port) {
            std::cout << "Looking for connection..." << std::endl;
            socket_.connect({asio::ip::address::from_string(ip_address), port});
            std::cout << "Connected!" << std::endl;
            connected_ = true;
        }
    
        void Listen(uint16_t port) {
            acceptor_.open(tcp::v4());
            acceptor_.set_option(tcp::acceptor::reuse_address(true));
            acceptor_.bind(tcp::endpoint{{}, port});
            acceptor_.listen();
    
            socket_ = acceptor_.accept();
            std::cout << "Connection has been found!" << std::endl;
            connected_ = true;
        }
    
        void Send(std::string const& msg) {
            assert(isConnected());
            asio::write(socket_, asio::buffer(msg + "\n"));
        }
    
        std::string Receive() {
            assert(isConnected());
            asio::read_until(socket_, buf_, "\n");
    
            std::string msg;
            getline(std::istream(&buf_), msg);
            return msg;
        }
    
        bool isConnected() const { return connected_; }
    
      private:
        tcp::socket     socket_{asio::system_executor{}};
        tcp::acceptor   acceptor_{asio::system_executor{}};
        bool            connected_ = false;
        asio::streambuf buf_;
    };
    
    int main(int argc, char**) try {
        if (argc > 1) { // server
            Object one;
            one.Listen(7878);
    
            std::cout << "Received message " << quoted(one.Receive()) << std::endl;
            one.Send("Response from server");
        } else { // client
            Object two;
            two.Connect("127.0.0.1", 7878);
            
            two.Send("Hello from client");
            std::cout << "Peer responded with " << quoted(two.Receive()) << std::endl;
        }
    } catch (boost::system::system_error const& se) {
        std::cout << "Failed: " << se.code().message() << std::endl;
    }
    

    Demo:

    enter image description here