原文鏈接:http://blog.csdn.net/ithiker/article/details/22153001
一 簡介
Boost Asio ( asynchronous input and output)關注異步輸入輸出。Boost Asio庫提供了平臺無關性的異步數據處理能力(當然它也支持同步數據處理)。一般的數據傳輸過程需要通過函數的返回值來判斷數據傳輸是否成功。Boost Asio將數據傳輸分爲兩個獨立的步驟:
- 採用異步任務的方式開始數據傳輸。
- 將傳輸結果通知調用端
與傳統方式相比,優點在於程序在數據傳輸期間不會阻塞。
二 I/O services and I/O objects
應用程序採用Boost.Asio進行異步數據處理主要基於 I/O services 和 I/O objects。I/O services抽象系統I/O接口,提供異步數據傳輸的能力,它是應用程序和系統I/O接口的橋樑。I/O objects 用來初始化某些特定操作,如TCP socket,提供TCP方面可靠數據傳輸的能力。Boost.Asio只提供一個類實現 I/O
services, boost::asio::io_service。提供多個I/O objects對象,如boost::asio::ip::tcp::socket(用來收發數據)和
boost::asio::deadline_timer(用來提供計時器的功能,計時器可以在某個時間點或經歷某個時間段後生效)
。由於計時器不涉及到太多的網絡方面的內容,用其舉例說明一下Boost.Asio的初步用法:
- #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();
- }
首先定義一個boost::asio::io_service來初始化I/O objects實例timer. 由於
I/O objects對象
構造函數的第一個參數都是一個I/O service對象,timer也不例外,timer的第二個參數可以是時間點或時間段,上例中是一個時間段。
async_wait()
表示時間到後調用handler,應用程序可以執行其它操作而不會阻塞在sync_wait處。async_wait()
是一種非阻塞的函數,timer也提供阻塞的函數wait(),由於它在調用結束後返回,因而不需要handler作爲其參數。
可以發現,在調用 async_wait()
之後,I/O service調用了run方法。這是必須的,因爲我們必須把控制權交給操作系統,以便在5s之後調用handler方法。也就是說,async_wait()
在調用後立即返回,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();
- }
從上面我們可以發現handler2的調用是在handler1調用5s後進行的,這也正是異步的精髓所在:timer2沒有等到timer1計時5s後才啓動。之所以異步操作看上去需要阻塞的run()方法,是因爲我們必須阻止程序終結:如果run()不阻塞,main()就會馬上退出了。由於run()會阻塞進程,如果不想阻塞當前進程,我們可以在另外一個進程中調用run().
三 可擴展性和多線程
採用Boost.Asio開發應用程序和通常的C++風格不同:在Boost.Asio中需要較長時間才返回的functions的調用不是有序的,這是因爲對於阻塞的方法,Boost.Asio 採用異步操作。通過上面所示的handler形式來實現當某個操作完成後就必須被調用的方法。採用這種方法的缺點是順序執行函數的人爲分割,使得相應的代碼更難理解。
採用Boost.Asio庫的主要目的是爲了實現程序的高效,不用等待某個function結束,應用程序可以在這期間進行其它任務的運行,例如,開始某個可能需要花費一段時間才能完成的操作。
擴展性是指程序有效的利用計算機的其它資源。推薦採用Boost.Asio的原因之一是持續時間長的操作不會阻塞其它操作,另外,由於現有計算機一般都是多核的,採用多線程可以有效的提升程序的可擴展性。
在上面的程序中,採用boost::asio::io_service調用run()方法,和boost::asio::io_service相關聯的handler將會在同一線程內觸發。通過採用多線程,應用程序可以同時調用多個run()方法。一旦某個異步操作完成,對應的I/O service將會執行某個線程中的handler方法。如果第二個異步操作在第一個結束後很快完成,I/O service 可以立刻執行其對應的handler,而不用等待第一個handler執行完畢。
- #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();
- }
上一小節中的程序現在被轉換爲了一個多線程的程序。通過使用定義在boost/thread.hpp中的boost::thread類,在main()中創建了兩個線程。這兩個線程爲同一個I/O service調用run()。這樣做的好處是,一旦獨立的異步操作完成,I/O service可以有效利用兩個線程來執行handler方法。
上面例子中的兩個timer都是讓時間停頓5s。由於有兩個線程,handler1和handler2可以同時執行。如果timer2在停頓期間,timer1對應的handler1仍然在執行,那麼handler2將會在第二個線程內執行。如果handler1已經結束了,那麼I/O service將會自由選擇線程來執行handler2。
線程可以增加程序的執行效率。因爲線程跑在CPU的核上,創建比CPU核數還多的線程是沒有意義的。這可以保證每個線程跑在各自的核上,而不會出現多個線程爲搶佔某個核的”核戰爭”。
應該注意到,採用線程也不是總是合理的。執行上面的代碼可能會導致各自信息在標準輸出流上產生混合的輸出,這是因爲兩個hander方法可能會並行的執行到,而他們訪問的是一個共享的標準輸出流std::cout。對共享資源的訪問需要進行同步,從而保證每條消息完全輸出後,另外一個線程才能夠向標準輸出寫入另外一條消息。如果線程各自的handler不能獨立的並行執行(handler1的輸出可能影響到handler2),在這種場景下使用線程不會帶來什麼好處。
基於Boost.Asio來提高程序的可擴展性推薦的方法是:採用單個I/O service多次調用run()方法。當然,也有另外的方法可以選擇:可以創建多個I/O service,而不是將所有的線程都綁定到一個I/O service上。每個I/O service對應於一個線程。如果I/O service的個數和計算機的核數相匹配,異步操作將會在各自對應的核上運行。下面是一個這樣的例子:
- #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();
- }
上面採用一個I/O service的程序被重寫成了採用兩個I/O service的程序。程序還是有兩個線程,只不過每個線程現在對應的是不同的I/O service,同時,timer和timer2也和不同的I/O service相對應。
程序的功能和以前的相同。擁有多個I/O service在某種情況下是有益的,理想情況下,每個I/O service擁有自己的線程,跑在各自的核上,這樣,不同的異步操作以及其對應的handler方法可以在局部執行。這樣就不會出現上面兩個線程共享同一個標準輸出的情況了。如果沒有訪問外部數據和方法的需要,每個I/O service就等同於一個獨立的程序。在制定優化策略前,由於需要了解硬件,操作系統,編譯器以及潛在的瓶頸等相關知識,採用多個I/O service只有在確實可以從中獲益的情況下才使用。
四 網絡編程
儘管Boost.Asio是一個可以進行異步數據處理的庫,它主要用在網絡編程上。這是因爲Boost.Asio在添加其它I/O objects前,很早就支持網絡功能了。網絡功能是一個完美的異步處理的例子,因爲數據在網絡上的傳輸可能需要花費更多的時間,相應的應答或出錯情況往往不是直接可以獲得的。
Boost.Asio提供很多I/O objects來開發網絡程序。下面的例子採用boost::asio::ip::tcp::socket來建立和不同PC間的連接,同時下載’Highscore'首頁--就像瀏覽器訪問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();
- }
上面例子中,最值得關注的是三個handler方法:一旦建立連接以及接收到數據,將會分別調用connect_handler()和read_handler(),那麼爲什麼需要resolve_handler()?
因特網採用IP地址來標識不同的計算機。IP地址實質上是一連串不好記的數字,記住域名比記住數字好的多。爲了用域名訪問計算機,必須將域名轉換爲對應的IP地址,這個過程也就是名稱解析。在Boost.Asio中,用boost::asio::ip::tcp::resolver來實現名稱解析。
名稱解析需要聯網才能完成。一些特定的PC(DNS服務器),負責將域名轉換爲IP地址。boost::asio::ip::tcp::resolver I/O object所完成的事情就是連接外網獲取域名對應的IP,由於名稱解析不是發生在本地,因而它也是作爲一個異步操作實現的。一旦名稱解析完成(不管成功還是返回失敗),就會調用resolve_handler()。
由於獲取數據的前提是成功建立鏈接,而成功建立鏈接的前提又是成功進行名稱解析,這樣不同的異步操作將會在不同的handler內部進行。resolve_handler()利用由it提供的獲取到的ip地址,通過I/O object sock來建立連接。在connect_handler()內,採用sock來發送HTTP請求來初始化數據接收。由於所有的操作都是異步的,各自的處理方法的函數名是通過參數的形式進行傳遞的。由於handler的不同,需要不同的參數,例如迭代器it,指向獲取到的IP地址;緩存buffer,儲存接收到的數據。
程序開始運行時就會創建一個query對象並用域名和端口號對齊進行初始化,接着query對象傳遞給async_resolve()方法去進行名字解析。最後,main()方法調用I/O service的run()方法來將異步操作的控制權交給操作系統。
一旦名稱解析完成,resolve_handler()將會被調用,首先它將檢查名稱解析是否成功,如果成功,包含各種錯誤情形的對象object ec,將會被設置爲0。只有在這種情況下,程序纔會訪問sock來創建一個連接。鏈接需要的IP地址由第二個參數it提供。
調用完async_connect後,connect_handler()又會自動被調用。在connect_handler()內部,同樣通過object ec對象來判斷連接是否成功建立。如果連接成功建立,會調用async_read_some()方法來初始化對應socket上的read操作。數據存儲在第一個參數表明的buffer內部。在上面的例子中,buffer是boost::array類型的,定義在boost/array.hpp中。
read_handler()方法在有數據接收並儲存到buffer中後就立刻被調用。接收到的數據大小通過參數bytes_transferred可以得到。同樣的,通過object ec對象來判斷接收過程中是否出錯。如果接收成功,數據將會重定向到標準輸出。
一旦數據寫到標準輸出 read_handler()將會再次調用async_read_some(),這是因爲數據不會一次讀完。
上面的例子用來獲取網頁內容,下面的例子則是實現了一個簡單的web server.最重要的區別是,程序不會連接到別的服務器,而是等待別人向其發起連接,如本機IP是192.168.100.100,我們在瀏覽器中輸入http://192.168.100.100,將會出現Hello,world!。
- #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();
- }
上面用帶有協議和端口號的endpoint對象初始化了的I/O object acceptor,acceptor主要用來等待從其它PC過來的連接。在初始化acceptor後,main()函數中首先調用listen()方法來將acceptor設置爲接收模式,然後調用async_accept來等待連接。用來接收和發送數據的socket在第一個參數中進行了指定。
一旦有計算機試圖建立連接,accept_handler將會自動被調用。如果連接請求成功,可以獨立運行的函數boost::asio::async_write將會被調用,它將存儲在data中的數據通過socket發送出去,boost也提供了async_write_some方法,只要有數據發送出去,該方法將會觸發相應的handler方法,handler方法需要計算已經發送了多少數據,同時再次觸發async_write_some,直到數據都被傳送出去。採用async_write方法可以避免上面分析中的複雜過程,因爲async_write的異步操作只有在所有的數據都發送出去後纔會停止。
在上面的例子中,一旦所有的數據都發送出去了,空方法write_handler將會被調用。由於所有的異步操作都結束了,整個應用程序就結束了。建立的連接也相應的關閉了。
【原文】 http://en.highscore.de/cpp/boost/asio.html