Skip to content

Wiki

UDP communication

About 11 min read

Existing English translations were reused. 2 current source blocks remain in Simplified Chinese, without triggering paid translation.

UDP is a connectionless datagram protocol.
In robotics, UDP is commonly used for: low-latency local area network status broadcasting, sensor data broadcasting, custom lightweight communication, host computer device discovery, and so on.

The biggest difference between UDP and TCP:

TCP 是字节流,没有消息边界。
UDP 是数据报,一次 send_to 对应对端一次 receive_from 的一个数据报。

However, UDP does not guarantee reliability, does not guarantee order, and does not guarantee delivery.


1.Example 1: regular main() write a synchronous UDP Receiver in it

1.1.Program Objectives

Write a UDP receiver:

  1. Bind to local port 9001;
  2. Block and wait for one UDP datagram;
  3. Print the sender address and content;
  4. The program ends.

1.2.Complete code

#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <array>
#include <print>
#include <string>

using boost::asio::ip::udp;

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    // io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
    boost::asio::io_context io;
    boost::system::error_code ec;

    // UDP socket 用来发送或接收无连接的数据报。
    udp::socket socket(io, udp::endpoint(udp::v4(), 9001));

    std::array<char, 1024> data;
    udp::endpoint sender_endpoint;

    std::println("receiver:监听 UDP 0.0.0.0:9001,等待数据");

    // receive_from 会等待并接收一个 UDP 数据报。
    std::size_t n = socket.receive_from(boost::asio::buffer(data), sender_endpoint, 0, ec);
    if (ec)
    {
        std::println("receiver:接收失败:{}", ec.message());
        return 1;
    }

    std::string msg(data.data(), n);

    std::println("receiver:收到来自 {}:{} 的 {} 字节:{}", sender_endpoint.address().to_string(), sender_endpoint.port(), n, msg);

    return 0;
}

Running result : See “Running output and chronological order” below; if the example involves timers, threads, networks, or external devices, the specific timing and order may vary slightly depending on the environment.

1.3.Compile and run

Terminal 1: run the receiver.

g++ demo1_udp_receiver.cpp -o demo1_udp_receiver -std=c++23 -lboost_system -pthread
./demo1_udp_receiver

Terminal 2: use nc to send UDP data.

echo "hello udp" | nc -u 127.0.0.1 9001

1.4.Run output and chronological order

As soon as the receiving end starts, output immediately:

receiver:监听 UDP 0.0.0.0:9001,等待数据

At this point the program is blocked at:

socket.receive_from(...)

After sending the data, the receiver outputs something like:

receiver:收到来自 127.0.0.1:xxxxx 的 10 字节:hello udp

where xxxxx is the sender's ephemeral port, which may differ each time.

1.5.Points to note in this example

The UDP receiver does not need accept() because UDP has no connection.

As long as a port is bound, it can receive datagrams sent by others.

1.6.Key Function Description

1.6.1.udp::socket socket(io, udp::endpoint(udp::v4(), 9001));

Function: Create a UDP socket and bind it to local port 9001.

1.6.2.socket.receive_from(buffer, sender_endpoint, 0, ec);

Purpose: Synchronously receive one UDP datagram.

Return value: the number of bytes received.

sender_endpoint will be filled with the sender's IP and port.


2.示例 2:普通 main() 里写同步 UDP Sender

2.1.Program Objectives

Write a UDP sender that sends a message to 127.0.0.1:9001.

2.2.Complete code

#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <print>
#include <string>

using boost::asio::ip::udp;

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    // io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
    boost::asio::io_context io;
    boost::system::error_code ec;

    // UDP socket 用来发送或接收无连接的数据报。
    udp::socket socket(io);
    socket.open(udp::v4(), ec);
    if (ec)
    {
        std::println("sender:打开 socket 失败:{}", ec.message());
        return 1;
    }

    udp::endpoint receiver_endpoint(boost::asio::ip::make_address("127.0.0.1"), 9001);

    std::string msg = "hello udp";

    std::println("sender:准备发送到 127.0.0.1:9001");

    // send_to 指定目标地址发送 UDP 数据报。
    std::size_t n = socket.send_to(boost::asio::buffer(msg), receiver_endpoint, 0, ec);
    if (ec)
    {
        std::println("sender:发送失败:{}", ec.message());
        return 1;
    }

    std::println("sender:发送完成,字节数 = {}", n);

    return 0;
}

