第 19.1.5 節

UDP 通信

0瀏覽次數0訪問次數--跳出率--平均停留

UDP 是無連接的數據報協議。
在機器人裏,UDP 常用於:局域網低延遲狀態廣播、傳感器數據廣播、自定義輕量通信、上位機發現設備等。

UDP 和 TCP 最大區別:

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

但 UDP 不保證可靠、不保證順序、不保證一定送達。


示例 1:普通 main() 裏寫同步 UDP Receiver

程序目標

寫一個 UDP 接收端:

  1. 綁定本地 9001 端口;
  2. 阻塞等待一個 UDP 數據報;
  3. 打印發送方地址和內容;
  4. 程序結束。

完整代碼

#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <array>
#include <iostream>
#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::cout << "receiver:监听 UDP 0.0.0.0:9001,等待数据" << std::endl;

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

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

    std::cout << "receiver:收到来自 " << sender_endpoint.address().to_string()
              << ":" << sender_endpoint.port()
              << " 的 " << n << " 字节:" << msg << std::endl;

    return 0;
}

運行結果:見下方“運行輸出與時間順序”;如果示例涉及定時器、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。

編譯運行

終端 1:運行接收端。

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

終端 2:用 nc 發送 UDP 數據。

echo "hello udp" | nc -u 127.0.0.1 9001

運行輸出與時間順序

接收端啓動後立刻輸出:

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

此時程序阻塞在:

socket.receive_from(...)

發送數據後,接收端輸出類似:

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

其中 xxxxx 是發送端臨時端口,每次可能不同。

本示例需要注意的點

UDP 接收端不需要 accept(),因為 UDP 沒有連接。

只要綁定端口,就可以接收別人發來的數據報。

關鍵函數説明

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

作用:創建 UDP socket,並綁定本地 9001 端口。

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

作用:同步接收一個 UDP 數據報。

返回值:接收到的字節數。

sender_endpoint 會被填充成發送方的 IP 和端口。


示例 2:普通 main() 裏寫同步 UDP Sender

程序目標

寫一個 UDP 發送端,向 127.0.0.1:9001 發送一條消息。

完整代碼

#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <iostream>
#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::cout << "sender:打开 socket 失败:" << ec.message() << std::endl;
        return 1;
    }

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

    std::string msg = "hello udp";

    std::cout << "sender:准备发送到 127.0.0.1:9001" << std::endl;

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

    std::cout << "sender:发送完成,字节数 = " << n << std::endl;

    return 0;
}

運行結果:見下方“運行輸出與時間順序”;如果示例涉及定時器、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。

編譯運行

終端 1:先運行示例 1 接收端。

./demo1_udp_receiver

終端 2:運行發送端。

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

運行輸出與時間順序

發送端輸出:

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

接收端輸出類似:

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

本示例需要注意的點

UDP 發送不需要先連接服務器。

只要知道對方 IP 和端口,就可以:

send_to(...)

但這不代表對方一定收到了。


示例 3:普通 main() 裏寫異步 UDP Receiver

程序目標

把示例 1 改成異步版本:

  1. 註冊 async_receive_from()
  2. io.run() 等待數據;
  3. 收到一個 UDP 數據報後執行回調;
  4. 程序結束。

完整代碼

#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <array>
#include <functional>
#include <iostream>
#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::cout << "on_receive:接收失败:" << ec.message() << std::endl;
        return;
    }

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

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

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::cout << "main:注册 async_receive_from" << std::endl;

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

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

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

    std::cout << "main:io.run() 返回" << std::endl;

    return 0;
}

運行結果:見下方“運行輸出與時間順序”;如果示例涉及定時器、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。

編譯運行

終端 1:運行異步接收端。

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

終端 2:發送 UDP 數據。

echo "imu udp" | nc -u 127.0.0.1 9001

運行輸出與時間順序

接收端啓動後立刻輸出:

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

發送數據後輸出:

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

完整輸出類似:

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

本示例需要注意的點

datasender_endpoint 都是局部變量,但是本例安全,因為:

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

