Boost.Asio asynchronous IO library
This chapter is for learning about robots, ROS2, lower-level computer serial ports, and TCP/UDP communication. This tutorial intentionally uses
std::bindand temporarily avoids lambda, so that beginners can first clearly understand "callback functions, placeholders, member function binding, and asynchronous execution order."
The chapter order of this tutorial.
Recommended learning order is as follows:
ch19-1-Boost.Asio异步IO库.md
ch19-1-1-定时器与异步IO.md
ch19-1-2-Boost.Asio基础.md
ch19-1-3-串口通信.md
ch19-1-4-TCP通信.md
ch19-1-5-UDP通信.md
ch19-1-6-机器人工程写法与ROS2集成.md
I put the 'timer' first because it doesn't depend on serial port hardware or a network peer, and is most suitable for seeing clearly:
io_context.run()why does it block;async_wait()why doesn't it execute the callback immediately;- The difference between one timer and two timers;
- Which thread does the callback function actually execute in;
- What exactly do
std::bind's_1,this, and&Class::funcmean?
Unified Conventions for This Tutorial
Use standard library style consistently.
This tutorial uses the standard library as much as possible:
#include <chrono>
#include <functional>
#include <memory>
#include <thread>
#include <string>
For example, unify timer time settings:
std::chrono::seconds(1)
std::chrono::milliseconds(100)
Instead of writing first:
boost::asio::chrono::seconds(1)
Callbacks uniformly use std::bind
In this tutorial, asynchronous callbacks should be written as:
timer.async_wait(std::bind(on_timer, std::placeholders::_1));
Member function callback written as:
timer_.async_wait(std::bind(&Printer::on_timer, this, std::placeholders::_1));
When the read-write callback has two parameters, write it as:
socket.async_read_some(
boost::asio::buffer(data_),
std::bind(&Session::on_read,
this,
std::placeholders::_1,
std::placeholders::_2));
Among them:
std::placeholders::_1
std::placeholders::_2
"Indicates the first and second parameters that Boost.Asio automatically passes to the callback function when the asynchronous operation completes."
Error codes still use Boost.Asio's types.
Do not change this arbitrarily:
const boost::system::error_code& ec
The reason is that Boost.Asio's asynchronous callback by default passes the error to boost::system::error_code. If you later switch to standalone Asio or a standard networking library, then consider the corresponding type.
The core idea of Boost.Asio
Boost.Asio can be roughly understood as:
io_context = 事件循环 / 调度器
socket / serial_port / timer = IO对象
async_xxx() = 注册一个异步任务
handler = 异步任务完成后执行的回调函数
run() = 开始处理异步任务和回调函数
A minimal asynchronous program looks roughly like this:
boost::asio::io_context io;
boost::asio::steady_timer timer(io, std::chrono::seconds(2));
timer.async_wait(std::bind(on_timer, std::placeholders::_1));
io.run();
The execution logic is not:
async_wait 立刻执行 on_timer
but rather:
async_wait 注册任务
io.run() 进入事件循环
等待 2 秒
timer 到期
io.run() 调用 on_timer
没有任务了
io.run() 返回
build environment
Ubuntu / Debian
sudo apt update
sudo apt install libboost-all-dev g++ cmake
Fedora
sudo dnf install boost-devel gcc-c++ cmake
Single-file compilation command
Many examples can be compiled directly like this:
g++ demo.cpp -o demo -std=c++17 -lboost_system -pthread
If your Boost version is relatively new, you may not need to explicitly link -lboost_system on some Linux distributions, but it’s recommended to include it in the early learning stages to reduce environment discrepancies.
Recommended CMake template
cmake_minimum_required(VERSION 3.16)
project(asio_demo)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Boost REQUIRED COMPONENTS system)
add_executable(demo demo.cpp)
target_link_libraries(demo PRIVATE Boost::system pthread)
Why learn timers first?
In robot communication, the most common issue isn't "whether you know how to call the API," but rather:
- When is the callback executed;
- In which thread is the callback executed?
- When is an object destructed;
- When can buffer data still be used;
run()why is it stuck;run()why would it return early again;- Why does data race occur in multithreading?
These problems can all be understood by first grasping timers. Once you understand timers, serial ports, TCP, and UDP are essentially just "different events to wait for":
timer 等待时间到期
serial_port 等待串口可读 / 可写
tcp::socket 等待网络可读 / 可写 / 连接完成
udp::socket 等待收到一个数据报
After completing this tutorial, what level should be achieved?
After learning, you should be able to:
- Understand the official Boost.Asio timer / TCP / UDP tutorial;
- Can use
std::bindto write ordinary function callbacks and member function callbacks; - Can explain the blocking and return conditions of
io_context.run(); - Can you write an asynchronous serial port read of the lower computer's data;
- Can write TCP client/server;
- Can write UDP sender / receiver / echo server;
- Able to encapsulate the Asio communication module into a class;
- It can integrate the communication module into the ROS2 node, instead of writing a blocking infinite loop in the ROS2 callback.
The 5 sentences you need to remember most right now
async_xxx()just registers an asynchronous task, not immediately executing the callback.io_context.run()is where asynchronous task execution is truly driven.- The callback function will only be called in the thread that is executing
io_context.run(). - Asynchronous buffer, socket, and timer objects must live until the callback completes execution.
- When binding a member function in a class, write
std::bind(&Class::func, this, _1, _2).