Boost學習之深入理解asio庫

Asio簡介

Boost C++ 庫 Asio,它是異步輸入輸出的核心。 名字本身就說明了一切:Asio 即異步輸入/輸出。 該庫可以讓 C++ 異步地處理數據,且平臺獨立。 異步數據處理就是指,任務觸發後不需要等待它們完成。 相反,Boost.Asio 會在任務完成時觸發一個應用。 異步任務的主要優點在於,在等待任務完成時不需要阻塞應用程序,可以去執行其它任務。

異步任務的典型例子是網絡應用。 如果數據被髮送出去了,比如發送至 Internet,通常需要知道數據是否發送成功。 如果沒有一個像 Boost.Asio 這樣的庫,就必須要等待接收函數的返回值,並得到一個確認或是錯誤代碼。 而使用 Boost.Asio,這個過程被分爲兩個單獨的步驟:第一步是作爲一個異步任務開始數據傳輸。 一旦傳輸完成,不論成功或是錯誤,應用程序都會在第二步中得到相應的結果通知。 主要的區別在於,應用程序無需阻塞至傳輸完成,而可以在這段時間裏執行其它操作。

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 << "5s" << 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();
	return 0;
}

函數 main() 首先定義了一個 I/O 服務 io_service ,用於初始化 I/O 對象 timer 。 由於 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 handler(const boost::system::error_code& ec)
{
	std::cout << "5s" << std::endl;
}

void handler1(const boost::system::error_code& ec)
{
	std::cout << "2m " << 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);

	boost::asio::deadline_timer timer1(io_service, boost::posix_time::minutes(2));
	timer1.async_wait(handler1); 

	io_service.run(); 
	return 0;
}

上面的程序用了兩個 boost::asio::deadline_timer 類型的 I/O 對象。 第一個 I/O 對象表示一個5秒後觸發的鬧鐘,而第二個則表示一個2分鐘後觸發的鬧鐘。 每一段指定時長過去後,都會相應地調用函數 handler() handler1()

main() 的最後,再次在唯一的 I/O 服務之上調用了 run() 方法。 如前所述,這個函數將阻塞執行,把控制權交給操作系統以接管異步處理。 在操作系統的幫助下,handler() 函數會在五秒後被調用,而 handler1() 函數則在2分鐘後被調用。

很多人會問,爲什麼異步處理還要調用阻塞式的 run() 方法?很明顯,應用程序必須防止被中止執行,所以這樣做實際上不會有任何問題。 如果 run() 不是阻塞的,main() 就會結束從而中止該應用程序。 如果應用程序不應被阻塞,那麼就應該在一個新的線程內部調用 run(),它自然就會僅僅阻塞那個線程。

一旦特定的 I/O 服務的所有異步操作都完成了,控制權就會返回給 run() 方法,然後它就會返回。 以上兩個例子中,應用程序都會在鬧鐘到時間後馬上結束。

擴展與多線程

用 Boost.Asio 這樣的庫來開發應用程序,與一般的 C++ 風格不同。 那些可能需要較長時間才返回的函數不再是以順序的方式來調用。 不再是調用阻塞式的函數,Boost.Asio 是啓動一個異步操作。 而那些需要在操作結束後調用的函數則實現爲相應的句柄。 這種方法的缺點是,本來順序執行的功能變得在物理上分割開來了,從而令相應的代碼更難理解。

像 Boost.Asio 這樣的庫通常是爲了使應用程序具有更高的效率。 應用程序不需要等待特定的函數執行完成,而可以在期間執行其它任務,如開始另一個需要較長時間的操作。一般用在併發操作中,每次執行的任務都是相同的原子性任務,每個任務執行先後沒有順序概念。比如博主最近實現的httpserver,多個客戶端請求實時視頻,可以抽象爲一個個原子操作,每個原子任務可以單獨異步執行,沒有任何先後順序。

如果在某個 boost::asio::io_service 類型的對象之上調用 run() 方法,則相關聯的句柄也會在同一個線程內被執行。 通過使用多線程,應用程序可以同時調用多個 run() 方法。 一旦某個異步操作結束,相應的 I/O 服務就將在這些線程中的某一個之中執行句柄。 如果第二個操作在第一個操作之後很快也結束了,則 I/O 服務可以在另一個線程中執行句柄,而無需等待第一個句柄終止。

#include<boost/asio.hpp>
#include<boost/thread/thread.hpp>
#include<iostream>

boost::asio::io_service io_service; //創建io_service對象,後續初始化所有boost對象都需要傳入該服務。與操作系統進行交互。

void handler(const boost::system::error_code& ec)
{
	std::cout << "handler 5 s" << std::endl;
}

void handler1(const boost::system::error_code& ec)
{
	std::cout << "handler1 20 s" << std::endl;
}

void run()
{
	io_service.run();
}

int main()
{
	boost::asio::deadline_timer timer1(io_service, boost::posix_time::seconds(20));
	timer1.async_wait(handler1);

	boost::asio::deadline_timer timer(io_service, boost::posix_time::seconds(5));
	timer.async_wait(handler);

	boost::thread thread1(run);
	boost::thread thread2(run);

	thread1.join();
	thread2.join();
	return 0;
}

運行結果:
在這裏插入圖片描述
以上代碼段是多線程的應用, 通過使用在 boost/thread.hpp 中定義的 boost::thread 類,在 main() 中創建了兩個線程。 這兩個線程均針對同一個 I/O 服務調用了 run() 方法。 這樣當異步操作完成時,這個 I/O 服務就可以使用兩個線程中的任意一個去執行句柄函數。

