- 一.boost asio概述
- 1.1 boost库安装
- 1.2 概述
- 1.3 asio基础概念学习
- 1.3.1 I/O Context
- 1.3.2 Timer(I/O对象:Timer,socket等)
- 1.3.3 Echo Server
- 二.同步请求
- 2.1 同步请求步骤
- 2.2 同步请求方式下echo server设计
- 2.3 同步请求方式下echo client设计
- 三.异步请求
- 3.1 异步请求步骤
- 3.2 异步请求echo server
- 3.3 异步请求echo client(客户端可以不采用异步方式)
- 四. Some tips
- 4.1 过时接口剔除
- 4.2 尽量少包含头文件
- 4.3 Handler 签名问题
- 4.4 Handler 的 error_code 参数到底是不是引用?
- 4.5 Bind 占位符
- 4.6 Server 也可以用 Resolver
- 4.7 Move Acceptable Handler
由于asio依赖于c++ boost库,因此需要首先安装boost库,下面是基于linux系统安装boost库的过程:
- 首先去下载最新的boost代码包,网址www.boost.org
- 解压压缩包:tar -jxvf boost_1_79_0.tar.bz2
- 进入解压后的boost文件夹:cd boost_1_79_0
- 编译前的配置工作:./bootstrap.sh
- 编译boost:./b2 install
如果像上面这样不指定额外选项,Boost 将编译 release 版本的库文件,把头文件安装到“/usr/local/include”中,把库文件安装到“/usr/local/lib”中。如下图所示: - 安装后想马上使用boost库进行编译,还需要执行一下这个命令,更新一下系统的动态链接库:ldconfig
- 代码测试1:头文件
里的三个宏:BOOST_PLATFORM、BOOST_COMPILER 和 BOOST_STDLIB,分别定义了当前的操作系统、编译器和标准库。
testcpp.cpp:
#include//包含 Boost 头文件 #include //包含 Boost 头文件 int main() { cout << BOOST_VERSION << endl; //Boost 版本号 cout << BOOST_LIB_VERSION << endl; //Boost 版本号 cout << BOOST_PLATFORM << endl; //操作系统 cout << BOOST_COMPILER << endl; //编译器 cout << BOOST_STDLIB << endl; //标准库 return 0; }
编译:g++ testcpp.cpp
执行结果:
107900
1_79
linux
GNU C++ version 4.8.5 20150623 (Red Hat 4.8.5-44)
GNU libstdc++ version 20150623
- 代码测试2:boost_thread多线程测试
testcpp.cpp:
#include//包含boost头文件 #include #include using namespace std; volatile bool isRuning = true; void func1() { static int cnt1 = 0; while(isRuning) { cout << "func1:" << cnt1++ << endl; sleep(1); } } void func2() { static int cnt2 = 0; while(isRuning) { cout << "tfunc2:" << cnt2++ << endl; sleep(2); } } int main() { boost::thread thread1(&func1); boost::thread thread2(&func2); system("read"); isRuning = false; thread2.join(); thread1.join(); cout << "exit" << endl; return 0; }
编译:g++ testcpp.cpp -lboost_thread -lpthread
注意:此处编译必须添加两个动态库文件
执行:./a.out报错
error while loading shared libraries: libboost_thread.so.1.79.0: cannot open shared object file: No such file or directory
这是因为要用到的库不在默认的环境变量里,可以使用下面的命令添加:
ldconfig /usr/local/lib
添加后,再执行./a.out,这样你就完成了你的第一个boost::thread程序。
执行结果:
1.2 概述func1:0
func2:0
func1:1
func1:2
func2:1
func1:3
func1:4
func2:2
func1:5
func1:6
func2:3
概述:Boost.Asio是一个跨平台的C++库,用于网络和底层I/O编程,可以在I/O对象(如socket)上执行同步和异步操作。其官方教程网址如下:
https://wizardforcel.gitbooks.io/the-boost-cpp-libraries/content/7.html
简略的分析其使用过程,以建立socket为例:首先,在程序中需要至少定义一个io_context对象:boost::asio::io_context io_context。其中io_context表示程序到操作系统I/O服务的“连接”。在asio之前版本中为io_service,也就是:io_context=io_service。
为执行I/O操作,还需要一个I/O对象(通常需要使用io_context构造),如一个TCP套接字:boost::asio::ip::tcp::socket socket(io_context),也就是先创建一个io_context对象,然后将io_context作为参数初始化一个io对象为socket。
小结:
1.3 asio基础概念学习 1.3.1 I/O Context
- 创建io_context:boost::asio::io_context io_context
- 创建io对象:boost::asio::ip::tcp::socket socket(io_context)
- 创建io其他对象,例如Timer等:boost::asio::steady_timer timer(io_context, std::chrono::seconds(3));
每个 Asio 程序都至少有一个 io_context 对象,它代表了操作系统的 I/O 服务(io_context 在 Boost 1.66 之前一直叫 io_service),把你的程序和这些服务链接起来。下面这个程序空有 io_context 对象,却没有任何异步操作,所以它其实什么也没做,也没有任何输出。
int main() {
boost::asio::io_context io_context;
io_context.run();
return 0;
}
io_context.run 是一个阻塞(epoll)调用,适用于asio异步网络请求操作,姑且把它想象成一个 loop(事件循环),直到所有异步操作完成后,loop才结束,run才返回。但是这个程序没有任何异步操作,所以loop直接就结束了。
1.3.2 Timer(I/O对象:Timer,socket等)有了 io_context 还不足以完成 I/O 操作,用户一般也不跟 io_context 直接交互。根据 I/O 操作的不同,Asio 提供了不同的 I/O 对象,比如 timer(定时器),socket,等等。 Timer 是最简单的一种 I/O 对象,可以用来实现异步调用的超时机制,下面是最简单的用法:
#include#include //using boost::asio::ip::tcp; //异步请求超时执行 void Print(boost::system::error_code ec) { std::cout << "Hello, world!" << std::endl; } int main() { boost::asio::io_context ioc; //第一步:创建一个io_context boost::asio::steady_timer timer(ioc, std::chrono::seconds(3)); //第二步,使用io_context初始化一个I/O对象timer,以实现异步调用的超时机制 timer.async_wait(&Print); //超时执行Print回调函数 ioc.run(); return 0; }
编译:g++ testcpp.cpp -lpthread -std=c++11
执行:等待3s后输出Hello, world!
以下几点需要注意:
- 所有 I/O 对象都依赖 io_context,一般在构造时指定。
- async_wait 初始化了一个异步操作,但是这个异步操作的执行,要等到 io_context.run 时才开始。
- Timer 除了异步等待(async_wait),还可以同步等待(wait)。同步等待是阻塞的,直到 timer 超时结束。基本上所有 I/O 对象的操作都有同步和异步两个版本,也许是出于设计上的完整性。
- async_wait 的参数是一个函数对象,异步操作完成时它会被调用,所以也叫 completion handler,简称 handler,可以理解成回调函数。
- 所有 I/O 对象的 async_xyz 函数都有 handler 参数,对于 handler 的签名,不同的异步操作有不同的要求,除了官方文档里的说明,也可以直接查看 Boost 源码。
async_wait 的 handler 签名为 void (boost::system::error_code),如果要传递额外的参数,就得用 bind。不妨修改一下 Print,让它每隔一秒打印一次计数,从 0 递增到 3。与前版相比,Print 多了两个参数,以便访问当前计数及重启 timer。
#include#include void Print(boost::system::error_code ec, boost::asio::steady_timer* timer, int* count) { if (*count < 3) { std::cout << *count << std::endl; ++(*count); timer->expires_after(std::chrono::seconds(1)); timer->async_wait(std::bind(&Print, std::placeholders::_1, timer, count)); } } int main() { boost::asio::io_context ioc; boost::asio::steady_timer timer(ioc, std::chrono::seconds(1)); int count = 0; timer.async_wait(std::bind(&Print, std::placeholders::_1, &timer, &count)); ioc.run(); return 0; }
调用 bind 时,使用了占位符(placeholder)std::placeholders::_1,print函数的第一个参数ec保留,而传入timer和count两个参数。数字占位符共有 9 个,_1 - _9。占位符也有很多种写法,这里就不详述了。
输出结果:每隔1s打印一次,3秒停止
1.3.3 Echo Server0
1
2
Socket 也是一种 I/O 对象,这一点前面已经提及。相比于 timer,socket 更为常用,毕竟 Asio 是一个网络程序库。
下面以经典的 Echo 程序为例,实现一个 TCP Server。所谓 Echo,就是 Server 把 Client 发来的内容原封不动发回给 Client。
先从同步方式开始,异步太复杂,慢慢来。具体实现方式见二、三两部分。
二.同步请求 2.1 同步请求步骤 前面提到设计一个Echo Server,实现asio同步网络请求,同步请求的实现步骤如下:
(1)程序通过I/O对象启动连接操作:socket.connect(server_endpoint);
(2)I/O对象(socket)将请求转发给io_context;
(3)io_context请求操作系统去执行连接操作;
(4)操作系统将操作结果返回给io_context;
(5)io_context将操作的(错误)结果转换成boost::system::error_code对象,并回传给I/O对象(socket);
(6)如果操作失败,I/O对象抛出boost::system::system_error异常。如果是使用以下方式,则只设置错误码,不会抛出异常:
2.2 同步请求方式下echo server设计boost::system::error_code ec;
socket.connect(server_endpoint, ec);
Session 代表会话,负责管理一个 client 的连接。参数 socket 传的是值,但是会用到 move 语义来避免拷贝。
echoserver.cpp:
#include#include #include using namespace std; using tcp = boost::asio::ip::tcp; // #define BUF_SIZE 1024 enum { BUF_SIZE = 1024 }; void Session(tcp::socket socket) { cout<<"客户端连接!"< while (true) { char data[BUF_SIZE]={0}; boost::system::error_code ec; //将数据读取到data buffer中,读取到的实际数据长度为length std::size_t length = socket.read_some(boost::asio::buffer(data), ec); cout<<"接收到client发送的数据为: "< std::cout << "连接被 client 妥善的关闭了" << std::endl; break; } else if (ec) { // 其他错误 throw boost::system::system_error(ec); } //将数据data[length]发送给client boost::asio::write(socket, boost::asio::buffer(data, length)); cout<<"echo server发送数据完毕!"< std::cerr << "Exception: " << e.what() << std::endl; } } int main(int argc, char* argv[]) { if (argc != 2) { std::cerr << "Usage: " << argv[0] << " " << std::endl; return 1; } unsigned short port = std::atoi(argv[1]); //获取port值 boost::asio::io_context ioc; //定义io_context // 创建 Acceptor 侦听新的连接,endpoint socket: localhost:port tcp::acceptor acceptor(ioc, tcp::endpoint(tcp::v4(), port)); try { // 一次处理一个连接 while (true) { //client请求放在队列中,循环逐个处理,处理完继续阻塞 Session(acceptor.accept()); //acceptor.accept()会生成tcp::socket对象 } } catch (const std::exception& e) { std::cerr << "Exception: " << e.what() << std::endl; } return 0; }
其中,tcp 即 boost::asio::ip::tcp;BUF_SIZE 定义为 enum { BUF_SIZE = 1024 };。这些都是细节,后面的例子不再赘述。
编译:g++ echoserver.cpp -lpthread -std=c++11
执行:./a.out 12345 (其中12345为监听端口port)
因为 Client 部分还未实现,先用 netcat 测试一下:nc localhost 12345
建立与echoserver的连接,连接port=12345。
然后,client发送数据,server做echo处理:
server端输出结果为:
客户端连接!
接收到client发送的数据为: 11111
length=6
echo server发送数据完毕!
接收到client发送的数据为: 22222
length=6
echo server发送数据完毕!
接收到client发送的数据为: length=0
连接被 client 妥善的关闭了
客户端连接!
接收到client发送的数据为: zzzzzz
length=7
echo server发送数据完毕!
分析:一开始,server调用acceptor.accept()函数阻塞等待客户端连接,当有client连接后调用执行Session()函数,此时server会阻塞在socket.read_some()处,等待client发送数据,也就是说此时只能同时处理一个client连接,当此时若有另一个client发起连接,则会被记录在acceptor.accept()的等待队列中,当上一个client断开连接后会退出Session()函数,则会根据缓存队列中未处理的客户端重新调用Session()函数。总的来看:server通过acceptor.accept()接收client的请求并放在缓存队列中,然后循环逐个处理,全部处理完再继续阻塞。
以下几点需要注意:
- tcp::acceptor 也是一种 I/O 对象,用来接收 TCP 连接,连接端口由 tcp::endpoint 指定。
- 数据 buffer 以 boost::array
表示,也可以用 char data[BUF_SIZE],或 std::vector data(BUF_SIZE)。事实上,用 std::vector 是最推荐的,因为它不但可以动态调整大小,还支持 Buffer Debugging。 - 同步方式下,没有调用 io_context.run,因为 accept、read_some 和 write 都是阻塞的。这也意味着一次只能处理一个 Client 连接,但是可以连续 echo,除非 Client 断开连接。
- 写回数据时,没有直接调用 socket.write_some,因为它不能保证一次写完所有数据,但是 boost::asio::write 可以。我觉得这是 Asio 接口设计不周,应该提供 socket.write。
- acceptor.accept 返回一个新的 socket 对象,利用 move 语义,直接就转移给了 Session 的参数,期间并没有拷贝开销。
虽然用 netcat 测试 Echo Server 非常方便,但是自己动手写一个 Echo Client 仍然十分必要。 还是先考虑同步方式。
首先通过 host 和 port 解析出 endpoints(对,是复数!):
tcp::resolver resolver(ioc); auto endpoints = resolver.resolve(tcp::v4(), host, port);
resolve 返回的 endpoints 类型为 tcp::resolver::results_type,代之以 auto 可以简化代码。类型推导应适当使用,至于连 int 都用 auto 就没有必要了。 host 和 port 通过命令行参数指定,比如 localhost 和 8080。
接着创建 socket,建立连接:
tcp::socket socket(ioc); //创建一个io对象为socket,初始化参数为io_context boost::asio::connect(socket, endpoints); //连接到endpoints(server)
这里没有直接调用 socket.connect,因为 endpoints 可能会有多个,boost::asio::connect 会挨个尝试,逐一调用 socket.connect 直到连接成功。
其实这样说不太严谨,根据我的测试,resolve 在没有指定 protocol 时,确实会返回多个 endpoints,一个是 IPv6,一个是 IPv4。但是我们已经指定了 protocol 为 tcp::v4():resolver.resolve(tcp::v4(), host, port)。所以,应该只有一个 endpoint。
接下来,从标准输入(std::cin)读一行数据,然后通过 boost::asio::write 发送给 echo server:
char request[BUF_SIZE];
std::size_t request_length = 0;
do {
std::cout << "Enter message: ";
std::cin.getline(request, BUF_SIZE);
request_length = std::strlen(request);
} while (request_length == 0);
//调用boost::asio::write函数将request buffer的内容发送给echo server
boost::asio::write(socket, boost::asio::buffer(request, request_length));
do...while 是为了防止用户直接 Enter 导致输入为空(直接enter那么request_length=0,继续do循环)。boost::asio::write 是阻塞调用,发送完才返回。
从 Server 同步接收数据有两种方式:
- 使用 boost::asio::read(对应于 boost::asio::write);知道确定的读取数据长度。
- 使用 socket.read_some。读到数据就返回,需要循环读取并知道读取长度。
两者的差别是,boost::asio::read 读到指定长度时,就会返回,你需要知道你想读多少;而 socket.read_some 一旦读到一些数据就会返回,所以必须放在循环里,然后手动判断是否已经读到想要的长度,否则无法退出循环。
下面分别是两种实现的代码:
使用 boost::asio::read:
char reply[BUF_SIZE];
std::size_t reply_length = boost::asio::read(
socket,
boost::asio::buffer(reply, request_length));
std::cout.write(reply, reply_length);
使用 socket.read_some:
std::size_t total_reply_length = 0;
while (true) {
std::array reply;
std::size_t reply_length = socket.read_some(boost::asio::buffer(reply));
std::cout.write(reply.data(), reply_length);
total_reply_length += reply_length;
if (total_reply_length >= request_length) {
break;
}
}
不难看出,socket.read_some 用起来更为复杂。 Echo 程序的特殊之处就是,你可以假定 Server 会原封不动的把请求发回来,所以你知道 Client 要读多少。 但是很多时候,我们不知道要读多少数据。 所以,socket.read_some 反倒更为实用。
此外,在这个例子中,我们没有为各函数指定输出参数 boost::system::error_code,而是使用了异常,把整个代码块放在 try…catch 中。
try {
// ...
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
}
Asio 的 API 基本都通过重载(overload),提供了 error_code 和 exception 两种错误处理方式。使用异常更易于错误处理,也可以简化代码,但是 try...catch 该包含多少代码,并不是那么明显,新手很容易误用,什么都往 try...catch 里放。
一般来说,异步方式下,使用 error_code 更方便一些。所以 complete handler 的参数都有 error_code。
整体代码设计如下:
echoclient.cpp:
#include#include #include using namespace std; using tcp = boost::asio::ip::tcp; #define BUF_SIZE 1024 int main(int argc, char* argv[]) { if (argc != 3) { std::cerr << "Usage: " << argv[0] << " "<< " " << std::endl; return 1; } boost::asio::io_context ioc; //定义io_context tcp::resolver resolver(ioc); auto endpoints = resolver.resolve(tcp::v4(), argv[1], argv[2]); tcp::socket socket(ioc); //创建一个io对象为socket,初始化参数为io_context try{ boost::asio::connect(socket, endpoints); //连接到endpoints(server) cout<<"connect success!"< std::cout << "Enter message: "; std::cin.getline(request, BUF_SIZE); request_length = std::strlen(request); } while (request_length == 0); //调用boost::asio::write函数将request buffer的内容发送给echo server boost::asio::write(socket, boost::asio::buffer(request, request_length)); char reply[BUF_SIZE]; //调用boost::asio::read函数从echo server读取特定长度(request_length)的数据 std::size_t reply_length = boost::asio::read(socket,boost::asio::buffer(reply, request_length)); std::cout.write(reply, reply_length); //输出recv到的数据 } catch (const std::exception& e) { std::cerr << "Exception: " << e.what() << std::endl; } return 0; }
编译:g++ echoclient.cpp -o client -lpthread -std=c++11
client输出:
connect success!
Enter message: 1234235234gdsagfsd666
1234235234gdsagfsd66
server输出:
三.异步请求 3.1 异步请求步骤客户端连接!
接收到client发送的数据为: 1234235234gdsagfsd666length=21
echo server发送数据完毕!
接收到client发送的数据为: length=0
连接被 client 妥善的关闭了
(1)程序通过I/O对象启动连接操作:socket.async_connect(server_endpoint, your_completion_handler);其中your_completion_handler是一个函数(对象),原型:void your_completion_handler(const boost::system::error_code& ec);
(2)I/O对象将请求转发给io_context;
(3)io_context发信号给操作系统,告知它去开始一个异步的连接操作;一段时间过去… …注意,在同步的情形下,程序会一直等待连接操作完成,而异步则是先立即返回。
(4)连接操作完成时,操作系统把结果放在队列中;
(5)程序必须调用io_context::run()(或类似函数)以取得操作结果。一般在你刚启动第一个异步操作时就要调用run();
io_context对象未停止(stopped()返回false)且还有未完成的操作时,run()会一直阻塞,否则直接返回。
我的理解(io_context对象未停止时):如果当前有未完成的异步操作且队列为空,则需要等待,因此run()将阻塞(在Linux下借助pstack可知是阻塞于epoll_wait()或pthread_cond_wait()等)。操作系统完成某个异步操作后,把结果放到队列并通知应用程序。run()被“唤醒”,从队列中取出结果并调用相应的回调函数;如果当前没有未完成的异步操作且队列为空,表示所有异步操作已经完成,则run()将直接返回;当然,如果当前队列非空,则run()直接取出结果并调用回调函数。asio保证了回调函数只会被run()所在线程调用。因此,若没有run(),回调函数永远不会被调用。
(6)在run()中io_context将操作结果取出队列并翻译成error_code,然后传递给your_completion_handler。
异步方式下,困难在于对象的生命周期,可以用 shared_ptr 解决。
为了同时处理多个 Client 连接,需要保留每个连接的 socket 对象,于是抽象出一个表示连接会话的类,叫 Session:
class Session : public std::enable_shared_from_this{ public: Session(tcp::socket socket) : socket_(std::move(socket)) { } void Start() { DoRead(); } void DoRead() { auto self(shared_from_this()); //增加引用计数 socket_.async_read_some( boost::asio::buffer(buffer_), [this, self](boost::system::error_code ec, std::size_t length) { if (!ec) { DoWrite(length); } }); } void DoWrite(std::size_t length) { auto self(shared_from_this()); //增加引用计数 boost::asio::async_write( socket_, boost::asio::buffer(buffer_, length), [this, self](boost::system::error_code ec, std::size_t length) { if (!ec) { DoRead(); } }); } private: tcp::socket socket_; std::array buffer_; };
就代码风格来说,有以下几点需要注意:
- 优先使用 STL,比如 std::enable_shared_from_this,std::bind,std::array,等等。
- 定义 handler 时,尽量使用匿名函数(lambda 表达式)。
- 以 C++ std::size_t 替 C size_t。 刚开始,你可能会不习惯,我也是这样,过了好久才慢慢拥抱 C++11 乃至 C++14。
Session 有两个成员变量,socket_ 与 Client 通信,buffer_ 是接收 Client 数据的缓存。只要 Session 对象在,socket 就在,连接就不断。Socket 对象是构造时传进来的,而且是通过 move 语义转移进来的:Session(tcp::socket socket)。
虽然还没看到 Session 对象是如何创建的,但可以肯定的是,它必须用 std::shared_ptr 进行封装,这样才能保证异步模式下对象的生命周期。
此外,在 Session::DoRead 和 Session::DoWrite 中,因为读写都是异步的,同样为了防止当前 Session 不被销毁(因为超出作用域),所以要增加它的引用计数,即 auto self(shared_from_this()); 这一句的作用。
至于读写的逻辑,基本上就是把 read_some 换成 async_read_some,把 write 换成 async_write,然后以匿名函数作为 completion handler。
接收 Client 连接的代码,提取出来,抽象成一个类 Server:
class Server {
public:
Server(boost::asio::io_context& ioc, std::uint16_t port)
: acceptor_(ioc, tcp::endpoint(tcp::v4(), port)) {
DoAccept();
}
private:
void DoAccept() {
acceptor_.async_accept(
[this](boost::system::error_code ec, tcp::socket socket) {
if (!ec) {
std::make_shared(std::move(socket))->Start();
}
DoAccept();
});
}
private:
tcp::acceptor acceptor_;
};
同样,async_accept 替换了 accept。async_accept 不再阻塞,DoAccept 即刻就会返回。 为了保证 Session 对象继续存在,使用 std::shared_ptr 代替普通的栈对象,同时把新接收的 socket 对象转移过去。
最后是 main():
int main(int argc, char* argv[]) {
if (argc != 2) {
std::cerr << "Usage: " << argv[0] << " " << std::endl;
return 1;
}
std::uint16_t port = std::atoi(argv[1]);
boost::asio::io_context ioc;
Server server(ioc, port);
ioc.run();
return 0;
}
完整程序,echoserver.cpp:
#include#include #include #include using namespace std; using tcp = boost::asio::ip::tcp; // #define BUF_SIZE 1024 enum { BUF_SIZE = 1024 }; //定义一个session,用来处理一个client发起的连接 class Session : public std::enable_shared_from_this { public: //Session构造函数,初始化变量为tcp::socket对象 Session(tcp::socket socket) : socket_(std::move(socket)) { cout< cout< DoRead(); } //异步读数据函数:从client中读取数据并放在Session对象的buffer中 void DoRead() { auto self(shared_from_this()); //增加引用计数 //异步读数据,从client读取数据 socket_.async_read_some( boost::asio::buffer(buffer_), //将数据读到buffer中 //采用函数对象(lambda表达式)作为handler函数 [this, self](boost::system::error_code ec, std::size_t length) { if (!ec) { cout<<"recv from: "< auto self(shared_from_this()); //增加引用计数 //异步写数据,发送数据给client boost::asio::async_write( socket_, //1.socket boost::asio::buffer(buffer_, length), //2.write buffer //3.handler:lambad表达式 [this, self](boost::system::error_code ec, std::size_t length) { if (!ec) { DoRead(); } }); } private: tcp::socket socket_; //连接的client socket //std::array buffer_; //数据读写的缓存buffer char buffer_[BUF_SIZE]; //数据读写的缓存buffer }; //接收client数据,抽象成server处理 class Server { public: //Server类构造函数:io_context,port Server(boost::asio::io_context& ioc, std::uint16_t port):acceptor_(ioc, tcp::endpoint(tcp::v4(), port)) { DoAccept(); //异步accept函数处理 } private: //异步accept函数,不会阻塞 void DoAccept() { //开启异步accept,不会阻塞 acceptor_.async_accept( [this](boost::system::error_code ec, tcp::socket socket) { if (!ec) { //创建一个指向Session对象的指针,并使用move(socket)初始化该Session对象,并调用Start函数,执行Doread操作 std::make_shared (std::move(socket))->Start(); } //继续accept监听,此处很重要,如果没有那么只能接收一路连接,执行完之后就退出ioc.run,因此需要开启DoAccept继续监听其他请求 DoAccept(); }); } private: tcp::acceptor acceptor_; }; int main(int argc, char* argv[]) { if (argc != 2) { std::cerr << "Usage: " << argv[0] << " " << std::endl; return 1; } std::uint16_t port = std::atoi(argv[1]); //获取port值 try{ boost::asio::io_context ioc; //定义io_context Server server(ioc, port); //创建一个server对象 cout<<"ioc.run start!"< cout< 3.3 异步请求echo client(客户端可以不采用异步方式) 就 Client 来说,异步也许并非必要,除非想同时连接多个 Server。
异步读写前面已经涉及,我们就先看 async_resolve 和 async_connect。
首先,抽取出一个类 Client:
class Client { public: Client(boost::asio::io_context& ioc,const std::string& host, const std::string& port) : socket_(ioc), resolver_(ioc) {} private: tcp::socket socket_; tcp::resolver resolver_; char cin_buf_[BUF_SIZE]; std::arraybuf_; }; resolver_ 是为了 async_resolve,作为成员变量,生命周期便得到了保证,不会因为函数结束而失效。
下面来看 async_resolve 实现(代码在构造函数中):
Client(...) { resolver_.async_resolve(tcp::v4(), host, port, std::bind(&Client::OnResolve, this, std::placeholders::_1, std::placeholders::_2)); }async_resolve 的 handler:
void OnResolve(boost::system::error_code ec, tcp::resolver::results_type endpoints) { if (ec) { std::cerr << "Resolve: " << ec.message() << std::endl; } else { boost::asio::async_connect(socket_, endpoints, std::bind(&Client::OnConnect, this, std::placeholders::_1, std::placeholders::_2)); } }async_connect 的 handler:
void OnConnect(boost::system::error_code ec, tcp::endpoint endpoint) { if (ec) { std::cout << "Connect failed: " << ec.message() << std::endl; socket_.close(); } else { DoWrite(); } }连接成功后,调用 DoWrite,从标准输入读取一行数据,然后异步发送给 Server。 下面是异步读写相关的函数,一并给出:
void DoWrite() { std::size_t len = 0; do { std::cout << "Enter message: "; std::cin.getline(cin_buf_, BUF_SIZE); len = strlen(cin_buf_); } while (len == 0); boost::asio::async_write(socket_, boost::asio::buffer(cin_buf_, len), std::bind(&Client::OnWrite, this, std::placeholders::_1)); } void OnWrite(boost::system::error_code ec) { if (!ec) { std::cout << "Reply is: "; socket_.async_read_some(boost::asio::buffer(buf_), std::bind(&Client::OnRead, this, std::placeholders::_1, std::placeholders::_2)); } } void OnRead(boost::system::error_code ec, std::size_t length) { if (!ec) { std::cout.write(buf_.data(), length); std::cout << std::endl; // 如果想继续下一轮,可以在这里调用 DoWrite()。 } }异步读写在异步 Server 那一节已经介绍过,这里就不再赘述了。
最后是 main():
int main(int argc, char* argv[]) { if (argc != 3) { std::cerr << "Usage: " << argv[0] << "" << std::endl; return 1; } const char* host = argv[1]; const char* port = argv[2]; boost::asio::io_context ioc; Client client(ioc, host, port); ioc.run(); return 0; } 至此,异步方式的 async Client 就算实现了,下面是client端完整程序:
echoclient.cpp:
#include四. Some tips 4.1 过时接口剔除#include #include #include using namespace std; using tcp = boost::asio::ip::tcp; #define BUF_SIZE 1024 class ClientAsync { typedef ClientAsync this_type; typedef boost::asio::ip::tcp::endpoint endpoint_type; typedef boost::asio::ip::address address_type; typedef boost::asio::ip::tcp::socket socket_type; typedef boost::asio::io_context io_service_type; typedef boost::shared_ptr sock_ptr; typedef std::vector buffer_type; private: io_service_type m_io; buffer_type m_buf; endpoint_type m_ept; socket_type sock; public: ClientAsync():m_buf(100, 0),m_ept(address_type::from_string("127.0.0.1"), 12345),sock(m_io) { cout<<"ClientAsync构造函数调用!"< m_io.run( ); } void Start() { cout<<"创建socket指针成功!"< if(ec)return;}); std::cout << "connect server: " << sock.remote_endpoint().address()<<":"< ({ if (ec) return; }); } ); cout<<"消息已发送,准备读取来自server的消息:"< if (ec) return; std::cout << &m_buf[0] << std::endl; } }; //main主函数 int main(int argc, char** argv) { try { std::cout << "client start..." << std::endl; ClientAsync client; client.Run( ); } catch(std::exception except){ std::cout << except.what() << std::endl; } getchar(); } 在包含 Asio 头文件之前,定义宏 BOOST_ASIO_NO_DEPRECATED,这样在编译时,Asio 就会剔除那些已经过时的接口。
比如在最新的 Boost 1.66 中,io_service 已经改名为 io_context,如果没有 BOOST_ASIO_NO_DEPRECATED,还是可以用 io_service 的,虽然那只是 io_context 的一个 typedef。
BOOST_ASIO_NO_DEPRECATED 可以保证你用的是最新修订的 API。长期来看,有便于代码的维护。何况,这些修订正是 Asio 进入标准库的前奏。
#define BOOST_ASIO_NO_DEPRECATED #include "boost/asio/io_context.hpp" #include "boost/asio/steady_timer.hpp" ...4.2 尽量少包含头文件尽量不要直接包含大而全的 boost/asio.hpp。 这样做,是为了帮助自己记忆哪个类源于哪个具体的头文件,以及避免包含那些不必要的头文件。
在实际项目中,在你自己的某个「头文件」里简单粗暴的包含 boost/asio.hpp 是很不妥的;当然,在你的「源文件」里包含 boost/asio.hpp 是可以接受的,毕竟实际项目依赖的东西比较多,很难搞清楚每一个定义源自哪里。
4.3 Handler 签名问题虽然关于 Handler 的签名,文档里都有说明,但是直接定位到源码,更方便,也更精确。
以 steady_timer.async_wait() 为例,在 IDE 里定位到 async_wait() 的定义,代码(片段)如下:
templateBOOST_ASIO_INITFN_RESULT_TYPE(WaitHandler, void (boost::system::error_code)) async_wait(BOOST_ASIO_MOVE_ARG(WaitHandler) handler) { 通过宏 BOOST_ASIO_INITFN_RESULT_TYPE,WaitHandler 的签名一目了然。
4.4 Handler 的 error_code 参数到底是不是引用?其实,早期的版本应该是 const boost::system::error_code&,现在文档和代码注释里还有这么写的,估计是没来得及更新。 前面在说 Handler 签名时,已经看到 BOOST_ASIO_INITFN_RESULT_TYPE 这个宏的提示作用,翻一翻 Asio 源码,error_code 其实都已经传值了。
奇怪的是,即使你的 Handler 传 error_code 为引用,编译运行也都没有问题。
void Print(const boost::system::error_code& ec) { std::cout << "Hello, world!" << std::endl; } int main() { boost::asio::io_context ioc; boost::asio::steady_timer timer(ioc, std::chrono::seconds(3)); timer.async_wait(&Print); ioc.run(); return 0; }4.5 Bind 占位符调用 bind 时,使用了占位符(placeholder),其实下面四种写法都可以:
boost::bind(Print, boost::asio::placeholders::error, &timer, &count) boost::bind(Print, boost::placeholders::_1, &timer, &count); boost::bind(Print, _1, &timer, &count); std::bind(Print, std::placeholders::_1, &timer, &count);第四种,STL Bind,类似于 Boost Bind,只是没有声明 using namespace std::placeholders;。
四种写法,推荐使用二或四。至于是用 Boost Bind 还是 STL Bind,没那么重要。 此外,数字占位符共有 9 个,_1 - _9。
4.6 Server 也可以用 ResolverTCP Server 的 acceptor 一般是这样构造的:
tcp::acceptor(io_context, tcp::endpoint(tcp::v4(), port))也就是说,指定 protocol (tcp::v4()) 和 port 就行了。
但是,Asio 的 http 这个例子,确实用了 resolver,根据 IP 地址 resolve 出 endpoint:
tcp::resolver resolver(io_context_); tcp::resolver::results_type endpoints = resolver.resolve(address, port); tcp::endpoint endpoint = *endpoints.begin(); acceptor_.open(endpoint.protocol()); acceptor_.set_option(tcp::acceptor::reuse_address(true)); acceptor_.bind(endpoint); acceptor_.listen(); acceptor_.async_accept(...);http 这个例子之所以这么写,主要是初始化 acceptor_ 时,还拿不到 endpoint,否则可以直接用下面这个构造函数:
basic_socket_acceptor(boost::asio::io_context& io_context, const endpoint_type& endpoint, bool reuse_addr = true)这个构造函数注释说它等价于下面这段代码:
basic_socket_acceptoracceptor(io_context); acceptor.open(endpoint.protocol()); if (reuse_addr) acceptor.set_option(socket_base::reuse_address(true)); acceptor.bind(endpoint); acceptor.listen(listen_backlog); 下面是不同的 address 对应的 endpoints 结果(假定 port 都是 8080):
4.7 Move Acceptable Handler
- “localhost”: [::1]:8080, v6; [127.0.0.1]:8080, v4
- “0.0.0.0”: 0.0.0.0:8080, v4
- “0::0”: [::]:8080, v6
- 本机实际 IP 地址 (e.g., IPv4 “10.123.164.142”): 10.123.164.142:8080, v4。这时候,本机 client 无法通过 “localhost” 连接到这个 server,通过具体的 IP 地址则可以。
- 一个具体的非本机地址 (e.g., IPv4 “10.123.164.145”): exception: bind: The requested address is not valid in its context
使用 acceptor.async_accept 时,发现了 Move Acceptable Handler。
简单来说,async_accept 接受两种 AcceptHandler,直接看源码:
templateBOOST_ASIO_INITFN_RESULT_TYPE(MoveAcceptHandler, void (boost::system::error_code, typename Protocol::socket)) async_accept(BOOST_ASIO_MOVE_ARG(MoveAcceptHandler) handler) templateBOOST_ASIO_INITFN_RESULT_TYPE(AcceptHandler, void (boost::system::error_code)) async_accept(basic_socket & peer, BOOST_ASIO_MOVE_ARG(AcceptHandler) handler, typename enable_if ::value>::type* = 0) 第一种是 MoveAcceptableHandler,它的第二个参数是新 accept 的 socket。 第二种是普通的 Handler,它的第一个参数是预先构造的 socket。
对于 MoveAcceptableHandler,用 bind 行不通。比如给定:
void Server::HandleAccept(boost::system::error_code ec, boost::asio::ip::tcp::socket socket) { }std::bind 可以编译,boost::bind 则不行。
// std::bind 可以,boost::bind 不可以。 acceptor_.async_accept(std::bind(&Server::HandleAccept, this, std::placeholders::_1, std::placeholders::_2));结论是,对于 Move Acceptable Handler,不要用 bind,直接用 lambda 表达式:
void DoAccept() { acceptor_.async_accept( [this](boost::system::error_code ec, boost::asio::ip::tcp::socket socket) { // Check whether the server was stopped by a signal before this // completion handler had a chance to run. if (!acceptor_.is_open()) { return; } if (!ec) { connection_manager_.Start( std::make_shared(std::move(socket), connection_manager_, request_handler_)); } DoAccept(); }); } 参考:
https://github.com/sprinfall/boost-asio-study/blob/master/Tutorial_zh-CN.md
https://github.com/sprinfall/boost-asio study/blob/master/Asio_Tips_And_Notes_zh-CN.md



