ASIO模型
事件驱动,把网络IO和处理逻辑封装成一个事件,丢进事件列表,启动事件处理。
ASIO API
基于ASIO 1.85.0,万变不离其宗,默认using namespace boost::asio;
1. IO上下文
事件驱动,得有个处理事件的东西。io_context
类就是处理事件的东西,在设计中它被抽象为一个IO上下文。所谓上下文,就是理解当前内容所需的前提。比如要“改变当前变量的值为100”,就要先知道“当前变量是什么,类型是什么”。一个IO上下文就是进行IO操作的时候需要知道的东西,比如“程序运行在计算机上还是座机上”。有了上下文,才能执行IO操作。
io_context io_ctx;
2. 端点
ip + port = endpoint,就是一种封装
// 以tcp server为例,ip::tcp::v4()就是自己的ip,12345是要监听的端口号,这俩组合成了端点
ip::tcp::endpoint ep(ip::tcp::v4(), 12345);
3. 接受器(acceptor)
同样以tcp server为例,一个tcp server的完整工作流程是bind -> listen -> accept -> process -> close
一个接受器的本职工作是完成accept,要完成accept则必须要进行bind和listen。bind和listen所需的参数其实只有一个端点。可能有人会在想,原生网络编程中,bind和listen还需要一个socket,为什么只有端点一个参数呢:因为接受器就是对这个socket的抽象。
既然接受器是个socket,它就得能够IO。而IO是IO上下文提供的服务,所以接受器要绑定在一个IO上下文上。
ip::tcp::acceptor acceptor(io_ctx, ep);
4. 套接字
刚说过接受器是个socket,裸socket就只是少了端点参数,只用绑定IO上下文即可。
ip::tcp::socket sock(io_ctx);
5. 异步accept
异步accpet是接受器的成员,用于在(IO上下文的)事件列表中添加异步accept事件。
accept时会成功建立一个连接,在网络编程中一个连接也被抽象为一个socket,因此异步accept函数的调用要传入一个用于表示连接的socket。
如果只是接受连接请求,那么提供一个socket就足够了。但是在accept后更重要的是对数据的处理。由于将异步accept作为一个事件进行处理,我们并不会等待accept处理完再执行后续指令,因此我们需要提前指定当accept完成后应当干什么。
我们就要传递一个函数对象来指定accept完成后的操作。
acceptor.async_accept(sock, std::function<void(const boost::system::error_code &ec)>)
6. 异步write
与异步accept类似,我们通过异步wrtie往代表连接的socket里写入数据完成通信。
同样的,我们需要传递一个函数对象来指定write结束后应当做什么。
sock.async_write_some(buffer("Hello from server!"), std::function<void(const boost::system::error_code &ec, std::size_t)>) {
7. 开启事件循环
所有的异步操作都作为事件添加进了(IO上下文的)事件列表,接下来就可以执行这个事件列表了。如果事件列表中尚有异步操作未完成,则事件列表不会执行完毕,从外部来看,就是线程阻塞在了开启事件循环的这个语句上。
io_ctx.run()
异步IO的异步性则体现在了io_ctx内部,io_ctx事件列表中的各个事件是并发的。
面向过程的基本例子
#include <boost/asio.hpp>
#include <exception>
#include <iostream>
int main()
{
using namespace boost::asio;
// 1. 创建io上下文
io_context io_ctx;
// 2. 创建endpoint(ip与端点的组合)
ip::tcp::endpoint ep(ip::tcp::v4(), 12345);
// 3. 定义accept这个动作的上下文,要accept则必须listen,这里定义的就是listen这个动作
// 但是并没有开始listen,也并没有开始accept
ip::tcp::acceptor acceptor(io_ctx, ep);
// 4. 创建socket
ip::tcp::socket sock(io_ctx);
// 5. 定义异步accept这个动作
// accpet时绑定socket;当accept成功时,会调用lambda函数
acceptor.async_accept(sock, [&sock](const boost::system::error_code &ec) {
// acceptor.async_accept::lambda
// 5.1 如果有错误,抛出异常
if (ec)
throw boost::system::system_error(ec);
// 5.2 如果没有错误,输出Accepted!
std::cout << "Accepted!" << std::endl;
// 6. 进一步定义accept成功后的操作,这里是异步write,实际上还有异步read等操作
// write是向socket写入Hello from server!,当写入成功时,调用lambda函数
sock.async_write_some(buffer("Hello from server!"), [&sock](const boost::system::error_code &ec, std::size_t) {
// sock.async_write_some::lambda
// 6.1 如果有错误,抛出异常
if (ec)
throw boost::system::system_error(ec);
// 6.2 如果没有错误,输出Data sent!
std::cout << "Data sent!" << std::endl;
// 6.3 关闭socket,断开连接
sock.close();
});
});
// 到此为止,server没有进行bind, listen, accept, write, close等任何操作
// 我们只是在io_ctx上下文中定义了这些操作
// io_ctx更像是一个TODO list,里面有很多操作,但是并没有开始执行
// 7. 现在我们开始io_ctx的事件循环
io_ctx.run();
return 0;
}
1.程序编译运行后,因为异步accept事件尚未结束(未成功accept),所以程序进行事件循环,即不断监听端口
* 正在执行任务: xmake run asio
2.通过数据包发送软件向127.0.0.1:12345发送任意tcp数据流
数据包发送软件收到回复
Hello from server!\00
服务器terminal
* 正在执行任务: xmake run asio
Accepted!
Data sent!
* 终端将被任务重用,按任意键关闭。
可以看到异步accept完成后它的std::function成为了下一个异步事件并被触发,在这个std::function里又添加了一个异步write事件。异步write事件完成后,又触发了它的std::function,这个std::function运行结束后,所有异步事件结束,异步事件列表为空,io_ctx.run()返回,主函数返回,程序结束。
2 条评论
怎么收藏这篇文章?
叼茂SEO.bfbikes.com