非阻塞编程主要解决了网络通讯中高并发的问题,采用非阻塞方式,服务器不必为每个连接启动单独的进程或线程,从而大大地减少了系统资源的浪费;但是现实网络应用中,阻塞应用又是不可避免的,如我们对数据库编程时使用的数据操作的客户端库本身就是阻塞的。因此,单纯的非阻塞模式或阻塞模式均不能很好地胜任互联应用,如果能够将一些必要的阻塞过程融合进非阻塞过程中将会是一个现实的需求。本文主要介绍了如何保证在使用 acl_cpp 的非阻塞框架的同时,可以把阻塞的过程与非阻塞过程进行整合。关于非阻塞编程,可以参考 acl_cpp开发--非阻塞网络编程 中的章节。
本文讨论的内容是建立在 acl_cpp 非阻塞模块的IPC通信机制 内容的基础之上,有两个基础类:ipc_service 类用来粘合阻塞与非阻塞过程,ipc_request 做为阻塞调用过程的基础类,提供了需要子线程中进行阻塞式过程的虚方法。
ipc_service 类的继承关系图如下:
可以看出,ipc_service 是从 ipc_server 类继承而来,在 ipc_service 中定义了受保护的成员方法: request(ipc_request*) ,它将被子类对象调用向子线程或线程池发送任务请求;同时在 ipc_service 中还专门针对基于 win32 消息窗口定义了虚方法:win32_proc,该虚方法被子类继承后用于主窗口过程接收来自于子线程的结果处理消息;另外,在该图中还专门封装了三个实际应用的子类 http_service、db_service、dns_service。在 ipc_service 的构造函数可以指定任务线程池中最大线程数,同时还可以指定是否采用基于 win32 窗口消息的 IPC 通信机制。
ipc_request 类的继承关系图如下:
ipc_request 的类对象在独立的子线程中运行,运行结果通过 ipc_client 通道给非阻塞的主线程发送消息(对于win2的窗口过程,可以发送窗口消息,此时 ipc_service 的类对象将会通过 win32_proc 虚方法接收消息)。另外,ipc_request 的两个重载方法: run(ipc_client*) 和 run(HWND) 都是在子线程中运行的。
程序执行的流程为:
1)在主线程中创建 ipc_service 对象实例,并监听某一端口:ipc_service::open(aio_handle*, const char* addr="127.0.0.1:0") ---> 主线程进行异步事件循环过程:while (true) { aio_handle::check(); }
2)在主线程中创建 ipc_request 对象实例,并创建需要在子线程中处理的阻塞式任务 ---> 在主线程 ipc_service 的对象中调用 ipc_service 的异步请求方法 request(ipc_request*) 将阻塞请求任务 ipc_request 传递给子线程
3)在子线程中调用 ipc_request::run 阻塞过程处理用户的请求
4)在子线程中调用 ipc_client::send_message 方法将结果数据以 ipc 消息的方式通知主线程
5)在主线程中接收到来自于子线程的结果处理消息后,将结果传递给用户过程
以下以 dns_service 为例,详细讲解 ipc_service 与 ipc_request 在主线程和子线程中的行为过程:
1)主线程中 dns_service 的主要代码如下:
dns_service::dns_service(int nthread /* = 1 */, bool win32_gui /* = false */) : ipc_service(nthread, win32_gui) { } dns_service::~dns_service() { } // 主线程的 dns_service 对象接收到子线程消息的 ipc 连接请求时的回调函数 void dns_service::on_accept(aio_socket_stream* client) { // 创建 ipc_client 消息通道,同时用异步连接流作为通讯对象 ipc_client* ipc = NEW dns_ipc(this); ipc->open(client); // 添加消息回调对象 ipc->append_message(IPC_RES); // 异步等待来自于子线程的消息 ipc->wait(); } #ifdef WIN32 // 当接收 WIN32 窗口消息时的回调过程 void dns_service::win32_proc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam) { if (nMsg != IPC_RES + WM_USER) { logger_error("invalid nMsg(%d)", nMsg); return; } else if (lParam == 0) { logger_error("lParam invalid"); return; } DNS_IPC_DATA* dat = (DNS_IPC_DATA*) lParam; dns_res* res = dat->res; on_result(*res); delete res; // 在采用 WIN32 消息时该对象空间是动态分配的,所以需要释放 acl_myfree(dat); } #endif // 用户调用此函数查询某个域名,需要查询的域名及域名查询结果 // 均在 dns_result_callback 对象中记录 void dns_service::lookup(dns_result_callback* callback) { // 如果发现有针对相同域名的查询过程,只需要将本查询过程 // 下相同域名的查询过程合并,加入原来的查询过程的列表中, // 减少针对同一域名的查询次数 std::list<dns_result_callback*>::iterator it; const char* domain = callback->get_domain().c_str(); for (it= callbacks_.begin(); it != callbacks_.end(); it++) { if ((*it)->get_domain() == domain) { callbacks_.push_back(callback); return; } } callbacks_.push_back(callback); ipc_request* req = NEW dns_request(domain); // 调用基类 ipc_service 请求过程 request(req); } // 查询结果的回调函数 void dns_service::on_result(const dns_res& res) { std::list<dns_result_callback*>::iterator it, next; it= callbacks_.begin(); for (; it != callbacks_.end();) { next = it; next++; if ((*it)->get_domain() == res.domain_.c_str()) { // 通知请求对象的解析结果 (*it)->on_result((*it)->get_domain(), res); (*it)->destroy(); // 调用请求对象的销毁过程 callbacks_.erase(it); it = next; } else it++; } }
2)在 dns_service::on_accept 函数中创建 ipc_client 通道的子类定义如下:
class dns_ipc : public ipc_client { public: dns_ipc(dns_service* server) : server_(server) { } ~dns_ipc() { } // 在主线程接收到来自于子线程的消息时的回调函数 // 因为在 dns_service::on_accept 中将该类对象 // 与相应的消息号进行了绑定 virtual void on_message(int nMsg acl_unused, void* data, int dlen acl_unused) { if (nMsg != IPC_RES) { logger_error("invalid nMsg(%d)", nMsg); this->close(); return; } // 转换子线程传来的数据内容 DNS_IPC_DATA* dat = (DNS_IPC_DATA*) data; dns_res* res = dat->res; // 调用主线程中的结果接收过程 server_->on_result(*res); delete res; } protected: virtual void on_close() { // 因为该类对象在 dns_service::on_accept 中是动态创建的, // 所需要当 ipc_client 通道关闭时需要自行释放所占内存 delete this; } private: dns_service* server_; };
3)dns_request 类定义如下(该类对象是在主线程中创建的,但其中的阻塞方法:run 却是在子线程中运行的):
// 由子线程传递给主线程的数据类型 struct DNS_IPC_DATA { dns_res* res; }; // 由子线程传递给主线程的消息号 typedef enum { IPC_RES }; /////////////////////////////////////////////////////////////////////// class dns_request : public ipc_request { public: dns_request(const char* domain) : domain_(domain) { } ~dns_request() { } // 当 dns_request 类对象在 dns_service::lookup 方法中通过 // request(ipc_request) 过程传递给子线程后,子线程便会连接 // 主线程中 dns_service 监听的消息服务器地址,连接成功后调用 // dns_request::run 虚方法,同时将 ipc_client 通道传入 virtual void run(ipc_client* ipc) { // 阻塞式查询域名 ACL_DNS_DB* db = acl_gethostbyname(domain_.c_str(), NULL); // 将查询结果放在自定义的结构中 data_.res = NEW dns_res(domain_.c_str()); if (db != NULL) { ACL_ITER iter; acl_foreach(iter, db) { ACL_HOSTNAME* hn = (ACL_HOSTNAME*) iter.data; data_.res->ips_.push_back(hn->ip); } acl_netdb_free(db); } // 向主线程发送结果 ipc->send_message(IPC_RES, &data_, sizeof(data_)); // 销毁本类对象,因为其是动态分配的 delete this; } #ifdef WIN32 // 基类虚接口,使子线程可以在执行完任务后向主线程发送 WIN32 窗口消息 virtual void run(HWND hWnd) { ACL_DNS_DB* db = acl_gethostbyname(domain_.c_str(), NULL); DNS_IPC_DATA* data = (DNS_IPC_DATA*) acl_mymalloc(sizeof(DNS_IPC_DATA)); data->res = NEW dns_res(domain_.c_str()); if (db != NULL) { ACL_ITER iter; acl_foreach(iter, db) { ACL_HOSTNAME* hn = (ACL_HOSTNAME*) iter.data; data->res->ips_.push_back(hn->ip); } acl_netdb_free(db); } // 向主线程发送结果 ::PostMessage(hWnd, IPC_RES + WM_USER, 0, (LPARAM) data); // 销毁本类对象,因为其是动态分配的 delete this; } #endif private: string domain_; DNS_IPC_DATA data_; };
通过以上三个类的实现便完成了主线程的非阻塞过程与子线程的阻塞过程的结合。下面是一个具体的使用如上 dns_service 类的例子:
#ifdef WIN32 #include "acl/lib_acl.h" #else #include "lib_acl.h" #include <getopt.h> #endif #include <iostream> #include "aio_handle.hpp" #include "dns_service.hpp" using namespace acl; static void usage(const char* procname) { printf("usage: %s -h[help] -t[use thread]\n", procname); } static int __ncount = 0; class dns_result : public dns_result_callback { public: dns_result(const char* domain) : dns_result_callback(domain) { } ~dns_result() { } virtual void destroy() { delete this; __ncount--; } virtual void on_result(const char* domain, const dns_res& res) { std::cout << "result: domain: " << domain; if (res.ips_.size() == 0) { std::cout << ": null" << std::endl; return; } std::cout << std::endl; std::list<string>::const_iterator cit = res.ips_.begin(); for (; cit != res.ips_.end(); cit++) std::cout << "\t" << (*cit).c_str(); std::cout << std::endl; } }; int main(int argc, char* argv[]) { int ch, nthreads = 2; while ((ch = getopt(argc, argv, "ht:")) > 0) { switch (ch) { case 'h': usage(argv[0]); return (0); case 't': nthreads = atoi(optarg); break; default: break; } } acl_init(); aio_handle handle; const char* domain = "www.baidu.com"; dns_service* server = new dns_service(nthreads); // 使消息服务器监听 127.0.0.1 的地址 if (server->open(&handle) == false) { delete server; std::cout << "open server error!" << std::endl; getchar(); return (1); } // 创建查询结果接收对象,并进行查询 dns_result* result = new dns_result(domain); server->lookup(result); __ncount++; result = new dns_result(domain); server->lookup(result); __ncount++; result = new dns_result(domain); server->lookup(result); __ncount++; domain = "www.sina.com"; result = new dns_result(domain); server->lookup(result); __ncount++; domain = "www.51iker.com"; result = new dns_result(domain); server->lookup(result); __ncount++; domain = "www.hexun.com"; result = new dns_result(domain); server->lookup(result); __ncount++; domain = "www.com.dummy"; result = new dns_result(domain); server->lookup(result); __ncount++; result = new dns_result(domain); server->lookup(result); __ncount++; result = new dns_result(domain); server->lookup(result); __ncount++; // 异步消息循环过程 while (true) { if (handle.check() == false) { std::cout << "stop now!" << std::endl; break; } if (__ncount == 0) break; } // 销毁 dns_service 动态对象 delete server; // 做最后的清理工作,以释放延迟释放的连接对象 handle.check(); std::cout << "server stopped!" << std::endl; getchar(); return (0); }
个人微博:http://weibo.com/zsxxsz
示例代码:samples/aio_dns
acl_cpp 下载:http://sourceforge.net/projects/acl/
原文地址:http://zsxxsz.iteye.com/blog/1482208
更多文章:http://zsxxsz.iteye.com/
QQ 群:242722074