asio 定时器的基本使用
asio
的定时器可以提供同步或异步定时事件,我们通常用定时器来处理客户端连接超时的问题,比如服务器就设置一个超时时间,客户端连接成功之后,若没有发送消息给服务器的时间大于超时时间,则认为客户端连接已经断开了,可以关闭这个连接。
asio
定时器的用法比较简单,在 code4 目录下新建一个 code1.cpp 文件:
int g_count = 0;
void reset_timer(boost::asio::deadline_timer& timer){
timer.expires_from_now(boost::posix_time::seconds(1));// 重新设置超时时间为 1s
timer.async_wait([&timer](const boost::system::error_code& ec){ // async_wait() 不会阻塞程序。超时后会调用 lambda 表达式。
if(ec){
std::cout<<ec.message()<<std::endl;
return;
}
if(g_count==10){
return;
}
g_count++;
std::cout<<"timer event"<<std::endl;
reset_timer(timer);
});
}
int main()
{
try{
boost::asio::io_service io_service;
boost::asio::deadline_timer timer(io_service);// 初始化 timer,让 timer 作用于上条语句中定义的 io_service。
timer.expires_from_now(boost::posix_time::seconds(5)); // 超过 5s 后退出,从执行完这句语句开始计时。
std::cout << "Starting synchronous wait\n";
timer.wait(); // wait() 函数的作用为阻塞等待。程序会一直卡在本条语句,直到定时器超时。
std::cout << "Finished synchronous wait\n";
reset_timer(timer);// 调用上面定义的 reset_timer() 函数。
io_service.run();// run 函数的作用是阻塞等待 io_service 上所有的时间执行完毕后退出。
//在程序执行到这一句的时候,io_service 上只剩下 timer.async_wait() 没有执行完毕,所以此处 run() 的作用为阻塞等待超时。超时之后执行作为 async_wait() 参数的 lambda 表达式。
std::cout << "Finished asynchronous wait\n";
}
catch (std::exception& e){
std::cout << "Exception: " << e.what() << "\n";
}
return 0;
}
编译和运行代码:在 build 目录下执行
g++ ../code1.cpp -std=c++11 -o code1 -lboost_system && ./code1
输出结果:
Starting synchronous wait
Finished synchronous wait
timer event
timer event
timer event
timer event
timer event
timer event
timer event
timer event
timer event
timer event
Finished asynchronous wait
asio::deadline_timer
需要由一个 io_service
对象构造, io_service
可以认为是一个网络事件的反应器,任何网络事件都由它驱动和回调。
timer.expires_from_now(boost::posix_time::seconds(5))
设置定时器超时时间为 5 秒, timer.wait
则是同步方式等待定时器事件,等待 5 秒后就会打印 Finished synchronous wait
。
asio::deadline_timer
的异步事件则需要调用异步接口 timer.async_wait
。调用 timer.async_wait(func)
并不会导致程序阻塞,也不会立刻执行函数 func
,需要等到运行完 io_service.run();
语句之后, 回调函数 func
才会被执行。若调用 io_service.run();
和调用 timer.async_wait(func)
的时间差小于 timer.expires_from_now()
设置的时间,则io_service.run();
会阻塞等到超时才会调用 func
。
asio 服务端编程
服务器端通过监听特定端口等待客户端连接,asio
的 acceptor
对象提供了同步和异步监听的接口。
在 code4 目录下新建一个 code2.cpp 文件:
using boost::asio::ip::tcp;
class server{
public:
server(boost::asio::io_service& ios, short port) : ios_(ios),
acceptor_(ios, tcp::endpoint(tcp::v4(), port)) {
do_accept();
}
void do_accept()
{
std::cout<<"begin to listen and accept"<<std::endl;
auto socket = std::make_shared<tcp::socket>(ios_); // 使用智能指针保证安全回调。
acceptor_.async_accept(*socket, [this, socket](boost::system::error_code ec)
{
if (ec){ // 如果发送错误,则打印错误信息,并关闭套接字。
std::cout<<ec.message()<<std::endl;
socket->close();
}else{ // 没有错误,将套接字存入套接字队列中。
conns_.push_back(socket);
std::cout<<"new connection coming"<<std::endl;
}
do_accept(); // 无论是否发送错误,都要自己调用自己,继续监听端口。
});
}
private:
boost::asio::io_service& ios_;
tcp::acceptor acceptor_;
std::vector<std::shared_ptr<tcp::socket>> conns_;
};
int main() {
boost::asio::io_service ios;
server s(ios, 9000); //实例化一个服务器类,监听端口 9000
boost::asio::deadline_timer timer(ios);
timer.expires_from_now(boost::posix_time::seconds(5)); // 设置超时时间为 5s。
timer.async_wait([&timer,&ios](const boost::system::error_code& ec){ // 异步等待超时
if(ec){
std::cout<<ec.message()<<std::endl;
return;
}
ios.stop(); // 到 5s 之后手动停止 io_service。因为 server.do_accept() 一直调用自己,所以 io_service 上一直会有事件发生,不会自动停止。
std::cout<<"server stoped"<<std::endl;
});
ios.run();
return 0;
}
上面的代码中我们监听了 9000 端口,acceptor_.async_accept
接受 socket
和一个回调函数作为参数,这个回调函数就是异步事件,当有客户端连接过来的时候就会进入这个回调函数。
连接成功之后就把这个连接 socket
放到 vector
中,打印一条消息,然后继续监听和等待新的连接过来。
代码中还使用了 timer.expires_from_now(boost::posix_time::seconds(5));
定时,使得服务器在 5 秒之后自动退出。
接下来我们写一个客户端来连接这个服务器。
asio 客户端编程
客户端通过 connect
函数接入服务器。
在 code4 目录下新建一个 code3.cpp 文件:
using boost::asio::ip::tcp;
int main() {
try{
boost::asio::io_service io_service;
tcp::socket s(io_service);
tcp::resolver resolver(io_service); // 存储服务器的地址信息(IP,端口)。
boost::asio::connect(s, resolver.resolve({"127.0.0.1", "9000"})); // 连接服务器,执行 TCP 三次握手的过程。
std::this_thread::sleep_for(std::chrono::seconds(2)); // 本线程睡眠两秒
}
catch (std::exception& e)
{
std::cerr << "Exception: " << e.what() << "\n";
}
return 0;
}
调用 asio::connect
方法,传入 IP 和端口就可以连接服务器了,如果连接出错这个接口就会抛异常,因此我们需要通过 try-catch
来捕捉异常。
接下来我们测试一下服务器和客户端是否可以进行通信。
编译客户端和服务端
在 build 目录下执行
g++ ../code2.cpp -std=c++11 -o code2 -lboost_system -lpthread && g++ ../code3.cpp -std=c++11 -o code3 -lboost_system -lpthread
运行服务端和客户端代码
&
表示让 ./code2
在后台运行。
./code2 & ./code3
输出结果如下图所示。
错误处理
网络连接或者收发消息过程中经常会出现网络错误,如何处理这些网络错误呢? asio
提供了几种错误处理的方法,一种是在回调函数中传入错误码,如果错误码不为空则说明有网络错误,这时候我们可以根据需要记录错误码和错误原因等信息,然后关闭 socket
;另外一种方法就是抛异常,我们在调用接口时捕获异常,当有异常发生时我们就可以做错误处理了。
以下是服务端代码关于异常处理的部分:
void do_accept()
{
std::cout<<"begin to listen and accept"<<std::endl;
auto socket = std::make_shared<tcp::socket>(ios_); // 使用智能指针保证安全回调。
acceptor_.async_accept(*socket, [this, socket](boost::system::error_code ec)
{
if (ec){ // 如果发送错误,则打印错误信息,并关闭套接字。
std::cout<<ec.message()<<std::endl;
socket->close();
}else{ // 没有错误,将套接字存入套接字队列中。
conns_.push_back(socket);
std::cout<<"new connection coming"<<std::endl;
}
do_accept(); // 无论是否发送错误,都要自己调用自己,继续监听端口。
});
}
注意看这里的代码:
if (ec){
std::cout<<ec.message()<<std::endl;
socket->close();
}
这里对 error code 做了判断,如果不为空就说明存在网络错误。
以下是客户端代码关于异常处理的部分:
try{
boost::asio::io_service io_service;
tcp::socket s(io_service);
tcp::resolver resolver(io_service); // 存储服务器的地址信息(IP,端口)。
boost::asio::connect(s, resolver.resolve({"127.0.0.1", "9000"})); // 连接服务器,执行 TCP 三次握手的过程。
std::this_thread::sleep_for(std::chrono::seconds(2)); // 本线程睡眠两秒
}
catch (std::exception& e)
{
std::cerr << "Exception: " << e.what() << "\n";
}
如果服务器关闭,则这时候 connect
就会失败,然后就会抛异常,我们在外面就能记录错误原因了。