引言
文章的內容是翻譯 《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()循環一直保持忙碌狀態。