1 Asio介绍
在进行网络编程之前,我们先对Asio进行简单的介绍。首先Asio这个名字就说明了它的核心作用——异步输入/输出(Asynchronous input/output).也是就是说这个库设计的目的是让C++异步地处理数据且独立于平台,并不只是针对网络编程而已,只是它主要被应用于网络编程。除了网络编程以外他还包含了其他的IO功能。
异步数据处理就是指任务触发后不需要等待它们完成。相反,Boost.Asio会在任务完成时触发一个应用。异步任务的主要优点在于,在等待任务完成时不需要阻塞应用程序,可以去执行其它任务。异步任务的典型例子是网络应用。即在数据发送完毕后我们需要知道数据是否发送成功,异步通信可以让我们不必等待发送数据返回的结果。Boost.Asio将这个过程分成两个单独的步骤:第一步是作为一个异步任务开始传输数据。一旦数据传输完成,无论成功与否,应用程序会在第二步中国得到相关的结果通知。
2 I/O服务与I/O对象
使用Boost.Asio 进行异步数据处理的应用程序基于两个概念:I/O服务和IO/对象。I/O服务抽象了操作系统的接口,运行第一时间进行异步数据处理,而I/O对象则用于初始化特定的操作。鉴于Boost.Asio值提供了一个名为 boost::asio::io_service
的类作为I/O服务,它针对所支持的每一个操作系统分别实现了优化的类,另外库中还包含了针对不同I/O对象的几个类。其中,类 boost::asio::ip::tcp::socket
用于通过网络发送和接收数据,而类 boost::asio::deadline_timer
则提供了一个计时器,用于测量某个固定时间点到来或是一段指定的时长过去了。下面给出了一个计时器的例子,因为相较于其他I/O对象而言,它不需要任何有关网络编程的知识。
#include <boost/asio.hpp>
#include <iostream>
void Handler(const boost::system::error_code &ec)
{
std::cout<<"3 s."<<std::endl;
}
int main()
{
boost::asio::io_service ioService;
boost::asio::deadline_timer timer(ioService,boost::posix_time::seconds(3));
timer.async_wait(Handler);
std::cout<<"waiting..."<<std::endl;
ioService.run();
system("pause");
return 0;
}
在main()中首先定义了一个I/O服务 io_service
,用于初始化I/O对象timer.就像本例中 deadline_timer
一样,所有I/O对象都需要一个I/O服务作为他们的构造函数的第一个参数。timer的第二个参数是时间用于指定定时的时长。这里通过调用 async_wait
来实现异步通信,其参数为回调函数的句柄值。这里的程序并不会阻塞在这里等待定时结束,而是立刻往下执行(这里打印waiting).如果我们使用的是 wait
函数则会阻塞在这里。最后程序又调用了I/O服务的 run()
方法,这是必须的,因为控制权必须被操作系统接管,才能在五秒之后调用 hander()
函数。 注意run()方法是阻塞的 ,在程序运行到这里时会"停止执行"。这一点挺有讽刺意味的,即在许多操作系统中都是通过阻塞来支持异步操作。在本例中如果run方法不是阻塞的,那可能在异步通信回调函数调用之前程序就运行完毕结束了。在调用完run后程序会继续向下运行,为了防止程序结束这里调用了一个暂停函数,方便观察结果。
这里我们可能会提出一些问题,比如如果异步程序是通过run方法的阻塞来完成的,那么多个异步程序怎么执行?如果不希望线程阻塞应该怎么做?为了深刻的了解run的工作原理,我们首先看一下下面这个例子:
#include <boost/asio.hpp>
#include <iostream>
void Handler1(const boost::system::error_code &ec)
{
std::cout<<"2 s."<<std::endl;
}
void Handler2(const boost::system::error_code &ec)
{
std::cout<<"4 s."<<std::endl;
}
int main()
{
boost::asio::io_service ioService;
boost::asio::deadline_timer timer1(ioService,boost::posix_time::seconds(2));
timer1.async_wait(Handler1);
boost::asio::deadline_timer timer2(ioService,boost::posix_time::seconds(4));
timer2.async_wait(Handler2);
std::cout<<"waiting..."<<std::endl;
ioService.run();
system("pause");
return 0;
}
这这里例子中可以看到多个异步通信可以基于同一个I/O服务。当然我们也可以为每一个异步通信都建立一个I/O服务,不过之前说过ioService.run是会阻塞程序的,我们必须要让回调函数先执行的I/O服务的run方法先执行(这里等待3秒异步通信)。即:
boost::asio::io_service ioService1;
boost::asio::io_service ioService2;
boost::asio::deadline_timer timer1(ioService1,boost::posix_time::seconds(2));
timer1.async_wait(Handler1);
boost::asio::deadline_timer timer2(ioService2,boost::posix_time::seconds(4));
timer2.async_wait(Handler2);
std::cout<<"waiting..."<<std::endl;
ioService1.run();
ioService2.run();
绝大多数情况下这个方法是很危险的,更一般的情况是让每一个I/O服务运行于一个单独的线程。这样它就只会阻塞它所在的线程对其他线程没有任何影响。这也解决了我们上面提出的第二个问题——防止run方法的阻塞。
一旦特定的I/O服务中所有异步操作都完成了,控制权就会返回 run()
方法,然后它就会返回。上面的例子中当两个闹铃都响过之后,程序就会向下运行。
3 多线程与异步操作
现如今的PC即通常都具有多核处理器,所以使用多线程的应用可以进一步提高应用程序的执行效率。但是创建了比处理器个数更多的线程并不会在性能上有所提升,boost库中提供了获取逻辑处理器个数的函数 boost::thread::hardware_concurrency()
,逻辑处理器个数和内核数不一样,一个内核可以通过超线程技术超线程为双核,也称伪双核,和真双核的区别是它们是共享缓存。比如我的电脑就是使用了超线程技术变为双核四线程的。
前面我们提到过使用多线程的方式能够防止run方法的阻塞,进而可以使多个异步操作同时运行。下面就来具体讨论多线程和异步操作的联合使用。
3.1 基于一个I/O服务的多异步任务处理
关于多线程和异步操作的联合使用主要有两种方式,首先我们先介绍一个I/O服务和多个异步任务的处理方式。 io_service::run()
方法是线程安全的,因此我们可用在多个线程中调用同一个run方法,下面是一个使用示例:
#include <boost/asio.hpp>
#include <boost/thread.hpp>
#include <iostream>
void Handler1(const boost::system::error_code &ec)
{
std::cout<<"2 s."<<std::endl;
}
void Handler2(const boost::system::error_code &ec)
{
std::cout<<"2 s."<<std::endl;
}
boost::asio::io_service IoService;
void Run()
{
IoService.run();
}
int main()
{
boost::asio::deadline_timer timer1(IoService,boost::posix_time::seconds(2));
timer1.async_wait(Handler1);
boost::asio::deadline_timer timer2(IoService,boost::posix_time::seconds(2));
timer2.async_wait(Handler2);
std::cout<<"waiting..."<<std::endl;
boost::thread thread1(Run);
boost::thread thread2(Run);
thread1.join();
thread2.join();
system("pause");
return 0;
}
在上面这个示例中,我们分别在两个线程中调用了IoService的run方法。这样就可以使两个异步操作并行的执行,如果我们在执行的第二个异步操作的回调函数时,第一个回调函数正在运行,则第二个回调函数会自动在第二个线程中运行。如果在执行第二个回调函数时,第一个已经运行完毕了,那么第二个回调函数就会在第一个线程中运行。当然我们也可以在两个线程中运行4个异步操作,如果我们确定没有两个以上异步操作会同时运行,那么就不会产生等待(如两个线程在2s后运行,两个在4秒后运行)。如果有两个以上异步回调函数同时运行,就会有一个异步操作必须等待(如三个在2s后运行,一个在4s后运行)。
上面的程序有一个bug,即这两个异步操作可能在两个线程中同时访问临界资源 std::cout
的buffer缓冲区,这样有可能会使终端打印的信息混合。解决的办法是使用互斥锁。这部分内容属于多线程编程的知识点,这里不做过多介绍,可以参考 boost多线程的使用 。
对于上面的代码由于临界资源的关系并不能让两个线程并行运行,所有这时使用多线程并不能提供多少的好处。关于异步这里在多说一点,异步操作不同于一般的C++风格。异步操作使得原本在逻辑上是顺序执行的关系在物理上分割开来了,令代码的阅读更加困难。这就使得我们在进行异步通信的编程时要更加的注意代码的组织形式。
3.2 多个I/O服务对应多个异步任务
另外一种I/O服务和异步任务的对应关系是——多个I/O服务对应多个异步任务。这个概念理解起来比较简单,就是将每一个异步任务都绑定到唯一的一个I/O服务上,如下例所示:
#include <boost/asio.hpp>
#include <boost/thread.hpp>
#include <iostream>
void Handler1(const boost::system::error_code &ec)
{
std::cout<<"2 s."<<std::endl;
}
void Handler2(const boost::system::error_code &ec)
{
std::cout<<"2 s."<<std::endl;
}
const int ThreadNum = 2;
boost::asio::io_service IoService[ThreadNum];
void Run(int i)
{
IoService[i].run();
}
int main()
{
boost::asio::deadline_timer timer1(IoService[0],boost::posix_time::seconds(2));
timer1.async_wait(Handler1);
boost::asio::deadline_timer timer2(IoService[1],boost::posix_time::seconds(2));
timer2.async_wait(Handler2);
std::cout<<"waiting..."<<std::endl;
boost::thread thread1(boost::bind(Run,0));
boost::thread thread2(boost::bind(Run,1));
thread1.join();
thread2.join();
system("pause");
return 0;
}
这里使用的 bind
将Run绑定为一个无参返回类型为void的函数对象,因为thread的构造函数只支持void的无参函数或函数对象。关于bind的使用可以参考 bind的使用 .这个应用程序的功能与前一个相同。只是这里的每一个异步操作都运行在独立的线程之中相当于小的自主应用,它们拥有独立的告诉缓存、内存页。由于在确定优化策略之前需要对底层硬件、操作系统、编译器以及潜在的瓶颈有专门的了解,所以应该仅在清除这些好处的情况下使用多个I/O服务。
当然我们也可以将上面两种方式进行结合,即某些线程可能处理多个异步操作,而另外一些线程则只处理单个的异步操作。
4 网络编程介绍
前面我们提到过虽然Boost.Asio是一个可以异步处理任何种类数据的库,但是它主要被用于网络编程。事实上,Boost.Asio在加入其它对象之前就已经支持网络功能了。网络编程主要分为客户端开发和服务器端开发,简单的来说客户端就是主动连接的一端,而服务器端是等待被连接的一端。下面是作为客户端访问 www.highscore.de 的一个例子:
#include <boost/asio.hpp>
#include <boost/array.hpp>
#include <iostream>
#include <string>
boost::asio::io_service IoService;
boost::asio::ip::tcp::resolver Resolver(IoService);
boost::asio::ip::tcp::socket Socket(IoService);
boost::array<char,4096> Buffer;
void ReadHandler(const boost::system::error_code& ec,std::size_t transferredNum)
{
if(!ec)
{
std::cout<<std::string(Buffer.data(),transferredNum)<<std::endl;
Socket.async_read_some(boost::asio::buffer(Buffer),ReadHandler);
}
}
void ConnectHandler(const boost::system::error_code &ec)
{
if(!ec)
{
boost::asio::write(Socket,boost::asio::buffer("GET / HTTP 1.1\r\nHost: highscore.de\r\n\r\n"));
Socket.async_read_some(boost::asio::buffer(Buffer),ReadHandler);
}
else
std::cout<<"连接失败"<<std::endl;
}
void ResolverHandler(const boost::system::error_code& ec,boost::asio::ip::tcp::resolver::iterator it)
{
if(!ec)
{
Socket.async_connect(*it,ConnectHandler);
std::cout<<(*it).service_name()<<std::endl;
}
else
std::cout<<"解析失败"<<std::endl;
}
int main()
{
boost::asio::ip::tcp::resolver::query query("www.highscore.de","80");
Resolver.async_resolve(query,ResolverHandler);
IoService.run();
system("pause");
return 0;
}
这里分别使用了三个回调函数来处理解析、连接和数据接收的异步操作。由于这三个异步操作在时间上有先后顺序并不会同时产生,所以这里使用了单线程的模式。互联网使用了IP地址来标识每台PC。IP地址实际上只是一串数字,难以记住。为了方便对互联网上的主机进行记忆,我们通常会给主机的地址起一个比较容易记住的名称,如这里的 www.highscore.de .通过域名解析的过程可以将这个域名翻译成相应的IP地址,而这个翻译就是对应的这里的 boost::asio::ip::tcp::resolver
。域名解析过程并不是在本地完成的,而是由互联网上专门的DNS服务器完成。因此这个解析过程通常也被实现为一个异步操作。一旦解析完成,无论是否成功,这里的 ResolverHandler()
都会被调用。
在域名解析完毕之后,我们就需要和我们需要通讯的服务器建立连接。建立连接的过程这里也用异步通信进行实现。建立连接需要正确的服务器解析,解析后的地址由迭代器 it
保存。在连接建立完毕后,就可以开始通讯了。这里先发送一个请求,然后进行异步的数据读取。在数据读取的回调函数 ReadHandler
中,我们先打印服务器返回的数据,然后再次的进行异步的读取。这个操作是必须的,因为不能抱枕一次异步操作就能将整个页面传输完毕。当整个页面传输完毕后,ReadHandler
会给出一个错误进而防止进一步的页面输出。
在上面这个示例中,输入输出缓冲区使用了 boost::array
容器,位于 boost/array.hpp
中,它提供了类似C语言中数组的概念比vector的效益要高一些。
下面是一个服务器端的例子:
#include <boost/asio.hpp>
#include <string>
#include <iostream>
boost::asio::io_service IoService;
boost::asio::ip::tcp::endpoint Endpoint(boost::asio::ip::tcp::v4(),80);
boost::asio::ip::tcp::socket Socket(IoService);
boost::asio::ip::tcp::acceptor Acceptor(IoService,Endpoint);
std::string Data = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello,world!";
void WriteHandler(const boost::system::error_code &ec,std::size_t transferredNum)
{
}
void AcceptHandler(const boost::system::error_code &ec)
{
if(!ec)
{
boost::asio::async_write(Socket,boost::asio::buffer(Data),WriteHandler);
}
else
std::cout<<"连接失败"<<std::endl;
}
int main()
{
Acceptor.listen();
Acceptor.async_accept(Socket,AcceptHandler);
IoService.run();
system("pause");
return 0;
}
上面这个是服务器端的例子,它以本机作为服务器等待客户端进行连接(本机回环地址为127.0.0.1),当客户端和其建立连接后它会给客户端发送 Data
中的数据。我们可以将上上个客户端示例中的查询部分主机地址改为 boost::asio::ip::tcp::resolver::query query("127.0.0.1","80");
。然后我们先运行服务器端的应用程序然后再运行客户端的应用程序。在客户端就可以接收到主机发送到的数据了。
main()
首先调用了 listen()
方法将接收器置于接收状态,然后调用 async_accept()
方法等待一个客户端的连接。当连接建立后,就会调用 AcceptHandler()
方法。我们可以在这里调用 boost::asio::async_write()
将 Data
中的数据进行发送,在将缓冲区中的数据全部发送完毕后就会调用指定的 WriteHandler
函数。
下面执行过程:
![](https://i-blog.csdnimg.cn/blog_migrate/fd757435f09806a9516296ccfd7063b3.gif)
这里只是对网络编程的一个简单的介绍,后续我们会更详细的介绍网络编程的概念。
参考文章:
中文教程:
Boost Asio C++ 网络编程
官方教程:
Boost.Asio
The Boost C++ Libraries(中文版) 第7章