The connection cannot be completed in Linux boost::asio::spawn

59 Views Asked by At

If the server starts after the connection cannot be reached.why? boost_1_73_0、Linux operating system

#include "boost/asio.hpp"
#include "boost/asio/spawn.hpp"

int main() {
    boost::asio::io_context ctx;
    auto ep        = boost::asio::ip::tcp::endpoint(boost::asio::ip::make_address("127.0.0.1"), 1560);
    bool isConnect = false;
    bool isRun     = false;
    boost::asio::ip::tcp::socket socket(ctx);
    while (true) {
        if (!isConnect && !isRun) {
            boost::asio::spawn([&](boost::asio::yield_context y) {
                isRun = true;
                boost::system::error_code ec;
                socket.async_connect(ep, y[ec]);
                if (ec) {
                    isRun = false;
                    socket.close();
                    printf("close\n");
                } else {
                    isConnect = true;
                    printf("connect\n");
                }
            });
        }
        ctx.poll_one();
        Sleep(1);
    }
}

It works fine on windows. My post looks mostly code, so I added this sentence. Thank you so much

1

There are 1 best solutions below

2
On

First Things First

Sleep is a WinAPI call. It expects milliseconds.

I suspect you might have ported this to Linux by... lowercasing Sleep to sleep. However sleep takes seconds. So you might be waiting 1000s each poll.

Fixing that by not using any platform-specific calls but instead using standard library functions already seems to "work" on my machine:

Live On Coliru

#include "boost/asio.hpp"
#include "boost/asio/spawn.hpp"
#include <iostream>
namespace asio = boost::asio;
using asio::ip::tcp;
using namespace std::chrono_literals;
using boost::system::error_code;
using std::this_thread::sleep_for;

int main() {
    asio::io_context ctx;
    auto ep        = tcp::endpoint({}, 1560);
    bool             isConnect = false, isRun = false;
    tcp::socket      socket(ctx);
    while (true) {
        if (!isConnect && !isRun) {
            asio::spawn([&](asio::yield_context y) {
                isRun = true;
                error_code ec;
                socket.async_connect(ep, y[ec]);
                if (ec) {
                    isRun = false;
                    socket.close();
                    std::cout << ec.message() << ": close" << std::endl;
                } else {
                    isConnect = true;
                    std::cout << ec.message() << ": connect" << std::endl;
                }
            });
        }
        ctx.poll_one();
        sleep_for(1s);
    }
}

enter image description here

Next: Choose Your Context!

You spawn without a context. This means it will default to a strand of the associated executor of the spawned function. There is none, so system_executor is defaulted. You can see this by printing the effective executor from inside the coro:

std::cerr << boost::core::demangle(y.get_executor().target_type().name()) << std::endl;

Which will print

boost::asio::strand<boost::asio::basic_system_executor<
    boost::asio::execution::detail::blocking::possibly_t<0>,
    boost::asio::execution::detail::relationship::fork_t<0>, std::allocator<void>>>

With spawn(ctx, ...) you'd see instead:

boost::asio::strand<boost::asio::io_context::basic_executor_type<std::allocator<void>, 0ul>>

What it means is that ctx.poll() never runs anything related to your coro or socket, because they're in a different execution context.

Alternatively you may fix it by binding your executor to the coro function with bind_executor(ctx.get_executor, [&](...) {...})

Why Did It Appear To Work?

It appeared to work above, because at least on Linux system_executor defaults to a thread pool which runs their threads in the background.

This is scary, because it made your program multi-threaded without you knowing: You had potential data-races waiting to happen on all the shared objects, as well as the problem that the loads of the boolean flags could easily be optimized in such a way that a change would never be detected.

What's The Goal

However, it's unclear what the poll loop accomplishes, really. You can simplify a lot by just taking the spawn out of the loop, and replacing the loop with the much more robust run()/run_for():

Live On Coliru

int main() {
    asio::io_context ctx;
    tcp::socket      socket(ctx);

    tcp::endpoint ep{{}, 1560};
    asio::spawn(ctx, [&](asio::yield_context y) {
        std::cerr << boost::core::demangle(y.get_executor().target_type().name()) << std::endl;

        error_code ec;
        socket.async_connect(ep, y[ec]);
        if (ec) {
            socket.close();
            std::cout << ec.message() << ": close" << std::endl;
        } else {
            std::cout << ec.message() << ": connect" << std::endl;
        }
    });

    ctx.run_for(10s); // max 10s
}

This works correctly and without multiple threads.

UPDATE: Retrying The Connection?

I had a sudden brainwave and realized that maybe you want to retry the connection UNTIL it succeeds. In that case you can just put the loop inside the coroutine:

Live On Coliru

#include "boost/asio.hpp"
#include "boost/asio/spawn.hpp"
#include <iostream>
namespace asio = boost::asio;
using asio::ip::tcp;
using namespace std::chrono_literals;
using boost::system::error_code;
using std::this_thread::sleep_for;

int main() {
    asio::io_context ctx;
    tcp::socket      socket(ctx);

    tcp::endpoint ep{{}, 1560};
    asio::spawn(ctx, [&](asio::yield_context y) {
        error_code ec = asio::error::connection_refused;
        for (int i = 1; i <= 10; ++i) {
            socket.async_connect(ep, y[ec]);
            std::cout << "attempt #" << i << ": " << ec.message() << std::endl;
            if (!ec.failed())
                break;
            sleep_for(500ms);
        }
        if (ec)
            std::cout << "failed: " << ec.message() << std::endl;
    });

    ctx.run();
}

With online demo:

g++ -std=c++20 -O2 -Wall -pedantic -pthread main.cpp -lboost_{thread,coroutine,context} 
(./a.out; ./a.out)&
(sleep 3; nc -tklp 1560 -w2 <<< "Hello there!")
wait

Outputting:

attempt #1: Connection refused
attempt #2: Software caused connection abort
attempt #3: Connection refused
attempt #4: Software caused connection abort
attempt #5: Connection refused
attempt #6: Software caused connection abort
attempt #7: Success

attempt #1: Success

Local demo for interactive output:

enter image description here

Async Sleep

To avoid blocking the io context with the sleep_for calls, use a waitable timer: Live On Coliru

asio::steady_timer(socket.get_executor(), 500ms).async_wait(y);