1. 程式人生 > >Boost.Asio入門(CSDN也有Markdown了,好開森)

Boost.Asio入門(CSDN也有Markdown了,好開森)

Boost.Asio入門

首先,讓我們先來了解一下什麼是 Boost.Asio?怎麼編譯它?瞭解的過程中我們會給出一些例子。然後在發現 Boost.Asio 不僅僅是一個網路庫的同時你也會接觸到 Boost.Asio 中最核心的類——io_service

什麼是Boost.Asio

簡單來說,Boost.Asio是一個跨平臺的、主要用於網路和其他一些底層輸入/輸出程式設計的 C++ 庫。

網路 API 的設計方式有很多種,但是 Boost.Asio 的的方式遠遠優於其它的設計方式。它在 2005 年就被引入到 Boost,然後被大量 Boost 使用者測試並在很多專案中使用,比如 Remobo(

http://www.remobo.com),可以讓你建立你自己的即時私有網路(IPN)的應用,libtorrent(http://www.rasterbar.com/products/libtorrent) 一個實現了位元流客戶端的庫,PokerTH (http://www.pokerth.net) 一個支援 LAN 和網際網路對戰的紙牌遊戲。

Boost.Asio 在網路通訊、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)

從前面的程式碼片段可以看出,這些函式支援傳入包含任意內容(不僅僅是一個socket,我們可以對它進行讀寫)的流例項。

作為一個跨平臺的庫,Boost.Asio可以在大多數作業系統上使用。能同時支援數千個併發的連線。其網路部分的靈感來源於伯克利軟體分發(BSD)socket,它提供了一套可以支援傳輸控制協議(TCP)**socket、使用者資料報協議(UDP)**socket和**Internet控制訊息協議(IMCP)**socket的API,而且如果有需要,你可以對其進行擴充套件以支援你自己的協議。

歷史

Boost.Asio在2003被開發出來,然後於2005年的12月引入到Boost 1.35版本中。原作者是Christopher M. Kohlhoff,你可以通過[email protected]聯絡他。

這個庫在以下的平臺和編譯器上測試通過:

  • 32-bit和64-bit Windows,使用Visual C++ 7.1及以上
  • Windows下使用MinGW
  • Windows下使用Cygwin(確保已經定義 __USE_232_SOCKETS)
  • 基於2.4和2.6核心的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以上等平臺上使用。(更多細節請諮詢www.boost.org

依賴

Boost.Asio依賴於如下的庫:

  • Boost.Regex:使用這個庫(可選的)以便你過載read_until()或者async_read_until()時使用boost::regex引數。
  • 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

這個庫有大量的例子,你可以連同本書中的例子一塊看看。

重要的巨集

如果設定了BOOST_ASIO_DISABLE_THREADS;不管你是否在編譯Boost的過程中使用了執行緒支援,Boost.Asio中的執行緒支援都會失效。

同步VS非同步

首先,非同步程式設計和同步程式設計是非常不同的。在同步程式設計中,所有的操作都是順序執行的,比如從socket中讀取(請求),然後寫入(迴應)到socket中。每一個操作都是阻塞的。因為操作是阻塞的,所以為了不影響主程式,當在socket上讀寫時,通常會建立一個或多個執行緒來處理socket的輸入/輸出。因此,同步的服務端/客戶端通常是多執行緒的。

相反的,非同步程式設計是事件驅動的。雖然啟動了一個操作,但是你不知道它何時會結束;它只是提供一個回撥給你,當操作結束時,它會呼叫這個API,並返回操作結果。對於有著豐富經驗的QT(諾基亞用來建立跨平臺圖形使用者介面應用程式的庫)程式設計師來說,這就是他們的第二天性。因此,在非同步程式設計中,你只需要一個執行緒。

因為中途做改變會非常困難而且容易出錯,所以你在專案初期(最好是一開始)就得決定用同步還是非同步的方式實現網路通訊。不僅API有極大的不同,你程式的語意也會完全改變(非同步網路通訊通常比同步網路通訊更加難以測試和除錯)。你需要考慮是採用阻塞呼叫和多執行緒的方式(同步,通常比較簡單),或者是更少的執行緒和事件驅動(非同步,通常更復雜)。

下面是一個基礎的同步客戶端例子:

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同作業系統的輸入/輸出服務進行互動。通常一個io_service的例項就足夠了。然後,建立你想要連線的地址和埠,再建立socket。把socket連線到你建立的地址和埠。

下面是一個簡單的使用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例項。然後你指定你想要監聽的埠,再建立一個接收器——一個用來接收客戶端連線的物件。 在接下來的迴圈中,你建立一個虛擬的socket來等待客戶端的連線。然後當一個連線被建立時,你建立一個執行緒來處理這個連線。

在client_session執行緒中來讀取一個客戶端的請求,進行解析,然後返回結果。

而建立一個非同步的客戶端,你需要做如下的事情:

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返回成功我們就可以知道連線成功了
}

