使用 acl 庫 rpc 功能類實現 阻塞任務過程與MFC 界面過程分離

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_cpp 的 rpc 相關類整合阻塞及非阻塞過程》

acl 庫下載地址:http://sourceforge.net/projects/acl/

acl 庫 SVN 地址:svn://svn.code.sf.net/p/acl/code/

我的微博:http://weibo.com/zsxxsz

QQ 羣:242722074

相關文章
相關標籤/搜索