# 第?7?章?異步輸入輸出
### 目錄
* [7.1 概述](asio.html#asio_general)
* [7.2 I/O 服務與 I/O 對象](asio.html#asio_ioservices_and_objects)
* [7.3 可擴展性與多線程](asio.html#asio_scalability)
* [7.4 網絡編程](asio.html#asio_networkprogramming)
* [7.5 開發 Boost.Asio 擴展](asio.html#asio_extensions)
* [7.6 練習](asio.html#asio_exercises)
[](http://creativecommons.org/licenses/by-nc-nd/3.0/de/deed.zh) 該書采用 [Creative Commons License](http://creativecommons.org/licenses/by-nc-nd/3.0/de/deed.zh) 授權
## 7.1.?概述
本章介紹了 Boost C++ 庫 [Asio](http://www.boost.org/libs/asio/),它是異步輸入輸出的核心。 名字本身就說明了一切:Asio 意即異步輸入/輸出。 該庫可以讓 C++ 異步地處理數據,且平臺獨立。 異步數據處理就是指,任務觸發后不需要等待它們完成。 相反,Boost.Asio 會在任務完成時觸發一個應用。 異步任務的主要優點在于,在等待任務完成時不需要阻塞應用程序,可以去執行其它任務。
異步任務的典型例子是網絡應用。 如果數據被發送出去了,比如發送至 Internet,通常需要知道數據是否發送成功。 如果沒有一個象 Boost.Asio 這樣的庫,就必須對函數的返回值進行求值。 但是,這樣就要求待至所有數據發送完畢,并得到一個確認或是錯誤代碼。 而使用 Boost.Asio,這個過程被分為兩個單獨的步驟:第一步是作為一個異步任務開始數據傳輸。 一旦傳輸完成,不論成功或是錯誤,應用程序都會在第二步中得到關于相應的結果通知。 主要的區別在于,應用程序無需阻塞至傳輸完成,而可以在這段時間里執行其它操作。
## 7.2.?I/O 服務與 I/O 對象
使用 Boost.Asio 進行異步數據處理的應用程序基于兩個概念:I/O 服務和 I/O 對象。 I/O 服務抽象了操作系統的接口,允許第一時間進行異步數據處理,而 I/O 對象則用于初始化特定的操作。 鑒于 Boost.Asio 只提供了一個名為 `boost::asio::io_service` 的類作為 I/O 服務,它針對所支持的每一個操作系統都分別實現了優化的類,另外庫中還包含了針對不同 I/O 對象的幾個類。 其中,類 `boost::asio::ip::tcp::socket` 用于通過網絡發送和接收數據,而類 `boost::asio::deadline_timer` 則提供了一個計時器,用于測量某個固定時間點到來或是一段指定的時長過去了。 以下第一個例子中就使用了計時器,因為與 Asio 所提供的其它 I/O 對象相比較而言,它不需要任何有關于網絡編程的知識。
```
#include <boost/asio.hpp>
#include <iostream>
void handler(const boost::system::error_code &ec)
{
std::cout << "5 s." << std::endl;
}
int main()
{
boost::asio::io_service io_service;
boost::asio::deadline_timer timer(io_service, boost::posix_time::seconds(5));
timer.async_wait(handler);
io_service.run();
}
```
* [下載源代碼](src/7.2.1/main.cpp)
函數 `main()` 首先定義了一個 I/O 服務 `io_service`,用于初始化 I/O 對象 `timer`。 就象 `boost::asio::deadline_timer` 那樣,所有 I/O 對象通常都需要一個 I/O 服務作為它們的構造函數的第一個參數。 由于 `timer` 的作用類似于一個鬧鐘,所以 `boost::asio::deadline_timer` 的構造函數可以傳入第二個參數,用于表示在某個時間點或是在某段時長之后鬧鐘停止。 以上例子指定了五秒的時長,該鬧鐘在 `timer` 被定義之后立即開始計時。
雖然我們可以調用一個在五秒后返回的函數,但是通過調用方法 `async_wait()` 并傳入 `handler()` 函數的名字作為唯一參數,可以讓 Asio 啟動一個異步操作。 請留意,我們只是傳入了 `handler()` 函數的名字,而該函數本身并沒有被調用。
`async_wait()` 的好處是,該函數調用會立即返回,而不是等待五秒鐘。 一旦鬧鐘時間到,作為參數所提供的函數就會被相應調用。 因此,應用程序可以在調用了 `async_wait()` 之后執行其它操作,而不是阻塞在這里。
象 `async_wait()` 這樣的方法被稱為是非阻塞式的。 I/O 對象通常還提供了阻塞式的方法,可以讓執行流在特定操作完成之前保持阻塞。 例如,可以調用阻塞式的 `wait()` 方法,取代 `boost::asio::deadline_timer` 的調用。 由于它會阻塞調用,所以它不需要傳入一個函數名,而是在指定時間點或指定時長之后返回。
再看看上面的源代碼,可以留意到在調用 `async_wait()` 之后,又在 I/O 服務之上調用了一個名為 `run()` 的方法。這是必須的,因為控制權必須被操作系統接管,才能在五秒之后調用 `handler()` 函數。
`async_wait()` 會啟動一個異步操作并立即返回,而 `run()` 則是阻塞的。因此調用 `run()` 后程序執行會停止。 具有諷刺意味的是,許多操作系統只是通過阻塞函數來支持異步操作。 以下例子顯示了為什么這個限制通常不會成為問題。
```
#include <boost/asio.hpp>
#include <iostream>
void handler1(const boost::system::error_code &ec)
{
std::cout << "5 s." << std::endl;
}
void handler2(const boost::system::error_code &ec)
{
std::cout << "10 s." << std::endl;
}
int main()
{
boost::asio::io_service io_service;
boost::asio::deadline_timer timer1(io_service, boost::posix_time::seconds(5));
timer1.async_wait(handler1);
boost::asio::deadline_timer timer2(io_service, boost::posix_time::seconds(10));
timer2.async_wait(handler2);
io_service.run();
}
```
* [下載源代碼](src/7.2.2/main.cpp)
上面的程序用了兩個 `boost::asio::deadline_timer` 類型的 I/O 對象。 第一個 I/O 對象表示一個五秒后觸發的鬧鐘,而第二個則表示一個十秒后觸發的鬧鐘。 每一段指定時長過去后,都會相應地調用函數 `handler1()` 和 `handler2()`。
在 `main()` 的最后,再次在唯一的 I/O 服務之上調用了 `run()` 方法。 如前所述,這個函數將阻塞執行,把控制權交給操作系統以接管異步處理。 在操作系統的幫助下,`handler1()` 函數會在五秒后被調用,而 `handler2()` 函數則在十秒后被調用。
乍一看,你可能會覺得有些奇怪,為什么異步處理還要調用阻塞式的 `run()` 方法。 然而,由于應用程序必須防止被中止執行,所以這樣做實際上不會有任何問題。 如果 `run()` 不是阻塞的,`main()` 就會結束從而中止該應用程序。 如果應用程序不應被阻塞,那么就應該在一個新的線程內部調用 `run()`,它自然就會僅僅阻塞那個線程。
一旦特定的 I/O 服務的所有異步操作都完成了,控制權就會返回給 `run()` 方法,然后它就會返回。 以上兩個例子中,應用程序都會在鬧鐘到時間后馬上結束。
## 7.3.?可擴展性與多線程
用 Boost.Asio 這樣的庫來開發應用程序,與一般的 C++ 風格不同。 那些可能需要較長時間才返回的函數不再是以順序的方式來調用。 不再是調用阻塞式的函數,Boost.Asio 是啟動一個異步操作。 而那些需要在操作結束后調用的函數則實現為相應的句柄。 這種方法的缺點是,本來順序執行的功能變得在物理上分割開來了,從而令相應的代碼更難理解。
象 Boost.Asio 這樣的庫通常是為了令應用程序具有更高的效率。 應用程序不需要等待特定的函數執行完成,而可以在期間執行其它任務,如開始另一個需要較長時間的操作。
可擴展性是指,一個應用程序從新增資源有效地獲得好處的能力。 如果那些執行時間較長的操作不應該阻塞其它操作的話,那么建議使用 Boost.Asio. 由于現今的PC機通常都具有多核處理器,所以線程的應用可以進一步提高一個基于 Boost.Asio 的應用程序的可擴展性。
如果在某個 `boost::asio::io_service` 類型的對象之上調用 `run()` 方法,則相關聯的句柄也會在同一個線程內被執行。 通過使用多線程,應用程序可以同時調用多個 `run()` 方法。 一旦某個異步操作結束,相應的 I/O 服務就將在這些線程中的某一個之中執行句柄。 如果第二個操作在第一個操作之后很快也結束了,則 I/O 服務可以在另一個線程中執行句柄,而無需等待第一個句柄終止。
```
#include <boost/asio.hpp>
#include <boost/thread.hpp>
#include <iostream>
void handler1(const boost::system::error_code &ec)
{
std::cout << "5 s." << std::endl;
}
void handler2(const boost::system::error_code &ec)
{
std::cout << "5 s." << std::endl;
}
boost::asio::io_service io_service;
void run()
{
io_service.run();
}
int main()
{
boost::asio::deadline_timer timer1(io_service, boost::posix_time::seconds(5));
timer1.async_wait(handler1);
boost::asio::deadline_timer timer2(io_service, boost::posix_time::seconds(5));
timer2.async_wait(handler2);
boost::thread thread1(run);
boost::thread thread2(run);
thread1.join();
thread2.join();
}
```
* [下載源代碼](src/7.3.1/main.cpp)
上一節中的例子現在變成了一個多線程的應用。 通過使用在 `boost/thread.hpp` 中定義的 `boost::thread` 類,它來自于 Boost C++ 庫 Thread,我們在 `main()` 中創建了兩個線程。 這兩個線程均針對同一個 I/O 服務調用了 `run()` 方法。 這樣當異步操作完成時,這個 I/O 服務就可以使用兩個線程去執行句柄函數。
這個例子中的兩個計時數均被設為在五秒后觸發。 由于有兩個線程,所以 `handler1()` 和 `handler2()` 可以同時執行。 如果第二個計時器觸發時第一個仍在執行,則第二個句柄就會在第二個線程中執行。 如果第一個計時器的句柄已經終止,則 I/O 服務可以自由選擇任一線程。
線程可以提高應用程序的性能。 因為線程是在處理器內核上執行的,所以創建比內核數更多的線程是沒有意義的。 這樣可以確保每個線程在其自己的內核上執行,而沒有同一內核上的其它線程與之競爭。
要注意,使用線程并不總是值得的。 以上例子的運行會導致不同信息在標準輸出流上混合輸出,因為這兩個句柄可能會并行運行,訪問同一個共享資源:標準輸出流 `std::cout`。 這種訪問必須被同步,以保證每一條信息在另一個線程可以向標準輸出流寫出另一條信息之前被完全寫出。 在這種情形下使用線程并不能提供多少好處,如果各個獨立句柄不能獨立地并行運行。
多次調用同一個 I/O 服務的 `run()` 方法,是為基于 Boost.Asio 的應用程序增加可擴展性的推薦方法。 另外還有一個不同的方法:不要綁定多個線程到單個 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 << "5 s." << std::endl;
}
void handler2(const boost::system::error_code &ec)
{
std::cout << "5 s." << std::endl;
}
boost::asio::io_service io_service1;
boost::asio::io_service io_service2;
void run1()
{
io_service1.run();
}
void run2()
{
io_service2.run();
}
int main()
{
boost::asio::deadline_timer timer1(io_service1, boost::posix_time::seconds(5));
timer1.async_wait(handler1);
boost::asio::deadline_timer timer2(io_service2, boost::posix_time::seconds(5));
timer2.async_wait(handler2);
boost::thread thread1(run1);
boost::thread thread2(run2);
thread1.join();
thread2.join();
}
```
* [下載源代碼](src/7.3.2/main.cpp)
前面的那個使用兩個計時器的例子被重寫為使用兩個 I/O 服務。 這個應用程序仍然基于兩個線程;但是現在每個線程被綁定至不同的 I/O 服務。 此外,兩個 I/O 對象 `timer1` 和 `timer2` 現在也被綁定至不同的 I/O 服務。
這個應用程序的功能與前一個相同。 在一定條件下使用多個 I/O 服務是有好處的,每個 I/O 服務有自己的線程,最好是運行在各自的處理器內核上,這樣每一個異步操作連同它們的句柄就可以局部化執行。 如果沒有遠端的數據或函數需要訪問,那么每一個 I/O 服務就象一個小的自主應用。 這里的局部和遠端是指象高速緩存、內存頁這樣的資源。 由于在確定優化策略之前需要對底層硬件、操作系統、編譯器以及潛在的瓶頸有專門的了解,所以應該僅在清楚這些好處的情況下使用多個 I/O 服務。
## 7.4.?網絡編程
雖然 Boost.Asio 是一個可以異步處理任何種類數據的庫,但是它主要被用于網絡編程。 這是由于,事實上 Boost.Asio 在加入其它 I/O 對象之前很久就已經支持網絡功能了。 網絡功能是異步處理的一個很好的例子,因為通過網絡進行數據傳輸可能會需要較長時間,從而不能直接獲得確認或錯誤條件。
Boost.Asio 提供了多個 I/O 對象以開發網絡應用。 以下例子使用了 `boost::asio::ip::tcp::socket` 類來建立與中另一臺PC的連接,并下載 'Highscore' 主頁;就象一個瀏覽器在指向 [www.highscore.de](http://www.highscore.de/) 時所要做的。
```
#include <boost/asio.hpp>
#include <boost/array.hpp>
#include <iostream>
#include <string>
boost::asio::io_service io_service;
boost::asio::ip::tcp::resolver resolver(io_service);
boost::asio::ip::tcp::socket sock(io_service);
boost::array<char, 4096> buffer;
void read_handler(const boost::system::error_code &ec, std::size_t bytes_transferred)
{
if (!ec)
{
std::cout << std::string(buffer.data(), bytes_transferred) << std::endl;
sock.async_read_some(boost::asio::buffer(buffer), read_handler);
}
}
void connect_handler(const boost::system::error_code &ec)
{
if (!ec)
{
boost::asio::write(sock, boost::asio::buffer("GET / HTTP 1.1\r\nHost: highscore.de\r\n\r\n"));
sock.async_read_some(boost::asio::buffer(buffer), read_handler);
}
}
void resolve_handler(const boost::system::error_code &ec, boost::asio::ip::tcp::resolver::iterator it)
{
if (!ec)
{
sock.async_connect(*it, connect_handler);
}
}
int main()
{
boost::asio::ip::tcp::resolver::query query("www.highscore.de", "80");
resolver.async_resolve(query, resolve_handler);
io_service.run();
}
```
* [下載源代碼](src/7.4.1/main.cpp)
這個程序最明顯的部分是三個句柄的使用:`connect_handler()` 和 `read_handler()` 函數會分別在連接被建立后以及接收到數據后被調用。 那么為什么需要 `resolve_handler()` 函數呢?
互聯網使用了所謂的IP地址來標識每臺PC。 IP地址實際上只是一長串數字,難以記住。 而記住象 www.highscore.de 這樣的名字就容易得多。 為了在互聯網上使用類似的名字,需要通過一個叫作域名解析的過程將它們翻譯成相應的IP地址。 這個過程由所謂的域名解析器來完成,對應的 I/O 對象是:`boost::asio::ip::tcp::resolver`。
域名解析也是一個需要連接到互聯網的過程。 有些專門的PC,被稱為DNS服務器,其作用就象是電話本,它知曉哪個IP地址被賦給了哪臺PC。 由于這個過程本身的透明的,只要明白其背后的概念以及為何需要 `boost::asio::ip::tcp::resolver` I/O 對象就可以了。 由于域名解析不是發生在本地的,所以它也被實現為一個異步操作。 一旦域名解析成功或被某個錯誤中斷,`resolve_handler()` 函數就會被調用。
因為接收數據需要一個成功的連接,進而需要一次成功的域名解析,所以這三個不同的異步操作要以三個不同的句柄來啟動。 `resolve_handler()` 訪問 I/O 對象 `sock`,用由迭代器 `it` 所提供的解析后地址創建一個連接。 而 `sock` 也在 `connect_handler()` 的內部被使用,發送 HTTP 請求并啟動數據的接收。 因為所有這些操作都是異步的,各個句柄的名字被作為參數傳遞。 取決于各個句柄,需要相應的其它參數,如指向解析后地址的迭代器 `it` 或用于保存接收到的數據的緩沖區 `buffer`。
開始執行后,該應用將創建一個類型為 `boost::asio::ip::tcp::resolver::query` 的對象 `query`,表示一個查詢,其中含有名字 www.highscore.de 以及互聯網常用的端口80。 這個查詢被傳遞給 `async_resolve()` 方法以解析該名字。 最后,`main()` 只要調用 I/O 服務的 `run()` 方法,將控制交給操作系統進行異步操作即可。
當域名解析的過程完成后,`resolve_handler()` 被調用,檢查域名是否能被解析。 如果解析成功,則存有錯誤條件的對象 `ec` 被設為0。 只有在這種情況下,才會相應地訪問 socket 以創建連接。 服務器的地址是通過類型為 `boost::asio::ip::tcp::resolver::iterator` 的第二個參數來提供的。
調用了 `async_connect()` 方法之后,`connect_handler()` 會被自動調用。 在該句柄的內部,會訪問 `ec` 對象以檢查連接是否已建立。 如果連接是有效的,則對相應的 socket 調用 `async_read_some()` 方法,啟動讀數據操作。 為了保存接收到的數據,要提供一個緩沖區作為第一個參數。 在以上例子中,緩沖區的類型是 `boost::array`,它來自 Boost C++ 庫 Array,定義于 `boost/array.hpp`.
每當有一個或多個字節被接收并保存至緩沖區時,`read_handler()` 函數就會被調用。 準確的字節數通過 `std::size_t` 類型的參數 `bytes_transferred` 給出。 同樣的規則,該句柄應該首先看看參數 `ec` 以檢查有沒有接收錯誤。 如果是成功接收,則將數據寫出至標準輸出流。
請留意,`read_handler()` 在將數據寫出至 `std::cout` 之后,會再次調用 `async_read_some()` 方法。 這是必需的,因為無法保證僅在一次異步操作中就可以接收到整個網頁。 `async_read_some()` 和 `read_handler()` 的交替調用只有當連接被破壞時才中止,如當 web 服務器已經傳送完整個網頁時。 這種情況下,在 `read_handler()` 內部將報告一個錯誤,以防止進一步將數據輸出至標準輸出流,以及進一步對該 socket 調用 `async_read()` 方法。 這時該例程將停止,因為沒有更多的異步操作了。
上個例子是用來取出 www.highscore.de 的網頁的,而下一個例子則示范了一個簡單的 web 服務器。 其主要差別在于,這個應用不會連接至其它PC,而是等待連接。
```
#include <boost/asio.hpp>
#include <string>
boost::asio::io_service io_service;
boost::asio::ip::tcp::endpoint endpoint(boost::asio::ip::tcp::v4(), 80);
boost::asio::ip::tcp::acceptor acceptor(io_service, endpoint);
boost::asio::ip::tcp::socket sock(io_service);
std::string data = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, world!";
void write_handler(const boost::system::error_code &ec, std::size_t bytes_transferred)
{
}
void accept_handler(const boost::system::error_code &ec)
{
if (!ec)
{
boost::asio::async_write(sock, boost::asio::buffer(data), write_handler);
}
}
int main()
{
acceptor.listen();
acceptor.async_accept(sock, accept_handler);
io_service.run();
}
```
* [下載源代碼](src/7.4.2/main.cpp)
類型為 `boost::asio::ip::tcp::acceptor` 的 I/O 對象 `acceptor` - 被初始化為指定的協議和端口號 - 用于等待從其它PC傳入的連接。 初始化工作是通過 `endpoint` 對象完成的,該對象的類型為 `boost::asio::ip::tcp::endpoint`,將本例子中的接收器配置為使用端口80來等待 IP v4 的傳入連接,這是 WWW 通常所使用的端口和協議。
接收器初始化完成后,`main()` 首先調用 `listen()` 方法將接收器置于接收狀態,然后再用 `async_accept()` 方法等待初始連接。 用于發送和接收數據的 socket 被作為第一個參數傳遞。
當一個PC試圖建立一個連接時,`accept_handler()` 被自動調用。 如果該連接請求成功,就執行自由函數 `boost::asio::async_write()` 來通過 socket 發送保存在 `data` 中的信息。 `boost::asio::ip::tcp::socket` 還有一個名為 `async_write_some()` 的方法也可以發送數據;不過它會在發送了至少一個字節之后調用相關聯的句柄。 該句柄需要計算還剩余多少字節,并反復調用 `async_write_some()` 直至所有字節發送完畢。 而使用 `boost::asio::async_write()` 可以避免這些,因為這個異步操作僅在緩沖區的所有字節都被發送后才結束。
在這個例子中,當所有數據發送完畢,空函數 `write_handler()` 將被調用。 由于所有異步操作都已完成,所以應用程序終止。 與其它PC的連接也被相應關閉。
## 7.5.?開發 Boost.Asio 擴展
雖然 Boost.Asio 主要是支持網絡功能的,但是加入其它 I/O 對象以執行其它的異步操作也非常容易。 本節將介紹 Boost.Asio 擴展的一個總體布局。 雖然這不是必須的,但它為其它擴展提供了一個可行的框架作為起點。
要向 Boost.Asio 中增加新的異步操作,需要實現以下三個類:
* 一個派生自 `boost::asio::basic_io_object` 的類,以表示新的 I/O 對象。使用這個新的 Boost.Asio 擴展的開發者將只會看到這個 I/O 對象。
* 一個派生自 `boost::asio::io_service::service` 的類,表示一個服務,它被注冊為 I/O 服務,可以從 I/O 對象訪問它。 服務與 I/O 對象之間的區別是很重要的,因為在任意給定的時間點,每個 I/O 服務只能有一個服務實例,而一個服務可以被多個 I/O 對象訪問。
* 一個不派生自任何其它類的類,表示該服務的具體實現。 由于在任意給定的時間點每個 I/O 服務只能有一個服務實例,所以服務會為每個 I/O 對象創建一個其具體實現的實例。 該實例管理與相應 I/O 對象有關的內部數據。
本節中開發的 Boost.Asio 擴展并不僅僅提供一個框架,而是模擬一個可用的 `boost::asio::deadline_timer` 對象。 它與原來的 `boost::asio::deadline_timer` 的區別在于,計時器的時長是作為參數傳遞給 `wait()` 或 `async_wait()` 方法的,而不是傳給構造函數。
```
#include <boost/asio.hpp>
#include <cstddef>
template <typename Service>
class basic_timer
: public boost::asio::basic_io_object<Service>
{
public:
explicit basic_timer(boost::asio::io_service &io_service)
: boost::asio::basic_io_object<Service>(io_service)
{
}
void wait(std::size_t seconds)
{
return this->service.wait(this->implementation, seconds);
}
template <typename Handler>
void async_wait(std::size_t seconds, Handler handler)
{
this->service.async_wait(this->implementation, seconds, handler);
}
};
```
* [下載源代碼](src/7.5.1/basic_timer.hpp)
每個 I/O 對象通常被實現為一個模板類,要求以一個服務來實例化 - 通常就是那個特定為此 I/O 對象開發的服務。 當一個 I/O 對象被實例化時,該服務會通過父類 `boost::asio::basic_io_object` 自動注冊為 I/O 服務,除非它之前已經注冊。 這樣可確保任何 I/O 對象所使用的服務只會每個 I/O 服務只注冊一次。
在 I/O 對象的內部,可以通過 `service` 引用來訪問相應的服務,通常的訪問就是將方法調用前轉至該服務。 由于服務需要為每一個 I/O 對象保存數據,所以要為每一個使用該服務的 I/O 對象自動創建一個實例。 這還是在父類 `boost::asio::basic_io_object` 的幫助下實現的。 實際的服務實現被作為一個參數傳遞給任一方法調用,使得服務可以知道是哪個 I/O 對象啟動了這次調用。 服務的具體實現是通過 `implementation` 屬性來訪問的。
一般一上諭,I/O 對象是相對簡單的:服務的安裝以及服務實現的創建都是由父類 `boost::asio::basic_io_object` 來完成的,方法調用則只是前轉至相應的服務;以 I/O 對象的實際服務實現作為參數即可。
```
#include <boost/asio.hpp>
#include <boost/thread.hpp>
#include <boost/bind.hpp>
#include <boost/scoped_ptr.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/weak_ptr.hpp>
#include <boost/system/error_code.hpp>
template <typename TimerImplementation = timer_impl>
class basic_timer_service
: public boost::asio::io_service::service
{
public:
static boost::asio::io_service::id id;
explicit basic_timer_service(boost::asio::io_service &io_service)
: boost::asio::io_service::service(io_service),
async_work_(new boost::asio::io_service::work(async_io_service_)),
async_thread_(boost::bind(&boost::asio::io_service::run, &async_io_service_))
{
}
~basic_timer_service()
{
async_work_.reset();
async_io_service_.stop();
async_thread_.join();
}
typedef boost::shared_ptr<TimerImplementation> implementation_type;
void construct(implementation_type &impl)
{
impl.reset(new TimerImplementation());
}
void destroy(implementation_type &impl)
{
impl->destroy();
impl.reset();
}
void wait(implementation_type &impl, std::size_t seconds)
{
boost::system::error_code ec;
impl->wait(seconds, ec);
boost::asio::detail::throw_error(ec);
}
template <typename Handler>
class wait_operation
{
public:
wait_operation(implementation_type &impl, boost::asio::io_service &io_service, std::size_t seconds, Handler handler)
: impl_(impl),
io_service_(io_service),
work_(io_service),
seconds_(seconds),
handler_(handler)
{
}
void operator()() const
{
implementation_type impl = impl_.lock();
if (impl)
{
boost::system::error_code ec;
impl->wait(seconds_, ec);
this->io_service_.post(boost::asio::detail::bind_handler(handler_, ec));
}
else
{
this->io_service_.post(boost::asio::detail::bind_handler(handler_, boost::asio::error::operation_aborted));
}
}
private:
boost::weak_ptr<TimerImplementation> impl_;
boost::asio::io_service &io_service_;
boost::asio::io_service::work work_;
std::size_t seconds_;
Handler handler_;
};
template <typename Handler>
void async_wait(implementation_type &impl, std::size_t seconds, Handler handler)
{
this->async_io_service_.post(wait_operation<Handler>(impl, this->get_io_service(), seconds, handler));
}
private:
void shutdown_service()
{
}
boost::asio::io_service async_io_service_;
boost::scoped_ptr<boost::asio::io_service::work> async_work_;
boost::thread async_thread_;
};
template <typename TimerImplementation>
boost::asio::io_service::id basic_timer_service<TimerImplementation>::id;
```
* [下載源代碼](src/7.5.1/basic_timer_service.hpp)
為了與 Boost.Asio 集成,一個服務必須符合幾個要求:
* 它必須派生自 `boost::asio::io_service::service`。 構造函數必須接受一個指向 I/O 服務的引用,該 I/O 服務會被相應地傳給 `boost::asio::io_service::service` 的構造函數。
* 任何服務都必須包含一個類型為 `boost::asio::io_service::id` 的靜態公有屬性 `id`。在 I/O 服務的內部是用該屬性來識別服務的。
* 必須定義兩個名為 `construct()` 和 `destruct()` 的公有方法,均要求一個類型為 `implementation_type` 的參數。 `implementation_type` 通常是該服務的具體實現的類型定義。 正如上面例子所示,在 `construct()` 中可以很容易地使用一個 `boost::shared_ptr` 對象來初始化一個服務實現,以及在 `destruct()` 中相應地析構它。 由于這兩個方法都會在一個 I/O 對象被創建或銷毀時自動被調用,所以一個服務可以分別使用 `construct()` 和 `destruct()` 為每個 I/O 對象創建和銷毀服務實現。
* 必須定義一個名為 `shutdown_service()` 的方法;不過它可以是私有的。 對于一般的 Boost.Asio 擴展來說,它通常是一個空方法。 只有與 Boost.Asio 集成得非常緊密的服務才會使用它。 但是這個方法必須要有,這樣擴展才能編譯成功。
為了將方法調用前轉至相應的服務,必須為相應的 I/O 對象定義要前轉的方法。 這些方法通常具有與 I/O 對象中的方法相似的名字,如上例中的 `wait()` 和 `async_wait()`。 同步方法,如 `wait()`,只是訪問該服務的具體實現去調用一個阻塞式的方法,而異步方法,如 `async_wait()`,則是在一個線程中調用這個阻塞式方法。
在線程的協助下使用異步操作,通常是通過訪問一個新的 I/O 服務來完成的。 上述例子中包含了一個名為 `async_io_service_` 的屬性,其類型為 `boost::asio::io_service`。 這個 I/O 服務的 `run()` 方法是在它自己的線程中啟動的,而它的線程是在該服務的構造函數內部由類型為 `boost::thread` 的 `async_thread_` 創建的。 第三個屬性 `async_work_` 的類型為 `boost::scoped_ptr<boost::asio::io_service::work>`,用于避免 `run()` 方法立即返回。 否則,這可能會發生,因為已沒有其它的異步操作在創建。 創建一個類型為 `boost::asio::io_service::work` 的對象并將它綁定至該 I/O 服務,這個動作也是發生在該服務的構造函數中,可以防止 `run()` 方法立即返回。
一個服務也可以無需訪問它自身的 I/O 服務來實現 - 單線程就足夠的。 為新增的線程使用一個新的 I/O 服務的原因是,這樣更簡單: 線程間可以用 I/O 服務來非常容易地相互通信。 在這個例子中,`async_wait()` 創建了一個類型為 `wait_operation` 的函數對象,并通過 `post()` 方法將它傳遞給內部的 I/O 服務。 然后,在用于執行這個內部 I/O 服務的 `run()` 方法的線程內,調用該函數對象的重載 `operator()()`。 `post()` 提供了一個簡單的方法,在另一個線程中執行一個函數對象。
`wait_operation` 的重載 `operator()()` 操作符基本上就是執行了和 `wait()` 方法相同的工作:調用服務實現中的阻塞式 `wait()` 方法。 但是,有可能這個 I/O 對象以及它的服務實現在這個線程執行 `operator()()` 操作符期間被銷毀。 如果服務實現是在 `destruct()` 中銷毀的,則 `operator()()` 操作符將不能再訪問它。 這種情形是通過使用一個弱指針來防止的,從第一章中我們知道:如果在調用 `lock()` 時服務實現仍然存在,則弱指針 `impl_` 返回它的一個共享指針,否則它將返回0。 在這種情況下,`operator()()` 不會訪問這個服務實現,而是以一個 `boost::asio::error::operation_aborted` 錯誤來調用句柄。
```
#include <boost/system/error_code.hpp>
#include <cstddef>
#include <windows.h>
class timer_impl
{
public:
timer_impl()
: handle_(CreateEvent(NULL, FALSE, FALSE, NULL))
{
}
~timer_impl()
{
CloseHandle(handle_);
}
void destroy()
{
SetEvent(handle_);
}
void wait(std::size_t seconds, boost::system::error_code &ec)
{
DWORD res = WaitForSingleObject(handle_, seconds * 1000);
if (res == WAIT_OBJECT_0)
ec = boost::asio::error::operation_aborted;
else
ec = boost::system::error_code();
}
private:
HANDLE handle_;
};
```
* [下載源代碼](src/7.5.1/timer_impl.hpp)
服務實現 `timer_impl` 使用了 Windows API 函數,只能在 Windows 中編譯和使用。 這個例子的目的只是為了說明一種潛在的實現。
`timer_impl` 提供兩個基本方法:`wait()` 用于等待數秒。 `destroy()` 則用于取消一個等待操作,這是必須要有的,因為對于異步操作來說,`wait()` 方法是在其自身的線程中調用的。 如果 I/O 對象及其服務實現被銷毀,那么阻塞式的 `wait()` 方法就要盡使用 `destroy()` 來取消。
這個 Boost.Asio 擴展可以如下使用。
```
#include <boost/asio.hpp>
#include <iostream>
#include "basic_timer.hpp"
#include "timer_impl.hpp"
#include "basic_timer_service.hpp"
void wait_handler(const boost::system::error_code &ec)
{
std::cout << "5 s." << std::endl;
}
typedef basic_timer<basic_timer_service<> > timer;
int main()
{
boost::asio::io_service io_service;
timer t(io_service);
t.async_wait(5, wait_handler);
io_service.run();
}
```
* [下載源代碼](src/7.5.1/main.cpp)
與本章開始的例子相比,這個 Boost.Asio 擴展的用法類似于 `boost::asio::deadline_timer`。 在實踐上,應該優先使用 `boost::asio::deadline_timer`,因為它已經集成在 Boost.Asio 中了。 這個擴展的唯一目的就是示范一下 Boost.Asio 是如何擴展新的異步操作的。
[目錄監視器(Directory Monitor)](http://www.highscore.de/boost/dir_monitor.zip) 是現實中的一個 Boost.Asio 擴展,它提供了一個可以監視目錄的 I/O 對象。 如果被監視目錄中的某個文件被創建、修改或是刪除,就會相應地調用一個句柄。 當前的版本支持 Windows 和 Linux (內核版本 2.6.13 或以上)。
## 7.6.?練習
You can buy [solutions to all exercises](http://en.highscore.de/shop/index.php?p=boost-solution) in this book as a ZIP file.
1. 修改 [第?7.4?節 “網絡編程”](asio.html#asio_networkprogramming "7.4.?網絡編程") 中的服務器程序,不在一次請求后即終止,而是可以處理任意多次請求。
2. 擴展 [第?7.4?節 “網絡編程”](asio.html#asio_networkprogramming "7.4.?網絡編程") 中的客戶端程序,即時在所接收到的HTML代碼中分析某個URL。 如果找到,則同時下載相應的資源。 對于本練習,只使用第一個URL。 理想情況下,網站及其資源應被保存在兩個文件中而不是同時寫出至標準輸出流。
3. 創建一個客戶端/服務器應用,在兩臺PC間傳送文件。 當服務器端啟動后,它應該顯示所有本地接口的IP地址并等待客戶端連接。 客戶端則應將服務器端的某一個IP地址以及某個本地文件的文件名作為命令行參數。 客戶端應將該文件傳送給服務器,后者則相應地保存它。 在傳送過程中,客戶端應向用戶提供一些進度的可視顯示。