1. 设置环境变量
在操作系统添加一个名为ACE_ROOT的用户环境变量,值为刚才ace的解压路径D:\Develop\ACE_wrappers。
添加用户的Path环境变量,值为%ACE_ROOT%\lib,这样才能保证系统能找到ace生成的动态连接库。
设置VS2005的C++开发项目信息,依次打开菜单 工具-选项-项目和解决方案-VC++目录 ,在右侧目录列表中选择"包含目录",添加$(ACE_ROOT),在右侧目录列表中选择"库文件",添加$(ACE_ROOT)\lib。
2. 在 ACE_wrappers\ace 目录下创建 config.h 文件,写入:
#include "ace/config-win32.h"
Code
3. 如果你希望使用标准的 C++ 头文件(例如 iostream、cstdio 等)在 #include "ace/config-win32.h" 前加入:
#define ACE_HAS_STANDARD_CPP_LIBRARY 1
4. 如果你希望使用 MFC 库,那么 config.h 中加入:
#define ACE_HAS_MFC 1
如果你希望使用 MFC 静态库,那么加入:
#define ACE_USES_STATIC_MFC
5. 如果你希望编译静态版本的 ACE 库,那么在 config.h 中加入:
#define ACE_AS_STATIC_LIBS
6. 如果你希望减少静态库的大小,可以禁止使用 inline,在 config.h 的 #include "ace/config-win32.h" 前加入:
#define ACE_NO_INLINE
ACE结构简介
1)ACE OS adaptation 层:封装了 OS API,对上层提供 OS 平台无关的接口。
2)C++ wrapper facades 层:位于 OS adaptation 之上,提供了与之相似的功能,这些功能使用 C++ 的类封装起来,而不是 C 语言 API。每个 wrapper facade 都包含一个或者一个以上的类。我们可以有选择的继承、聚合这些 wrapper facade。
3)框架层(Framework layer)
框架层在 C++ wrapper facades 层之上,它集成和扩充了 wrapper facade 类。
<1> 事件多路分离和分发框架
ACE Reactor 和 ACE Proactor 实现了 Reactor 模式和 Proactor 模式。
<2> 连接建立和服务初始化框架
ACE Acceptor-Connector 框架实现了 Acceptor-Connector 模式。
<3> 并发框架
ACE 提供了 Task 框架实现了并发模式。
<4> 服务配置框架
ACE 的服务配置框架实现了 Component Configurator 模式。
<5> 流框架
ACE 的流框架实现了 Pipes and Fiters 模式。
4)ACE 网络组件层
组件(component)就是软件系统中被封装的一个部分,ACE 发行包中的组件用于提供以下功能:
<1> 演示 ACE
<2> 提供常见网络服务的可复用实现。如提供日志记录、时间同步等服务的可复用实现。
线程的创建与管理
一.线程入口函数
所有线程必须从一个指定的函数开始执行,该函数称为线程函数,它必须具有下列原型:
void* worker(void *arg) {}
该函数输入一个void *型的参数,可以在创建线程时传入。
注意:所有的线程启动函数(方法)必须是静态的或全局的(就如同直接使用OS线程API时所要求的一样)。
二.线程基本操作
1.创建一个线程
一个进程的主线程是由操作系统自动生成,如果你要让一个主线程创建额外的线程,可以通过ACE_Thread::spawn()实现,该函数一般的使用方式如下:
ACE_thread_t threadId;
ACE_hthread_t threadHandle;
ACE_Thread::spawn(
(ACE_THR_FUNC)worker, //线程执行函数
NULL, //执行函数参数
THR_JOINABLE | THR_NEW_LWP,
&threadId,
&threadHandle
);
ACE_Thread::spawn((ACE_THR_FUNC)worker) 使用其默认参数,来创建一个worker的线程。
ACE_Thread::spawn_n函数来创建多个线程。
2.终止线程
在线程函数体中ACE_Thread::exit()调用即可终止线程执行。
3.设定线程的相对优先级
当一个线程被首次创建时,它的优先级等同于它所属进程的优先级。一个线程的优先级是相对于其所属的进程的优先级而言的。可以通过调用ACE_Thread::setprio函数改变线程的相对优先级,该函数的调用方式如下:
ACE_Thread::setprio(threadHandle,ACE_DEFAULT_THREAD_PRIORITY)
4.挂起及恢复线程
挂起线程可以通过来实现,它能暂停一个线程的执行,其调用方式如下ACE_Thread::suspend(threadHandle) 。
相应的,可以通过ACE_Thread::resume(threadHandle) 恢复被挂起的线程的执行。
5.等待线程结束
在主函数中调用ACE_Thread::join(threadHandle)可阻塞主函数,直道线程结束才能继续执行。
6.停止线程
在主函数中调用ACE_Thread::cancel (threadHandle)可停止线程的执行(在Unix底下可以,而在windows下好像不起作用,有待检验)。
三.程序示例
下面例子演示了如何用ace创建一个线程。
#include "ace/Thread.h"
#include "ace/Synch.h"
#pragma comment(lib, "ACEd.lib")
#include <iostream>
usingnamespace std;
void* worker(void *arg)
{
for(int i=0;i<10;i++)
{
ACE_OS::sleep(1);
cout<<endl<<"hello world"<<endl;
}
return NULL;
}
int main(int argc, char *argv[])
{
ACE_thread_t threadId;
ACE_hthread_t threadHandle;
ACE_Thread::spawn(
(ACE_THR_FUNC)worker, //线程执行函数
NULL, //执行函数参数
THR_JOINABLE | THR_NEW_LWP,
&threadId,
&threadHandle
);
ACE_Thread::join(threadHandle);
return 0;
}
在这个简单的例子中,创建了1个工作者线程,执行程序中定义的worker()函数。然后阻塞主函数,待线程结束后退出程序。
ACE Lock类属
锁类属包含的类包装简单的锁定机制,比如互斥体、信号量、读/写互斥体和令牌等。这里我就以互斥体为例简单的介绍一下其使用方法,对其它的锁类进行一些简单的说明。
1.互斥体 ACE_Thread_Mutex
互斥体用于保护共享的易变代码,也就是全局或静态数据。这样的数据必须通过互斥体进行保护,以防止它们在多个线程同时访问时损坏。
#include "ace/Thread.h"
#include "ace/Synch.h"
#include <iostream>
usingnamespace std;
ACE_Thread_Mutex mutex;
void* Thread1(void *arg)
{
mutex.acquire();
ACE_OS::sleep(3);
cout<<endl<<"hello thread1"<<endl;
mutex.release();
return NULL;
}
void* Thread2(void *arg)
{
mutex.acquire();
cout<<endl<<"hello thread2"<<endl;
mutex.release();
return NULL;
}
int main(int argc, char *argv[])
{
ACE_Thread::spawn((ACE_THR_FUNC)Thread1);
//Thread2 比Thread1晚创建1秒钟,故后尝试获取互斥体
ACE_OS::sleep(1);
ACE_Thread::spawn((ACE_THR_FUNC)Thread2);
while(true)
ACE_OS::sleep(10);
return 0;
}
ACE_Thread_Mutex主要有两个方法:
acquire():用来获取互斥体,如果无法获取,将阻塞至获取到为止。
release():用来释放互斥体,从而使自己或者其它线程能够获取互斥体。
当线程要访问共享资源时,首先调用acquire()方法获取互斥体,从而获取对改互斥体所保护的共享资源的唯一访问权限,访问结束时调用释放互斥体,使得其它线程能获取共享资源的访问权限。
2.ACE Lock类属简介。
ACE Lock类属列表如下:
ACE_Mutex
封装互斥机制(根据平台,可以是mutex_t、pthread_mutex_t等等)的包装类,用于提供简单而有效的机制来使对共享资源的访问序列化。它与二元信号量(binary semaphore)的功能相类似。可被用于线程和进程间的互斥。
ACE_Thread_Mutex
可用于替换ACE_Mutex,专用于线程同步。
ACE_Process_Mutex
可用于替换ACE_Mutex,专用于进程同步。
ACE_NULL_Mutex
提供了ACE_Mutex接口的"无为"(do-nothing)实现,可在不需要同步时用作替换。
ACE_RW_Mutex
封装读者/作者锁的包装类。它们是分别为读和写进行获取的锁,在没有作者在写的时候,多个读者可以同时进行读取。
ACE_RW_Thread_Mutex
可用于替换ACE_RW_Mutex,专用于线程同步。
ACE_RW_Process_Mutex
可用于替换ACE_RW_Mutex,专用于进程同步。
ACE_Semaphore
这些类实现计数信号量,在有固定数量的线程可以同时访问一个资源时很有用。在OS不提供这种同步机制的情况下,可通过互斥体来进行模拟。
ACE_Thread_Semaphore
应被用于替换ACE_Semaphore,专用于线程同步。
ACE_Process_Semaphore
应被用于替换ACE_Semaphore,专用于进程同步。
ACE_Token
提供"递归互斥体"(recursive mutex),也就是,当前持有某令牌的线程可以多次重新获取它,而不会阻塞。而且,当令牌被释放时,它确保下一个正阻塞并等待此令牌的线程就是下一个被放行的线程。
ACE_Null_Token
令牌接口的"无为"(do-nothing)实现,在你知道不会出现多个线程时使用。
ACE_Lock
定义锁定接口的接口类。一个纯虚类,如果使用的话,必须承受虚函数调用开销。
ACE_Lock_Adapter
基于模板的适配器,允许将前面提到的任意一种锁定机制适配到ACE_Lock接口。
可以简单的分为以下几类:
互斥锁
互斥锁(通常称为"互斥体"或"二元信号量")用于保护多线程控制并发访问的共享资源的完整性。互斥体通过定义临界区来序列化多线程控制的执行,在临界区中每一时刻只有一个线程在执行它的代码。互斥体简单而高效(时间和空间)。
ACE线程库提供了Mutex式的类(是一组互斥体对象,拥有类似的接口),他是一种简单而高效的类型是"非递归"互斥体。非递归互斥体不允许当前拥有互斥体的线程在释放它之前重新获取它。否则,将会立即发生死锁。递归互斥体在ACE Recursive_Thread_Mutex类中可移植地实现。
读者/作者锁
读者/作者锁与互斥体相类似。例如,获取读者/作者锁的线程也必须释放它。多个线程可同时获取一个读者/作者锁用于读,但只有一个线程可以获取该锁用于写。当互斥体保护的资源用于读远比用于写要频繁时,读者/作者互斥体有助于改善并发的执行。
ACE线程库提供了一个叫作RW_Mutex的类,在C++封装类中可移植地实现了读者/作者锁的语义。读者/作者锁将优先选择权给作者。因而,如果有多个读者和一个作者在锁上等待,作者将会首先获取它。
计数信号量
在概念上,计数信号量是可以原子地增减的整数。如果线程试图减少一个值为零的信号量的值,它就会阻塞,直到另一个线程增加该信号量的值。
计数信号量用于追踪共享程序状态的变化。它们记录某种特定事件的发生。因为信号量维护状态,它们允许线程根据该状态来作决定,即使事件是发生在过去。
信号量比互斥体效率要低,但是,它们要更为通用,因为它们无需被最初获取它们的同一线程获取和释放。这使得它们能够用于异步的执行上下文中(比如信号处理器)。ACE线程库提供一个叫作Semaphore的类来可移植地在C++包装类中实现信号量语义。
ACE Guard类属
与C一级的互斥体API相比较,Mutex包装为同步多线程控制提供了一种优雅的接口。但是,Mutex潜在地容易出错,因为程序员有可能忘记调用release方法(当然,C级的互斥体API更容易出错)。这可能由于程序员的疏忽或是C++异常的发生而发生,然而,其导致及其严重的后果--死锁。
因此,为改善应用的健壮性,ACE同步机制有效地利用C++类构造器和析构器的语义来确保Mutex锁被自动获取和释放。
ACE提供了一个称为Guard、Write_Guard和Read_Guard的类族,确保在进入和退出C++代码块时分别自动获取和释放锁。
Guard类是最基本的守卫机制,定义可以简化如下(实际定义比这相对要复杂而完善一点):
template <class LOCK>
class Guard
{
public:
Guard (LOCK &l): lock_ (&l){ lock_.acquire (); }
˜Guard (void) { lock_.release (); }
private:
LOCK lock_;
}
Guard类的对象定义一"块"代码,在其上锁被自动获取,并在退出块时自动释放,即使是程序抛异常也能保证自动解锁。这种机制也能为Mutex、RW_Mutex和Semaphore同步封装工作。
对于读写锁,由于加锁接口不一样,ace也提供了相应的Read_Guard和Write_Guard类,Read_Guard和Write_Guard类有着与Guard类相同的接口。但是,它们的acquire方法分别对锁进行读和写。
缺省地, Guard类构造器将会阻塞程序,直到锁被获取。会有这样的情况,程序必须使用非阻塞的acquire调用(例如,防止死锁)。因此,可以传给ACE Guard的构造器第二个参数(请参看原始代码,而不是我这里的简化代码),指示它使用锁的try_acquire方法,而不是acquire。随后调用者可以使用Guard的locked方法来原子地测试实际上锁是否已被获取。
用Guard重写上一节的Thread1方法如下(注释了的部分是原有代码):
void* Thread1(void *arg)
{
ACE_Guard<ACE_Thread_Mutex> guard(mutex);
//mutex.acquire();
ACE_OS::sleep(3);
cout<<endl<<"hello thread1"<<endl;
//mutex.release();
return NULL;
}
相比较而言,使用Guard更加简洁,并且会自动解锁,免除了一部分后顾之忧。
注意:
Guard只能帮你自动加解锁,并不能解决死锁问题,特别是对于那些非递归的互斥体来说使用Guard尤其要注意防止死锁。
Guard是在Guard变量析构时解锁,如果在同一函数中两次对同一互斥体变量使用Guard要注意其对象生命周期,否则容易造成死锁。
ACE Condition类属
ACE Condition类属(条件变量)提供风格与互斥体、读者/作者锁和计数信号量不同的锁定机制。当持有锁的线程在临界区执行代码时,这三种机制让协作线程进行等待。相反,条件变量通常被一个线程用于使自己等待,直到一个涉及共享数据的条件表达式到达特定的状态。当另外的协作线程指示共享数据的状态已发生变化,调度器就唤醒一个在该条件变量上挂起的线程。于是新唤醒的线程重新对它的条件表达式进行求值,如果共享数据已到达合适状态,就恢复处理。
ACE线程库提供一个叫作Condition的类来可移植地在C++包装类中实现条件变量语义。定义方式如下:
ACE_Thread_Mutex mutex;
ACE_Condition<ACE_Thread_Mutex> cond(mutex);
该对象有两个常用方法。
signal()
向使用该条件变量的其它线程发送满足条件信号。
wait()
查询是否满足条件,如果满足,则继续往下执行;如果不满足条件,主线程就等待在此条件变量上。条件变量随即自动释放互斥体,并使主线程进入睡眠。
条件变量总是与互斥体一起使用。这是一种可如下描述的一般模式:
while( expression NOT TRUE ) wait on condition variable;
条件变量不是用于互斥,往往用于线程间的协作,下面例子演示了通过条件变量实现线程协作。
#include "ace/Thread.h"
#include "ace/Synch.h"
#include <iostream>
usingnamespace std;
ACE_Thread_Mutex mutex;
ACE_Condition<ACE_Thread_Mutex> cond(mutex);
void* worker(void *arg)
{
ACE_OS::sleep(2); //保证eater线程的cond.wait()在worker线程的cond.signal()先执行
mutex.acquire();
ACE_OS::sleep(1);
cout<<endl<<"produce"<<endl;
cond.signal();
mutex.release();
return NULL;
}
void* eater(void *arg)
{
mutex.acquire();
cond.wait();
cout<<endl<<"eat"<<endl;
mutex.release();
return NULL;
}
int main(int argc, char *argv[])
{
ACE_Thread::spawn((ACE_THR_FUNC)worker);
ACE_OS::sleep(1);
ACE_Thread::spawn((ACE_THR_FUNC)eater);
while(true)
ACE_OS::sleep(10);
return 0;
}
这个例子中,首先创建了一个生产者线程worker和一个消费者线程eater,消费者线程执行比生产者快,两个线程不加限制并发执行会导致先消费,后生产的情况(只是加互斥锁也不能很好的解决,以为无法保证生产者一定先获得互斥体)。所以这里通过条件变量的通知方式保证线程的顺序执行:
1. 消费者线程获取互斥体,等待条件满足(生产者生产了食品)。同时释放互斥体,进入休眠状态。
2. 生产者获取互斥体(虽然是消费者先获取的互斥体,但消费者调用的wait函数会释放消费者的互斥体),生产商品后,通过条件变量发送信号(调用signal函数)通知消费者生产完成,结束生产过程,释放互斥体。
3. 消费者收到信号后,重新获取互斥体,完成消费过程。
使用条件变量的注意事项:
1. 条件变量必须和互斥体一起使用,也就是说使用前必须加锁(调用互斥体acquire函数),使用完后需释放互斥体。
条件变量中的wait()和signal()成对使用的话,必须保证wait()函数在signal()之前执行,这样才能保证wait()能收到条件满足通知,不至于一直等待下去,形成死锁(worker线程中的第一句话就是起的这个作用)。
ACE Synchronization类
这一类并发控制对象一般也叫做杂项并发类,这类对象一般用得不多,这里我只是对其作一些简单的介绍。
1.Atomic_Op类
ACE_Atomic_Op类用于将同步透明地参数化进基本的算术运算中。
ACE_Atomic_Op是一种模板类,锁定机制和需要参数化的类型被作为参数传入其中,重载所有算术操作符,并确保在操作前获取锁,在操作后释放它。运算本身被委托给通过模板传入的的类。
使用ACE_Atomic_Op进行变量封装时,对于那些用ACE_Atomic_Op封装了的变量操作都变成了线程安全的,而并看不到显式的加解锁代码,代码变得更简洁,优雅。
2.ACE中的栅栏(Barrier)
一组线程可以使用栅栏来进行共同的相互同步。组中的每个线程各自执行,直到到达栅栏,就阻塞在那里。在所有相关线程到达栅栏后,它们就全部继续它们的执行。就是说,它们一个接一个地阻塞,等待其他的线程到达栅栏;一旦所有线程都到达了它们的执行路径中的"栅栏点",它们就一起重新启动。
在ACE中,栅栏在ACE_Barrier类中实现。在栅栏对象被实例化时,它将要等待的线程的数目会作为参数传入。一旦到达执行路径中的"栅栏点",每个线程都在栅栏对象上发出wait()调用。它们在这里阻塞,直到其他线程到达它们各自的"栅栏点",然后再一起继续执行。当栅栏从相关线程那里接收了适当数目的wait()调用时,它就同时唤醒所有阻塞的线程。
举个简单的例子,运动员进行赛跑比赛时,虽然他们到达终点有先后顺序,但会等到所有的运动员跑完比赛后才一起领奖。
面向对象的线程类ACE_Task
我们在前一章中使用ACE_Thread包装时,你一定已经注意到了一些不够"优雅"的地方。那一章中的大多数程序都被分解为函数、而不是对象。这是因为ACE_Thread包装需要一个全局函数名、或是静态方法作为参数。随后该函数(静态方法)就被用作所派生的线程的"启动点"。这自然就使得程序员要为每个线程写一个函数。如我们已经看到的,这可能会导致非面向对象的程序分解。
ACE_Task对常用线程处理进行了OO包装,通过ACE_Task,能对线程进行更好的操作。
ACE_Task是ACE中的任务或主动对象“处理结构”的基类。ACE使用此类来实现主动对象模式。所有希望成为“主动对象”的对象都必须由此类派生。同时可将它看作是更高级的、更为面向对象的线程。
每个任务都含有一或多个线程,以及一个底层消息队列。各个任务通过消息队列进行通信。至于消息队列实现的内在细节程序员不必关注。发送任务用putq() 将消息插入到另一任务的消息队列中,接收任务通过使用getq()将消息提取出来。这样的体系结构大大简化了多线程程序的编程模型。
其主要成员如下:
open():初始化资源
close():释放资源
activate():启动线程,可指定线程的数目
svc():线程的启动位置
putq():放置消息到任务的消息队列中
getq():从任务的消息队列中取出消息
thr_count():返回任务中线程的数目
last_thread():返回任务中将线程计数器从1降为0的线程的ID
要创建任务,需要进行以下步骤:
实现服务初始化和终止方法:
open()方法应该包含所有专属于任务的初始化代码。其中可能包括诸如连接控制块、锁和内存这样的资源。close()方法是相应的终止方法。
调用启用(Activation)方法:
在主动对象实例化后,你必须通过调用activate()启用它。要在主动对象中创建的线程的数目,以及其他一些参数,被传递给activate()方法。activate()方法会使svc()方法成为所有它生成的线程的启动点。
实现服务专有的处理方法:
如上面所提到的,在主动对象被启用后,各个新线程在svc()方法中启动。应用开发者必须在子类中定义此方法。
下面的例子演示怎样去创建任务:
#include "ace/Task.h"
#include "ace/OS.h"
#include <iostream>
usingnamespace std;
class TaskThread: public ACE_Task<ACE_MT_SYNCH>
{
public:
virtualint svc(void)
{
for(int i=0;i<10;i++)
{
ACE_OS::sleep(1);
cout<<endl<<"hello thread1"<<endl;
}
return 0;
}
};
int main(int argc, char *argv[])
{
TaskThread task;
task.activate();
while(true)
ACE_OS::sleep(10);
return 0;
}
ACE_Task也封装了常用线程操作,如暂停,恢复及停止等,是不是非常简单和方便呢。
其实ACE_Task的使用还不仅仅是这些,通过它还可实现一种很常用的网络编程模式--主动对象模式,其具体功能在后续的设计模式部分将作详细的介绍。
ACE_Message_Queue
在Windows和Linux的config文件中都没有定义"ACE_HAS_TIMED_MESSAGE_BLOCKS"这个宏,所以msg_deadline_time和msg_execution_time都不起任何作用.
ACE_Message_Queue_Factory这个工厂提供三个静态函数分别用来创建静态消息队列和两种类型的动态消息队列。静态消息队列的消息也支持优先级,但是消息的优先级是静态的,不需要通过动态计算而来。水位用来控制消息队列中数据的大小,高水位(high_water_mark)用于控制消息队列的上限,它用于控制生产者往里面放数据的量,如果消息队列中数据量已经达到高水位,而用使用了锁,既使“ACE_Message_Queue_Factory<ACE_MT_SYNCH>::create_static_message_queue();”创建消息队列,那么生产者将被阻塞。高水位很容易理解,但是低水位是用来做什么的呢?
只要消息队列中还有数据消费者就不会被阻塞的,而当数据量超过高水位时,生产者会被阻塞,既然会被阻塞,那么它肯定需要被唤醒,那么什么时候由谁来唤醒生产者呢?这就是低水位的作用,消费者一直消费数据,当数据低于低水位时它就唤醒生产者。
下面的代码很好的展示了静态消息队列的使用。
#include "ace/Message_Queue.h"
#include "ace/Get_Opt.h"
#include "ace/OS.h"
#include <ace/Thread_Manager.h>
#include <ace/Synch.h>
//消息队列指针
ACE_Message_Queue<ACE_MT_SYNCH>* mq;
constchar S1[] = "C++";
constchar S2[] = "Java";
constchar S3[] = "PHP";
constchar S4[] = "C#";
//四个消息指针
ACE_Message_Block* mb1, * mb2, * mb3, * mb4;
//生产者
staticvoid* produce(void *arg)
{
staticint loop = 1;
while(true)
{
ACE_OS::sleep(2);
ACE_DEBUG((LM_DEBUG, "(%P : %t) producer...\n"));
while(true)
{
if(loop == 1)
{
//将高水位设置为10, S1+S2的长度为3+4+2=9<10,因此可以将S3放进去
//但是再放入S4时生产者将会被阻塞
//需要注意的是水位的大小并不是消息的个数,而是消息队列中消息里面的数据量之和
//如果也能以消息的个数作为高低水位的值就好了
mq->high_water_mark(10);
mq->enqueue_prio (mb1);
mq->enqueue_prio (mb2);
mq->enqueue_prio (mb3);
ACE_DEBUG((LM_DEBUG, "(%P : %t) producer will pending!!\n"));
//因为消费者在睡眠6秒之后才会调用deactivate,因此生产者会在这儿阻塞几秒钟
//可以不断地将msg_bytes打印出来观察观察
int ret = mq->enqueue_prio (mb4);
ACE_DEBUG((LM_DEBUG, "(%P : %t) producer waken up by deactivate, ret = %d!!\n", ret));
++loop;
}
if(loop == 2)
{
ACE_OS::sleep(6);
//将低水位设置为5,因为高水位仍然为10,当前的数据量又超过了10,
//所以下面的入队操作仍会将生产者阻塞
//这样消费者消费消息,当数据量小于5时,将唤醒生产者
//生产者在此处等待被消费者唤醒
mq->low_water_mark(5);
ACE_DEBUG((LM_DEBUG, "(%P : %t) producer will pending again!!\n"));
mq->enqueue_prio (mb4);
ACE_DEBUG((LM_DEBUG, "(%P : %t) producer waken up by consumer!!\n"));
++loop;
}
}
}
return NULL;
}
//消费者
void* consume(void *arg)
{
staticint loop = 1;
while(true)
{
ACE_OS::sleep(2);
ACE_DEBUG((LM_DEBUG, "(%P : %t) consumer...\n"));
if(loop == 1)
{
//等待6秒,此时生产者和消费者都将被阻塞
ACE_OS::sleep(6);
//deactivate会唤醒所有的线程,将消息队列设置为不可用
//以后所存取操作都会返回-1
//这个操作会唤醒生产者
mq->deactivate();
++loop;
}
if(loop == 2)
{
ACE_OS::sleep(2);
//将消息队列的状态设置成ACTIVATED
//消息又可以使用了
mq->activate();
++loop;
}
if(loop == 3)
{
ACE_OS::sleep(10);
//消费两个消息之后,数据量就小于5了,低于低水位将唤醒生产者
ACE_Message_Block *mb;
mq->dequeue_head (mb);
mq->dequeue_head (mb);
ACE_DEBUG((LM_DEBUG, "(%P : %t) consumer wake up producer!!\n"));
++loop;
}
}
return NULL;
}
int main(int argc, char* argv[])
{
mq = ACE_Message_Queue_Factory<ACE_MT_SYNCH>::create_static_message_queue();
int priority;
//使用随机数作为消息的优先级
//数字越高,优先级越高
priority = ACE_OS::rand() % 100;
mb1 = new ACE_Message_Block(S1, sizeof S1, priority);
priority = ACE_OS::rand() % 100;
mb2 = new ACE_Message_Block(S2, sizeof S2, priority);
priority = ACE_OS::rand() % 100;
mb3 = new ACE_Message_Block(S3, sizeof S3, priority);
priority = ACE_OS::rand() % 100;
mb4 = new ACE_Message_Block(S4, sizeof S4, priority);
//将消息压入队列中,enqueue_prio根据消息的优先级将消息放到适当的位置上
//enqueue_head只是简单地将数据存入队列中,而不考虑消息的优先级
//使用enqueue_prio压入消息后,可以简单通过dequeue_head和dequeue_tail
//分别按优先级从高到低和从低到高取消息
//如果使用enqueue_head和enqueue_tail压入消息
//则需要通过dequeue_prio来按照消息的优先级依次将消息出队列
//没有必要既使用enqueue_prio压入消息,又实用dequeue_prio来取消息
mq->enqueue_prio (mb1);
mq->enqueue_prio (mb2);
mq->enqueue_prio (mb3);
mq->enqueue_prio (mb4);
//输出静态消息队列的相关信息
//高低水位默认值均为16384
ACE_DEBUG((LM_DEBUG, "count : %d, bytes : %d, length : %d, high_water_mark : %d, low_water_mark : %d, status : %d\n",
mq->message_count(), mq->message_bytes(), mq->message_length(),
mq->high_water_mark(), mq->low_water_mark(),
mq->state()));
ACE_Message_Block *mb;
//使用next遍历消息,遍历的顺序为高优先级到底优先级
ACE_DEBUG((LM_DEBUG, "===========next=============\n"));
//peek一下,并不弹出消息,类似Windows的PeekMessage
mq->peek_dequeue_head(mb);
do
{
ACE_DEBUG((LM_DEBUG, "message: %s, priority: %d\n", mb->rd_ptr(), mb->msg_priority()));
}while(mb = mb->next());
//使用迭代器遍历消息队列,遍历的顺序为高优先级到底优先级
ACE_DEBUG((LM_DEBUG, "=========iterator=============\n"));
ACE_Message_Queue<ACE_MT_SYNCH>::ITERATOR iterator (*mq);
for (ACE_Message_Block *entry = 0;
iterator.next (entry) != 0;
iterator.advance ())
{
ACE_DEBUG((LM_DEBUG, "message: %s, priority: %d\n", entry->rd_ptr(), entry->msg_priority()));
}
ACE_DEBUG((LM_DEBUG, "============dequeue_head==========\n"));
while(mq->dequeue_head (mb) != -1)
{
ACE_DEBUG((LM_DEBUG, "message: %s, priority: %d\n", mb->rd_ptr(), mb->msg_priority()));
//这里如果不判断的话,消息队列空时会导致主线程被阻塞
if(mq->is_empty())
break;
}
ACE_DEBUG((LM_DEBUG, "\n\n"));
//////////////////////////////////测试高低水位和队列的state使用,进行测试之前mq队列已空///////////////////////////
//产生一个生产者线程
ACE_Thread_Manager::instance()->spawn_n
(
1,
(ACE_THR_FUNC) produce
);
////产生两个消费者线程
ACE_Thread_Manager::instance()->spawn_n
(
1,
(ACE_THR_FUNC) consume
);
//挂起主线程
ACE_Thread_Manager::instance()->wait();
return 0;
}
ACE中TCP通信
Tcp通信过程一般为如下步骤:
1. 服务器绑定端口,等待客户端连接。
2. 客户端通过服务器的ip和服务器绑定的端口连接服务器。
3. 服务器和客户端通过网络建立一条数据通路,通过这条数据通路进行数据交互。
常用API:
1. ACE_INET_Addr类。
ACE"地址"类ACE_Addr的子类,表示TCP/IP和UDP/IP的地址。它通常包含机器的ip和端口信息,通过它可以定位到所通信的进程。
定义方式:
ACE_INET_Addr addInfo(3000,"192.168.1.100");
常用方法:
1. get_host_name 获取主机名
2. get_ip_address 获取ip地址
3. get_port_number 获取端口号
2. ACE_SOCK_Acceptor类。
服务期端使用,用于绑定端口和被动地接受连接。
常用方法:
1. open 绑定端口
2. accept建立和客户段的连接
3.ACE_SOCK_Connector类。
客户端使用,用于主动的建立和服务器的连接。
常用方法:
1. connect() 建立和服务期的连接。
4. ACE_SOCK_Stream类。
客户端和服务器都使用,表示客户段和服务器之间的数据通路。
常用方法:
1. send () 发送数据
2. recv () 接收数据
3. close() 关闭连接(实际上就是断开了socket连接)。
代码示例:
下面例子演示了如何如何用ACE创建TCP通信的Server端。
#include "ace/SOCK_Acceptor.h"
#include "ace/SOCK_Stream.h"
#include "ace/INET_Addr.h"
#include "ace/OS.h"
#include <string>
#include <iostream>
usingnamespace std;
int main(int argc, char *argv[])
{
ACE_INET_Addr port_to_listen(3000); //绑定的端口
ACE_SOCK_Acceptor acceptor;
if (acceptor.open (port_to_listen, 1) == -1) //绑定端口
{
cout<<endl<<"bind port fail"<<endl;
return -1;
}
while(true)
{
ACE_SOCK_Stream peer; //和客户端的数据通路
ACE_Time_Value timeout (10, 0);
if (acceptor.accept (peer) != -1) //建立和客户端的连接
{
cout<<endl<<endl<<"client connect. "<<endl;
char buffer[1024];
ssize_t bytes_received;
ACE_INET_Addr raddr;
peer.get_local_addr(raddr);
cout<<endl<<"local port\t"<<raddr.get_host_name()<<"\t"<<raddr.get_port_number()<<endl;
while ((bytes_received =
peer.recv (buffer, sizeof(buffer))) != -1) //读取客户端发送的数据
{
peer.send(buffer, bytes_received); //对客户端发数据
}
peer.close ();
}
}
return 0;
}
这个例子实现的功能很简单,服务器端绑定3000号端口,等待一个客户端的连接,然后将从客户端读取的数据再次转发给客户端,也就是实现了一个EchoServer的功能。
相应的客户端程序也比较简单,代码如下:
#include <ace/SOCK_Stream.h>
#include <ace/SOCK_Connector.h>
#include <ace/INET_Addr.h>
#include <ace/Time_Value.h>
#include <string>
#include <iostream>
usingnamespace std;
int main(int argc, char *argv[])
{
ACE_INET_Addr addr(3000,"127.0.0.1");
ACE_SOCK_Connector connector;
ACE_Time_Value timeout(5,0);
ACE_SOCK_Stream peer;
if(connector.connect(peer,addr,&timeout) != 0)
{
cout<<"connection failed !"<<endl;
return 1;
}
cout<<"conneced !"<<endl;
string s="hello world";
peer.send(s.c_str(),s.length()); //发送数据
cout<<endl<<"send:\t"<<s<<endl;
ssize_t bc=0; //接收的字节数
char buf[1024];
bc=peer.recv(buf,1024,&timeout); //接收数据
if(bc>=0)
{
buf[bc]='\0';
cout<<endl<<"rev:\t"<<buf<<endl;
}
peer.close();
return 0;
}
ACE中UDP通信
udp是一种无连接的协议,提供无连接不可靠的服务。
在ace中,通过ACE_SOCK_Dgram类提供udp通信服务,ACE_SOCK_Dgram和ACE_SOCK_Stream的API非常类似,一样提供了send,recv及close等常用操作,这里就不再累述了。
udp通信时无需像tcp那样建立连接和关闭连接,tcp编程时需要通过accept和connect来建立连接,而udp通信省略了这一步骤,相对来说编程更为简单。
由于udp通信时无建立连接,服务器端不能像Tcp通信那样在建立连接的时候就获得客户端的地址信息,故服务器端不能主动对客户端发送信息(不知道客户端的地址),只有等到收到客户端发送的udp信息时才能确定客户端的地址信息,从而进行通信。
udp通信过程如下:
服务器端绑定一固定udp端口,等待接收客户端的通信。
客户端通过服务器的ip和地址信息直接对服务器端发送消息。
服务器端收到客户端发送的消息后获取客户端的ip和端口信息,通过该地址信息和客户端通信。
下面代码为EchoServer的udp版:
//server.cpp
#include <ace/SOCK_Dgram.h>
#include <ace/INET_Addr.h>
#include <ace/Time_Value.h>
#include <string>
#include <iostream>
usingnamespace std;
int main(int argc, char *argv[])
{
ACE_INET_Addr port_to_listen(3000); //绑定的端口
ACE_SOCK_Dgram peer(port_to_listen); //通信通道
char buf[100];
while(true)
{
ACE_INET_Addr remoteAddr; //所连接的远程地址
int bc = peer.recv(buf,100,remoteAddr); //接收消息,获取远程地址信息
if( bc != -1)
{
string s(buf,bc);
cout<<endl<<"rev:\t"<<s<<endl;
}
peer.send(buf,bc,remoteAddr); //和远程地址通信
}
return 0;
}
相应的客户端程序如下:
//client.cpp
#include <ace/SOCK_Dgram.h>
#include <ace/INET_Addr.h>
#include <ace/Time_Value.h>
#include <string>
#include <iostream>
usingnamespace std;
int main(int argc, char *argv[])
{
ACE_INET_Addr remoteAddr(3000,"127.0.0.1"); //所连接的远程地址
ACE_INET_Addr localAddr; //本地地址信息
ACE_SOCK_Dgram peer(localAddr); //通信通道
peer.send("hello",5,remoteAddr); //发送消息
char buf[100];
int bc = peer.recv(buf,100,remoteAddr); //接收消息
if( bc != -1)
{
string s(buf,bc);
cout<<endl<<"rev:\t"<<s<<endl;
}
return 0;
}
和tcp编程相比,udp无需通过acceptor,connector来建立连接,故代码相对tcp编程来说要简单许多。另外,由于udp是一种无连接的通信方式,ACE_SOCK_Dgram的实例对象中无法保存远端地址信息(保存了本地地址信息),故通信的时候需要加上远端地址信息。
ACE主动对象模式
主动对象模式用于降低方法执行和方法调用之间的耦合。该模式描述了另外一种更为透明的任务间通信方法。
传统上,所有的对象都是被动的代码段,对象中的代码是在对它发出方法调用的线程中执行的,当方法被调用时,调用线程将阻塞,直至调用结束。而主动对象却不一样。这些对象具有自己的命令执行线程,主动对象的方法将在自己的执行线程中执行,不会阻塞调用方法。
例如,设想对象"A"已在你的程序的main()函数中被实例化。当你的程序启动时,OS创建一个线程,以从main()函数开始执行。如果你调用对象A的任何方法,该线程将"流过"那个方法,并执行其中的代码。一旦执行完成,该线程返回调用该方法的点并继续它的执行。但是,如果"A"是主动对象,事情就不是这样了。在这种情况下,主线程不会被主动对象借用。相反,当"A"的方法被调用时,方法的执行发生在主动对象持有的线程中。另一种思考方法:如果调用的是被动对象的方法(常规对象),调用会阻塞(同步的);而另一方面,如果调用的是主动对象的方法,调用不会阻塞(异步的)。
由于主动对象的方法调用不会阻塞,这样就提高了系统响应速度,在网络编程中是大有用武之地的。
在这里我们将一个"Logger"(日志记录器)对象为例来介绍如何将一个传统对象改造为主动对象,从而提高系统响应速度。
Logger的功能是将一些系统事件的记录在存储器上以备查询,由于Logger使用慢速的I/O系统来记录发送给它的消息,因此对Logger的操作将会导致系统长时间的等待。
其功能代码简化如下:
class Logger: public ACE_Task<ACE_MT_SYNCH>
{
public:
void LogMsg(const string& msg)
{
cout<<endl<<msg<<endl;
ACE_OS::sleep(2);
}
};
为了实现记录日志操作的主动执行,我们需要用命令模式将其封装,从而使得记录日志的方法能在合适的时间和地方主动执行,封装方式如下:
class LogMsgCmd: public ACE_Method_Object
{
public:
LogMsgCmd(Logger *plog,conststring& msg)
{
this->log=plog;
this->msg=msg;
}
int call()
{
this->log->LogMsg(msg);
return 0;
}
private:
Logger *log;
string msg;
};
class Logger: public ACE_Task<ACE_MT_SYNCH>
{
public:
void LogMsg(conststring& msg)
{
cout<<endl<<msg<<endl;
ACE_OS::sleep(2);
}
LogMsgCmd *LogMsgActive(conststring& msg)
{
new LogMsgCmd(this,msg);
}
};
这里对代码功能做一下简单的说明:
ACE_Method_Object是ACE提供的命令模式借口,命令接口调用函数为int call(),在这里通过它可以把每个操作日志的调用封装为一个LogMsgCmd对象,这样,当原来需要调用LogMsg的方法的地方只要调用LogMsgActive即可生成一个LogMsgCmd对象,由于调用LogMsgActive方法,只是对命令进行了封装,并没有进行日志操作,所以该方法会立即返回。然后再新开一个线程,将LogMsgCmd对象作为参数传入,在该线程中执行LogMsgCmd对象的call方法,从而实现无阻塞调用。
然而,每次对一个LogMsg调用都开启一个新线程,无疑是对资源的一种浪费,实际上我们往往将生成的LogMsgCmd对象插入一个命令队列中,只新开一个命令执行线程依次执行命令队列中的所有命令。并且,为了实现对象的封装,命令队列和命令执行线程往往也封装到Logger对象中,代码如下所示:
#include "ace/OS.h"
#include "ace/Task.h"
#include "ace/Method_Object.h"
#include "ace/Activation_Queue.h"
#include "ace/Auto_Ptr.h"
#include <string>
#include <iostream>
usingnamespace std;
class Logger: public ACE_Task<ACE_MT_SYNCH>
{
public:
Logger()
{
this->activate();
}
int svc();
void LogMsg(conststring& msg);
void LogMsgActive (conststring& msg);
private:
ACE_Activation_Queue cmdQueue; //命令队列
};
class LogMsgCmd: public ACE_Method_Object
{
public:
LogMsgCmd(Logger *plog,conststring& msg)
{
this->log=plog;
this->msg=msg;
}
int call()
{
this->log->LogMsg(msg);
return 0;
}
private:
Logger *log;
string msg;
};
void Logger::LogMsg(conststring& msg)
{
cout<<endl<<msg<<endl;
ACE_OS::sleep(2);
}
//以主动的方式记录日志
void Logger::LogMsgActive(conststring& msg)
{
//生成命令对象,插入到命令队列中
cmdQueue.enqueue(new LogMsgCmd(this,msg));
}
int Logger::svc()
{
while(true)
{
//遍历命令队列,执行命令
auto_ptr<ACE_Method_Object> mo
(this->cmdQueue.dequeue ());
if (mo->call () == -1)
break;
}
return 0;
}
int main (int argc, ACE_TCHAR *argv[])
{
Logger log;
log. LogMsgActive ("hello");
ACE_OS::sleep(1);
log.LogMsgActive("abcd");
while(true)
ACE_OS::sleep(1);
return 0;
}
在这里需要注意一下命令队列ACE_Activation_Queue对象,它是线程安全的,使用方法比较简单,这里我也不多介绍了。
主动对象的基本结构就是这样,然而,由于主动对象是异步调用的,又引出了如下两个新问题:
方法调用线程如何知道该方法已经执行完成?
如何或得方法的返回值?
要解决这两个问题,首先得介绍一下ACE_Future对象,ACE_Future是表示一个会在将来被赋值的"期货"对象,可以通过ready()函数查询它是否已经被赋值。该对象创建的时候是未赋值的,后期可以通过set()函数来进行赋值,所赋的值可以通过get()函数来获取。
下面代码演示了它的基本用法:
5. #include "ace/Future.h"
6.
7. #include <string>
8. #include <iostream>
9. usingnamespace std;
10.
11.void get_info(ACE_Future<string> &fu)
12.{
13. string state = fu.ready()?"ready":"not ready";
14. cout<<endl<<state<<endl;
15. if(fu.ready())
16. {
17. stringvalue;
18. fu.get(value);
19. cout<<"value:\t"<<value<<endl;
20. }
21.}
22.
23.int main(int argc, char *argv[])
24.{
25. ACE_Future<string> fu;
26. get_info(fu);
27. fu.set("12345");
28. get_info(fu);
29.
30. return 0;
}
通过ACE_Future对象来解决上述两个问题的方法如下:
· 首先创建ACE_Future对象用以保留返回值。
· 调用主动命令时将ACE_Future对象作为参数传入,生成的命令对象中保存ACE_Future对象的指针。
· 命令执行线程执行完命令后,将返回值通过set()函数设置到ACE_Future对象中。
· 调用线程可以通过ACE_Future对象的ready()函数查询该命令是否执行完成,如果命令执行完成,则可通过get()函数来获取返回值。
使用的时候要注意一下ACE_Future对象的生命周期。
为了演示了如何获取主动命令的执行状态和结果,我将上篇文章中的代码改动了一下,日志类记录日志后,会将记录的内容作为返回值返回,该返回值会通过ACE_Future对象返回,代码如下
#include "ace/OS.h"
#include "ace/Task.h"
#include "ace/Method_Object.h"
#include "ace/Activation_Queue.h"
#include "ace/Auto_Ptr.h"
#include "ace/Future.h"
#include <string>
#include <iostream>
usingnamespace std;
class Logger: public ACE_Task<ACE_MT_SYNCH>
{
public:
Logger()
{
this->activate();
}
int svc();
string LogMsg(conststring& msg);
void LogMsgActive (conststring& msg,ACE_Future<string> *result);
private:
ACE_Activation_Queue cmdQueue; //命令队列
};
class LogMsgCmd: public ACE_Method_Object
{
public:
LogMsgCmd(Logger *plog,conststring& msg,ACE_Future<string> *result)
{
this->log=plog;
this->msg=msg;
this->result=result;
}
int call()
{
string reply = this->log->LogMsg(msg);
result->set(reply);
return 0;
}
private:
ACE_Future<string> *result;
Logger *log;
string msg;
};
string Logger::LogMsg(conststring& msg)
{
ACE_OS::sleep(2);
cout<<endl<<msg<<endl;
return msg;
}
//以主动的方式记录日志
void Logger::LogMsgActive(conststring& msg,ACE_Future<string> *result)
{
//生成命令对象,插入到命令队列中
cmdQueue.enqueue(new LogMsgCmd(this,msg,result));
}
int Logger::svc()
{
while(true)
{
//遍历命令队列,执行命令
auto_ptr<ACE_Method_Object> mo
(this->cmdQueue.dequeue ());
if (mo->call () == -1)
break;
}
return 0;
}
void get_info(ACE_Future<string> &fu)
{
string state = fu.ready()?"ready":"not ready";
cout<<endl<<state<<endl;
if(fu.ready())
{
stringvalue;
fu.get(value);
cout<<"value:\t"<<value<<endl;
}
}
int main (int argc, ACE_TCHAR *argv[])
{
ACE_Future<string> result;
Logger log;
log.LogMsgActive ("hello",&result);
while(true)
{
get_info(result);
if(result.ready())
break;
ACE_OS::sleep(1);
}
cout<<endl<<"cmd end"<<endl;
while(true)
ACE_OS::sleep(1);
return 0;
}
这种查询模式比较简单有效,但存在一个问题:调用线程必须不断轮询ACE_Future对象以获取返回值,这样的效率比较低。可以通过观察者模式解决这个问题:在ACE_Future对象上注册一个观察者,当ACE_Future对象的值发生改变(异步命令执行完成)时主动通知该观察者,从而获取返回值。
ACE中的观察者模式可以通过ACE_Future_Observer来实现,使用方法如下:
通过观察者模式,可以更有效,及时的获取异步命令的返回值,但同时也增加了程序结构的复杂度并且难以调试,使用的时候应该根据需要选取合适的方式。
#include "ace/Future.h"
#include <string>
#include <iostream>
usingnamespace std;
class MyObserver:public ACE_Future_Observer<string>
{
virtualvoid update (const ACE_Future<string> &future)
{
stringvalue;
future.get(value);
cout<<endl<<"change:\t"<<value<<endl;
}
};
int main(int argc, char *argv[])
{
MyObserver obv;
ACE_Future<string> fu;
fu.attach(&obv);
ACE_OS::sleep(3);
fu.set("12345");
while(true)
ACE_OS::sleep(3);
return 0;
}
ACE反应器(Reactor)模式
反应器(Reactor):用于事件多路分离和分派的体系结构模式
通常的,对一个文件描述符指定的文件或设备, 有两种工作方式: 阻塞与非阻塞。所谓阻塞方式的意思是指, 当试图对该文件描述符进行读写时, 如果当时没有东西可读,或者暂时不可写, 程序就进入等待状态, 直到有东西可读或者可写为止。而对于非阻塞状态, 如果没有东西可读, 或者不可写, 读写函数马上返回, 而不会等待。
在前面的章节中提到的Tcp通信的例子中,就是采用的阻塞式的工作方式:当接收tcp数据时,如果远端没有数据可以读,则会一直阻塞到读到需要的数据为止。这种方式的传输和传统的被动方法的调用类似,非常直观,并且简单有效,但是同样也存在一个效率问题,如果你是开发一个面对着数千个连接的服务器程序,对每一个客户端都采用阻塞的方式通信,如果存在某个非常耗时的读写操作时,其它的客户端通信将无法响应,效率非常低下。
一种常用做法是:每建立一个Socket连接时,同时创建一个新线程对该Socket进行单独通信(采用阻塞的方式通信)。这种方式具有很高的响应速度,并且控制起来也很简单,在连接数较少的时候非常有效,但是如果对每一个连接都产生一个线程的无疑是对系统资源的一种浪费,如果连接数较多将会出现资源不足的情况。
另一种较高效的做法是:服务器端保存一个Socket连接列表,然后对这个列表进行轮询,如果发现某个Socket端口上有数据可读时(读就绪),则调用该socket连接的相应读操作;如果发现某个Socket端口上有数据可写时(写就绪),则调用该socket连接的相应写操作;如果某个端口的Socket连接已经中断,则调用相应的析构方法关闭该端口。这样能充分利用服务器资源,效率得到了很大提高。
在Socket编程中就可以通过select等相关API实现这一方式。但直接用这些API控制起来比较麻烦,并且也难以控制和移植,在ACE中可以通过Reactor模式简化这一开发过程。
反应器本质上提供一组更高级的编程抽象,简化了事件驱动的分布式应用的设计和实现。除此而外,反应器还将若干不同种类的事件的多路分离集成到易于使用的API中。特别地,反应器对基于定时器的事件、信号事件、基于I/O端口监控的事件和用户定义的通知进行统一地处理。
ACE中的反应器与若干内部和外部组件协同工作。其基本概念是反应器框架检测事件的发生(通过在OS事件多路分离接口上进行侦听),并发出对预登记事件处理器(event handler)对象中的方法的"回调"(callback)。该方法由应用开发者实现,其中含有应用处理此事件的特定代码。
使用ACE的反应器,只需如下几步:
1. 创建事件处理器,以处理他所感兴趣的某事件。
2. 在反应器上登记,通知说他有兴趣处理某事件,同时传递他想要用以处理此事件的事件处理器的指针给反应器。
随后反应器框架将自动地:
1. 在内部维护一些表,将不同的事件类型与事件处理器对象关联起来。
2. 在用户已登记的某个事件发生时,反应器发出对处理器中相应方法的回调。
反应器模式在ACE中被实现为ACE_Reactor类,它提供反应器框架的功能接口。
如上面所提到的,反应器将事件处理器对象作为服务提供者使用。反应器内部记录某个事件处理器的特定事件的相关回调方法。当这些事件发生时,反应器会创建这种事件和相应的事件处理器的关联。
事件处理器
事件处理器就是需要通过轮询发生事件改变的对象列表中的对象,如在上面的例子中就是连接的客户端,每个客户端都可以看成一个事件处理器。
回调事件
就是反应器支持的事件,如Socket读就绪,写就绪。拿上面的例子来说,如果某个客户端(事件处理器)在反应器中注册了读就绪事件,当客户端给服务器发送一条消息的时候,就会触发这个客户端的数据可读的回调函数。
在反应器框架中,所有应用特有的事件处理器都必须由ACE_Event_Handler的抽象接口类派生。可以通过重载相应的"handle_"方法实现相关的回调方法。
使用ACE_Reactor基本上有三个步骤:
创建ACE_Event_Handler的子类,并在其中实现适当的"handle_"方法,以处理你想要此事件处理器为之服务的事件类型。
通过调用反应器对象的register_handler(),将你的事件处理器登记到反应器。
在事件发生时,反应器将自动回调相应的事件处理器对象的适当的handle_"方法。
下面我就以一个Socket客户端的例子为例简单的说明反应器的基本用法。
#include <ace/OS.h>
#include <ace/Reactor.h>
#include <ace/SOCK_Connector.h>
#include <string>
#include <iostream>
usingnamespace std;
class MyClient:public ACE_Event_Handler
{
public:
bool open()
{
ACE_SOCK_Connector connector;
ACE_INET_Addr addr(3000,"127.0.0.1");
ACE_Time_Value timeout(5,0);
if(connector.connect(peer,addr,&timeout) != 0)
{
cout<<endl<<"connecetd fail";
returnfalse;
}
ACE_Reactor::instance()->register_handler(this,ACE_Event_Handler::READ_MASK);
cout<<endl<<"connecetd ";
returntrue;
}
ACE_HANDLE get_handle(void) const
{
return peer.get_handle();
}
int handle_input (ACE_HANDLE fd)
{
int rev=0;
ACE_Time_Value timeout(5,0);
if((rev=peer.recv(buffer,1000,&timeout))>0)
{
buffer[rev]='\0';
cout<<endl<<"rev:\t"<<buffer<<endl;
}
return 3;
}
private:
ACE_SOCK_Stream peer;
char buffer[1024];
};
int main(int argc, char *argv[])
{
MyClient client;
client.open();
while(true)
{
ACE_Reactor::instance()->handle_events();
}
return 0;
}
在这个例子中,客户端连接上服务器后,通过ACE_Reactor::instance()->register_handler(this,ACE_Event_Handler::READ_MASK)注册了一个读就绪的回调函数,当服务器端给客户端发消息的时候,会自动触发handle_input()函数,将接收到的信息打印出来。
在Socket编程中,常见的事件就是"读就绪","写就绪",通过对这两个事件的捕获分发,可以实现Socket中的异步操作。
Socket编程中的事件处理器
在前面我们已经介绍过,在ACE反应器框架中,任何都必须派生自ACE_Event_Handler类,并通过重载其相应会调事件处理函数来实现相应的回调处理的。在Socket编程中,我们通常需要重载的函数有
handle_input()
当I/O句柄(比如UNIX中的文件描述符)上的输入可用时,反应器自动回调该方法。
handle_output()
当I/O设备的输出队列有可用空间时,反应器自动回调该方法。
handle_close()
当事件处理器中的事件从Reactor中移除的时候调用。
此外,为了使Reactor能通过I/O句柄找到对应的事件处理器,还必须重载其get_handle()方法以使得Reactor建立起I/O句柄和事件处理器的关联。
使用Reactor框架。
下面我们将以一个客户端的程序为例,介绍如何在Socket编程中使用Reactor框架。
一.建立一个客户端对象(事件处理器)。
客户端对象就是一个事件处理器,其声明如下:
class Client:public ACE_Event_Handler
{
public:
ACE_HANDLE get_handle(void) const;
int handle_input (ACE_HANDLE fd);
int handle_close (ACE_HANDLE handle,
ACE_Reactor_Mask close_mask);
ACE_SOCK_Stream& Peer();
private:
ACE_SOCK_Stream peer;
};
在Client端中我只关心"读就绪"事件,故只重载了handle_input函数(大多数应用下只需要重载handle_input函数)。另外,在客户端还保存了一个ACE_SOCK_Stream的peer对象用来进行Socket通信,同时封装了一个Peer()函数返回它的引用。
二.重载相应回调处理函数
ACE_SOCK_Stream& Client::Peer()
{
return peer;
}
ACE_HANDLE Client::get_handle(void) const
{
return peer.get_handle();
}
int Client::handle_input (ACE_HANDLE fd)
{
int rev=0;
if((rev = peer.recv(buffer,1000))>0)
{
buffer[rev]='\0';
cout<<endl<<"rev:\t"<<buffer<<endl;
return 0;
}
else//Socket连接发生错误,返回-1,在Reactor中注销事件,触发handle_close函数
{
return -1;
}
}
int Client::handle_close (ACE_HANDLE handle,
ACE_Reactor_Mask close_mask)
{
cout<<endl<<"connecetd closed";
return ACE_Event_Handler::handle_close(handle,close_mask);
}
三.在Reactor中注册事件
首先让我们来看看相应的main函数的代码:
int main(int argc, char *argv[])
{
Client client;
ACE_SOCK_Connector connector;
ACE_INET_Addr addr(3000,"127.0.0.1");
ACE_Time_Value timeout(5,0);
if(connector.connect(client.Peer(),addr,&timeout) != 0)
{
cout<<endl<<"connecetd fail";
return 0;
}
ACE_Reactor::instance()->register_handler(&client,ACE_Event_Handler::READ_MASK);
while(true)
{
ACE_Reactor::instance()->handle_events();
}
return 0;
}
在这里可以看到,使用Reactor框架后,依然首先通过ACE_SOCK_Connector的connect函数来建立连接。建立连接后,可以通过ACE_Reactor::instance()->register_handler函数来实现Reactor的注册,实现I/O事件和Client对象的handle_input方法相关联,它的第一个参数是事件处理器的地址,第二个参数是事件类型,由于这里只关心读就绪事件,故注册的事件类型是ACE_Event_Handler::READ_MASK。
四.启动Reactor事件循环
通过如上设置后,我们就可以通过ACE_Reactor::instance()->handle_events()启动Reactor循环了,这样,每当服务器端有数据发送给客户端时,当客户端的数据就绪时,就回触发Client对象的handle_input函数,将接收的数据打印出来。
通常的做法是,将Reactor事件循环作为一个单独的线程来处理,这样就不会阻塞main函数。
五.注销Reactor事件
Reactor事件的注销一般有两种方式,显式和隐式,下面将分别给予介绍。
隐式注销。
当Reactor捕获事件后,会触发相应的"handle_"处理函数,当"handle_"处理函数返回值大于或等于0时,表示处理事件成功,当返回值小于0时,表示处理事件失败,这时Reactor会自动注销该句柄所注册的所有事件,并触发handle_close函数,以执行相应的资源清理工作。
在本例中,当handle_input函数里的recv函数接收到0个数时,说明socket发生错误(大多为Socket连接中断),此时返回-1,则系统自动注销相应事件。
显示注销。
调用Reactor对象的remove_handler方法移除,它有两个参数,第一个是所注册的事件反应器对象,第二个是所要注销的事件。
在这个示例程序里,连接方只有一个Socket连接,Reactor的优势并没有体现出来,但在一些网络管理系统里,连接方需要对多个需要管理的设备(服务器端)进行连接,在这种情况下使用Reactor模式,只需要多开一个Reactor事件循环线程就能实现事件多路分发复用,并且不会阻塞,通过面向对象的回调方式管理,使用起来非常方便。
Reactor框架的另外一个常用的地方就是服务器端,一般是一个服务器端对应多个客户端,这样用Reactor模式能大幅提高并发能力,这方面的编程方法将在下一章给与介绍。
在服务器端使用Reactor框架
使用Reactor框架的服务器端结构如下:
服务器端注册两种事件处理器,ClientAcceptor和ClientService ,ClientService类负责和客户端的通信,每一个ClientService对象对应一个客户端的Socket连接。 ClientAcceptor专门负责被动接受客户端的连接,并创建ClientService对象。这样,在一个N个Socket连接的服务器程序中,将存在1个ClientAcceptor对象和N个ClientService对象。
整个服务器端流程如下:
1. 首先创建一个ClientAcceptor对象,该对象在Reactor上注册ACCEPT_MASK事件,Reactor将自动在监听端口建立Socket监听。
2. 如果有对该端口的Socket连接时,Reactor将自动回调handle_input方法,ClientAcceptor重载此方法,并创建一个ClientService对象,用于处理和Client的通信。
3. ClientService对象根据服务器的具体功能实现,其处理过程和客户端程序类似,注册相应的回调事件并分发即可。
代码如下:
#include <ace/OS.h>
#include <ace/Reactor.h>
#include <ace/SOCK_Connector.h>
#include <ace/SOCK_Acceptor.h>
#include <ace/Auto_Ptr.h>
class ClientService : public ACE_Event_Handler
{
public:
ACE_SOCK_Stream &peer (void) { returnthis->sock_; }
int open (void)
{
//注册读就绪回调函数
returnthis->reactor()->register_handler(this, ACE_Event_Handler::READ_MASK);
}
virtual ACE_HANDLE get_handle (void) const { returnthis->sock_.get_handle (); }
virtualint handle_input (ACE_HANDLE fd )
{
//一个简单的EchoServer,将客户端的信息返回
int rev = peer().recv(buf,100);
if(rev<=0)
return -1;
peer().send(buf,rev);
return 0;
}
// 释放相应资源
virtualint handle_close (ACE_HANDLE, ACE_Reactor_Mask mask)
{
if (mask == ACE_Event_Handler::WRITE_MASK)
return 0;
mask = ACE_Event_Handler::ALL_EVENTS_MASK |
ACE_Event_Handler::DONT_CALL;
this->reactor ()->remove_handler (this, mask);
this->sock_.close ();
delete this; //socket出错时,将自动删除该客户端,释放相应资源
return 0;
}
protected:
char buf[100];
ACE_SOCK_Stream sock_;
};
class ClientAcceptor : public ACE_Event_Handler
{
public:
virtual ~ClientAcceptor (){this->handle_close (ACE_INVALID_HANDLE, 0);}
int open (const ACE_INET_Addr &listen_addr)
{
if (this->acceptor_.open (listen_addr, 1) == -1)
{
ACE_OS::printf("open port fail");
return -1;
}
//注册接受连接回调事件
returnthis->reactor()->register_handler(this, ACE_Event_Handler::ACCEPT_MASK);
}
virtual ACE_HANDLE get_handle (void) const
{ returnthis->acceptor_.get_handle (); }
virtualint handle_input (ACE_HANDLE fd )
{
ClientService *client = new ClientService();
auto_ptr<ClientService> p (client);
if (this->acceptor_.accept (client->peer ()) == -1)
{
ACE_OS::printf("accept client fail");
return -1;
}
p.release ();
client->reactor (this->reactor ());
if (client->open () == -1)
client->handle_close (ACE_INVALID_HANDLE, 0);
return 0;
}
virtualint handle_close (ACE_HANDLE handle,
ACE_Reactor_Mask close_mask)
{
if (this->acceptor_.get_handle () != ACE_INVALID_HANDLE)
{
ACE_Reactor_Mask m = ACE_Event_Handler::ACCEPT_MASK |
ACE_Event_Handler::DONT_CALL;
this->reactor ()->remove_handler (this, m);
this->acceptor_.close ();
}
return 0;
}
protected:
ACE_SOCK_Acceptor acceptor_;
};
int main(int argc, char *argv[])
{
ACE_INET_Addr addr(3000,"192.168.1.142");
ClientAcceptor server;
server.reactor(ACE_Reactor::instance());
server.open(addr);
while(true)
{
ACE_Reactor::instance()->handle_events();
}
return 0;
}
代码功能比较简单,需要注意以下几点:
1. 这里注册事件的方式和前面的文章中方式不一样,是通过ACE_Event_Handler类的reactor()方法设置和获取reactor的指针,比较直观和方便。前面的文章是通过ACE_Reactor::instance()来获取的一个单体reactor的指针。
当客户端Socket连接关闭时,需要释放相应资源,需要注意一下ClientService对象的handle_close方法中释放资源的相应代码。
定时器的实现
通过Reactor机制,还可以很容易的实现定时器的功能,使用方式如下。
1. 编写一个事件反应器,重载handle_timeout()方法,该方法是定时器的触发时间到时,会自动触发该方法。
2. 通过Reactor的schedule_timer()方法注册定时器。
3. 启动reacotr的handle_events()事件分发循环。
4. 当不想使用定时器时,可以通过Reactor的cancel_timer()方法注销定时器。
下面的代码简单的实现了一个定时器,并具有基本的开启,关闭功能。
#include <ace/OS.h>
#include <ace/Reactor.h>
class MyTimerHandler : public ACE_Event_Handler
{
private:
int inteval; //执行时间间隔
int delay; //延迟执行时间
int timerid;
public:
MyTimerHandler(int delay,int inteval)
{
this->delay=delay;
this->inteval=inteval;
}
int open() //注册定时器
{
ACE_Time_Value delaytime(inteval);
ACE_Time_Value intevaltime(inteval);
timerid = reactor()->schedule_timer(this,
0, //传递handle_timeout给的参数
delaytime,
intevaltime);
return timerid;
}
int close() //取消定时器
{
return reactor()->cancel_timer(timerid);
}
//定时器回调函数
int handle_timeout (const ACE_Time_Value ¤t_time,
constvoid * = 0)
{
time_t epoch = ((timespec_t)current_time).tv_sec;
ACE_DEBUG ((LM_INFO,
ACE_TEXT ("handle_timeout: %s\n"),
ACE_OS::ctime (&epoch)));
return 0;
}
};
int main(int argc, char *argv[])
{
MyTimerHandler * timer = new MyTimerHandler (3,5);
timer->reactor(ACE_Reactor::instance());
timer->open();
for(int i=0;i<2;i++) //触发次handle_timeout事件
{
ACE_OS::printf("\n%d\n",i);
ACE_Reactor::instance()->handle_events();
}
timer->close();
ACE_OS::printf("cancel timer");
while(true)
ACE_Reactor::instance()->handle_events();
return 0;
}
代码功能比较简单,这里就不多做介绍了。