Running result : See “Running output and chronological order” below; if the example involves timers, threads, networks, or external devices, the specific timing and order may vary slightly depending on the environment.

2.3.Compile and run

Terminal 1: First run the receiving end of Example 1.

./demo1_udp_receiver

Terminal 2: Run the sender.

g++ demo2_udp_sender.cpp -o demo2_udp_sender -std=c++23 -lboost_system -pthread
./demo2_udp_sender

2.4.Run output and chronological order

Sender output:

sender:准备发送到 127.0.0.1:9001
sender:发送完成,字节数 = 9

The receiving end outputs something like:

receiver:监听 UDP 0.0.0.0:9001,等待数据
receiver:收到来自 127.0.0.1:xxxxx 的 9 字节:hello udp

2.5.Points to note in this example

UDP sending does not require connecting to the server first.

As long as the other party's IP and port are known, it is possible to:

send_to(...)

However, this does not mean the other party necessarily received it.


3.示例 3:普通 main() 里写异步 UDP Receiver

3.1.Program Objectives

Change Example 1 to an asynchronous version:

  1. Register async_receive_from();
  2. io.run() Wait for data;
  3. Execute the callback after receiving one UDP datagram;
  4. The program ends.

3.2.Complete code

#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <array>
#include <functional>
#include <print>
#include <string>

using boost::asio::ip::udp;

void on_receive(const boost::system::error_code& ec,
                std::size_t bytes_transferred,
                std::array<char, 1024>* data,
                udp::endpoint* sender_endpoint)
{
    if (ec)
    {
        std::println("on_receive:接收失败:{}", ec.message());
        return;
    }

    std::string msg(data->data(), bytes_transferred);

    std::println("on_receive:收到来自 {}:{} 的 {} 字节:{}", sender_endpoint->address().to_string(), sender_endpoint->port(), bytes_transferred, msg);
}

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    // io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
    boost::asio::io_context io;
    // UDP socket 用来发送或接收无连接的数据报。
    udp::socket socket(io, udp::endpoint(udp::v4(), 9001));

    std::array<char, 1024> data;
    udp::endpoint sender_endpoint;

    std::println("main:注册 async_receive_from");

    socket.async_receive_from(boost::asio::buffer(data),
                              sender_endpoint,
                              std::bind(on_receive,
                                        std::placeholders::_1,
                                        std::placeholders::_2,
                                        &data,
                                        &sender_endpoint));

    std::println("main:async_receive_from 已返回,准备 io.run()");

    // 启动事件循环,前面注册的异步任务会在这里被调度执行。
    io.run();

    std::println("main:io.run() 返回");

    return 0;
}

Running result : See “Running output and chronological order” below; if the example involves timers, threads, networks, or external devices, the specific timing and order may vary slightly depending on the environment.

3.3.Compile and run

Terminal 1: Run the asynchronous receiving end.

g++ demo3_async_udp_receiver.cpp -o demo3_async_udp_receiver -std=c++23 -lboost_system -pthread
./demo3_async_udp_receiver

Terminal 2: Send UDP data.

echo "imu udp" | nc -u 127.0.0.1 9001

3.4.Run output and chronological order

As soon as the receiving end starts, output immediately:

main:注册 async_receive_from
main:async_receive_from 已返回,准备 io.run()

Output after sending data:

on_receive:收到来自 127.0.0.1:xxxxx 的 8 字节:imu udp
main:io.run() 返回

Full output similar to:

main:注册 async_receive_from
main:async_receive_from 已返回,准备 io.run()
on_receive:收到来自 127.0.0.1:xxxxx 的 8 字节:imu udp
main:io.run() 返回

3.5.Points to note in this example

data and sender_endpoint are both local variables, but this example is safe because:

main() 卡在 io.run()
回调执行完之前,data 和 sender_endpoint 不会析构

In engineering projects, it is more recommended to write it as a class member variable.

