Search code examples
c++boostboost-asiotcpclienttcpserver

boost TCP server does not correctly accept connections under Linux


I'm working on a client/server TCP applications. It works very well under Windows but I'm encountering problems under Linux: the server does not get notified when a new connection is established with the client (very rarely it is, but most often it is not).

I was able to isolate the issue in a very simple program. This program creates and runs a server (with its own boost::asio::io_service) accepting new connections and then establishs a connection from a client (also with its own boost::asio::io_service, as it would behave in seperate applications).

Here is the code:

#include <iostream>

#include <boost/asio.hpp>
#include <boost/thread.hpp>

#include <assert.h>

using boost::asio::ip::tcp;

std::shared_ptr<tcp::socket> m_nextConnection;
std::atomic_bool m_continueServerThread = true;

void server_thread_func(boost::asio::io_service* service)
{
    // http://www.boost.org/doc/libs/1_61_0/doc/html/boost_asio/reference/io_service.html#boost_asio.reference.io_service.effect_of_exceptions_thrown_from_handlers
    while (m_continueServerThread) {
        try {
            service->run();
            // don't break, keep looping, else we may exit before a connection is actually established
            //break; // exited normally
        }
        catch (std::exception const& e) {
            std::cerr << "[eventloop] error: " << e.what();
        }
        catch (...) {
            std::cerr << "[eventloop] unexpected error";
        }
    }

}

void on_accept_connection(std::error_code ec)
{
    if (!ec)
    {
        std::cout << "SERVER ACCEPTED CONNECTION" << std::endl;
    }
    else
    {
        std::cout << "Reader connection error " << ec << " (" << ec.message() << ")" << std::endl;
    }
}

void do_accept_connection(boost::asio::ip::tcp::acceptor& acceptor, boost::asio::io_service& service)
{
    m_nextConnection = std::shared_ptr<tcp::socket>(new boost::asio::ip::tcp::socket(service));

    acceptor.async_accept(
            *m_nextConnection, boost::bind(on_accept_connection, boost::asio::placeholders::error));
}

int main( int argc, char* argv[] )
{
    auto endpoint = boost::asio::ip::tcp::endpoint{ boost::asio::ip::address::from_string("127.0.0.1"), 1900 };

    try
    {
        // start server:
        boost::asio::io_service IOServiceServer;
        
        boost::thread serverThread( boost::bind(server_thread_func,&IOServiceServer) );

        boost::this_thread::sleep(boost::posix_time::milliseconds(500));

        boost::asio::ip::tcp::acceptor m_acceptor{ IOServiceServer, endpoint };

        // dunno if this is needed or not here
        //m_acceptor.set_option(tcp::acceptor::reuse_address(true));
        do_accept_connection(m_acceptor, IOServiceServer);

        boost::this_thread::sleep(boost::posix_time::milliseconds(500));

        // start client:
        boost::asio::io_service IOServiceClient;
        tcp::socket socket(IOServiceClient);

        std::cout << "Connecting socket..." << std::endl;        
        socket.connect(endpoint);
        std::cout << "Connected socket" << std::endl;

        boost::this_thread::sleep(boost::posix_time::milliseconds(500));

        // stop/close client:
        IOServiceClient.stop();
        socket.close();
        
        // stop server:
        IOServiceServer.stop();
        m_continueServerThread = false;
        serverThread.join();

        return 0;
    }
    catch (std::exception const& e) {
        std::cerr << "error: " << e.what();
    }
    catch (...) {
        std::cerr << "unexpected error";
    }

    return 1;
}

Ran under Windows, this shows:

Connecting socket...
Connected socket
SERVER ACCEPTED CONNECTION

Ran under Linux, this only shows:

Connecting socket...
Connected socket

As you can see on_accept_connection is not being called, so the server does not get notified that a new connection was established.

Am I doing something wrong?


Edit: Also tried without m_nextConnection:

void on_accept_connection(std::error_code ec, tcp::socket socket)
{
    if (!ec)
    {
        std::cout << "SERVER ACCEPTED CONNECTION" << std::endl;
    }
    else
    {
        std::cout << "Reader connection error " << ec << " (" << ec.message() << ")" << std::endl;
    }
}

void do_accept_connection(boost::asio::ip::tcp::acceptor& acceptor, boost::asio::io_service& service)
{
    acceptor.async_accept(service, [&](const std::error_code& ec, tcp::socket newSocket) {
        on_accept_connection(ec, std::move(newSocket));
        });
}

Does not solve the problem!


Solution

  • The server thread was modified from here. The comment is telling:

    // don't break, keep looping, else we may exit before a connection is actually established
    

    This is not true. The reason you see it exit before the connection is established is because the IO service runs out of work. Since you let the service run out of work, looping makes zero sense, unless you actually use restart():

    Subsequent calls to run(), run_one(), poll() or poll_one() will return immediately unless there is a prior call to restart()

    So, the real fix is to keep the loop as it was intended but place work before starting it, e.g. changing

    std::thread serverThread(server_thread_func, std::ref(ioc));
    do_accept_connection(m_acceptor, ioc);
    

    To

    do_accept_connection(m_acceptor, ioc);
    std::thread serverThread(server_thread_func, std::ref(ioc));
    

    Other Issues

    The global connection variable is a problem, especially once you accept more than 1 connection. Fix it by not using a global, e.g.:

    void on_accept_connection(std::error_code ec, std::shared_ptr<tcp::socket> s) {
        if (!ec)
            std::cout << "SERVER ACCEPTED CONNECTION from " << s->remote_endpoint() << std::endl;
        else
            std::cout << "Reader connection error " << ec << " (" << ec.message() << ")" << std::endl;
    }
    
    void do_accept_connection(tcp::acceptor& acceptor, asio::io_context& service) {
        auto s = std::make_shared<tcp::socket>(service);
        acceptor.async_accept(*s, bind(on_accept_connection, _1, s));
    }
    

    Note how we bind the shared pointer into the completion handler, to keep it alive.

    This already works: https://coliru.stacked-crooked.com/a/df135c6936c5e37e

    Connecting socket...
    Connected socket
    SERVER ACCEPTED CONNECTION from 127.0.0.1:45958
    [eventloop] exit
    Closing socket
    

    Fixing Everything

    However, you should take the overload that moves the socket, use builtin thread pool, not use a second io service, use io_context instead of the deprecated io_service and other stuff like not use unncessary raw pointers, boost::bind (or bind at all) and boost::thread, passing around execution contexts instead of executors etc.: Live On Coliru

    #include <boost/asio.hpp>
    #include <iostream>
    
    namespace asio = boost::asio;
    using namespace std::chrono_literals;
    using boost::asio::ip::tcp;
    
    void on_accept_connection(std::error_code ec, tcp::socket s) {
        if (!ec)
            std::cout << "Accepted from " << s.remote_endpoint() << std::endl;
        else
            std::cout << std::endl;
    }
    
    void accept_loop(tcp::acceptor& acc) {
        acc.async_accept(make_strand(acc.get_executor()), on_accept_connection);
    }
    
    int main() {
        tcp::endpoint ep{{}, 1900};
        using std::this_thread::sleep_for;
    
        asio::thread_pool ioc(1);
    
        tcp::acceptor listener{ioc, ep};
        accept_loop(listener);
    
        sleep_for(500ms);
    
        {
            // start client:
            tcp::socket socket(ioc);
    
            std::cout << "Connecting socket..." << std::endl;
            socket.connect(ep);
            std::cout << "Connected socket" << std::endl;
    
            sleep_for(500ms);
            std::cout << "Closing socket" << std::endl;
        }
    
        ioc.join();
    }
    

    Printing

    Connecting socket...
    Connected socket
    Accepted from 127.0.0.1:47014
    Closing socket
    

    Bonus

    To make it complete, actually make accept_loop loop:

    Live On Coliru

    #include <boost/asio.hpp>
    #include <iostream>
    
    namespace asio = boost::asio;
    using namespace std::chrono_literals;
    using boost::system::error_code;
    using boost::asio::ip::tcp;
    
    struct session : std::enable_shared_from_this<session> {
        session(tcp::socket s) : s_(std::move(s)) {}
    
        void run() {
            std::cout << "Session from " << s_.remote_endpoint() << std::endl;
            async_write(s_,
                        asio::buffer(message_), //
                        consign(asio::detached, shared_from_this()));
        }
    
      private:
        tcp::socket s_;
        std::string message_ = "Hello from server\n";
    };
    
    struct listener {
        listener(tcp::endpoint ep) : acc_(ioc_, ep) { accept_loop(); }
    
      private:
        asio::thread_pool ioc_{1};
        tcp::acceptor     acc_;
    
        void accept_loop() {
            acc_.async_accept(make_strand(acc_.get_executor()), [this](error_code ec, tcp::socket s) {
                if (!ec) {
                    std::make_shared<session>(std::move(s))->run();
                    accept_loop();
                } else
                    std::cout << std::endl;
            });
        }
    };
    
    int main() {
        tcp::endpoint ep{{}, 1900};
        listener      server(ep);
    
        std::this_thread::sleep_for(50ms);
    
        for (auto i = 0; i < 10; ++i) {
            tcp::iostream is(ep);
            std::cout << "Connected: " << is.rdbuf() << std::endl;
        }
    }
    

    Printing e.g.

    Connected: Session from 127.0.0.1:48586
    Hello from server
    
    Connected: Session from 127.0.0.1:48588
    Hello from server
    
    Connected: Session from 127.0.0.1:48590
    Hello from server
    
    Connected: Session from 127.0.0.1:48592
    Hello from server
    
    Connected: Session from 127.0.0.1:48594
    Hello from server
    
    Connected: Session from 127.0.0.1:48596
    Hello from server
    
    Connected: Session from 127.0.0.1:48598
    Hello from server
    
    Connected: Session from 127.0.0.1:48600
    Hello from server
    
    Connected: Session from 127.0.0.1:48602
    Hello from server
    
    Connected: Session from 127.0.0.1:48604
    Hello from server