Boost.Asio入门
原文地址:https://mmoaay.gitbooks.io/boost-asio-cpp-network-programming-chinese/content/Chapter1.html
Boost.Asio入門
首先,讓我們先來了解一下什么是Boost.Asio?怎么編譯它?了解的過程中我們會給出一些例子。然后在發(fā)現(xiàn)Boost.Asio不僅僅是一個網(wǎng)絡(luò)庫的同時你也會接觸到Boost.Asio中最核心的類——io_service。
什么是Boost.Asio
簡單來說,Boost.Asio是一個跨平臺的、主要用于網(wǎng)絡(luò)和其他一些底層輸入/輸出編程的C++庫。
計算機網(wǎng)絡(luò)的設(shè)計方式有很多種,但是Boost.Asio的的方式遠遠優(yōu)于其它的設(shè)計方式。它在2005年就被包含進Boost,然后被大量Boost的用戶測試并在很多項目中使用,比如Remobo(http://www.remobo.com),可以讓你創(chuàng)建你自己的即時私有網(wǎng)絡(luò)(IPN)的應用,libtorrent(http://www.rasterbar.com/products/libtorrent))一個實現(xiàn)了比特流客戶端的庫,PokerTH (http://www.pokerth.net)一個支持LAN和互聯(lián)網(wǎng)對戰(zhàn)的紙牌游戲。
Boost.Asio在網(wǎng)絡(luò)通信、COM串行端口和文件上成功地抽象了輸入輸出的概念。你可以基于這些進行同步或者異步的輸入輸出編程。
read(stream, buffer [, extra options]) async_read(stream, buffer [, extra options], handler) write(stream, buffer [, extra options]) async_write(stream, buffer [, extra options], handler)從前面的代碼片段可以看出,這些函數(shù)支持傳入包含任意內(nèi)容(不僅僅是一個socket,我們可以對它進行讀寫)的流實例。
作為一個跨平臺的庫,Boost.Asio可以在大多數(shù)操作系統(tǒng)上使用。能同時支持數(shù)千個并發(fā)的連接。其網(wǎng)絡(luò)部分的靈感來源于伯克利軟件分發(fā)(BSD)socket,它提供了一套可以支持傳輸控制協(xié)議(TCP)socket、用戶數(shù)據(jù)報協(xié)議(UDP)socket和Internet控制消息協(xié)議(IMCP)socket的API,而且如果有需要,你可以對其進行擴展以支持你自己的協(xié)議。
歷史
Boost.Asio在2003被開發(fā)出來,然后于2005年的12月引入到Boost 1.35版本中。原作者是Christopher M. Kohlhoff,你可以通過chris@kohlhoff.com聯(lián)系他。
這個庫在以下的平臺和編譯器上測試通過:
- 32-bit和64-bit Windows,使用Visual C++ 7.1及以上
- Windows下使用MinGW
- Windows下使用Cygwin(確保已經(jīng)定義 __USE_232_SOCKETS)
- 基于2.4和2.6內(nèi)核的Linux,使用g++ 3.3及以上
- Solaris下使用g++ 3.3及以上
- MAC OS X 10.4以上下使用g++ 3.3及以上
它也可能能在諸如AIX 5.3,HP-UX 11i v3,QNX Neutrino 6.3,Solaris下使用Sun Studio 11以上,True64 v5.1,Windows下使用Borland C++ 5.9.2以上等平臺上使用。(更多細節(jié)請咨詢www.boost.org)
依賴
Boost.Asio依賴于如下的庫:
- Boost.System:這個庫為Boost庫提供操作系統(tǒng)支持(http://www.boost.org/doc/libs/1_51_0/doc/html/boost_system/index.html)
- Boost.Regex:使用這個庫(可選的)以便你重載read_until()或者async_read_until()時使用boost::regex參數(shù)。
- Boost.DateTime:使用這個庫(可選的)以便你使用Boost.Asio中的計時器
- OpenSSL:使用這個庫(可選的)以便你使用Boost.Asio提供的SSL支持。
編譯Boost.Asio
Boost.Asio是一個只需要引入頭文件就可以使用的庫。然而,考慮到你的編譯器和程序的大小,你可以選擇用源文件的方式來編譯Boost.Asio。如果你想要這么做以減少編譯時間,有如下幾種方式:
在某個源文件中,添加#include "boost/asio/impl/src.hpp"(如果你在使用SSL,添加#include "boost/asio/ssl/impl/src.hpp")在所有的源文件中,添加#define BOOST_ASIO_SEPARATE_COMPILATION
注意Boost.Asio依賴于Boost.System,必要的時候還依賴于Boost.Regex,所以你需要用如下的指令先編譯Boost:
bjam –with-system –with-regex stage
如果你還想同時編譯tests,你需要使用如下的指令:
bjam –with-system –with-thread –with-date_time –with-regex –with-serialization stage
這個庫有大量的例子,你可以連同本書中的例子一塊看看。
重要的宏
如果設(shè)置了BOOST_ASIO_DISABLE_THREADS;不管你是否在編譯Boost的過程中使用了線程支持,Boost.Asio中的線程支持都會失效。
同步VS異步
首先,異步編程和同步編程是非常不同的。在同步編程中,所有的操作都是順序執(zhí)行的,比如從socket中讀取(請求),然后寫入(回應)到socket中。每一個操作都是阻塞的。因為操作是阻塞的,所以為了不影響主程序,當在socket上讀寫時,通常會創(chuàng)建一個或多個線程來處理socket的輸入/輸出。因此,同步的服務端/客戶端通常是多線程的。
相反的,異步編程是事件驅(qū)動的。雖然啟動了一個操作,但是你不知道它何時會結(jié)束;它只是提供一個回調(diào)給你,當操作結(jié)束時,它會調(diào)用這個API,并返回操作結(jié)果。對于有著豐富經(jīng)驗的QT(諾基亞用來創(chuàng)建跨平臺圖形用戶界面應用程序的庫)程序員來說,這就是他們的第二天性。因此,在異步編程中,你只需要一個線程。
因為中途做改變會非常困難而且容易出錯,所以你在項目初期(最好是一開始)就得決定用同步還是異步的方式實現(xiàn)網(wǎng)絡(luò)通信。不僅API有極大的不同,你程序的語意也會完全改變(異步網(wǎng)絡(luò)通信通常比同步網(wǎng)絡(luò)通信更加難以測試和調(diào)試)。你需要考慮是采用阻塞調(diào)用和多線程的方式(同步,通常比較簡單),或者是更少的線程和事件驅(qū)動(異步,通常更復雜)。
下面是一個基礎(chǔ)的同步客戶端例子:
using boost::asio; io_service service; ip::tcp::endpoint ep( ip::address::from_string("127.0.0.1"), 2001); ip::tcp::socket sock(service); sock.connect(ep);首先,你的程序至少需要一個io_service實例。Boost.Asio使用io_service同操作系統(tǒng)的輸入/輸出服務進行交互。通常一個io_service的實例就足夠了。然后,創(chuàng)建你想要連接的地址和端口,再建立socket。把socket連接到你創(chuàng)建的地址和端口。
下面是一個簡單的使用Boost.Asio的服務端:
typedef boost::shared_ptr<ip::tcp::socket> socket_ptr; io_service service; ip::tcp::endpoint ep( ip::tcp::v4(), 2001)); // listen on 2001 ip::tcp::acceptor acc(service, ep); while ( true) {socket_ptr sock(new ip::tcp::socket(service));acc.accept(*sock);boost::thread( boost::bind(client_session, sock)); } void client_session(socket_ptr sock) {while ( true) {char data[512];size_t len = sock->read_some(buffer(data));if ( len > 0)write(*sock, buffer("ok", 2));} }首先,同樣是至少需要一個io_service實例。然后你指定你想要監(jiān)聽的端口,再創(chuàng)建一個接收器——一個用來接收客戶端連接的對象。 在接下來的循環(huán)中,你創(chuàng)建一個虛擬的socket來等待客戶端的連接。然后當一個連接被建立時,你創(chuàng)建一個線程來處理這個連接。
在client_session線程中來讀取一個客戶端的請求,進行解析,然后返回結(jié)果。
而創(chuàng)建一個異步的客戶端,你需要做如下的事情:
using boost::asio; io_service service; ip::tcp::endpoint ep( ip::address::from_string("127.0.0.1"), 2001); ip::tcp::socket sock(service); sock.async_connect(ep, connect_handler); service.run(); void connect_handler(const boost::system::error_code & ec) {// 如果ec返回成功我們就可以知道連接成功了 }在程序中你需要創(chuàng)建至少一個io_service實例。你需要指定連接的地址以及創(chuàng)建socket。
當連接完成時(其完成處理程序)你就異步地連接到了指定的地址和端口,也就是說,connect_handler被調(diào)用了。
當connect_handler被調(diào)用時,檢查錯誤代碼(ec),如果成功,你就可以向服務端進行異步的寫入。
注意:只要還有待處理的異步操作,servece.run()循環(huán)就會一直運行。在上述例子中,只執(zhí)行了一個這樣的操作,就是socket的async_connect。在這之后,service.run()就退出了。
每一個異步操作都有一個完成處理程序——一個操作完成之后被調(diào)用的函數(shù)。 下面的代碼是一個基本的異步服務端
using boost::asio; typedef boost::shared_ptr<ip::tcp::socket> socket_ptr; io_service service; ip::tcp::endpoint ep( ip::tcp::v4(), 2001)); // 監(jiān)聽端口2001 ip::tcp::acceptor acc(service, ep); socket_ptr sock(new ip::tcp::socket(service)); start_accept(sock); service.run(); void start_accept(socket_ptr sock) {acc.async_accept(*sock, boost::bind( handle_accept, sock, _1) ); } void handle_accept(socket_ptr sock, const boost::system::error_code & err) {if ( err) return;// 從這里開始, 你可以從socket讀取或者寫入socket_ptr sock(new ip::tcp::socket(service));start_accept(sock); }在上述代碼片段中,首先,你創(chuàng)建一個io_service實例,指定監(jiān)聽的端口。然后,你創(chuàng)建接收器acc——一個接受客戶端連接,創(chuàng)建虛擬的socket,異步等待客戶端連接的對象。
最后,運行異步service.run()循環(huán)。當接收到客戶端連接時,handle_accept被調(diào)用(調(diào)用async_accept的完成處理程序)。如果沒有錯誤,這個socket就可以用來做讀寫操作。
在使用這個socket之后,你創(chuàng)建了一個新的socket,然后再次調(diào)用start_accept(),用來創(chuàng)建另外一個“等待客戶端連接”的異步操作,從而使service.run()循環(huán)一直保持忙碌狀態(tài)。
異常處理VS錯誤代碼
Boost.Asio允許同時使用異常處理或者錯誤代碼,所有的異步函數(shù)都有拋出錯誤和返回錯誤碼兩種方式的重載。當函數(shù)拋出錯誤時,它通常拋出boost::system::system_error的錯誤。
using boost::asio; ip::tcp::endpoint ep; ip::tcp::socket sock(service); sock.connect(ep); // 第一行 boost::system::error_code err; sock.connect(ep, err); // 第二行在前面的代碼中,sock.connect(ep)會拋出錯誤,sock.connect(ep, err)則會返回一個錯誤碼。
看一下下面的代碼片段:
try {sock.connect(ep); } catch(boost::system::system_error e) {std::cout << e.code() << std::endl; }下面的代碼片段和前面的是一樣的:
boost::system::error_code err; sock.connect(ep, err); if ( err)std::cout << err << std::endl;當使用異步函數(shù)時,你可以在你的回調(diào)函數(shù)里面檢查其返回的錯誤碼。異步函數(shù)從來不拋出異常,因為這樣做毫無意義。那誰會捕獲到它呢?
在你的異步函數(shù)中,你可以使用異常處理或者錯誤碼(隨心所欲),但要保持一致性。同時使用這兩種方式會導致問題,大部分時候是崩潰(當你不小心出錯,忘記去處理一個拋出來的異常時)。如果你的代碼很復雜(調(diào)用很多socket讀寫函數(shù)),你最好選擇異常處理的方式,把你的讀寫包含在一個函數(shù)try {} catch塊里面。
void client_session(socket_ptr sock) {try {...} catch ( boost::system::system_error e) {// 處理錯誤} }如果使用錯誤碼,你可以使用下面的代碼片段很好地檢測連接是何時關(guān)閉的:
char data[512]; boost::system::error_code error; size_t length = sock.read_some(buffer(data), error); if (error == error::eof)return; // 連接關(guān)閉Boost.Asio的所有錯誤碼都包含在?的命名空間中(以便你創(chuàng)造一個大型的switch來檢查錯誤的原因)。如果想要了解更多的細節(jié),請參照boost/asio/error.hpp頭文件
Boost.Asio中的線程
當說到Boost.Asio的線程時,我們經(jīng)常在討論:
- io_service:io_service是線程安全的。幾個線程可以同時調(diào)用io_service::run()。大多數(shù)情況下你可能在一個單線程函數(shù)中調(diào)用io_service::run(),這個函數(shù)必須等待所有異步操作完成之后才能繼續(xù)執(zhí)行。然而,事實上你可以在多個線程中調(diào)用io_service::run()。這會阻塞所有調(diào)用io_service::run()的線程。只要當中任何一個線程調(diào)用了io_service::run(),所有的回調(diào)都會同時被調(diào)用;這也就意味著,當你在一個線程中調(diào)用io_service::run()時,所有的回調(diào)都被調(diào)用了。
- socket:socket類不是線程安全的。所以,你要避免在某個線程里讀一個socket時,同時在另外一個線程里面對其進行寫入操作。(通常來說這種操作都是不推薦的,更別說Boost.Asio)。
- utility:就utility來說,因為它不是線程安全的,所以通常也不提倡在多個線程里面同時使用。里面的方法經(jīng)常只是在很短的時間里面使用一下,然后就釋放了。
除了你自己創(chuàng)建的線程,Boost.Asio本身也包含幾個線程。但是可以保證的是那些線程不會調(diào)用你的代碼。這也意味著,只有調(diào)用了io_service::run()方法的線程才會調(diào)用回調(diào)函數(shù)。
不僅僅是網(wǎng)絡(luò)通信
除了網(wǎng)絡(luò)通信,Boost.Asio還包含了其他的I/O功能。
Boost.Asio支持信號量,比如SIGTERM(軟件終止)、SIGINT(中斷信號)、SIGSEGV(段錯誤)等等。 你可以創(chuàng)建一個signal_set實例,指定異步等待的信號量,然后當這些信號量產(chǎn)生時,就會調(diào)用你的異步處理程序:
void signal_handler(const boost::system::error_code & err, int signal) {// 紀錄日志,然后退出應用 } boost::asio::signal_set sig(service, SIGINT, SIGTERM); sig.async_wait(signal_handler);如果SIGINT產(chǎn)生,你就能在你的signal_handler回調(diào)中捕獲到它。
你可以使用Boost.Asio輕松地連接到一個串行端口。在Windows上端口名稱是COM7,在POSIX平臺上是/dev/ttyS0。
io_service service; serial_port sp(service, "COM7");打開端口后,你就可以使用下面的代碼設(shè)置一些端口選項,比如端口的波特率、奇偶校驗和停止位。
serial_port::baud_rate rate(9600); sp.set_option(rate);打開端口后,你可以把這個串行端口看做一個流,然后基于它使用自由函數(shù)對串行端口進行讀/寫操作。比如async_read(), write, async_write(), 就像下面的代碼片段:
char data[512]; read(sp, buffer(data, 512));Boost.Asio也可以連接到Windows的文件,然后同樣使用自由函數(shù),比如read(), asyn_read()等等,就像下面的代碼片段:
HANDLE h = ::OpenFile(...); windows::stream_handle sh(service, h); char data[512]; read(h, buffer(data, 512));對于POXIS文件描述符,比如管道,標準I/O和各種設(shè)備(但不包括普通文件)你也可以這樣做,就像下面的代碼所做的一樣:
posix::stream_descriptor sd_in(service, ::dup(STDIN_FILENO)); char data[512]; read(sd_in, buffer(data, 512));計時器
一些I/O操作需要一個超時時間。這只能應用在異步操作上(同步意味著阻塞,因此沒有超時時間)。例如,下一條信息必須在100毫秒內(nèi)從你的同伴那傳遞給你。
bool read = false; void deadline_handler(const boost::system::error_code &) {std::cout << (read ? "read successfully" : "read failed") << std::endl; } void read_handler(const boost::system::error_code &) {read = true; } ip::tcp::socket sock(service); … read = false; char data[512]; sock.async_read_some(buffer(data, 512)); deadline_timer t(service, boost::posix_time::milliseconds(100)); t.async_wait(&deadline_handler); service.run();在上述代碼片段中,如果你在超時之前讀完了數(shù)據(jù),read則被設(shè)置成true,這樣我們的伙伴就及時地通知了我們。否則,當deadline_handler被調(diào)用時,read還是false,也就意味著我們的操作超時了。
Boost.Asio也支持同步計時器,但是它們通常和一個簡單的sleep操作是一樣的。boost::this_thread::sleep(500);這段代碼和下面的代碼片段完成了同一件事情:
deadline_timer t(service, boost::posix_time::milliseconds(500)); t.wait();io_service類
你應該已經(jīng)發(fā)現(xiàn)大部分使用Boost.Asio編寫的代碼都會使用幾個io_service的實例。io_service是這個庫里面最重要的類;它負責和操作系統(tǒng)打交道,等待所有異步操作的結(jié)束,然后為每一個異步操作調(diào)用其完成處理程序。
如果你選擇用同步的方式來創(chuàng)建你的應用,你則不需要考慮我將在這一節(jié)向你展示的東西。你有多種不同的方式來使用io_service。在下面的例子中,我們有3個異步操作,2個socket連接操作和一個計時器等待操作:
- 有一個io_service實例和一個處理線程的單線程例子: io_service service; // 所有socket操作都由service來處理 ip::tcp::socket sock1(service); // all the socket operations are handled by service ip::tcp::socket sock2(service); sock1.asyncconnect( ep, connect_handler); sock2.async_connect( ep, connect_handler); deadline_timer t(service, boost::posixtime::seconds(5)); t.async_wait(timeout_handler); service.run();
- 有一個io_service實例和多個處理線程的多線程例子:
- 有多個io_service實例和多個處理線程的多線程例子:
首先,要注意你不能擁有多個io_service實例卻只有一個線程。下面的代碼片段沒有任何意義:
for ( int i = 0; i < 2; ++i)service[i].run();上面的代碼片段沒有意義是因為service[1].run()需要service[0].run()先結(jié)束。因此,所有由service[1]處理的異步操作都需要等待,這顯然不是一個好主意。
在前面的3個方案中,我們在等待3個異步操作結(jié)束。為了解釋它們之間的不同點,我們假設(shè):過一會操作1完成,然后接著操作2完成。同時我們假設(shè)每一個完成處理程序需要1秒鐘來完成執(zhí)行。
在第一個例子中,我們在一個線程中等待三個操作全部完成,第1個操作一完成,我們就調(diào)用它的完成處理程序。盡管操作2緊接著完成了,但是操作2的完成處理程序需要在1秒鐘后,也就是操作1的完成處理程序完成時才會被調(diào)用。
第二個例子,我們在兩個線程中等待3個異步操作結(jié)束。當操作1完成時,我們在第1個線程中調(diào)用它的完成處理程序。當操作2完成時,緊接著,我們就在第2個線程中調(diào)用它的完成處理程序(當線程1在忙著響應操作1的處理程序時,線程2空閑著并且可以回應任何新進來的操作)。
在第三個例子中,因為操作1是sock1的connect,操作2是sock2的connect,所以應用程序會表現(xiàn)得像第二個例子一樣。線程1會處理sock1 connect操作的完成處理程序,線程2會處理sock2的connect操作的完成處理程序。然而,如果sock1的connect操作是操作1,deadline_timer t的超時操作是操作2,線程1會結(jié)束正在處理的sock1 connect操作的完成處理程序。因而,deadline_timer t的超時操作必須等sock1 connect操作的完成處理程序結(jié)束(等待1秒鐘),因為線程1要處理sock1的連接處理程序和t的超時處理程序。
下面是你需要從前面的例子中學到的:
- 第一種情況是非常基礎(chǔ)的應用程序。因為是串行的方式,所以當幾個處理程序需要被同時調(diào)用時,你通常會遇到瓶頸。如果一個處理程序需要花費很長的時間來執(zhí)行,所有隨后的處理程序都不得不等待。
- 第二種情況是比較適用的應用程序。他是非常強壯的——如果幾個處理程序被同時調(diào)用了(這是有可能的),它們會在各自的線程里面被調(diào)用。唯一的瓶頸就是所有的處理線程都很忙的同時又有新的處理程序被調(diào)用。然而,這是有快速的解決方式的,增加處理線程的數(shù)目即可。
- 第三種情況是最復雜和最難理解的。你只有在第二種情況不能滿足需求時才使用它。這種情況一般就是當你有成千上萬實時(socket)連接時。你可以認為每一個處理線程(運行io_service::run()的線程)有它自己的select/epoll循環(huán);它等待任意一個socket連接,然后等待一個讀寫操作,當它發(fā)現(xiàn)這種操作時,就執(zhí)行。大部分情況下,你不需要擔心什么,唯一你需要擔心的就是當你監(jiān)控的socket數(shù)目以指數(shù)級的方式增長時(超過1000個的socket)。在那種情況下,有多個select/epoll循環(huán)會增加應用的響應時間。
如果你覺得你的應用程序可能需要轉(zhuǎn)換到第三種模式,請確保監(jiān)聽操作的這段代碼(調(diào)用io_service::run()的代碼)和應用程序其他部分是隔離的,這樣你就可以很輕松地對其進行更改。
最后,需要一直記住的是如果沒有其他需要監(jiān)控的操作,.run()就會結(jié)束,就像下面的代碼片段:
io_service service; tcp::socket sock(service); sock.async_connect( ep, connect_handler); service.run();在上面的例子中,只要sock建立了一個連接,connect_handler就會被調(diào)用,然后接著service.run()就會完成執(zhí)行。
如果你想要service.run()接著執(zhí)行,你需要分配更多的工作給它。這里有兩個方式來完成這個目標。一種方式是在connect_handler中啟動另外一個異步操作來分配更多的工作。 另一種方式會模擬一些工作給它,用下面的代碼片段:
typedef boost::shared_ptr work_ptr; work_ptr dummy_work(new io_service::work(service));上面的代碼可以保證service.run()一直運行直到你調(diào)用useservice.stop()或者 dummy_work.reset(0);// 銷毀 dummy_work.
總結(jié)
做為一個復雜的庫,Boost.Asio讓網(wǎng)絡(luò)編程變得異常簡單。構(gòu)建起來也簡單。而且在避免使用宏這一點上也做得很好;它雖然定義了少部分的宏來做選項開關(guān),但是你需要關(guān)心的很少。
Boost.Asio支持同步和異步編程。他們有很大不同;你需要在項目早期就選擇其中的一種來實現(xiàn),因為它們之間的轉(zhuǎn)換是非常復雜而且易錯的。
如果你選擇同步,你可以選擇異常處理或者錯誤碼,從異常處理轉(zhuǎn)到錯誤碼;只需要在call函數(shù)中增加一個參數(shù)即可(錯誤碼)。
Boost.Asio不僅僅可以用來做網(wǎng)絡(luò)編程。它還有其他更多的特性,這讓它顯得更有價值,比如信號量,計時器等等。
下一章我們將深入研究大量Boost.Asio中用來做網(wǎng)絡(luò)編程的函數(shù)和類。同時我們也會學一些異步編程的訣竅。總結(jié)
以上是生活随笔為你收集整理的Boost.Asio入门的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Boost.Asio技术文档汇总
- 下一篇: boost asio 异步实现tcp通讯