3.6.async_receive_from() Description

socket.async_receive_from(buffer, sender_endpoint, handler);

Purpose: asynchronously receive a UDP datagram.

Callback parameter:

const boost::system::error_code& ec
std::size_t bytes_transferred

4.Example 4: Writing an Asynchronous UDP Echo Server in a Class

4.1.Program Objectives

Write an asynchronous UDP Echo Server:

  1. Continuously receive UDP datagrams;
  2. Print the sender and content;
  3. Send it back to the sender as is;
  4. Continue receiving the next one.

4.2.Complete code

#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <array>
#include <functional>
#include <print>
#include <string>

using boost::asio::ip::udp;

class UdpEchoServer
{
public:
    UdpEchoServer(boost::asio::io_context& io, unsigned short port)
        : socket_(io, udp::endpoint(udp::v4(), port))
    {
        std::println("UdpEchoServer:监听 UDP 端口 {}", port);
        start_receive();
    }

private:
    void start_receive()
    {
        socket_.async_receive_from(boost::asio::buffer(data_),
                                   remote_endpoint_,
                                   std::bind(&UdpEchoServer::on_receive,
                                             this,
                                             std::placeholders::_1,
                                             std::placeholders::_2));
    }

    void on_receive(const boost::system::error_code& ec, std::size_t bytes_transferred)
    {
        if (ec)
        {
            std::println("UdpEchoServer:接收失败:{}", ec.message());
            start_receive();
            return;
        }

        std::string msg(data_.data(), bytes_transferred);

        std::println("收到 {}:{} 的数据:{}", remote_endpoint_.address().to_string(), remote_endpoint_.port(), msg);

        socket_.async_send_to(boost::asio::buffer(data_, bytes_transferred),
                              remote_endpoint_,
                              std::bind(&UdpEchoServer::on_send,
                                        this,
                                        std::placeholders::_1,
                                        std::placeholders::_2));
    }

    void on_send(const boost::system::error_code& ec, std::size_t bytes_transferred)
    {
        if (ec)
        {
            std::println("UdpEchoServer:发送失败:{}", ec.message());
        }
        else
        {
            std::println("回显完成,字节数 = {}", bytes_transferred);
        }

        start_receive();
    }

private:
    // UDP socket 用来发送或接收无连接的数据报。
    udp::socket socket_;
    udp::endpoint remote_endpoint_;
    std::array<char, 1024> data_;
};

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    // io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
    boost::asio::io_context io;
    UdpEchoServer server(io, 9001);

    std::println("main:调用 io.run()");

    // 启动事件循环,前面注册的异步任务会在这里被调度执行。
    io.run();

    std::println("main:io.run() 返回");

    return 0;
}

Running result : See “Running output and chronological order” below; if the example involves timers, threads, networks, or external devices, the specific timing and order may vary slightly depending on the environment.

4.3.Compile and run

Terminal 1: Run a UDP Echo Server.

g++ demo4_udp_echo_server.cpp -o demo4_udp_echo_server -std=c++23 -lboost_system -pthread
./demo4_udp_echo_server

Terminal 2: Use nc to send and receive the echo.

nc -u 127.0.0.1 9001

Then enter:

hello
robot

4.4.Run output and chronological order

Server startup output:

UdpEchoServer:监听 UDP 端口 9001
main:调用 io.run()

Client input hello After pressing Enter, the server outputs something like:

收到 127.0.0.1:xxxxx 的数据:hello
回显完成,字节数 = 6

The client will see the echo:

hello

Continue inputting robot After that, the server outputs:

收到 127.0.0.1:xxxxx 的数据:robot
回显完成,字节数 = 6

4.5.Points to note in this example

In the UDP Echo Server, these two member variables are very important:

udp::endpoint remote_endpoint_;
std::array<char, 1024> data_;

They must live until the asynchronous callback completes.

4.6.async_send_to() Description

socket_.async_send_to(buffer, remote_endpoint_, handler);

Function: asynchronously sends a UDP datagram to the specified endpoint.

Callback parameter:

const boost::system::error_code& ec
std::size_t bytes_transferred

5.Example 5: Writing a periodic UDP sender in a class

