Search code examples
c++linuxsocketsudpboost-asio

Cannot receive UDP packets after using async_receive_from on Linux, but can work on Windows


I'm now writing a C++ program which uses UDP to send and receive message with a hardware device. That program works well on Windows but when I put the same code on Linux(Ubuntu 22.04 LTS), it works bad. It can receive packets sometimes but in most cases it just received nothing.

Here is part of my code:

JULidar::JULidar(const std::string& local_ip, const std::string& local_port,
    const std::string& device_ip, const std::string& device_port)
    : io_context(),
    socket(io_context),
    local_endpoint(boost::asio::ip::address::from_string(local_ip), std::stoi(local_port)),
    device_endpoint(boost::asio::ip::address::from_string(device_ip), std::stoi(device_port)),
    receive_thr(&JULidar::receive_thread, this),
    process_thr(&JULidar::process_thread, this),
    output_flag(false),
    param_pkg_operated_completed(false),
    frame_update_completed(false)
{
    try
    {
        std::cout << "binding local ip ..." << std::endl;
        socket.open(boost::asio::ip::udp::v4());
        socket.bind(local_endpoint);
        asyncRecvFrom();
    }
    catch (boost::system::system_error)
    {
        std::cout << "local network config error, please modify the ip & port above." << std::endl;
    }
}

JULidar::~JULidar()
{
    socket.close();
    io_context.stop();
    receive_thr.interrupt();
    receive_thr.join();

    process_thr.interrupt();
    process_thr.join();
}

void JULidar::asyncRecvFrom()
{
    boost::system::error_code error;
    socket.async_receive_from(
        boost::asio::buffer(udpBuffer), 
        device_endpoint, 
        boost::bind(
            &JULidar::recvHandler, 
            this, 
            boost::asio::placeholders::error, 
            boost::asio::placeholders::bytes_transferred));
}

void JULidar::recvHandler(const boost::system::error_code& error, size_t bytes_received)
{
    if (bytes_received != 0)
    {
        // ...
    }
    asyncRecvFrom();
}

void JULidar::receive_thread()
{
    while (1)
    {
        try
        {
            io_context.run();
            boost::this_thread::interruption_point();
        }
        catch (...)
        {
            break;
        }
    }
    std::cout << "recv_thr ending..." << std::endl;
}

I created a thread to run the io_context and kept receiving message from the device endpoint. Each time the packet arrives, function recvHandler() will do something. It worked as expected on Windows. Why it's not working on Linux?

Really appreciate if anyone can help!!!

It would be great if the program can work as how it worked on Windows.


Solution

    1. You're using the execution context in an a-typical ("wrong") manner. E.g in this loop

      while (1) {
          try {
              io_context.run();
              boost::this_thread::interruption_point();
          } catch (...) {
              break;
          }
      }
      

      the entire interruption_point() can only be useful if io_context.run() is expected to run out of work. If that's true, though, the code is wrong, because io_context.restart() (or reset(), formerly) is never called, which would be required.

    2. There's also an issue with the threads being created before the socket is even opened/first async work is posted. This means that io_context might have run out of work IMMEDIATELY, i.e. before work was ever started.

    3. The difference in behaviour between Linux and Windows might be explained by timing difference.

      Keep in mind that UDP doesn't have guaranteed delivery, so if your machine is "busy" some packets getting lost is to be expected.


    I'd rewrite the logic using a work guard to avoid the context running out of work instead:

    Live On Coliru

    #include <boost/asio.hpp>
    #include <boost/asio/serial_port.hpp>
    #include <boost/thread.hpp>
    #include <iomanip>
    #include <iostream>
    
    namespace asio = boost::asio;
    using asio::ip::udp;
    using boost::system::error_code;
    
    struct JULidar {
        JULidar(std::string const& local_ip, std::string const& local_port, //
                std::string const& device_ip, std::string const& device_port);
    
        ~JULidar();
    
      private:
        void asyncRecvFrom();
        void recvHandler(error_code error, size_t bytes_received);
        void receive_thread();
        void process_thread() {
            // TODO
        }
    
        using Executor = asio::io_context::executor_type;
        using Work = asio::executor_work_guard<Executor>;
    
        asio::io_context io_context;
        Work             work{io_context.get_executor()};
    
        udp::socket             socket{io_context};
        udp::endpoint           local_endpoint, device_endpoint;
        std::array<char, 65000> udpBuffer;
    
        std::thread      receive_thr, process_thr;
        std::atomic_bool output_flag{false};
        std::atomic_bool param_pkg_operated_completed{false};
        std::atomic_bool frame_update_completed{false};
    };
    
    JULidar::JULidar(std::string const& local_ip, std::string const& local_port, std::string const& device_ip,
                     std::string const& device_port) try
        : local_endpoint(asio::ip::address::from_string(local_ip), static_cast<uint16_t>(std::stoi(local_port)))
        , device_endpoint(asio::ip::address::from_string(device_ip),
                          static_cast<uint16_t>(std::stoi(device_port)))
        , receive_thr(&JULidar::receive_thread, this)
        , process_thr(&JULidar::process_thread, this) //
    {
        std::cout << "binding local ip ..." << std::endl;
        socket.open(local_endpoint.protocol());
        socket.bind(local_endpoint);
        asyncRecvFrom();
    } catch (boost::system::system_error const&) {
        std::cout << "local network config error, please modify the ip & port above." << std::endl;
    }
    
    JULidar::~JULidar() {
        post(socket.get_executor(), [this] { socket.cancel(); });
        work.reset(); // allow the context to run out of work
    
        if (receive_thr.joinable())
            receive_thr.join();
        if (process_thr.joinable())
            process_thr.join();
    }
    
    void JULidar::asyncRecvFrom() {
        using namespace std::placeholders;
        socket.async_receive_from(asio::buffer(udpBuffer), device_endpoint,
                                  std::bind(&JULidar::recvHandler, this, _1, _2));
    }
    
    void JULidar::recvHandler(error_code error, size_t bytes_received) {
        std::cerr << "recvHandler(" << error.message() << ", " << bytes_received << ")" << std::endl;
        if (!error.failed()) {
            if (bytes_received != 0) {
                // ...
            }
            asyncRecvFrom();
        }
    }
    
    void JULidar::receive_thread() {
        for (;;) {
            try {
                io_context.run();
                break; // exited normally
            } catch (std::exception const& e) {
                std::cerr << "[receive_thread] " << e.what() << std::endl;
            } catch (...) {
                std::cerr << "[receive_thread] unknown exception" << std::endl;
            }
        }
        std::cout << "receive_thread ending..." << std::endl;
    }
    
    int main() {
        {
            JULidar lidar("127.0.0.1", "8989", "127.0.0.1", "8990");
    
            using namespace std::literals;
            std::this_thread::sleep_for(30s);
        } // destructor will shutdown io_context
    }
    

    Local demo

    enter image description here