简介
- 参考 boost-asio-study
- asio,异步io,Asynchronous Input/Output,是一个跨平台的C++库,用于网络和底层I/O编程,它为开发者提供了一种一致的异步模型,采用现代C++的方法
示例1
- 每个 asio 程序都有一个 io_context 对象,代表一个 io 服务,通过这个服务操作 asio。
- 下面的代码是一个空 io_context 的示例,
context.run() 是一个阻塞的操作,将事件全部消费后就会退出。这段代码因为没有任何事件,执行后就立即结束了。
void test001()
{
asio::io_context context;
context.run();
}
示例2
- 所以需要给服务生产事件才能让其发挥作用,下面则是通过定时器来实现
void test002()
{
asio::io_context context;
asio::steady_timer timer(context, std::chrono::seconds(3));
timer.async_wait([](const system::error_code& ec) {
std::cout << "3 sec tick" << std::endl;
});
context.run();
}
- 创建定时器
steady_timer, 指定 3 秒,然后异步等待,时间到后触发执行 lambda 函数打印内容
- 有几点需要注意
- 所有IO对象都需要一个
io_context,一般在构造时传入
async_wait 是一个异步操作,但真正执行是在 context.run()
async_wait 传入的参数是一个回调函数,超时后会被调用
- 每个IO对象的
async_xxx 都有回调函数,而每个回调函数的参数是不一样的,具体的需要看文档
- 除了
async_wait 异步等待,也可以同步等待 wait,此时就会阻塞,直到超时结束
示例3
async_wait 的回调函数的参数是 boost::system::error_code,如果需要给回调函数增加额外的参数,就需要使用 bind
void test003_1(const system::error_code& ec, asio::steady_timer* timer,
int* counter)
{
if (*counter >= 3) {
return;
}
std::cout << "tick: " << *counter << std::endl;
++(*counter);
timer->expires_after(std::chrono::seconds(1));
timer->async_wait(
std::bind(&test003_1, std::placeholders::_1, timer, counter));
}
void test003()
{
int counter = 0;
asio::io_context context;
asio::steady_timer timer(context, std::chrono::seconds(1));
timer.async_wait(
std::bind(&test003_1, std::placeholders::_1, &timer, &counter));
context.run();
}
- 这个示例中通过
std::bind 生成了一个新的回调函数,这个函数的第一个参数使用了占位符 std::placeholders::_1,表示在调用这个回调函数时,需要传入第1个参数,而第2个和第3个参数则是使用了 timer 和 counter,不需要额外传入,其实就是相当于默认参数
示例4
- 除了使用定时器,还可以使用
socket 来生产事件,下面是一个 echo server 的实现
- 首先使用同步方式
void test004_1(asio::ip::tcp::socket socket)
{
try {
while (true) {
array<char, 1024> buff;
system::error_code ec;
std::size_t length = socket.read_some(asio::buffer(buff), ec);
if (ec == asio::error::eof) {
std::cout << "session close" << std::endl;
break;
} else if (ec) {
throw system::system_error(ec);
}
asio::write(socket, asio::buffer(buff, length));
}
}
catch (const std::exception& e) {
std::cerr << e.what() << '\n';
}
}
void test004()
{
asio::io_context context;
asio::ip::tcp::acceptor acceptor(
context, asio::ip::tcp::endpoint(asio::ip::tcp::v4(), 12345));
try {
while (true) {
test004_1(acceptor.accept());
}
}
catch (const std::exception& e) {
std::cerr << "exception: " << e.what() << std::endl;
}
}
- 上面的示例启动时会监听
12345 端口
- 在
acceptor.accept() 时阻塞等待客户端建立连接
- 一旦有连接上来,则触发
test004_1 函数
- 接着通过
socket.read_some 阻塞等待客户端发送内容
- 收到内容后通过
boost::asio::write 将内容发送给客户端
- 有几点需要注意
tcp::acceptor 是一个IO对象,用来处理TCP连接,端口由 tcp::endpoint 指定
- 因为直接使用
accept,read_some,write 这些阻塞操作,整个过程是同步的,所以每次只能处理一个连接,但可以连续发消息,除非客户端断开连接
- 写回数据时没有使用
socket.write_some,因为它不能一次性写完所有数据,但 boost::asio::write 可以
test004_1 函数中的参数虽然是值传递,但是 accept 返回的结果使用了移动语义,所以没有拷贝值,这个是 C++11 中引入的一项优化,当一个函数返回一个局部对象时,这个对象会被移动,而不是被复制
示例5
- 接着是使用异步的方式
- 使用异步,主要的问题是对象生命周期,可以使用
shared_ptr 解决
- 为了同时处理多个连接,需要保留每个连接的
socket 对象,所以抽象一个处理连接的类 Session
class Session : public std::enable_shared_from_this<Session> {
public:
Session(asio::ip::tcp::socket socket) : socket_(std::move(socket)) {}
void Start()
{
DoRead();
}
void DoRead()
{
auto self(shared_from_this());
socket_.async_read_some(
asio::buffer(buffer_),
[this, self](const system::error_code& ec, std::size_t length) {
if (!ec) {
DoWrite(length);
}
});
}
void DoWrite(std::size_t length)
{
auto self(shared_from_this());
asio::async_write(
socket_, asio::buffer(buffer_, length),
[this, self](const system::error_code& ec, std::size_t length) {
if (!ec) {
DoRead();
}
});
}
private:
asio::ip::tcp::socket socket_;
std::array<char, 1024> buffer_;
};
class Server {
public:
Server(asio::io_context& context, std::uint16_t port)
: acceptor_(context,
asio::ip::tcp::endpoint(asio::ip::tcp::v4(), port))
{
DoAccept();
}
private:
void DoAccept()
{
acceptor_.async_accept([this](const system::error_code& ec,
asio::ip::tcp::socket socket) {
if (!ec) {
std::make_shared<Session>(std::move(socket))->Start();
}
DoAccept();
});
}
private:
asio::ip::tcp::acceptor acceptor_;
};
void test005()
{
asio::io_context context;
Server server(context, 12345);
context.run();
}
- 上面的示例启动后创建
Server 类,该类监听 12345 端口
- 使用
async_accept 等待客户端连接,这个是异步等待
- 当有连接上来后,创建
Session 类处理连接
Session 里使用 async_read_some 异步等待客户端发送数据
- 收到数据后使用
async_write 异步发送数据
- 可以看到每个读写逻辑都是用了
async_xxx 来实现,这样程序就不会阻塞在读写初可以处理后面的任务
- 有几点需要注意
Session 里有两个成员变量,socket_ 负责跟客户端通信,在构造时通过move移动语义传递进来;buffer_ 负责缓存读取的数据
socket_ 变量一旦销毁,连接就会中断,所以需要保证 Session 不销毁
async_accept 后通过 std::make_shared 生成 Session 的 shared_ptr 对象,此时计数+1,如果不做任何处理,那么在执行 async_read_some 后,shared_ptr 对象就会离开作用域,进而销毁 Session 对象,因为 async_read_some 是异步的,里面的 lambda 函数只有在有数据时才会执行。所以需要将 Session 的引用传入到 lambda 函数里引用住,这就是 auto self(shared_from_this()); 和 lambda 里的 [this, self] 的作用。
- 使用
shared_from_this() 共享了 Session 的控制权,避免多个 shared_ptr 同时持有同一个对象而导致的重复销毁问题,传入 this 是让 lambda 函数能像正常的成员函数一样去调用成员变量和其他成员函数,传入 self 则是为了引用住 Session 对象,延后其生命周期,保证在数据达到并触发 lambda 函数后 Session 对象依然存在
async_write 原理跟 async_read_some 一样,也是通过 shared_from_this 延续 Session 对象的生命周期,同时在发送数据后重新 async_read_some 等待下一次数据的到达