[譯]Boost.Asio - 進(jìn)階話題

2018-06-19 15:35 更新

Boost.Asio-進(jìn)階話題

這一章對(duì)Boost.Asio的一些進(jìn)階話題進(jìn)行了闡述。在日常編程中深入研究這些問(wèn)題是不太可能的,但是知道這些肯定是有好處的:

  • 如果調(diào)試失敗,你需要看Boost.Asio能幫到你什么
  • 如果你需要處理SSL,看Boost.Asio能幫你多少
  • 如果你指定一個(gè)操作系統(tǒng),看Boost.Asio為你準(zhǔn)備了哪些額外的特性

    Asio VS Boost.Asio

    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被定義在asio::的命名空間中,而B(niǎo)oost.Asio被定義在boost::asio::
  • Asio的主頭文件是asio.hpp,而B(niǎo)oost.Asio的頭文件是boost/asio.hpp
  • Asio也有一個(gè)啟動(dòng)線程的類(和boost::thread一樣)
  • Asio提供它自己的錯(cuò)誤碼類(asio::error_code代替boost::system::error_code,然后asio:system_error代替boost::systrem::system_error)

你可以在這里查閱更多Asio的信息:http://think_async.com

你需要自己決定你選擇的版本,我選擇Boost.Asio。下面是一些當(dāng)你做選擇時(shí)需要考慮的問(wèn)題:

  • Asio的新版本比Boost.Asio的新版本發(fā)布要早(因?yàn)锽oost的版本更新比較少)
  • Asio只有頭文件(而B(niǎo)oost.Asio的部分依賴于其他Boost庫(kù),這些庫(kù)可能需要編譯)
  • Asio和Boost.Asio都是非常成熟的,所以除非你非常需要一些Asio新發(fā)布的特性,Boost.Asio是非常保險(xiǎn)的選擇,而且你也可以同時(shí)擁有其他Boost庫(kù)的資源

盡管我不推薦這樣,你可以在一個(gè)應(yīng)用中同時(shí)使用Asio和Boost.Asio。在允許的情況下這是很自然的,比如你使用Asio,然后一些第三方庫(kù)是Boost.Asio,反之亦然。

調(diào)試

調(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í)例可以是下面任何一種:

  • \>n:這個(gè)在我們進(jìn)入handler n的時(shí)候使用。description實(shí)例包含了我們發(fā)送給handler的參數(shù)。
  • <n:這個(gè)在我們退出handler n的時(shí)候使用。
  • !n:這個(gè)當(dāng)我們因?yàn)楫惓M顺鰄andler n的時(shí)候使用。
  • -n:這個(gè)當(dāng)handler n在沒(méi)有調(diào)用的情況就退出的時(shí)候使用;可能是因?yàn)閕o_service實(shí)例被刪除地太快了(在n有機(jī)會(huì)被調(diào)用之前)
  • nm:這個(gè)當(dāng)handler n創(chuàng)建了一個(gè)新的有完成處理hanlder m的異步操作時(shí)被調(diào)用。description實(shí)例展示的就是異步操作開(kāi)始的地方。當(dāng)你看到>m(開(kāi)始)和<m(結(jié)束)時(shí)completion句柄被調(diào)用了。
  • n:就像在description中展示的一樣,這個(gè)當(dāng)handler n做了一個(gè)操作的時(shí)候使用(可能是close或者cancel操作)。你一般可以忽略這些信息。

當(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è)例子

為了向你展示一個(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

讓我們一行一行分析:

  • 我們進(jìn)入async_connect,它創(chuàng)建了句柄1(在這個(gè)例子中,所有的句柄都是talk_to_svr::step
  • 句柄1被調(diào)用(當(dāng)成功連接到服務(wù)端時(shí))
  • 句柄1調(diào)用async_send,這創(chuàng)建了句柄2(這里,我們發(fā)送登錄信息到服務(wù)端)
  • 句柄1退出
  • 句柄2被調(diào)用,11個(gè)字節(jié)被發(fā)送出去(login John)
  • 句柄2調(diào)用async_receive,這創(chuàng)建了句柄3(我們等待服務(wù)端返回登錄的結(jié)果)
  • 句柄2退出
  • 句柄3被調(diào)用,我們收到了9個(gè)字節(jié)(login ok)
  • 句柄3調(diào)用on_answer_from_server(這創(chuàng)建了句柄4)
  • 句柄3退出
  • 句柄4被調(diào)用,這會(huì)輸出John logged in
  • 句柄4調(diào)用了另外一個(gè)step(句柄5),這會(huì)寫(xiě)入ask_clients
  • 句柄4退出
  • 句柄5進(jìn)入
  • 句柄5,async_send_ask_clients,創(chuàng)建句柄6
  • 句柄5退出
  • 句柄6調(diào)用async_receive,這創(chuàng)建了句柄7(我們等待服務(wù)端發(fā)送給我們已存在的客戶端列表)
  • 句柄6退出
  • 句柄7被調(diào)用,我們接受到了客戶端列表
  • 句柄7調(diào)用on_answer_from_server(這創(chuàng)建了句柄8)
  • 句柄7退出
  • 句柄8進(jìn)去,然后輸出客戶端列表(on_clients

這需要時(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);

SSL

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ì)有一些圍繞它的封裝類:

  • ssl::stream:它代替ip:<protocol>::socket來(lái)告訴你用什么
  • ssl::context:這是給第一次握手用的上下文
  • ssl::rfc2818_verification:使用這個(gè)類可以根據(jù)RFC 2818協(xié)議非常簡(jiǎn)單地通過(guò)證書(shū)認(rèn)證一個(gè)主機(jī)名

首先,你創(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中找到。

Boost.Asio的Windows特性

接下來(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就被定義

隨機(jī)訪問(wèn)句柄

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就被定義

對(duì)象句柄

你可以通過(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);

Boost.Asio POSIX特性

這些特性只在Unix操作系統(tǒng)上可用

本地socket

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(如果被定義)

連接本地socket

最終,你可以連接兩個(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í)它非常有用。

POSIX文件描述符

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(如果定義了)

Fork

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é)

為簡(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)備的特性。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)