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
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.
startListening
may be more aptly named startReceive
catch
block seems inefficient since it would only handle the error thrown locally#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:
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):
#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: