1、概述html
MFC 程序員在編寫 Windows 界面程序時常常須要處理一些阻塞任務過程,爲了不阻塞窗口的消息過程,通常會將阻塞過程將由一個子線程處理,該子線程在處理過程當中經過向界面線程發送 Windows 窗口消息將處理結果傳遞給窗口線程。在 acl 庫中的 rpc 功能類實現了更爲方便的處理方式,經過 rpc 功能類,用戶能夠在主線程中進行非阻塞過程(如:界面消息過程或網絡非阻塞通信過程),而將阻塞任務交由子線程處理(如:網絡阻塞通信或數據庫操做等),子線程能夠將任務處理的中間狀態和最終狀態經過 rpc 功能類傳遞給主線程。linux
acl 的 rpc 類不只能實現網絡通信方面的阻塞與非阻塞的粘合,同時還實現了阻塞過程與 MFC 界面過程的粘合,本文將以一個具體的 HTTP 下載過程爲例來描述這一過程(示例在 acl 庫中的 acl/lib_acl_cpp/samples/gui_rpc 目錄下)。關於 acl 庫中 rpc 相關類的使用,用戶能夠參考 《acl_cpp 的 rpc 相關類整合阻塞及非阻塞過程》(在該文中的例子描述了非阻塞主線程與阻塞子線程的交互過程,其示例代碼適用於 win32 及 linux 平臺)。程序員
2、實例數據庫
一、在界面主線程中初始化時建立 rpc 服務對象:acl::rpc_service編程
// 全局靜態變量 static acl::aio_handle* handle_; static acl::rpc_service* service_; ...... // 建立非阻塞框架句柄,並採用 WIN32 消息模式:acl::ENGINE_WINMSG handle_ = new acl::aio_handle(acl::ENGINE_WINMSG); // 建立 rpc 服務對象 int max_threads = 10; // 服務最大子線程數量 service_ = new acl::rpc_service(max_threads, true); // 打開消息服務 if (service_->open(handle_) == false) logger_fatal("open service error: %s", acl::last_serror());
在上面代碼中,有幾點須要注意:1)建立的 rpc 服務對象是全局性的;2)在建立非阻塞句柄時必須指定爲 win32 界面消息事件類型:acl::ENGINE_WINMSG;3)在建立 rpc_service 時的第二個參數 win32_gui 必須爲 true。服務器
二、建立 rpc 中 acl::rpc_request 類的子類,以實現阻塞非阻塞粘合過程,本例中該子類爲:http_download,在 http_download 類中必須實現父類 acl::rpc_request 中定義的兩個純虛接口:rpc_run,rpc_onover。網絡
其中,http_download 的頭文件以下:框架
/** * http 請求過程類,該類對象在子線程中發起遠程 HTTP 請求過程,將處理結果 * 返回給主線程 */ class http_download : public acl::rpc_request { public: /** * 構造函數 * @param addr {const char*} HTTP 服務器地址,格式:domain:port * @param url {const char*} http url 地址 * @param callback {rpc_callback*} http 請求結果經過此類對象 * 通知主線程過程 */ http_download(const char* addr, const char* url, rpc_callback* callback); protected: ~http_download() {} // 基類虛函數:子線程處理函數 virtual void rpc_run(); // 基類虛函數:主線程處理過程,收到子線程任務完成的消息 virtual void rpc_onover(); // 基類虛函數:主線程處理過程,收到子線程的通知消息 virtual void rpc_wakeup(void* ctx); ......
在 http_download 類的構造參數中有一個接口類:rpc_callback,這是一個純虛類,主要是爲了方便將 http 的結果數據返回給主線程,該類的聲明以下:dom
// 純虛類,子類須實現該類中的純虛接口 class rpc_callback { public: rpc_callback() {} virtual ~rpc_callback() {} // 設置 HTTP 請求頭數據虛函數 virtual void SetRequestHdr(const char* hdr) = 0; // 設置 HTTP 響應頭數據虛函數 virtual void SetResponseHdr(const char* hdr) = 0; // 下載過程當中的回調函數虛函數 virtual void OnDownloading(long long int content_length, long long int total_read) = 0; // 下載完成時的回調函數虛函數 virtual void OnDownloadOver(long long int total_read, double spent) = 0; };
http_download 類的函數實現以下:svn
#include "stdafx.h" #include <assert.h> #include "http_download.h" // 由子線程動態建立的 DOWN_CTX 對象的數據類型 typedef enum { CTX_T_REQ_HDR, // 爲 HTTP 請求頭數據 CTX_T_RES_HDR, // 爲 HTTP 響應頭數據 CTX_T_CONTENT_LENGTH, // 爲 HTTP 響應體的長度 CTX_T_PARTIAL_LENGTH, // 爲 HTTP 下載數據體的長度 CTX_T_END } ctx_t; // 子線程動態建立的數據對象,主線程接收此數據 struct DOWN_CTX { ctx_t type; long long int length; }; // 用來精確計算時間截間隔的函數,精確到毫秒級別 static double stamp_sub(const struct timeval *from, const struct timeval *sub_by) { struct timeval res; memcpy(&res, from, sizeof(struct timeval)); res.tv_usec -= sub_by->tv_usec; if (res.tv_usec < 0) { --res.tv_sec; res.tv_usec += 1000000; } res.tv_sec -= sub_by->tv_sec; return (res.tv_sec * 1000.0 + res.tv_usec/1000.0); } ////////////////////////////////////////////////////////////////////////// // 子線程處理函數 void http_download::rpc_run() { acl::http_request req(addr_); // HTTP 請求對象 // 設置 HTTP 請求頭信息 req.request_header().set_url(url_.c_str()) .set_content_type("text/html") .set_host(addr_.c_str()) .set_method(acl::HTTP_METHOD_GET); req.request_header().build_request(req_hdr_); DOWN_CTX* ctx = new DOWN_CTX; ctx->type = CTX_T_REQ_HDR; rpc_signal(ctx); // 通知主線程 HTTP 請求頭數據 struct timeval begin, end;; gettimeofday(&begin, NULL); // 發送 HTTP 請求數據 if (req.request(NULL, 0) == false) { logger_error("send request error"); error_ = false; gettimeofday(&end, NULL); total_spent_ = stamp_sub(&end, &begin); return; } // 得到 HTTP 請求的鏈接對象 acl::http_client* conn = req.get_client(); assert(conn); (void) conn->get_respond_head(&res_hdr_); ctx = new DOWN_CTX; ctx->type = CTX_T_RES_HDR; rpc_signal(ctx); // 通知主線程 HTTP 響應頭數據 ctx = new DOWN_CTX; ctx->type = CTX_T_CONTENT_LENGTH; ctx->length = conn->body_length(); // 得到 HTTP 響應數據的數據體長度 content_length_ = ctx->length; rpc_signal(ctx); // 通知主線程 HTTP 響應體數據長度 acl::string buf(8192); int real_size; while (true) { // 讀 HTTP 響應數據體 int ret = req.read_body(buf, true, &real_size); if (ret <= 0) { ctx = new DOWN_CTX; ctx->type = CTX_T_END; ctx->length = ret; rpc_signal(ctx); // 通知主線程下載完畢 break; } ctx = new DOWN_CTX; ctx->type = CTX_T_PARTIAL_LENGTH; ctx->length = real_size; // 通知主線程當前已經下載的大小 rpc_signal(ctx); } // 計算下載過程總時長 gettimeofday(&end, NULL); total_spent_ = stamp_sub(&end, &begin); // 至此,子線程運行完畢,主線程的 rpc_onover 過程將被調用 } ////////////////////////////////////////////////////////////////////////// http_download::http_download(const char* addr, const char* url, rpc_callback* callback) : addr_(addr) , url_(url) , callback_(callback) , error_(false) , total_read_(0) , content_length_(0) , total_spent_(0) { } ////////////////////////////////////////////////////////////////////////// // 主線程處理過程,收到子線程任務完成的消息 void http_download::rpc_onover() { logger("http download(%s) over, 共 %I64d 字節,耗時 %.3f 毫秒", url_.c_str(), total_read_, total_spent_); callback_->OnDownloadOver(total_read_, total_spent_); delete this; // 銷燬本對象 } // 主線程處理過程,收到子線程的通知消息 void http_download::rpc_wakeup(void* ctx) { DOWN_CTX* down_ctx = (DOWN_CTX*) ctx; // 根據子線程中傳來的不一樣的下載階段進行處理 switch (down_ctx->type) { case CTX_T_REQ_HDR: callback_->SetRequestHdr(req_hdr_.c_str()); break; case CTX_T_RES_HDR: callback_->SetResponseHdr(res_hdr_.c_str()); break; case CTX_T_CONTENT_LENGTH: break; case CTX_T_PARTIAL_LENGTH: total_read_ += down_ctx->length; callback_->OnDownloading(content_length_, total_read_); break; case CTX_T_END: logger("%s: read over", addr_.c_str()); break; default: logger_error("%s: ERROR", addr_.c_str()); break; } // 刪除在子線程中動態分配的對象 delete down_ctx; } //////////////////////////////////////////////////////////////////////////
三、在 MFC 界面類中建立 rpc_callback 的子類,接收子線程的 HTTP 處理結果:本例直接將對話框類繼承了 rpc_callback 接口類,其中部份內容以下:
// Cgui_rpcDlg 對話框 class Cgui_rpcDlg : public CDialog , public rpc_callback { // 構造 public: Cgui_rpcDlg(CWnd* pParent = NULL); // 標準構造函數 ~Cgui_rpcDlg(); ...... public: // 基類 rpc_callback 虛函數 // 設置 HTTP 請求頭數據虛函數 virtual void SetRequestHdr(const char* hdr); // 設置 HTTP 響應頭數據虛函數 virtual void SetResponseHdr(const char* hdr); // 下載過程當中的回調函數虛函數 virtual void OnDownloading(long long int content_length, long long int total_read); // 下載完成時的回調函數虛函數 virtual void OnDownloadOver(long long int total_read, double spent); ...... };
四、用 VC2003 編譯該例子,運行可執行程序能夠獲得以下的界面:
運行這個例子,在 URL 中輸入地址(如:http://www.sina.com.cn),點「開始運行」按鈕,在下載 URL 數據的過程當中移動界面窗口,能夠看到界面窗口的消息過程並未被阻塞(由於 HTTP 阻塞下載過程是在子線程中進行的),同時界面的狀態欄還能實時顯示當前 URL 下載的進度狀態(子線程經過 rpc_request 的消息傳遞方式將下載狀態通知界面主線程)。
4、小結
在界面編程中,將阻塞過程與界面過程分離( 即將阻塞過程交由子線程處理)是一種編程思想,不只能夠用在 PC 機的界面編程中,同時對於手機 APP 開發也有用處,這樣作的好處是:一方面能夠利用多核,更重要的是使得界面編程更爲簡單(要比全部模塊所有采用非阻塞編程要容易得多)。
5、參考
acl 庫下載地址:http://sourceforge.net/projects/acl/
acl 庫 SVN 地址:svn://svn.code.sf.net/p/acl/code/
QQ 羣:242722074