在程式中你需要建立至少一個io_service例項。你需要指定連線的地址以及建立socket。

當連線完成時(其完成處理程式)你就非同步地連線到了指定的地址和埠,也就是說,connect_handler被呼叫了。

connect_handler被呼叫時,檢查錯誤程式碼(ec),如果成功,你就可以向服務端進行非同步的寫入。

注意:只要還有待處理的非同步操作,servece.run()迴圈就會一直執行。在上述例子中,只執行了一個這樣的操作,就是socket的async_connect。在這之後,service.run()就退出了。

每一個非同步操作都有一個完成處理程式——一個操作完成之後被呼叫的函式。 下面的程式碼是一個基本的非同步服務端

using boost::asio;
typedef boost::shared_ptr<ip::tcp::socket> socket_ptr;
io_service service;
ip::tcp::endpoint ep( ip::tcp::v4(), 2001)); // 監聽埠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);
}

在上述程式碼片段中,首先,你建立一個io_service例項,指定監聽的埠。然後,你建立接收器acc——一個接受客戶端連線,建立虛擬的socket,非同步等待客戶端連線的物件。

最後,執行非同步service.run()迴圈。當接收到客戶端連線時,handle_accept被呼叫(呼叫async_accept的完成處理程式)。如果沒有錯誤,這個socket就可以用來做讀寫操作。

在使用這個socket之後,你建立了一個新的socket,然後再次呼叫start_accept(),用來建立另外一個“等待客戶端連線”的非同步操作,從而使service.run()迴圈一直保持忙碌狀態。

異常處理VS錯誤程式碼

Boost.Asio允許同時使用異常處理或者錯誤程式碼,所有的非同步函式都有丟擲錯誤和返回錯誤碼兩種方式的過載。當函式丟擲錯誤時,它通常丟擲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;

當使用非同步函式時,你可以在你的回撥函式裡面檢查其返回的錯誤碼。非同步函式從來不丟擲異常,因為這樣做毫無意義。那誰會捕獲到它呢?

在你的非同步函式中,你可以使用異常處理或者錯誤碼(隨心所欲),但要保持一致性。同時使用這兩種方式會導致問題,大部分時候是崩潰(當你不小心出錯,忘記去處理一個丟擲來的異常時)。如果你的程式碼很複雜(呼叫很多socket讀寫函式),你最好選擇異常處理的方式,把你的讀寫包含在一個函式try {} catch塊裡面。

void client_session(socket_ptr sock) {
    try {
        ...
    } catch ( boost::system::system_error e) {
        // 處理錯誤
    }
}

如果使用錯誤碼,你可以使用下面的程式碼片段很好地檢測連線是何時關閉的:

char data[512];
boost::system::error_code error;
size_t length = sock.read_some(buffer(data), error);
if (error == error::eof)
    return; // 連線關閉

Boost.Asio的所有錯誤碼都包含在ˆ的名稱空間中(以便你創造一個大型的switch來檢查錯誤的原因)。如果想要了解更多的細節,請參照boost/asio/error.hpp標頭檔案

Boost.Asio中的執行緒

當說到Boost.Asio的執行緒時,我們經常在討論:

  • io_service:io_service是執行緒安全的。幾個執行緒可以同時呼叫io_service::run()。大多數情況下你可能在一個單執行緒函式中呼叫io_service::run(),這個函式必須等待所有非同步操作完成之後才能繼續執行。然而,事實上你可以在多個執行緒中呼叫io_service::run()。這會阻塞所有呼叫io_service::run()的執行緒。只要當中任何一個執行緒呼叫了io_service::run(),所有的回撥都會同時被呼叫;這也就意味著,當你在一個執行緒中呼叫io_service::run()時,所有的回撥都被呼叫了。
  • socket:socket類不是執行緒安全的。所以,你要避免在某個執行緒裡讀一個socket時,同時在另外一個執行緒裡面對其進行寫入操作。(通常來說這種操作都是不推薦的,更別說Boost.Asio)。
  • utility:就utility來說,因為它不是執行緒安全的,所以通常也不提倡在多個執行緒裡面同時使用。裡面的方法經常只是在很短的時間裡面使用一下,然後就釋放了。

