這一章對(duì)Boost.Asio的一些進(jìn)階話題進(jìn)行了闡述。在日常編程中深入研究這些問(wèn)題是不太可能的,但是知道這些肯定是有好處的:
Boost.Asio的作者也保持了Asio。你可以用Asio的方式來(lái)思考,因?yàn)樗趦煞N情況中都有:Asio(非Boost的)和Boost.Asio。作者聲明過(guò)更新都會(huì)先在非Boost中出現(xiàn),然后過(guò)段時(shí)間后,再加入到Boost的發(fā)布中。
不同點(diǎn)被歸納到下面幾條:
你可以在這里查閱更多Asio的信息:http://think_async.com
你需要自己決定你選擇的版本,我選擇Boost.Asio。下面是一些當(dāng)你做選擇時(shí)需要考慮的問(wèn)題:
盡管我不推薦這樣,你可以在一個(gè)應(yīng)用中同時(shí)使用Asio和Boost.Asio。在允許的情況下這是很自然的,比如你使用Asio,然后一些第三方庫(kù)是Boost.Asio,反之亦然。
調(diào)試同步應(yīng)用往往比調(diào)試異步應(yīng)用要簡(jiǎn)單。對(duì)于同步應(yīng)用,如果阻塞了,你會(huì)跳轉(zhuǎn)進(jìn)入調(diào)試,然后你會(huì)知道你在哪(同步意味著有序的)。然而如果是異步,事件不是有序發(fā)生的,所以在調(diào)試中是非常難知道到底發(fā)生了什么的。
為了避免這種情況,首先,你需要深入了解協(xié)程。如果實(shí)現(xiàn)正確,基本上你一點(diǎn)也不會(huì)碰到異步調(diào)試的問(wèn)題。
以防萬(wàn)一,在做異步編碼的時(shí)候,Boost.Asio還是對(duì)你伸出了援手;Boost.Asio允許“句柄追蹤”,當(dāng)BOOST_ASIO_ENABLE_HANDLER_TRACKING被定義時(shí),Boost.Asio會(huì)寫(xiě)很多輔助的輸出到標(biāo)準(zhǔn)錯(cuò)誤流,紀(jì)錄時(shí)間,異步操作,以及操作和完成處理handler的關(guān)系。
雖然輸出信息不是那么容易理解,但是有總比沒(méi)有好。Boost.Asio的輸出是@asio|<timestamp>|<action>|<description> 。 第一個(gè)標(biāo)簽永遠(yuǎn)都是@asio,因?yàn)槠渌a也會(huì)輸出到標(biāo)準(zhǔn)錯(cuò)誤流(和std::error相當(dāng)),所以你可以非常簡(jiǎn)單的用這個(gè)標(biāo)簽過(guò)濾從Boost.Asio打印出來(lái)的信息。timestamp實(shí)例從1970年1月1號(hào)到現(xiàn)在的秒數(shù)和毫秒數(shù)。action實(shí)例可以是下面任何一種:
當(dāng)n是0時(shí),操作是在所有(異步)handler之外被執(zhí)行的;你經(jīng)常會(huì)在第一個(gè)操作時(shí)看到這個(gè),或者當(dāng)你使用的信號(hào)量其中一個(gè)被觸發(fā)時(shí)。
你需要特別注意類型為!n和-n的信息,這些信息大部分都意味著你的代碼有錯(cuò)誤。在第一種情形中,異步方法沒(méi)有拋出異常,所以,異常一定是你自己造成的;你不能讓異常跑出你的completion句柄。第二種情形中,你可能太早就銷毀了io_service實(shí)例,在所有完成處理句被調(diào)用之前。
為了向你展示一個(gè)帶輔助輸出信息的例子,我們修改了在第六章 Boost.Asio其他特性 中使用的例子。你所需要做的僅僅是在包含boost/asio.hpp之前添加一個(gè)#define
#define BOOST_ASIO_ENABLE_HANDLER_TRACKING
#include <boost/asio.hpp>
...
同時(shí),我們也在用戶登錄和接收到第一個(gè)客戶端列表時(shí)將信息輸出到控制臺(tái)中。輸出會(huì)如下所示:
@asio|1355603116.602867|0*1|socket@008D4EF8.async_connect
@asio|1355603116.604867|>1|ec=system:0
@asio|1355603116.604867|1*2|socket@008D4EF8.async_send
@asio|1355603116.604867|<1|
@asio|1355603116.604867|>2|ec=system:0,bytes_transferred=11
@asio|1355603116.604867|2*3|socket@008D4EF8.async_receive
@asio|1355603116.604867|<2|
@asio|1355603116.605867|>3|ec=system:0,bytes_transferred=9
@asio|1355603116.605867|3*4|io_service@008D4BC8.post
@asio|1355603116.605867|<3|
@asio|1355603116.605867|>4|
John logged in
@asio|1355603116.606867|4*5|io_service@008D4BC8.post
@asio|1355603116.606867|<4|
@asio|1355603116.606867|>5|
@asio|1355603116.606867|5*6|socket@008D4EF8.async_send
@asio|1355603116.606867|<5|
@asio|1355603116.606867|>6|ec=system:0,bytes_transferred=12
@asio|1355603116.606867|6*7|socket@008D4EF8.async_receive
@asio|1355603116.606867|<6|
@asio|1355603116.606867|>7|ec=system:0,bytes_transferred=14
@asio|1355603116.606867|7*8|io_service@008D4BC8.post
@asio|1355603116.607867|<7|
@asio|1355603116.607867|>8|
John, new client list: John
讓我們一行一行分析:
這需要時(shí)間去理解,但是一旦你理解了,你就可以分辨出有問(wèn)題的輸出,從而找出需要被修復(fù)的那段代碼。
默認(rèn)情況下,句柄的追蹤信息被輸出到標(biāo)準(zhǔn)錯(cuò)誤流(相當(dāng)于std::cerr)。而把輸出重定向到其他地方的可能性是非常高的。對(duì)于控制臺(tái)應(yīng)用,輸出和錯(cuò)誤輸出都被默認(rèn)輸出到相同的地方,也就是控制臺(tái)。但是對(duì)于一個(gè)windows(非命令行)應(yīng)用來(lái)說(shuō),默認(rèn)的錯(cuò)誤流是null。
你可以通過(guò)命令行把錯(cuò)誤輸出重定向,比如:
some_application 2>err.txt
或者,如果你不是很懶,你可以代碼實(shí)現(xiàn),就像下面的代碼片段
// 對(duì)于Windows
HANDLE h = CreateFile("err.txt", GENERIC_WRITE, 0, 0, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL , 0);
SetStdHandle(STD_ERROR_HANDLE, h);
// 對(duì)于Unix
int err_file = open("err.txt", O_WRONLY);
dup2(err_file, STDERR_FILENO);
Boost.Asio提供了一些支持基本SSL的類。它在幕后使用的其實(shí)是OpenSSL,所以,如果你想使用SSL,首先從www.openssl.org下載OpenSSL然后構(gòu)建它。你需要注意,構(gòu)建OpenSSL通常來(lái)說(shuō)不是一個(gè)簡(jiǎn)單的任務(wù),尤其是你沒(méi)有一個(gè)常用的編譯器,比如Visual Studio。
假如你成功構(gòu)建了OpenSSL,Boost.Asio就會(huì)有一些圍繞它的封裝類:
首先,你創(chuàng)建和初始化SSL上下文,然后使用這個(gè)上下文打開(kāi)一個(gè)連接到指定遠(yuǎn)程主機(jī)的socket,然后做SSL握手。握手一結(jié)束,你就可以使用Boost.Asio的read/write**等自由函數(shù)。
下面是一個(gè)連接到Y(jié)ahoo!的HTTPS客戶端例子:
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
using namespace boost::asio;
io_service service;
int main(int argc, char* argv[]) {
typedef ssl::stream<ip::tcp::socket> ssl_socket;
ssl::context ctx(ssl::context::sslv23);
ctx.set_default_verify_paths();
// 打開(kāi)一個(gè)到指定主機(jī)的SSL socket
io_service service;
ssl_socket sock(service, ctx);
ip::tcp::resolver resolver(service);
std::string host = "www.yahoo.com";
ip::tcp::resolver::query query(host, "https");
connect(sock.lowest_layer(), resolver.resolve(query));
// SSL 握手
sock.set_verify_mode(ssl::verify_none);
sock.set_verify_callback(ssl::rfc2818_verification(host));
sock.handshake(ssl_socket::client);
std::string req = "GET /index.html HTTP/1.0\r\nHost: " + host + "\r\nAccept: */*\r\nConnection: close\r\n\r\n";
write(sock, buffer(req.c_str(), req.length()));
char buff[512];
boost::system::error_code ec;
while ( !ec) {
int bytes = read(sock, buffer(buff), ec);
std::cout << std::string(buff, bytes);
}
}
第一行能很好的自釋。當(dāng)你連接到遠(yuǎn)程主機(jī),你使用sock.lowest_layer(),也就是說(shuō),你使用底層的socket(因?yàn)?em>ssl::stream僅僅是一個(gè)封裝)。接下來(lái)三行進(jìn)行了握手。握手一結(jié)束,你使用Booat.Asio的write()方法做了一個(gè)HTTP請(qǐng)求,然后讀?。?em>read())所有接收到的字節(jié)。
當(dāng)實(shí)現(xiàn)SSL服務(wù)端的時(shí)候,事情會(huì)變的有點(diǎn)復(fù)雜。Boost.Asio有一個(gè)SSL服務(wù)端的例子,你可以在boost/libs/asio/example/ssl/server.cpp中找到。
接下來(lái)的特性只賦予Windows操作系統(tǒng)
Boost.Asio允許你在一個(gè)Windows句柄上創(chuàng)建封裝,這樣你就可以使用大部分的自由函數(shù),比如read(),read_until(),write(),async_read(),async_read_until()和async_write()。下面告訴你如何從一個(gè)文件讀取一行:
HANDLE file = ::CreateFile("readme.txt", GENERIC_READ, 0, 0, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, 0);
windows::stream_handle h(service, file);
streambuf buf;
int bytes = read_until(h, buf, '\n');
std::istream in(&buf);
std::string line;
std::getline(in, line);
std::cout << line << std::endl;
stream_handle類只有在I/O完成處理端口正在被使用的情況下才有效(這是默認(rèn)情況)。如果情況滿足,BOOST_ASIO_HAS_WINDOWS_STREAM_HANDLE就被定義
Boost.Asio允許對(duì)一個(gè)指向普通文件的句柄進(jìn)行隨機(jī)讀取和寫(xiě)入。同樣,你為這個(gè)句柄創(chuàng)建一個(gè)封裝,然后使用自由函數(shù),比如read_at(),write_at(),async_read_at(),async_write_at()。要從1000的地方讀取50個(gè)字節(jié),你需要使用下面的代碼片段:
HANDLE file = ::CreateFile("readme.txt", GENERIC_READ, 0, 0, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, 0);
windows::random_access_handle h(service, file);
char buf[50];
int bytes = read_at(h, 1000, buffer( buf));
std::string msg(buf, bytes);
std::cout << msg << std::endl;
對(duì)于Boost.Asio,隨機(jī)訪問(wèn)句柄只提供隨機(jī)訪問(wèn),你不能把它們當(dāng)作流句柄使用。也就是說(shuō),自由函數(shù),比如:read(),read_until(),write()以及他們的相對(duì)的異步方法都不能在一個(gè)隨機(jī)訪問(wèn)的句柄上使用。
random_access_handle類只有在I/O完成處理端口在使用中才有效(這是默認(rèn)情況)。如果情況滿足,BOOST_ASIO_HAS_WINDOWS_RANDOM_ACCESS_HANDLE就被定義
你可以通過(guò)Windows句柄等待內(nèi)核對(duì)象,比如修改通知,控制臺(tái)輸入,事件,內(nèi)存資源通知,進(jìn)程,信號(hào)量,線程或者可等待的計(jì)時(shí)器?;蛘吆?jiǎn)單來(lái)說(shuō),所有可以調(diào)用WaitForSingleObject的東西。你可以在它們上面創(chuàng)建一個(gè)object_handle封裝,然后在上面使用wait()或者async_wait():
void on_wait_complete(boost::system::error_code err) {}
...
HANDLE evt = ::CreateEvent(0, true, true, 0);
windows::object_handle h(service, evt);
// 同步等待
h.wait();
// 異步等待
h.async_wait(on_wait_complete);
這些特性只在Unix操作系統(tǒng)上可用
Boost.Asio提供了對(duì)本地socket的基本支持(也就是著名的Unix 域socket)。
本地socket是一種只能被運(yùn)行在主機(jī)上的應(yīng)用訪問(wèn)的socket。你可以使用本地socket來(lái)實(shí)現(xiàn)簡(jiǎn)單的進(jìn)程間通訊,連接兩端的方式是把一個(gè)當(dāng)作客戶端而另一個(gè)當(dāng)作服務(wù)端。對(duì)于本地socket,端點(diǎn)是一個(gè)文件,比如/tmp/whatever。很酷的一件事情是你可以給指定的文件賦予權(quán)限,從而禁止機(jī)器上指定的用戶在文件上創(chuàng)建socket。
你可以用客戶端socket的方式連接,如下面的代碼片段:
local::stream_protocol::endpoint ep("/tmp/my_cool_app");
local::stream_protocol::socket sock(service);
sock.connect(ep);
你可以創(chuàng)建一個(gè)服務(wù)端socket,如下面的代碼片段:
::unlink("/tmp/my_cool_app");
local::stream_protocol::endpoint ep("/tmp/my_cool_app");
local::stream_protocol::acceptor acceptor(service, ep);
local::stream_protocol::socket sock(service);
acceptor.accept(sock);
只要socket被成功創(chuàng)建,你就可以像用普通socket一樣使用它;它和其他socket類有相同的成員方法,而且你也可以在使用了socket的自由函數(shù)中使用。
注意本地socket只有在目標(biāo)操作系統(tǒng)支持它們的時(shí)候才可用,也就是BOOST_ASIO_HAS_LOCAL_SOCKETS(如果被定義)
最終,你可以連接兩個(gè)socket,或者是無(wú)連接的(數(shù)據(jù)報(bào)),或者是基于連接的(流):
// 基于連接
local::stream_protocol::socket s1(service);
local::stream_protocol::socket s2(service);
local::connect_pair(s1, s2);
// 數(shù)據(jù)報(bào)
local::datagram_protocol::socket s1(service);
local::datagram_protocol::socket s2(service);
local::connect_pair(s1, s2);
在內(nèi)部,connect_pair使用的是不那么著名的POSIX socketpair()方法?;旧纤鞯氖虑榫褪窃跊](méi)有復(fù)雜socket創(chuàng)建過(guò)程的情況下連接兩個(gè)socket;而且只需要一行代碼就可以完成。這在過(guò)去是實(shí)現(xiàn)線程通信的一種簡(jiǎn)單方式。而在現(xiàn)代編程中,你可以避免它,然后你會(huì)發(fā)現(xiàn)在處理使用了socket的遺留代碼時(shí)它非常有用。
Boost.Asio允許在一些POSIX文件描述符,比如管道,標(biāo)準(zhǔn)I/O和其他設(shè)備(但是不是在普通文件上)上做一些同步和異步的操作。 一旦你為這樣一個(gè)POSIX文件描述符創(chuàng)建了一個(gè)stream_descriptor實(shí)例,你就可以使用一些Boost.Asio提供的自由函數(shù)。比如read(),read_until(),write(),async_read(),async_read_until()和async_write()。
下面告訴你如何從stdin讀取一行然后輸出到stdout:
size_t read_up_to_enter(error_code err, size_t bytes) { ... }
posix::stream_descriptor in(service, ::dup(STDIN_FILENO));
posix::stream_descriptor out(service, ::dup(STDOUT_FILENO));
char buff[512];
int bytes = read(in, buffer(buff), read_up_to_enter);
write(out, buffer(buff, bytes));
stream_descriptor類只在目標(biāo)操作系統(tǒng)支持的情況下有效,也就是BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR(如果定義了)
Boost.Asio支持在程序中使用fork()系統(tǒng)調(diào)用。你需要告訴io_service實(shí)例fork()方法什么時(shí)候會(huì)發(fā)生以及什么時(shí)候發(fā)生了。參考下面的代碼片段:
service.notify_fork(io_service::fork_prepare);
if (fork() == 0) {
// 子進(jìn)程
service.notify_fork(io_service::fork_child);
...
} else {
// 父進(jìn)程
service.notify_fork(io_service::fork_parent);
...
}
這意味著會(huì)在不同的線程使用即將被調(diào)用的service。盡管Boost.Asio允許這樣,我還是強(qiáng)烈推薦你使用多線程,因?yàn)槭褂?em>boost::thread簡(jiǎn)直就是小菜一碟。
為簡(jiǎn)單明了的代碼而奮斗。學(xué)習(xí)和使用協(xié)程會(huì)最小化你需要做的調(diào)試工作,但僅僅是在代碼中有潛在bug的情況下,Boost.Asio才會(huì)伸出援手,這一點(diǎn)在關(guān)于調(diào)試的章節(jié)中就已經(jīng)講過(guò)。
如果你需要使用SSL,Boost.Asio是支持基本的SSL編碼的
最終,如果已經(jīng)知道應(yīng)用是針對(duì)專門(mén)的操作系統(tǒng)的,你可以享用Boost.Asio為那個(gè)特定的操作系統(tǒng)準(zhǔn)備的特性。
更多建議: