译者序:
一个英语从未及格的程序员,学习Boost.Asio而苦啃,留下只言片语,只为他日重品。
地道的中国式英语,看客可不屑。
美丽的分隔线
-------------------------------------------------
如果设置BOOST_ASIO_DISABLE_THREADS,它禁用Boost线程支持,
Asio无论是否有线程支持都可以编译。
首先,异步编程和同步编程有非常大的差异。在同步编程中你需要按顺序的操作,比如从一个socket中读取数据,然后向socket中写入数据。每个操作都是阻塞的。由于操作是阻塞的,当正在从一个socket读取或者向socket写入时,就不能中断主程序,通常会创建一个或多个线程处理socket的输输入、输出。因此同步的客户端/服务器程序通常是多线程的。
相比之下,异步编程是事件驱动的。当启动一个操作,但是你不知道什么时候结束,你提供一个回调函数在操作结束的时候由API调用,同时提供操作的结果。有经验的程序员试用Qt(一个Nokia的跨平台类库)创建GUI应用程序,这是第二种种类型。因此,在异步编程中,你不一定需要多线程。
你最好在项目一开始就决定在网络部分使用同步还是异步,中途切换是非常困难和容易出错的,不仅是API有所不同,你的程序语义将完全改变(异步网络编程要比同步网络编程难以测试和调试)。你要去思考两者多线程的阻塞调用(同步,通常是简单的)或者更少的线程和事件操作(异步,通常比较复杂)。
这是一个基础的同步客户端例子:
using boost::asio;
io_service service;
ip::tcp::endpoint ep(ip::address::from_string(”127.0.0.1”)),2001);
ip::tcp::socket socket(service);
sock.connect(ep);
首先,你的程序需要至少一个io_service实例。Boost.Asio使用io_service去告诉操作系统输入输出/服务。通常一个io_servcie实例就足够了。接下来创建一个希望连接的地址和端口。创建socket。使用socket连接到给定的地址和端口:
这儿有一个简单的使用boost::asio的同步服务器。
typedef boost::shared_ptr
io_service service;
ip::tcp::endpoint ep( ip::tcp::v4(), 2001)); // 在2001监听
ip::tcp::acceptor acc(service, ep);
while ( true) {
socket_ptr sock(new ip::tcp::socket(service));
acc.accept(*sock);
boost::thread( boost::bind(client_session, sock));
}
void client_session(socket_ptr sock) {
while ( true) {
char data[512];
size_t len = sock->read_some(buffer(data));
if ( len > 0)
write(*sock, buffer("ok", 2));
}
}
同样你的程序至少需要一个io_service实例。然后在指定的端口监听,创建接收器(acceptor),一个对象接受客户端的连接。
在下面的循环中,你创建一个虚拟的(dummy)socket等待客户端的连接。一旦建立了连接,你创建一个线程来处理该连接。
在client_session线程中,读取一个客户端请求,解析并回复。
创建一个基础的异步客户端,你可以按照如下方式:
using boost::asio;
io_service service;
ip::tcp::endpoint ep( ip::address::from_string("127.0.0.1"), 2001);
ip::tcp::socket sock(service);
sock.async_connect(ep, connect_handler);
service.run();
void connect_handler(const boost::system::error_code & ec) {
// here we know we connected successfully
// if ec indicates success
}
你的程序需要至少一个io_service实例。指定要连接的ip地址和端口并且创建一个socket。
然后异步等待连接到指定的ip和端口,完成后connect_handler被调用。
当connect_handler被调用时,首先检查错误代码(ec),如果是成功的,你就可以异步的向服务器写入数据。
注意,在等待异步操作期间,service.run()循环将一直运行。在前面的例子中,只有一个这样的操作,即async_connect,在这之后,service.run()退出。
每个异步操作都有一个完成处理程序,在完成后都会调用这个函数。
下面的代码是一个基础的异步服务器。
using boost::asio;
typedef boost::shared_ptr
io_service service;
ip::tcp::endpoint ep( ip::tcp::v4(), 2001)); // listen on 2001
ip::tcp::acceptor acc(service, ep);
socket_ptr sock(new ip::tcp::socket(service));
start_accept(sock);
service.run();
void start_accept(socket_ptr sock) {
acc.async_accept(*sock, boost::bind( handle_accept, sock, _1) );
}
void handle_accept(socket_ptr sock, const boost::system::error_code &
err) {
if ( err) return;
// at this point, you can read/write to the socket
socket_ptr sock(new ip::tcp::socket(service));
start_accept(sock);
}
在前面的代码片段中,首先创建一个io_service实例。然后指定需要监听哪个端口。然后,创建一个接收器(acceptor对象),一个对象接收客户端连接,创建一个虚拟(dummy)socket,异步等待客户端连接。
最后,执行异步循环service.run()。当客户端连接时,handle_accept被调用(完成处理程序被async_accept调用)。如果这里没有错误,你可以使用这个socket进行读写操作。
使用这个socket之后,创建一个新的socket并且再次调用start_accept,追加等待另一个”等待客户端连接“的异步操作,保持service.run()继续运行。
Boost.Asio允许同时使用异常和错误代码。所有的同步函数都有一个重载,在出现错误的情况下返回一个错误代码。如果函数抛出异常,则是boost:system::system_error error。
using boost::asio;
ip::tcp::endpoint ep;
ip::tcp::socket sock(service);
sock.connect(ep); // Line 1
boost::system::error_code err;
sock.connect(ep, err); // Line 2
在前面的代码中,socket.connect(ep)会抛出一个错误,socket.coonnect(ep,err)会返回一个错误代码。
看下面的代码片段:
try {
sock.connect(ep);
} catch(boost::system::system_error e) {
std::cout << e.code() << std::endl;
}
下面的代码类似于前面的:
boost::system::error_code err;
sock.connect(ep, err);
if ( err)
std::cout << err << std::endl;
如果使用异步函数,检查回调函数发现他们都会返回一个错误代码。异步函数不会抛出异常,因为这样做毫无意义。谁能抓住(catch)它?
在同步函数中,你可以使用异常或者错误代码(做你你想做的),这样坚持的做。混合起来使用会产生问题或者可能是崩溃(当你忘记处理引发的异常)。如果你的代码很复杂(socket读/写操作),你应该会更喜欢使用try{}catch块包含你的读/写。
void client_session(socket_ptr sock) {
try {
...
} catch ( boost::system::system_error e) {
// handle the error
}
}
如果使用错误代码,可以比较清晰的看到当前连接是关闭,如下面的代码片段所示:
char data[512];
boost::system::error_code error;
size_t length = sock.read_some(buffer(data), error);
if (error == error::eof)
return; // Connection closed
所有的错误代码都在命名空间boost::asio::error下(如果你想创建一个大的switch去检查错误原因)。在boost/asio/error.hpp头文件查看更多细节。
我们谈论如下内容:
io_service:io_service类是线程安全的。多个线程可以调用
io_servcie::run()。大部分情况下你会在单个线程内调用io_servcie::run()直到所有的异步操作都完成。然而,你可以从多个线程调用io_service::run().调用io_servcie::run()会阻塞所有的线程。所有的回调会在任意调用了io_servcie::run()的线程上下文中被调用。这也意味着,如果你在一个线程中调用io_service::run(),所有的回调在他的线程上下文中被调用。
socket:socket类是非线程安全的。因此,应该避免诸如在一个线程中从套接字读取并且在不同线程中向当前套接字写入(一般情况下这是不推荐的)。
utility:实用工具类通常也不应用于多线程,他们也不是线程安全的。他们大部分都在很短时间内被使用,然后离开自己的作用域。
Boost.Asio库自己使用多线程,他保证这些线程不会调用你的任何代码。反过来又意味着回调只是从调用了io_service::run()的线程中被调用。
Boost.Asio除了网络功能之外,还提供其它的输入、输出能力。
Boost.Asio允许等待信号,如:SIGTERM(软件终止),SIGINT(中断信号),SIGSEGV(段违规),等等。
创建一个signal_set实例,指定要异步等待的信号,当任意信号发生时,异步操作被调用:
void signal_handler(const boost::system::error_code & err, int signal)
{
// log this, and terminate application
}
boost::asio::signal_set sig(service, SIGINT, SIGTERM);
sig.async_wait(signal_handler);
如果产生了SIGINT信号,你可以在singal_hanler回调中捕获他。
使用Boost.Asio你可以容易的连接串口。在Windows上端口为COM7,在POSIX平台上为/dev/ttyS0:
io_service service;
serial_port sp(service, "COM7");
一旦打开了,你可以设置一些选项,如波特率,奇偶校验,停止位,如下代码片段:
serial_port::baud_rate rate(9600);
sp.set_option(rate);
一旦端口是打开的,你可以把串口当作一个流(stream),此外,可以使用任意的函数读取或者写入串口,如:read(),async_read(),write,async_write(),如下代码片段:
char data[512];
read(sp, buffer(data, 512));
Boost.Asio允许你连接到Windows文件,并且可以使用任意函数,如:read(),asyn_read()等等,如下面代码片段所示:
HANDLE h = ::OpenFile(...);
windows::stream_handle sh(service, h);
char data[512];
read(h, buffer(data, 512));
也可以在POSIX下做同样的事,如管道,标准I/O,各种设备(但不是普通文件),如下代码片段所示:
posix::stream_descriptor sd_in(service, ::dup(STDIN_FILENO));
char data[512];
read(sd_in, buffer(data, 512));
一些I/O操作需要在一定指定的时间内完成。这个只在异步操作下适用(同步采用阻塞手段,因此没有定时器)。例如:下一条消息从你的伙伴哪里获取需要10毫秒:
bool read = false;
void deadline_handler(const boost::system::error_code &) {
std::cout << (read ? "read successfully" : "read failed") <<
std::endl;
}
void read_handler(const boost::system::error_code &) {
read = true;
}
ip::tcp::socket sock(service);
…
read = false;
char data[512];
sock.async_read_some(buffer(data, 512));
deadline_timer t(service, boost::posix_time::milliseconds(100));
t.async_wait(&deadline_handler);
service.run();
在前面的代码片段中,如果在截至时间前读取到数据,read设置为true,我们的伙伴在指定时间内到达。否则,当deadline_handle被调用时,read还是false,那么就意味着不符合我们的最后期限。
Boost.Asio允许使用同步定时器,但他们通常是一个简单的睡眠操作。
boost::this_thread::sleep(500)和下面的代码片段实现相同的事情:
deadline_timer t(service, boost::posix_time::milliseconds(500));
t.wait();
你已经看到大部分代码都使用一些io_service实例。io_service是类库中最重要的一个类;它设计操作系统,等待所有的异步操作完成,每次操作完成都调用完成处理程序。
如果你选择创建同步应用程序,也不需要担心,这部分我会告诉你。
你可以采用多种方式使用io_service实例。在下面的例子中,我们有三个异步操作,两个socket连接和定时器。
io_service service_;
// all the socket operations are handled by service_
ip::tcp::socket sock1(service_);
// all the socket operations are handled by service_
ip::tcp::socket sock2(service_);
sock1.async_connect( ep, connect_handler);
sock2.async_connect( ep, connect_handler);
deadline_timer t(service_, boost::posix_time::seconds(5));
t.async_wait(timeout_handler);
service_.run();
多个线程使用一个io_service实例,并且有多线程的处理程序。
io_service service_;
ip::tcp::socket sock1(service_);
ip::tcp::socket sock2(service_);
sock1.async_connect( ep, connect_handler);
sock2.async_connect( ep, connect_handler);
deadline_timer t(service_, boost::posix_time::seconds(5));
t.async_wait(timeout_handler);
for ( int i = 0; i < 5; ++i)
boost::thread( run_service);
void run_service() {
service_.run();
}
多线程使用多个io_servcie实例,多个实例:
io_service service_[2];
ip::tcp::socket sock1(service_[0]);
ip::tcp::socket sock2(service_[1]);
sock1.async_connect( ep, connect_handler);
sock2.async_connect( ep, connect_handler);
deadline_timer t(service_[0], boost::posix_time::seconds(5));
t.async_wait(timeout_handler);
for ( int i = 0; i < 2; ++i)
boost::thread( boost::bind(run_service, i));
void run_service(int idx) {
service_[idx].run();
}
首先,注意不要在单个线程中使用多个io_service实例。如下面毫无意义的代码:
for ( int i = 0; i < 2; ++i)
service_[i].run();
前面的代码片段是没有意义的,因为service_[1]::run(),需要等待service_[0]完成,因此,service_[1]的所有异步操作都需要等待,这不是一个好办法。
在前面的三种情况,我们等待三个异步操作完成。关于解释这些差异,我们假定,一段时间后,操作1完成,然后,操作2完成。我们也可以建设每个完成处理程序takes(???) 第二个完成处理程序。
第一种情况,我们在一个线程中等待所有的操作完成。一旦操作1完成,我们调用其完成处理程序。即时操作2在之后完成,操作2的完成处理程序在操作1的完成处理程序后被调用。
第二种情况,我们在2个线程中等待三个操作完成。操作1完成,在第一个线程中调用其完成处理程序。之后,操作2立马完成,在第二个线程中调用他的完成处理程序(当线程1忙于处理操作1完成处理程序,线程2可以任意的回答任何到来的新操作)。
第三种情况,如果操作1连接socket1,操作2连接操作2,应用程序将属于第二种情况。线程1负责socket1完成处理程序,线程2负责socket2完成处理程序。然而,如果的操作1是socket连接,操作2是超时检测,线程1在操作1完成连接后终止。因此,超时检测处理程序等待socket1完成处理程序完成后(它将等待1秒)。然后,线程1处理socket1连接和超时检测。
这里总结从前面的例子学的内容:
情景1 非常基础的应用。常遇到在同一时间处理多个处理器(handler)的瓶颈,他们是串行的。如果一个处理程序需要长时间运行来完成,所有后续的程序不得不等待。
情景2 大部分应用情况。具有很强的鲁棒性-如果多个处理程序在自己的线程中被同时调用。瓶颈在于如果正在忙着处理程序,同时有新的处理程序到来。然后作为一种快速的方案,只能增加处理器的线程数。
情景3 负责和灵活的应用。使用情景2无法满足的情况。可能有成千上万的并发连接。你可以使每个线程(线程运行io_service::run())选择自己的select/epoll循环。他等待任意的socket,监视读写操作,如果发现有任意操作,则执行它。大多数情况下,你不需要担心这个方法,只要关心如果socket数量程指数级增长(大于1000个socket)。在这种情况下,增加多个select/epoll循环增加响应时间。
如果,你的应用程序需要切换到第三种情况,需要确保监视操作的代码(调用io_service::run())与其它代码隔离,这样可以比较容易修改。
最后,切记。run()在没有操作的时候终止。如下面的代码片段所示:
io_service service_;
tcp::socket sock(service_);
sock.async_connect( ep, connect_handler);
service_.run();
在前面的情况,一旦socket建立了连接,connect_handler将被调用,之后,service.run()将终止。
如果你想确保service_.run()继续运行,需要分配给他更多的操作。有两种方式做这个。一种方式在connect_handler中启动另外一个异步操作。
另一种方式,模拟一些工作,如下代码片段所示:
typedef boost::shared_ptr
work_ptr dummy_work(new io_service::work(service_));
上面的代码确server_.run()一直运行,除非你调用service_.stop()或者
dummy_work.reset(0)。//销毁dummy_work
Boost.Asio是一个复杂的类库,使网络编程更简单。很容易的编译它。避免过多的使用宏,他有一些宏来避免打开/关闭选项,通常不需要担心。
Boost.Asio允许使用同步和异步编程。他们有很大区别的,你应该尽早选择一种方式,因为切换是比较负责和容易出错的。
如果你选择同步方式,你可以选择异常和错误代码,从异常到错误代码是简单的,只需要增加调用函数的一个参数(错误代码)。
Boost.Asio不仅仅是一个网络库,他包含更多的功能,如信号,定时器等等。
下一章节中,我们将更深入研究Boost.Asio提供的网络功能,同时了解一些异步编程技巧。