UDP 通信
UDP 是無連接的數據報協議。
在機器人裏,UDP 常用於:局域網低延遲狀態廣播、傳感器數據廣播、自定義輕量通信、上位機發現設備等。
UDP 和 TCP 最大區別:
TCP 是字节流,没有消息边界。
UDP 是数据报,一次 send_to 对应对端一次 receive_from 的一个数据报。
但 UDP 不保證可靠、不保證順序、不保證一定送達。
示例 1:普通 main() 裏寫同步 UDP Receiver
程序目標
寫一個 UDP 接收端:
- 綁定本地 9001 端口;
- 阻塞等待一個 UDP 數據報;
- 打印發送方地址和內容;
- 程序結束。
完整代碼
#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 改成異步版本:
- 註冊
async_receive_from(); io.run()等待數據;- 收到一個 UDP 數據報後執行回調;
- 程序結束。
完整代碼
#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() 返回
本示例需要注意的點
data 和 sender_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:
- 持續接收 UDP 數據報;
- 打印發送方和內容;
- 原樣發回發送方;
- 繼續接收下一條。
完整代碼
#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
這就是很多機器人工程裏“週期上報狀態”的基本結構。
本節總結
- UDP 不需要連接,也沒有
accept()。 - UDP 一次發送對應一個數據報,但不保證送達。
- 接收端要保存發送方
endpoint,回包時用它。 - 異步 UDP 的 buffer 和 endpoint 必須活到回調完成。
- 週期 UDP 發送可以用
steady_timer實現。