5.1.Program Objectives

Robots often need to periodically broadcast status, for example:

robot alive
robot alive
robot alive

This example uses a timer to send a message via UDP once every 1 second, for a total of 5 sends.

5.2.Complete code

#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <chrono>
#include <functional>
#include <print>
#include <string>

using boost::asio::ip::udp;

class UdpHeartbeatSender
{
public:
    UdpHeartbeatSender(boost::asio::io_context& io,
                       const std::string& host,
                       unsigned short port)
        : socket_(io),
          endpoint_(boost::asio::ip::make_address(host), port),
          timer_(io),
          count_(0)
    {
        socket_.open(udp::v4());
    }

    void start()
    {
        schedule_send();
    }

private:
    void schedule_send()
    {
        timer_.expires_after(std::chrono::seconds(1));
        // 注册异步等待:这一行不会阻塞,回调会在定时器到期后执行。
        timer_.async_wait(std::bind(&UdpHeartbeatSender::on_timer,
                                    this,
                                    std::placeholders::_1));
    }

    void on_timer(const boost::system::error_code& ec)
    {
        if (ec)
        {
            std::println("timer 取消:{}", ec.message());
            return;
        }

        ++count_;
        msg_ = "heartbeat " + std::to_string(count_);

        socket_.async_send_to(boost::asio::buffer(msg_),
                              endpoint_,
                              std::bind(&UdpHeartbeatSender::on_send,
                                        this,
                                        std::placeholders::_1,
                                        std::placeholders::_2));
    }

    void on_send(const boost::system::error_code& ec, std::size_t bytes_transferred)
    {
        if (ec)
        {
            std::println("发送失败:{}", ec.message());
            return;
        }

        std::println("发送 {},字节数 = {}", msg_, bytes_transferred);

        if (count_ < 5)
        {
            schedule_send();
        }
        else
        {
            std::println("发送 5 次完成");
        }
    }

private:
    udp::socket socket_;
    udp::endpoint endpoint_;
    // 创建定时器,并设置到期时间。
    boost::asio::steady_timer timer_;
    std::string msg_;
    int count_;
};

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    // io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
    boost::asio::io_context io;
    UdpHeartbeatSender sender(io, "127.0.0.1", 9001);

    sender.start();

    std::println("main:调用 io.run()");

    io.run();

    std::println("main:io.run() 返回");

    return 0;
}

Running result : See “Running output and chronological order” below; if the example involves timers, threads, networks, or external devices, the specific timing and order may vary slightly depending on the environment.

5.3.Compile and run

Terminal 1: Listen for UDP.

nc -u -l 9001

Terminal 2: Run the program.

g++ demo5_udp_heartbeat.cpp -o demo5_udp_heartbeat -std=c++23 -lboost_system -pthread
./demo5_udp_heartbeat

5.4.Run output and chronological order

The program immediately outputs:

main:调用 io.run()

Output after about 1 second:

发送 heartbeat 1,字节数 = 11

Afterward, output once every approximately 1 second:

发送 heartbeat 2,字节数 = 11
发送 heartbeat 3,字节数 = 11
发送 heartbeat 4,字节数 = 11
发送 heartbeat 5,字节数 = 11
发送 5 次完成
main:io.run() 返回

The listening terminal will receive in sequence:

heartbeat 1heartbeat 2heartbeat 3heartbeat 4heartbeat 5

Some nc versions do not automatically display line by line because we did not add \n at the end of the message. You can put:

msg_ = "heartbeat " + std::to_string(count_);

Change to:

msg_ = "heartbeat " + std::to_string(count_) + "\n";

5.5.Points to note in this example

This example combines the timer with UDP:

timer 到期 -> 发送 UDP -> 发送完成 -> 再注册下一次 timer

This is the basic structure of "periodically reporting status" in much robot engineering.


6.Summary of this section

  1. UDP does not require a connection, and has no accept().
  2. One UDP send corresponds to one datagram, but delivery is not guaranteed.
  3. The receiver must save the sender's endpointand use it when replying.
  4. The buffer and endpoint of asynchronous UDP must remain alive until the callback completes.
  5. Periodic UDP sending can be implemented with steady_timer.