除了你自己建立的執行緒,Boost.Asio本身也包含幾個執行緒。但是可以保證的是那些執行緒不會呼叫你的程式碼。這也意味著,只有呼叫了io_service::run()方法的執行緒才會呼叫回撥函式。

不僅僅是網路通訊

除了網路通訊,Boost.Asio還包含了其他的I/O功能。

Boost.Asio支援訊號量,比如SIGTERM(軟體終止)、SIGINT(中斷訊號)、SIGSEGV(段錯誤)等等。 你可以建立一個signal_set例項,指定非同步等待的訊號量,然後當這些訊號量產生時,就會呼叫你的非同步處理程式:

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產生,你就能在你的signal_handler回撥中捕獲到它。

你可以使用Boost.Asio輕鬆地連線到一個串列埠。在Windows上埠名稱是COM7,在POSIX平臺上是/dev/ttyS0

io_service service;
serial_port sp(service, "COM7");

開啟埠後,你就可以使用下面的程式碼設定一些埠選項,比如埠的波特率、奇偶校驗和停止位。

serial_port::baud_rate rate(9600);
sp.set_option(rate);

開啟埠後,你可以把這個串列埠看做一個流,然後基於它使用自由函式對串列埠進行讀/寫操作。比如async_read(), write, async_write(), 就像下面的程式碼片段:

char data[512];
read(sp, buffer(data, 512));

Boost.Asio也可以連線到Windows的檔案,然後同樣使用自由函式,比如read(), asyn_read()等等,就像下面的程式碼片段:

HANDLE h = ::OpenFile(...);
windows::stream_handle sh(service, h);
char data[512];
read(h, buffer(data, 512));

對於POXIS檔案描述符,比如管道,標準I/O和各種裝置(但不包括普通檔案)你也可以這樣做,就像下面的程式碼所做的一樣:

posix::stream_descriptor sd_in(service, ::dup(STDIN_FILENO));
char data[512];
read(sd_in, buffer(data, 512));

計時器

一些I/O操作需要一個超時時間。這隻能應用在非同步操作上(同步意味著阻塞,因此沒有超時時間)。例如,下一條資訊必須在100毫秒內從你的同伴那傳遞給你。

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

在上述程式碼片段中,如果你在超時之前讀完了資料,read則被設定成true,這樣我們的夥伴就及時地通知了我們。否則,當deadline_handler被呼叫時,read還是false,也就意味著我們的操作超時了。

Boost.Asio也支援同步計時器,但是它們通常和一個簡單的sleep操作是一樣的。boost::this_thread::sleep(500);這段程式碼和下面的程式碼片段完成了同一件事情:

deadline_timer t(service, boost::posix_time::milliseconds(500));
t.wait();

io_service類

你應該已經發現大部分使用Boost.Asio編寫的程式碼都會使用幾個io_service的例項。io_service是這個庫裡面最重要的類;它負責和作業系統打交道,等待所有非同步操作的結束,然後為每一個非同步操作呼叫其完成處理程式。

如果你選擇用同步的方式來建立你的應用,你則不需要考慮我將在這一節向你展示的東西。
你有多種不同的方式來使用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 service;
ip::tcp::socket sock1(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);
for ( int i = 0; i < 5; ++i)
    boost::thread( run_service);
void run_service()
{
    service.run();
}
  • 有多個io_service例項和多個處理執行緒的多執行緒例子:
io_service service[2];
ip::tcp::socket sock1(service[0]);
ip::tcp::socket sock2(service[1]);
sock1.asyncconnect( ep, connect_handler);
sock2.async_connect( ep, connect_handler);
deadline_timer t(service[0], boost::posixtime::seconds(5));
t.async_wait(timeout_handler);
for ( int i = 0; i < 2; ++i)
    boost::thread( boost::bind(run_service, i));
void run_service(int idx)
{
    service[idx].run();
}

首先,要注意你不能擁有多個io_service例項卻只有一個執行緒。下面的程式碼片段沒有任何意義:

for ( int i = 0; i < 2; ++i)
    service[i].run();

上面的程式碼片段沒有意義是因為service[1].run()需要service[0].run()先結束。因此,所有由service[1]處理的非同步操作都需要等待,這顯然不是一個好主意。

在前面的3個方案中,我們在等待3個非同步操作結束。為了解釋它們之間的不同點,我們假設:過一會操作1完成,然後接著操作2完成。同時我們假設每一個完成處理程式需要1秒鐘來完成執行。

