1. 程式人生 > >Boost Asio介紹--之一

Boost Asio介紹--之一

一  簡介 

        Boost Asio ( asynchronous input and output)關注非同步輸入輸出。Boost Asio庫提供了平臺無關性的非同步資料處理能力(當然它也支援同步資料處理)。一般的資料傳輸過程需要通過函式的返回值來判斷資料傳輸是否成功。Boost Asio將資料傳輸分為兩個獨立的步驟:

    1. 採用非同步任務的方式開始資料傳輸。
    2. 將傳輸結果通知呼叫端

        與傳統方式相比,優點在於程式在資料傳輸期間不會阻塞。

二 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