Boost.Asio C++ 網絡編程之二:同步和異步

引言

文章的內容是翻譯 《Boost.Asio C++ Network Programming》這本書。

編譯IDE環境是 vs2017

同步和異步

首先,異步編程和同步編程是截然不同的。

在同步編程中,所有的操作都是順序執行的,比如從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連接到你創建的地址和端口。

下面是一個簡單的同步服務器端:

using 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),如果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()循環一直保持忙碌狀態。

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