在第一個例子中,我們在一個執行緒中等待三個操作全部完成,第1個操作一完成,我們就呼叫它的完成處理程式。儘管操作2緊接著完成了,但是操作2的完成處理程式需要在1秒鐘後,也就是操作1的完成處理程式完成時才會被呼叫。

第二個例子,我們在兩個執行緒中等待3個非同步操作結束。當操作1完成時,我們在第1個執行緒中呼叫它的完成處理程式。當操作2完成時,緊接著,我們就在第2個執行緒中呼叫它的完成處理程式(當執行緒1在忙著響應操作1的處理程式時,執行緒2空閒著並且可以迴應任何新進來的操作)。

在第三個例子中,因為操作1是sock1connect,操作2是sock2connect,所以應用程式會表現得像第二個例子一樣。執行緒1會處理sock1 connect操作的完成處理程式,執行緒2會處理sock2connect操作的完成處理程式。然而,如果sock1connect操作是操作1,deadline_timer t的超時操作是操作2,執行緒1會結束正在處理的sock1 connect操作的完成處理程式。因而,deadline_timer t的超時操作必須等sock1 connect操作的完成處理程式結束(等待1秒鐘),因為執行緒1要處理sock1的連線處理程式和t的超時處理程式。

下面是你需要從前面的例子中學到的:
* 第一種情況是非常基礎的應用程式。因為是序列的方式,所以當幾個處理程式需要被同時呼叫時,你通常會遇到瓶頸。如果一個處理程式需要花費很長的時間來執行,所有隨後的處理程式都不得不等待。
* 第二種情況是比較適用的應用程式。他是非常強壯的——如果幾個處理程式被同時呼叫了(這是有可能的),它們會在各自的執行緒裡面被呼叫。唯一的瓶頸就是所有的處理執行緒都很忙的同時又有新的處理程式被呼叫。然而,這是有快速的解決方式的,增加處理執行緒的數目即可。
* 第三種情況是最複雜和最難理解的。你只有在第二種情況不能滿足需求時才使用它。這種情況一般就是當你有成千上萬實時(socket)連線時。你可以認為每一個處理執行緒(執行io_service::run()的執行緒)有它自己的select/epoll迴圈;它等待任意一個socket連線,然後等待一個讀寫操作,當它發現這種操作時,就執行。大部分情況下,你不需要擔心什麼,唯一你需要擔心的就是當你監控的socket數目以指數級的方式增長時(超過1000個的socket)。在那種情況下,有多個select/epoll迴圈會增加應用的響應時間。

如果你覺得你的應用程式可能需要轉換到第三種模式,請確保監聽操作的這段程式碼(呼叫io_service::run()的程式碼)和應用程式其他部分是隔離的,這樣你就可以很輕鬆地對其進行更改。

最後,需要一直記住的是如果沒有其他需要監控的操作,.run()就會結束,就像下面的程式碼片段:

io_service service; 
tcp::socket sock(service); 
sock.async_connect( ep, connect_handler); 
service.run();

在上面的例子中,只要sock建立了一個連線,connect_handler就會被呼叫,然後接著service.run()就會完成執行。

如果你想要service.run()接著執行,你需要分配更多的工作給它。這裡有兩個方式來完成這個目標。一種方式是在connect_handler中啟動另外一個非同步操作來分配更多的工作。 另一種方式會模擬一些工作給它,用下面的程式碼片段:

typedef boost::shared_ptr work_ptr;
work_ptr dummy_work(new io_service::work(service));

上面的程式碼可以保證service.run()一直執行直到你呼叫useservice.stop()或者 dummy_work.reset(0);// 銷燬 dummy_work.

總結

做為一個複雜的庫,Boost.Asio讓網路程式設計變得異常簡單。構建起來也簡單。而且在避免使用巨集這一點上也做得很好;它雖然定義了少部分的巨集來做選項開關,但是你需要關心的很少。

Boost.Asio支援同步和非同步程式設計。他們有很大不同;你需要在專案早期就選擇其中的一種來實現,因為它們之間的轉換是非常複雜而且易錯的。

如果你選擇同步,你可以選擇異常處理或者錯誤碼,從異常處理轉到錯誤碼;只需要在call函式中增加一個引數即可(錯誤碼)。

Boost.Asio不僅僅可以用來做網路程式設計。它還有其他更多的特性,這讓它顯得更有價值,比如訊號量,計時器等等。

下一章我們將深入研究大量Boost.Asio中用來做網路程式設計的函式和類。同時我們也會學一些非同步程式設計的訣竅。