工程裏更推薦寫成類成員變量。

async_receive_from() 説明

socket.async_receive_from(buffer, sender_endpoint, handler);

作用:異步接收一個 UDP 數據報。

回調參數:

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

示例 4:類裏寫異步 UDP Echo Server

程序目標

寫一個異步 UDP Echo Server:

  1. 持續接收 UDP 數據報;
  2. 打印發送方和內容;
  3. 原樣發回發送方;
  4. 繼續接收下一條。

完整代碼

#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <array>
#include <functional>
#include <iostream>
#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::cout << "UdpEchoServer:监听 UDP 端口 " << port << std::endl;
        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::cout << "UdpEchoServer:接收失败:" << ec.message() << std::endl;
            start_receive();
            return;
        }

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

        std::cout << "收到 " << remote_endpoint_.address().to_string()
                  << ":" << remote_endpoint_.port()
                  << " 的数据:" << msg << std::endl;

        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::cout << "UdpEchoServer:发送失败:" << ec.message() << std::endl;
        }
        else
        {
            std::cout << "回显完成,字节数 = " << bytes_transferred << std::endl;
        }

        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::cout << "main:调用 io.run()" << std::endl;

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

    std::cout << "main:io.run() 返回" << std::endl;

    return 0;
}

運行結果:見下方“運行輸出與時間順序”;如果示例涉及定時器、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。

編譯運行

終端 1:運行 UDP Echo Server。

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

終端 2:用 nc 發送並接收回顯。

nc -u 127.0.0.1 9001

然後輸入:

hello
robot

運行輸出與時間順序

服務端啓動輸出:

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

客户端輸入 hello 回車後,服務端輸出類似:

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

客户端會看到回顯:

hello

繼續輸入 robot 後,服務端輸出:

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

本示例需要注意的點

UDP Echo Server 中這兩個成員變量很重要:

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

它們必須活到異步回調完成。

async_send_to() 説明

socket_.async_send_to(buffer, remote_endpoint_, handler);

作用:異步發送一個 UDP 數據報到指定 endpoint。

回調參數:

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

示例 5:類裏寫週期性 UDP Sender

程序目標

機器人裏經常需要週期性廣播狀態,例如:

robot alive
robot alive
robot alive

本例使用 timer 每 1 秒通過 UDP 發一次消息,共發送 5 次。

完整代碼

#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <chrono>
#include <functional>
#include <iostream>
#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::cout << "timer 取消:" << ec.message() << std::endl;
            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::cout << "发送失败:" << ec.message() << std::endl;
            return;
        }

        std::cout << "发送 " << msg_ << ",字节数 = " << bytes_transferred << std::endl;

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

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::cout << "main:调用 io.run()" << std::endl;

    io.run();

    std::cout << "main:io.run() 返回" << std::endl;

    return 0;
}

運行結果:見下方“運行輸出與時間順序”;如果示例涉及定時器、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。

編譯運行

終端 1:監聽 UDP。

nc -u -l 9001

終端 2:運行程序。

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

運行輸出與時間順序

程序立刻輸出:

main:调用 io.run()

約 1 秒後輸出:

发送 heartbeat 1,字节数 = 11

之後每隔約 1 秒輸出一次:

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

監聽終端會依次收到:

heartbeat 1heartbeat 2heartbeat 3heartbeat 4heartbeat 5

有些 nc 版本不會自動按行顯示,因為我們沒有在消息末尾加 \n。你可以把:

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

改成:

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

本示例需要注意的點

這個例子把 timer 和 UDP 結合起來了:

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

這就是很多機器人工程裏“週期上報狀態”的基本結構。


本節總結

  1. UDP 不需要連接,也沒有 accept()
  2. UDP 一次發送對應一個數據報,但不保證送達。
  3. 接收端要保存發送方 endpoint,回包時用它。
  4. 異步 UDP 的 buffer 和 endpoint 必須活到回調完成。
  5. 週期 UDP 發送可以用 steady_timer 實現。
音乐页