Boost.Asio-進階話題
這一章對Boost.Asio的一些進階話題進行了闡述。在日常程式設計中深入研究這些問題是不太可能的,但是知道這些肯定是有好處的:
* 如果調試失敗,你需要看Boost.Asio能幫到你什麼
* 如果你需要處理SSL,看Boost.Asio能幫你多少
* 如果你指定一個作業系統,看Boost.Asio為你準備了哪些額外的特性
Asio VS Boost.Asio
Boost.Asio的作者也保持了Asio。你可以用Asio的方式來思考,因為它在兩種情況中都有:Asio(非Boost的)和Boost.Asio。作者聲明過更新都會先在非Boost中出現,然後過段時間後,再加入到Boost的釋出中。
不同點被歸納到下面幾條:
* Asio被定義在asio::的命名空間中,而Boost.Asio被定義在boost::asio::中
* Asio的主頭檔案是asio.hpp,而Boost.Asio的頭檔案是boost/asio.hpp
* Asio也有一個啟動線程的類(和boost::thread一樣)
* Asio提供它自己的錯誤碼類(asio::error_code代替boost::system::error_code,然後asio:system_error代替boost::systrem::system_error)
你可以在這裡查閱更多Asio的資訊:http://think_async.com
你需要自己決定你選擇的版本,我選擇Boost.Asio。下面是一些當你做選擇時需要考慮的問題:
* Asio的新版本比Boost.Asio的新版本釋出要早(因為Boost的版本更新比較少)
* Asio隻有頭檔案(而Boost.Asio的部分依賴于其他Boost庫,這些庫可能需要編譯)
* Asio和Boost.Asio都是非常成熟的,是以除非你非常需要一些Asio新釋出的特性,Boost.Asio是非常保險的選擇,而且你也可以同時擁有其他Boost庫的資源
盡管我不推薦這樣,你可以在一個應用中同時使用Asio和Boost.Asio。在允許的情況下這是很自然的,比如你使用Asio,然後一些第三方庫是Boost.Asio,反之亦然。
調試
調試同步應用往往比調試異步應用要簡單。對于同步應用,如果阻塞了,你會跳轉進入調試,然後你會知道你在哪(同步意味着有序的)。然而如果是異步,事件不是有序發生的,是以在調試中是非常難知道到底發生了什麼的。
為了避免這種情況,首先,你需要深入了解協程。如果實作正确,基本上你一點也不會碰到異步調試的問題。
以防萬一,在做異步編碼的時候,Boost.Asio還是對你伸出了援手;Boost.Asio允許“句柄追蹤”,當BOOST_ASIO_ENABLE_HANDLER_TRACKING被定義時,Boost.Asio會寫很多輔助的輸出到标準錯誤流,紀錄時間,異步操作,以及操作和完成處理handler的關系。
句柄追蹤資訊
雖然輸出資訊不是那麼容易了解,但是有總比沒有好。Boost.Asio的輸出是@asio|||
。
第一個标簽永遠都是@asio,因為其他代碼也會輸出到标準錯誤流(和std::error相當),是以你可以非常簡單的用這個标簽過濾從Boost.Asio列印出來的資訊。timestamp執行個體從1970年1月1号到現在的秒數和毫秒數。action執行個體可以是下面任何一種:
* >n:這個在我們進入handler n的時候使用。description執行個體包含了我們發送給handler的參數。
* *
一個例子
為了向你展示一個帶輔助輸出資訊的例子,我們修改了在第六章 Boost.Asio其他特性 中使用的例子。你所需要做的僅僅是在包含boost/asio.hpp之前添加一個#define
#define BOOST_ASIO_ENABLE_HANDLER_TRACKING
#include <boost/asio.hpp>
...
同時,我們也在使用者登入和接收到第一個用戶端清單時将資訊輸出到控制台中。輸出會如下所示:
@asio||*1|[email protected]_connect
@asio||>|ec=system:
@asio||*2|[email protected]_send
@asio||<|
@asio||>|ec=system:,bytes_transferred=
@asio||*3|[email protected]_receive
@asio||<|
@asio||>|ec=system:,bytes_transferred=
@asio||*4|[email protected]
@asio||<|
@asio||>|
John logged in
@asio||*5|[email protected]
@asio||<|
@asio||>|
@asio||*6|[email protected]_send
@asio||<|
@asio||>|ec=system:,bytes_transferred=
@asio||*7|[email protected]_receive
@asio||<|
@asio||>|ec=system:,bytes_transferred=
@asio||*8|[email protected]
@asio||<|
@asio||>|
John, new client list: John
讓我們一行一行分析:
* 我們進入async_connect,它建立了句柄1(在這個例子中,所有的句柄都是talk_to_svr::step)
* 句柄1被調用(當成功連接配接到服務端時)
* 句柄1調用async_send,這建立了句柄2(這裡,我們發送登入資訊到服務端)
* 句柄1退出
* 句柄2被調用,11個位元組被發送出去(login John)
* 句柄2調用async_receive,這建立了句柄3(我們等待服務端傳回登入的結果)
* 句柄2退出
* 句柄3被調用,我們收到了9個位元組(login ok)
* 句柄3調用on_answer_from_server(這建立了句柄4)
* 句柄3退出
* 句柄4被調用,這會輸出John logged in
* 句柄4調用了另外一個step(句柄5),這會寫入ask_clients
* 句柄4退出
* 句柄5進入
* 句柄5,async_send_ask_clients,建立句柄6
* 句柄5退出
* 句柄6調用async_receive,這建立了句柄7(我們等待服務端發送給我們已存在的用戶端清單)
* 句柄6退出
* 句柄7被調用,我們接受到了用戶端清單
* 句柄7調用on_answer_from_server(這建立了句柄8)
* 句柄7退出
* 句柄8進去,然後輸出用戶端清單(on_clients)
這需要時間去了解,但是一旦你了解了,你就可以分辨出有問題的輸出,進而找出需要被修複的那段代碼。
句柄追蹤資訊輸出到檔案
預設情況下,句柄的追蹤資訊被輸出到标準錯誤流(相當于std::cerr)。而把輸出重定向到其他地方的可能性是非常高的。對于控制台應用,輸出和錯誤輸出都被預設輸出到相同的地方,也就是控制台。但是對于一個windows(非指令行)應用來說,預設的錯誤流是null。
你可以通過指令行把錯誤輸出重定向,比如:
或者,如果你不是很懶,你可以代碼實作,就像下面的代碼片段
// 對于Windows
HANDLE h = CreateFile("err.txt", GENERIC_WRITE, , , CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL , );
SetStdHandle(STD_ERROR_HANDLE, h);
// 對于Unix
int err_file = open("err.txt", O_WRONLY);
dup2(err_file, STDERR_FILENO);
SSL
Boost.Asio提供了一些支援基本SSL的類。它在幕後使用的其實是OpenSSL,是以,如果你想使用SSL,首先從www.openssl.org下載下傳OpenSSL然後建構它。你需要注意,建構OpenSSL通常來說不是一個簡單的任務,尤其是你沒有一個常用的編譯器,比如Visual Studio。
假如你成功建構了OpenSSL,Boost.Asio就會有一些圍繞它的封裝類:
* ssl::stream:它代替ip:::socket來告訴你用什麼
* ssl::context:這是給第一次握手用的上下文
* ssl::rfc2818_verification:使用這個類可以根據RFC 2818協定非常簡單地通過證書認證一個主機名
首先,你建立和初始化SSL上下文,然後使用這個上下文打開一個連接配接到指定遠端主機的socket,然後做SSL握手。握手一結束,你就可以使用Boost.Asio的read/write**等自由函數。
下面是一個連接配接到Yahoo!的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();
// 打開一個到指定主機的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[];
boost::system::error_code ec;
while ( !ec) {
int bytes = read(sock, buffer(buff), ec);
std::cout << std::string(buff, bytes);
}
}
第一行能很好的自釋。當你連接配接到遠端主機,你使用sock.lowest_layer(),也就是說,你使用底層的socket(因為ssl::stream僅僅是一個封裝)。接下來三行進行了握手。握手一結束,你使用Booat.Asio的write()方法做了一個HTTP請求,然後讀取(read())所有接收到的位元組。
當實作SSL服務端的時候,事情會變的有點複雜。Boost.Asio有一個SSL服務端的例子,你可以在boost/libs/asio/example/ssl/server.cpp中找到。
Boost.Asio的Windows特性
接下來的特性隻賦予Windows作業系統
流處理
Boost.Asio允許你在一個Windows句柄上建立封裝,這樣你就可以使用大部分的自由函數,比如read(),read_until(),write(),async_read(),async_read_until()和async_write()。下面告訴你如何從一個檔案讀取一行:
HANDLE file = ::CreateFile("readme.txt", GENERIC_READ, , , OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, );
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完成處理端口正在被使用的情況下才有效(這是預設情況)。如果情況滿足,BOOST_ASIO_HAS_WINDOWS_STREAM_HANDLE就被定義
随機通路句柄
Boost.Asio允許對一個指向普通檔案的句柄進行随機讀取和寫入。同樣,你為這個句柄建立一個封裝,然後使用自由函數,比如read_at(),write_at(),async_read_at(),async_write_at()。要從1000的地方讀取50個位元組,你需要使用下面的代碼片段:
HANDLE file = ::CreateFile("readme.txt", GENERIC_READ, , , OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, );
windows::random_access_handle h(service, file);
char buf[];
int bytes = read_at(h, , buffer( buf));
std::string msg(buf, bytes);
std::cout << msg << std::endl;
對于Boost.Asio,随機通路句柄隻提供随機通路,你不能把它們當作流句柄使用。也就是說,自由函數,比如:read(),read_until(),write()以及他們的相對的異步方法都不能在一個随機通路的句柄上使用。
random_access_handle類隻有在I/O完成處理端口在使用中才有效(這是預設情況)。如果情況滿足,BOOST_ASIO_HAS_WINDOWS_RANDOM_ACCESS_HANDLE就被定義
對象句柄
你可以通過Windows句柄等待核心對象,比如修改通知,控制台輸入,事件,記憶體資源通知,程序,信号量,線程或者可等待的計時器。或者簡單來說,所有可以調用WaitForSingleObject的東西。你可以在它們上面建立一個object_handle封裝,然後在上面使用wait()或者async_wait():
void on_wait_complete(boost::system::error_code err) {}
...
HANDLE evt = ::CreateEvent(, true, true, );
windows::object_handle h(service, evt);
// 同步等待
h.wait();
// 異步等待
h.async_wait(on_wait_complete);
Boost.Asio POSIX特性
這些特性隻在Unix作業系統上可用
本地socket
Boost.Asio提供了對本地socket的基本支援(也就是著名的Unix 域socket)。
本地socket是一種隻能被運作在主機上的應用通路的socket。你可以使用本地socket來實作簡單的程序間通訊,連接配接兩端的方式是把一個當作用戶端而另一個當作服務端。對于本地socket,端點是一個檔案,比如/tmp/whatever。很酷的一件事情是你可以給指定的檔案賦予權限,進而禁止機器上指定的使用者在檔案上建立socket。
你可以用用戶端socket的方式連接配接,如下面的代碼片段:
local::stream_protocol::endpoint ep("/tmp/my_cool_app");
local::stream_protocol::socket sock(service);
sock.connect(ep);
你可以建立一個服務端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被成功建立,你就可以像用普通socket一樣使用它;它和其他socket類有相同的成員方法,而且你也可以在使用了socket的自由函數中使用。
注意本地socket隻有在目标作業系統支援它們的時候才可用,也就是BOOST_ASIO_HAS_LOCAL_SOCKETS(如果被定義)
連接配接本地socket
最終,你可以連接配接兩個socket,或者是無連接配接的(資料報),或者是基于連接配接的(流):
// 基于連接配接
local::stream_protocol::socket s1(service);
local::stream_protocol::socket s2(service);
local::connect_pair(s1, s2);
// 資料報
local::datagram_protocol::socket s1(service);
local::datagram_protocol::socket s2(service);
local::connect_pair(s1, s2);
在内部,connect_pair使用的是不那麼著名的POSIX socketpair()方法。基本上它所作的事情就是在沒有複雜socket建立過程的情況下連接配接兩個socket;而且隻需要一行代碼就可以完成。這在過去是實作線程通信的一種簡單方式。而在現代程式設計中,你可以避免它,然後你會發現在處理使用了socket的遺留代碼時它非常有用。
POSIX檔案描述符
Boost.Asio允許在一些POSIX檔案描述符,比如管道,标準I/O和其他裝置(但是不是在普通檔案上)上做一些同步和異步的操作。
一旦你為這樣一個POSIX檔案描述符建立了一個stream_descriptor執行個體,你就可以使用一些Boost.Asio提供的自由函數。比如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[];
int bytes = read(in, buffer(buff), read_up_to_enter);
write(out, buffer(buff, bytes));
stream_descriptor類隻在目标作業系統支援的情況下有效,也就是BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR(如果定義了)
Fork
Boost.Asio支援在程式中使用fork()系統調用。你需要告訴io_service執行個體fork()方法什麼時候會發生以及什麼時候發生了。參考下面的代碼片段:
service.notify_fork(io_service::fork_prepare);
if (fork() == ) {
// 子程序
service.notify_fork(io_service::fork_child);
...
} else {
// 父程序
service.notify_fork(io_service::fork_parent);
...
}
這意味着會在不同的線程使用即将被調用的service。盡管Boost.Asio允許這樣,我還是強烈推薦你使用多線程,因為使用boost::thread簡直就是小菜一碟。
總結
為簡單明了的代碼而奮鬥。學習和使用協程會最小化你需要做的調試工作,但僅僅是在代碼中有潛在bug的情況下,Boost.Asio才會伸出援手,這一點在關于調試的章節中就已經講過。
如果你需要使用SSL,Boost.Asio是支援基本的SSL編碼的
最終,如果已經知道應用是針對專門的作業系統的,你可以享用Boost.Asio為那個特定的作業系統準備的特性。