這個例子中的第一個計時數20s後執行,第二個5s後執行。 由於有兩個線程,所以 handler()handler1() 可以並行執行,通過運行結果可以看出,第二個執行完後,第一個過了15秒才執行結束。 如果第二個計時器觸發時第一個仍在執行,則第二個句柄就會在第二個線程中執行。 如果第一個計時器的句柄已經終止,則 I/O 服務可以自由選擇任一線程。

要注意,使用線程並不總是值得的。 多線程程序運行會導致不同信息在標準輸出流上混合輸出,因爲這兩個句柄可能會並行運行,訪問同一個共享資源:標準輸出流 std::cout。 這種訪問必須被同步,以保證每一條信息在另一個線程可以向標準輸出流寫出另一條信息之前被完全寫出。這就涉及到在多線程編程中,死鎖的情況,即多個線程搶佔同一份資源,導致死鎖。這種情況解決方法在後面的博客中會講到。

多次調用同一個 I/O 服務的 run() 方法,是爲基於 Boost.Asio 的應用程序增加可擴展性的推薦方法。 另外還有一個不同的方法:不要綁定多個線程到單個 I/O 服務,而是創建多個 I/O 服務。 然後每一個 I/O 服務使用一個線程。 如果 I/O 服務的數量與系統的處理器內核數量相匹配,則異步操作都可以在各自的內核上執行。

#include<boost/asio.hpp>
#include<boost/thread/thread.hpp>
#include<iostream>

boost::asio::io_service io_service1;
boost::asio::io_service io_service2;

void handler(const boost::system::error_code &ec)
{
	std::cout << "handler run 20s" << std::endl;
}

void handler1(const boost::system::error_code &ec)
{
	std::cout << "handler1 run 5s" << std::endl;
}

void run1()
{
	io_service1.run();
}

void run2()
{
	io_service2.run();
}

int main()
{
	boost::asio::deadline_timer timer1(io_service1, boost::posix_time::seconds(20));
	timer1.async_wait(handler);

	boost::asio::deadline_timer timer2(io_service2, boost::posix_time::seconds(5));
	timer2.async_wait(handler1);

	boost::thread thread1(run1);
	boost::thread thread2(run2);

	thread1.join();
	thread2.join(); 
	return 0;
}

運行結果:
在這裏插入圖片描述

前面使用兩個計時器的例子被重寫爲使用兩個 I/O 服務。 這個應用程序仍然基於兩個線程;但是現在每個線程被綁定至不同的 I/O 服務。 此外,兩個 I/O 對象 timer1timer2 現在也被綁定至不同的 I/O 服務。

這個應用程序的功能與前一個相同。 在一定條件下使用多個 I/O 服務是有好處的,每個 I/O 服務有自己的線程,最好是運行在各自的處理器內核上,這樣每一個異步操作連同它們的句柄就可以局部化執行。 如果沒有遠端的數據或函數需要訪問,那麼每一個 I/O 服務就像一個小的自主應用。 這裏的局部和遠端是指類似高速緩存、內存頁這樣的資源。 由於在確定優化策略之前需要對底層硬件、操作系統、編譯器以及潛在的瓶頸有專門的瞭解,所以應該僅在清楚這些好處的情況下使用多個I/O 服務。

Asio網絡編程

雖然 Boost.Asio 是一個可以異步處理任何種類數據的庫,但是它主要被用於網絡編程。 網絡功能是異步處理的一個很好的例子,因爲通過網絡進行數據傳輸可能會需要較長時間,從而不能直接獲得確認或錯誤條件。

Boost.Asio 提供了多個 I/O 對象以開發網絡應用。 以下例子使用了 boost::asio::ip::tcp::socket 類來建立與另一臺PC的連接,並下載 ‘baidu’ 主頁。

#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 transferred_byte)
{
	if (!ec)
	{
		std::cout << std::string(buffer.data(), transferred_byte) << 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: www.baidu.com\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.baidu.com", "80");
	resolver.async_resolve(query, resolve_handler);
	io_service.run();

	system("pause");
	return 0;
}

這個程序最明顯的部分是三個句柄的使用:connect_handler()read_handler() 函數會分別在連接被建立後以及接收到數據後被調用。 那麼爲什麼需要 resolve_handler() 函數呢?

互聯網使用了IP地址來標識每臺PC。 IP地址實際上只是一長串數字,難以記住。 而記住象 www.baidu.com 這樣的名字就容易得多。 爲了在互聯網上使用類似的名字,需要通過一個叫作域名解析的過程將它們翻譯成相應的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.baidu.com以及互聯網常用的端口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

每當有一個或多個字節被接收並保存至緩衝區時,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() 方法。 這時該例程將停止。

以下示例將講述通過本地連接,實現異步socket功能。

#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(); 
} 

類型爲 boost::asio::ip::tcp::acceptor 的 I/O 對象 acceptor , 被初始化爲指定的協議和端口號 ,用於等待從其它PC傳入的連接。 初始化工作是通過endpoint 對象完成的,該對象的類型爲 boost::asio::ip::tcp::endpoint,將本例中的接收器配置爲使用端口80來等待 IP v4 的傳入連接。

接收器初始化完成後,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的連接也被將被關閉。

寫在結尾

本文在深入學習研究Boost::asio庫後,對學習的每個示例,進行詳細步驟的分解。因爲異步操作代碼執行邏輯比較混亂,只看代碼理解起來有一定的難度。由於剛接觸boost庫,後續可能會在開發中大量使用,因此決定重頭開始研究boost庫的使用。本文參考其他資料初步學習總結,如果在閱讀中有發現錯誤或者其他bug,請隨時聯繫博主修改。如果本文對你學習或者工作有幫助,請點贊支持關注我,後續還會繼續輸出相關學習心得。

參考資料
https://theboostcpplibraries.com/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章