最近嘗試使用了一下Boost.Asio,不知道是否由於各大公司都有本身相對成熟的網絡庫的緣故,網絡上Asio相關的資料實在很少,並且不少翻來覆去就是那幾個簡單的示例,因此打算本身小結一下。總的來講Boost.Asio是個很是易用的庫,避免了你在各類系統底層API之間的掙扎,讓你能夠很是迅速的開發出高併發的網絡服務器程序。html
asio基於兩個概念:c++
I/O服務,抽象了操做系統的異步接口 boost::asio::io_service::service
:數組
boost::asio::io_service
I/O對象,有多種對象 boost::asio::basic_io_object
:緩存
boost::asio::ip::tcp::socket
boost::asio::ip::tcp::resolver
boost::asio::ip::tcp::acceptor
boost::asio::local::stream_protocol::socket
本地鏈接boost::asio::posix::stream_descriptor
面向流的文件描述符,好比stdout
, stdin
boost::asio::deadline_timer
定時器boost::asio::signal_set
信號處理全部 I/O 對象一般都須要一個 I/O 服務做爲它們的構造函數的第一個參數,好比:服務器
boost::asio::io_service io_service; boost::asio::deadline_timer timer(io_service, boost::posix_time::seconds(5));
在必定條件下使用多個 io_service
是有好處的,每一個 io_service
有本身的線程,最好是運行在各自的處理器內核上,這樣每個異步操做連同它們的句柄就能夠局部化執行。 若是沒有遠端的數據或函數須要訪問,那麼每個 io_service
就象一個小的自主應用。 這裏的局部和遠端是指象高速緩存、內存頁這樣的資源。 因爲在肯定優化策略以前須要對底層硬件、操做系統、編譯器以及潛在的瓶頸有專門的瞭解,因此應該僅在清楚這些好處的狀況下使用多個 io_service
。網絡
和咱們熟知的Reactor不一樣,Asio使用Proactor模型:多線程
能夠看出asio本質就是維護着一個任務隊列,調用post()
方法接收handler做爲參數加入隊列,或者調用async_*()
方法接收handler做爲參數和對應的I/O對象加入隊列(handler實際藉助boost::bind成爲一個closure,能夠複製到隊列),在Linux系統下會在epoll空閒時或有I/O事件觸發後執行。可是asio與Reactor不一樣的地方在於前者當事件到來時會自動讀寫緩衝區,等I/O操做完成後才調用原先註冊的handler,把執行流交割;而Reactor當事件到來時,即交由調用者本身處理剩下的I/O操做。閉包
work
類用於通知io_service
是否能夠結束,只要對象work(io_service)
存在,io_service
就不會結束。因此work
類用起來更像是一個標識,好比:併發
boost::asio::io_service io_service; boost::asio::io_service::work* work = new boost::asio::io_service::work( io_service ); // delete work; // 若是不註釋掉這一句,則run loop不會退出;通常用shared_ptr維護work對象,使用work.reset()來結束其生命週期。 io_service.run()
run()
和poll()
都循環執行I/O對象的事件,區別在於若是事件沒有被觸發(ready),run()
會等待,可是poll()
會當即返回。也就是說poll()
只會執行已經觸發的I/O事件。框架
好比I/O對象socket1
, socket2
, socket3
都綁定了socket.async_read_some()
事件,而此時socket1
、socket3
有數據過來。則調用poll()
會執行socket1
、socket3
相應的handler,而後返回;而調用run()
也會執行socket1
和socket3
的相應的handler,但會繼續等待socket2
的讀事件。
調用 io_service.stop()
會停止 run loop,通常在多線程中使用。
post()
和dispatch()
都是要求io_service
執行一個handler,可是dispatch()
要求當即執行,而post()
老是先把該handler加入事件隊列。
何時須要使用post()
?當不但願當即調用一個handler,而是異步調用該handler,則應該調用post()
把該handler交由io_service
放到事件隊列裏去執行。好比,Boost.Asio自帶的聊天室示例,其中實現了一個支持異步IO的聊天室客戶端,是個很好的例子。
chat_client.cpp 的write()
函數之因此要使用post()
,是爲了不臨界區同步問題。write()
調用和do_write()
裏async_write()
的執行分別屬於兩個線程,前者會往write_msgs_
裏寫數據,然後者會從write_msgs_
裏讀數據,若是不使用post()
,而直接調用do_write()
,顯然須要使用鎖來同步write_msgs_
。可是使用post()
至關於由io_service
來調度write_msgs_
的讀寫,這就在一個線程內完成,無需額外的鎖機制。
buffer類分mutable_buffer
和const_buffer
兩個類,buffer類特別簡單,僅有兩個成員變量:指向數據的指針 和 相應的數據長度。buffer類自己並不申請內存,只是提供了一個對現有內存的封裝。
須要注意的是,全部async_write()
、async_read()
之類函數接受的buffer類型是 MutableBufferSequence
/ ConstBufferSequence
,這意味着它們既能夠接受boost::asio::buffer
,也能夠接受 std::vector<boost::asio::buffer>
這樣的類型。
緩衝區的生命期是使用asio最須要重視的兩件事之一,緩衝區之因此須要重視的緣由在於Asio異步調用Reference裏的這段描述:
Although the buffers object may be copied as necessary, ownership of the underlying memory blocks is retained by the caller, which must guarantee that they remain valid until the handler is called.
這意味着緩衝區從發起異步調用到handler被執行,這段時間內須要交由io_service
控制,這個限制經常致使asio的某些代碼變得可能比Reactor相應代碼還要麻煩一些。
仍是舉上面聊天室的那個例子。chat_client.cpp 的do_write()
函數收到用戶輸入數據後,之因此把該數據保存到std::deque<std::string> write_msgs_
隊列,而不是存到相似char data[]
的數組裏,而後去調用async_write(..data..)
發送數據,是爲了不這種狀況:輸入數據速度過快,當上一次async_write()
調用的handler尚未來得及處理,又收到一份新的數據,若是直接保存到data
,會致使覆蓋上一次async_write()
的緩衝區。async_write()
要求這個緩衝區從調用async_write()
開始,直到handler處理這個時間段是不變的。
一樣的,在do_write()
函數裏調用async_write()
函數以前,先判斷write_msgs_
隊列是否爲空,也是爲了保證async_write()
老是從write_msgs_
隊列頭取得有效的數據,而在handle_write()
裏當數據發送完畢後,再pop_front()
彈出已經發送的數據包。以此避免出現前一個async_write()
的handler還沒執行完畢,就把隊列頭彈出去,致使對應的緩衝區失效問題。
這裏主要仍是由於async_write()
和async_read()
的區別,前者是主動發起的,後者能夠由io_service
控制,因此後者不用擔憂這種緩衝區被覆蓋問題。由於在同一個線程裏,哪怕須要讀取的事件觸發得再快,也須要由io_service
逐一處理。
在這個聊天室的例子裏,若是不考慮把數據按用戶輸入順序發送出去的話,可使用更簡單的辦法來處理do_write()
函數,例如:
:::c++ void do_write(chat_message msg) { chat_message* pmsg = new chat_message(msg); // implement copy ctor for chat_message firstly boost::asio::async_write(socket_, boost::asio::buffer(pmsg->data(), pmsg->length()), boost::bind(&chat_client::handle_write, this, boost::asio::placeholders::error, pmsg)); } void handle_write(const boost::system::error_code& error, chat_message* pmsg) { if (!error) { }else{ do_close(); } delete pmsg; }
這裏至關於給每一個異步調用分配一塊屬於本身的內存,異步調用完成即自動釋放掉,有些相似於閉包了。若是不但願頻繁new/delete內存,也能夠考慮使用boost::circular_buffer
一次性分配內存後逐項使用。
Boost.Asio最經常使用的對象應該就是socket了,經常使用的函數通常有這幾個:
read()
, async_read()
, write()
, async_write()
,爲了不所謂的short reads and writes,通常不使用receive()
, async_receive()
, send()
, async_send()
。receive()
, async_receive()
, send()
, async_send()
。receive_from()
, async_receive_from()
, send_to()
, async_send_to()
。而自由函數boost::asio::async_write()
和類成員函數socket.async_write_some()
的有什麼區別呢(boost::asio::async_read()
和socket.async_read_some()
相似):
boost::asio::async_write()
異步寫,當即返回。但它能夠保證寫完整個緩衝區的內容,不然將報錯。boost::asio::async_write()
是經過調用n次socket.async_write_some()
來實現的,因此代碼必須確保在boost::asio::async_write()
執行的時候,沒有其餘的寫操做在同一socket上執行。boost::asio::async_write()
的時候,若是指定buffer的length沒有寫完或出錯,是不會回調相應的handler的,它將一直在run loop中執行;直到buffer裏全部的數據都寫完或出錯(此時handler裏返回的長度確定會小於buffer length),纔會調用handler繼續處理;而socket.async_write_some()
不會有這樣的問題,它只會嘗試寫一次,寫完的長度會在handler的參數裏返回。因此,這裏強調使用asio時第二件須要重視的事情,就是handler的返回值(通常可能聲明爲boost::asio::placeholders::error
)。由於asio裏全部的任務都由io_service
異步執行,只有執行成功或者失敗以後纔會回調handler,因此返回值是你瞭解當前異步操做情況的惟一辦法,記住不要忽略任何handler的返回值處理。
Boost.Asio的信號處理很是簡單,聲明一個信號集合,而後把相應的異步handler綁上就能夠了。若是你但願在一個信號集裏處理全部的信號,那麼你能夠根據handler的第二個參數,來獲取當前觸發的是那個信號。好比:
boost::asio::signal_set signals(io_service, SIGINT, SIGTERM); signals.add(SIGUSR1); // 也能夠直接用add函數添加信號 signals.async_wait(boost::bind(handler, _1, _2)); void handler( const boost::system::error_code& error, int signal_number // 經過這個參數獲取當前觸發的信號值 );
Boost.Asio的定時器用起來根信號集同樣簡單,但因爲它太過簡單,也有不方便的地方。好比,在一個UDP伺服器裏,通常收到的每一個UDP包中都會包含一個sequence number,用於標識該UDP,以應對包處理超時狀況。假設每一個UDP包處理時間只有100ms,若是超時則直接給客戶端返回超時標記。這種最簡單的定時器經常使用的一些Reactor框架都有很完美的解決方案,通常是建一個定時器鏈表來實現,可是Asio中的定時器無法單獨完成這個工做。
boost::asio::deadline_timer
只有兩種狀態:超時和未超時。因此,只能很土的對每一個UDP包建立一個定時器,而後藉助std::map
和boost::shared_ptr
保存sequence number到定時器的映射,根據定時器handler的返回值判斷該定時器是超時,仍是被主動cancel。
在多線程中,多個I/O對象的handler要訪問同一塊臨界區,此時可使用strand
來保證這些handler之間的同步。
示例:
咱們向定時器註冊 func1 和 func2,它們可能會同時訪問全局的對象(好比 std::cout )。這時咱們但願對 func1 和 func2 的調用是同步的,即執行其中一個的時候,另外一個要等待。
這時就能夠用到 boost::asio::strand
類,它能夠把幾個cmd包裝成同步執行的。例如,咱們向定時器註冊 func1 和 func2 時,能夠改成:
boost::asio::strand the_strand; t1.async_wait(the_strand.wrap(func1)); //包裝爲同步執行的 t2.async_wait(the_strand.wrap(func2));
這樣就保證了在任什麼時候刻,func1 和 func2 都不會同時在執行。
還有就是若是你但願把一個io_service
對象綁定到多個線程。此時須要boost::asio::strand
來確保handler不會被同時執行,由於異步操做,好比async_write
、async_receive_from
之類會影響到臨界區buffer。
具體可參考asio examples裏的示例:HTTP Server 2和HTTP Server 3的connection.hpp設計。
關於boost asio最好的參考書固然就是官方文檔和它的源碼,此外還有兩個不錯的資料:
from:http://blog.jqian.net/post/boost-asio.html