c++socketsboostudptello-drone

Boost.Asio UDP socket, An invalid argument was supplied error


I am trying to create a thread to listen for UDP responses in c++ using the Boost ASIO library, this is my definition for the receive thread in my class file:

tello::tello(boost::asio::io_service& io_service, boost::asio::ip::udp::socket& socket, const boost::asio::ip::udp::endpoint& remote_endpoint)
    : ip(remote_endpoint.address().to_string()),
    port(remote_endpoint.port()),
    io_service(io_service),
    socket(socket),
    remote_endpoint(remote_endpoint)
{

}

void tello::ReceiveThread() {
    while (isListening_) {
        try {
            char receivedData[1024];
            boost::system::error_code error;

            size_t len = socket.receive_from(boost::asio::buffer(receivedData), remote_endpoint, 0, error);

            if (error && error != boost::asio::error::message_size) {
                throw boost::system::system_error(error);
            }

            receivedData[len] = '\0';

            spdlog::info("Received UDP data: {}", receivedData);
        }
        catch (const std::exception& e) {
            std::cerr << "Error receiving UDP data: " << e.what() << std::endl;
        }
    }
}

void tello::StartListening() {
    std::thread udpThread(&tello::ReceiveThread, this);
    udpThread.detach(); // Detach the thread so it runs independently
}

void tello::StopListening() {
    isListening_ = false;
}

In my main.cpp I have this snippet to start the thread:

boost::asio::io_service io_service;
boost::asio::ip::udp::socket socket(io_service);
boost::asio::ip::udp::endpoint remote_endpoint(boost::asio::ip::address::from_string("192.168.0.1"), 12345); // Replace with the actual IP address and port
socket.open(boost::asio::ip::udp::v4());

tello drone(io_service, socket, remote_endpoint);
drone.StartListening();

The code compiles with no errors, but trying to run the app results in the console error:

Error receiving UDP data: An invalid argument was supplied [system:10022 at C:\Users\(username)\boost_1_82_0\boost\asio\detail\win_iocp_socket_service.hpp:418:5 in function 'receive_from']

I'm very new to Boost ASIO and would appreciate some help as to what the proper way to create a UDP receive thread. I'm using Windows 64bit with C++14. The full code is here on github


Solution

  • You have data races.

    You start a thread which references both io_service and socket which are local variables in main. However, main exits immediately, destructing those local variables.

    The thread, which was detached, still runs causing Undefined Behaviour because of the stale references.


    Fix it by

    (a) avoiding the run-away thread
    (b) also synchronizing on its termination
    

    Also fixing some other issues and making up some code that might also be less safe (e.g. the potentially racy isListening_). There are more issues hiding e.g.

    Live On Coliru

    #include <boost/asio.hpp>
    #include <fmt/ranges.h>
    #include <iostream>
    
    namespace asio = boost::asio;
    using asio::ip::udp;
    using boost::system::error_code;
    using namespace std::chrono_literals;
    
    namespace spdlog {
        void info(auto fmt, auto const&... args) { fmt::print(fmt::runtime(fmt), args...); }
    } // namespace spdlog
    
    struct tello {
        tello(asio::io_context& io_context, udp::socket& socket, udp::endpoint const& remote_endpoint);
    
        void StartListening();
    
        void StopListening();
        void ReceiveThread();
    
      private:
        std::string       ip;
        uint16_t          port;
        asio::io_context& io_context;
        udp::socket&      socket;
        udp::endpoint     remote_endpoint;
        std::atomic_bool  isListening_{true};
        std::thread       udpThread;
    };
    
    tello::tello(asio::io_context& io_context, udp::socket& socket, udp::endpoint const& remote_endpoint)
        : ip(remote_endpoint.address().to_string())
        , port(remote_endpoint.port())
        , io_context(io_context)
        , socket(socket)
        , remote_endpoint(remote_endpoint) //
    {
        socket.bind(remote_endpoint);
    }
    
    void tello::ReceiveThread() {
        for (char receivedData[1024]; isListening_;) {
            try {
                error_code error;
                size_t len = socket.receive_from(asio::buffer(receivedData), remote_endpoint, 0, error);
    
                if (error && error != asio::error::message_size) {
                    throw boost::system::system_error(error);
                }
    
                receivedData[len] = '\0';
    
                spdlog::info("Received UDP data: {}", std::string_view(receivedData, len));
            } catch (std::exception const& e) {
                std::cerr << "Error receiving UDP data: " << e.what() << std::endl;
            }
        }
    }
    
    void tello::StartListening() { udpThread = std::thread(&tello::ReceiveThread, this); }
    
    void tello::StopListening() {
        isListening_ = false;
        udpThread.join();
    }
    
    int main() {
        asio::io_context io_context;
        udp::socket      socket(io_context);
        udp::endpoint    remote_endpoint({}, 12345);
        socket.open(udp::v4());
        tello drone(io_context, socket, remote_endpoint);
        drone.StartListening();
    
    
        std::this_thread::sleep_for(30s);
        drone.StopListening();
    }
    

    Local demo:

    enter image description here

    BONUS

    You want to use async IO so you can have much better behaviour. I think ironically it's a little bit simpler (about 30 LoC less):

    Live On Coliru

    #include <boost/asio.hpp>
    #include <fmt/ranges.h>
    #include <iostream>
    
    namespace asio = boost::asio;
    using asio::ip::udp;
    using boost::system::error_code;
    using namespace std::chrono_literals;
    
    namespace spdlog {
        void info(auto fmt, auto const&... args) { fmt::print(fmt::runtime(fmt), args...); }
    } // namespace spdlog
    
    struct tello {
        tello(asio::any_io_executor ex, udp::endpoint ep);
    
        void Start() { readLoop(); }
        void Stop();
      private:
        void readLoop() {
            socket_.async_receive_from(
                asio::buffer(buffer_), remote_ep_, [this](error_code ec, size_t len) {
                    if (ec && ec != asio::error::message_size) {
                        std::cerr << "Error receiving UDP data: " << ec.message() << std::endl;
                    } else {
                        spdlog::info("Received UDP data: {}", std::string_view(buffer_.data(), len));
                        readLoop();
                    }
                });
        }
    
        udp::socket            socket_;
        udp::endpoint          remote_ep_;
        std::array<char, 1024> buffer_;
    };
    
    tello::tello(asio::any_io_executor ex, udp::endpoint ep) : socket_(ex, ep.protocol()) { socket_.bind(ep); }
    
    void tello::Stop() { socket_.cancel(); }
    
    int main() {
        asio::io_context ioc;
        tello drone(ioc.get_executor(), {{}, 12345});
        drone.Start();
    
        ioc.run_for(30s);
        // not really needed:
        drone.Stop();
        ioc.run();
    }
    

    And more local demo:

    enter image description here