【C++】BOOST ASIO 異步客戶端代碼分析

參考資料:《Boost Asio C++網絡編程》第四章

這裏將對asio庫編寫的異步客戶端進行解讀。異步客戶端比同步客戶端更復雜。同步客戶端相對簡單,實用性不大,一般用於業務測試,基本就是一條線的邏輯代碼。

環境設置

asio客戶端項目僅需包含boost.asio頭文件,無需導入lib文件:

#include <boost/thread.hpp>
#include <boost/bind.hpp>
#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/enable_shared_from_this.hpp>
#include <boost/noncopyable.hpp> // 原文代碼缺了這行

using namespace boost::asio;
io_context service;

這裏對初學者陌生的是enable_shared_from_this,後面將進行解析。io_context 是asio框架中的調度器,所有異步io事件都是通過它來分發處理的,有個舊版本是io_service(也能用,用法一樣)。io_context本身是單線程運作的。在多線程項目中,幾個線程可以獨享一個io_context,每個io_context均可以進行單獨的異步操作。一般小規模業務中,採用的是一個io_context加異步/協程。大規模業務和大數據傳輸業務,需要開啓多線程多io_context。

主函數
int main(int argc, char* argv[]) {
	ip::tcp::endpoint ep(ip::address::from_string("127.0.0.1"), 8001);
	const char* names[] = { "John", "James", "Lucy", "Tracy", "Frank", "Abby",0 };
	for (const char ** name = names; *name; ++name) {
		talk_to_svr::start(ep, *name);
		boost::this_thread::sleep(boost::posix_time::millisec(100));
	}
	service.run();
}

主函數非常簡單,首先ip::tcp::endpoint是asio庫中表示tcp端口的類,指定連接主機地址和連接方式;ip::address::from_string可以直接把字符串ip地址和端口變爲ip地址數據;‘127.0.0.1’表示本機ip,做測試用的。
const char* names[] = {。。。}初始化字符串數組,其中原文缺少"const"會報錯,因爲右邊的字符串是不可更改的,帶有const屬性;字符串初始的最後一個是0,必須加上,在下面的for循環代碼中作爲終止條件。
talk_to_svr是之後要介紹的客戶端類;talk_to_svr::start是客戶端自定義的靜態成員函數,不是asio的api,用於生成一個talk_to_svr對象並啓動。
上述工作完成後,io_context.run()便開始執行異步調度工作,只要asio的api還有處於工作狀態的,io_context.run()便會一直循環下去;否則就會退出循環,結束程序。因此實際程序中,我們總是要保留至少一個客戶端對象,使其處於準備異步連接的狀態,保證程序一直處於工作中。

客戶端

客戶端屬性
class talk_to_svr : public boost::enable_shared_from_this<talk_to_svr>
	, boost::noncopyable{
	typedef talk_to_svr self_type;
  1. 根據業務邏輯,客戶端類是可不拷貝的(noncopyable),每個客戶端應該獨一無二。
  2. 客戶端類是異步的,當處於異步操作中,客戶端類有可能莫名地被析構,因此要使用智能指針來管理客戶端類,後面我們將看到這個屬性的必要性。
兩個start
#define MEM_FN(x) boost::bind(&self_type::x, shared_from_this())
#define MEM_FN1(x,y) boost::bind(&self_type::x, shared_from_this(),y)
void start(ip::tcp::endpoint ep) {
		sock_.async_connect(ep, MEM_FN1(on_connect, _1));
	}
static ptr start(ip::tcp::endpoint ep, const std::string & username) {
		ptr new_(new talk_to_svr(username));
		new_->start(ep);
		return new_;
	}

下面的靜態成員函數start生成一個對象,再調用成員函數start開始工作,調用async_connect進入等待連接狀態,此時客戶端對象進入異步等待狀態,佔用CPU資源極少。
async_connect的參數列表,第一個是端口,第二個是回調函數,指向成員函數void talk_to_svr::on_connect(const boost::system::error_code & err) ,當異步連接成功時,就會喚醒並調用這個成員函數。boost::system::error_code定義了幾種常見的系統錯誤代碼用於異常處理。如果沒有錯誤,on_connect函數就開始調用工作函數。
MEM_FN1(on_connect, _1)宏命令解析爲boost::bind(talk_to_svr::on_connect, shared_from_this(), _1),_1是bind函數所用的佔位符,async_connect會在_1處填入:error_code數據。shared_from_this()返回一個包含自身指針的智能指針,只有繼承了enable_shared_from_this<talk_to_svr>的類才能這樣用(已經加入標準庫了)。下面來看看爲什麼要用這個方法。

enable_shared_from_this

以start成員函數爲例子,根據其外在表現,可以類比下面的形式便於理解(實際實現更復雜):

void start(talk_to_svr* this, ip::tcp::socket sock, ip::tcp::endpoint ep){
	std::thread t(thread_connect, sock, ep, this, bind(talk_to_svr::callback,XXX));
	t.detach();//不用join阻塞,直接跑完這個代碼段
}
void thread_connect(ip::tcp::socket, ip::tcp::endpoint , talk_to_svr* this, std::function<void(talk_to_svr*,error_code)> f){
	....//連接工作
	(*f)(this, ec);
}
void callback(talk_to_svr* this, error_code arg){....}

std::function是函數指針。異步連接的外在表現,可以等價於新建了一個線程用於連接,線程有detach屬性,所以當前代碼段直接運行完畢退出。等線程執行完畢,又回調後續的處理函數。在線程運行階段,線程函數只有talk_to_svr的裸指針,如果talk_to_svr對象析構了,那麼這個指針就會指向異常地址而發生內存錯誤。爲了保證talk_to_svr對象指針在工作期間始終有效,我們將talk_to_svr類繼承boost::enable_shared_from_this屬性,這樣成員函數的第一個參數*this,可以用std::shared_ptr<talk_to_svr> this替代;然後將std::shared_ptr<talk_to_svr> this傳入異步工作區中,保證talk_to_svr對象始終是存在的。

長連接

由於使用了tcp連接,可以保持長時間的連接。實際工作中,talk_to_svr一直處於以下工作狀態:
async_write_some異步等待寫入,登錄----》async_read_some異步等待讀取,獲取結果—》async_write_some異步等待寫入,發送請求-----》async_read_some異步等待讀取,獲取請求結果-----》反覆重複或者關閉連接。
所有的操作指令和數據都轉換爲字符串進行傳遞,然後解析收到的迴應。剩下的其他工作就與asio無關了,具體業務調用具體的庫。

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