ACE的框架及其核心
ACE设计框架和基础模块的关联
*一、案例描述
视频电警开发,是基于ACE框架上的一次重复开发,本文档拟对ACE框架做一个梳理,以期对他人进行基
于ace的开发有所帮助。
*二、系统安装
ACE的安装是一件比较麻烦的事情,这里简单的记录了我在VS2005下安装ACE的过程,希望能给大家一个
参考。
安装环境:
l 操作系统:Windows XP 专业版
l 编译环境:VS2005中文版
l ACE版本:ACE-5.5.1
安装过程:
a) 下载安装包。Ace的安装文件可以在http://download.dre.vanderbilt.edu/中下载到,由于我是在
windows环境下安装并且不需要TAO等其它库,便下载了ACE-5.5.1.zip。
b) 下载完成后将其解压。我的解压路径为D:\Develop\ACE_wrappers。
c) 设置环境变量
d) 在操作系统添加一个名为ACE_ROOT的用户环境变量,值为刚才ace的解压路径D:\Develop
\ACE_wrappers。
e) 添加用户的Path环境变量,值为%ACE_ROOT%\lib,这样才能保证系统能找到ace生成的动态连接库。
f) 设置VS2005的C++开发项目信息,依次打开菜单 工具-选项-项目和解决方案-VC++目录 ,在右侧目录
列表中选择"包含目录",添加$(ACE_ROOT),在右侧目录列表中选择"库文件",添加$(ACE_ROOT)\lib。
g) 编译ACE,在ACE_ROOT\ace目录创建一个名为 config.h的文件。编辑文件并加入以下内容:
#define ACE_HAS_STANDARD_CPP_LIBRARY 1
#include "ace/config-win32.h"
其中第一行是因为我想用标准C++跨平台,第二行则是必须要的,表明当前是在win32的环境下进行ace的
项目。
h) 进入ACE_ROOT\ace目录中,能发现ACE现在已经带VS2005的编译项目了,直接打开ace_vc8.sln,直接
生成ACE项目的Debug版和Release版,编译过程还比较快,大概就几分钟的样子。编译链接完成后,在
ACE_ROOT\lib中一共生成了四个文件,分别是"ACE.dll","ACE.lib", "ACEd.dll","ACEd.lib",其中
带"d"表示的是Debug版本。
i) 检验 ACE
j) 打开VS2005,建立一个空项目,将ACE程序员手册中的第一个程序拷入其中。
k) 配置属性->链接器->常规->附加依赖项,添入ACEd.lib。
l) 编译,如果不出意外的话就能看到你的ace版的" hello world"啦。
注意:
ACE项目的字符集设置是"未设置",而VS2005的c++项目默认字符集是"使用 Unicode 字 符集",如果用
到了ACE链接库时需要将字符集改为"未设置"(在"项目属性->配置属性->项目默认值->字符集"中配置)
,否则可能出现链接错误。
至此,ACE的安装工作便算完成.下面是完成unicode编译的ACE设置:
*三、ACE的使用及其核心模块讲解等
下面为本人在使用ACE中遇到的一些问题的汇总,只介绍了大体的思路,具体的细节还需进佐证。
1. ACE配置模块的使用
就一个正常项目而言,一个配置文件是必不可少的,那就先从这里入手了。linux/unix 程序可能经常用
到命令行方式,不过我还是比较喜欢windows 的 ini 格式的,当然,有xml 的更好,不过 ACE 里暂时
没有提供。配置文件的使用很简单,ACE 提供的类也很友好。代码如下:
wps_clip_image-914
2. ACE的互斥管理机制
2.1、ACE Lock类属
锁类属包含的类包装简单的锁定机制,比如互斥体、信号量、读/写互斥体和令牌等。这里我就以互斥
体为例简单的介绍一下其使用方法,对其它的锁类进行一些简单的说明。
互斥体的使用
互斥体用于保护共享的易变代码,也就是全局或静态数据。这样的数据必须通过互斥体进行保护,以防
止它们在多个线程同时访问时损坏。在ACE中可以通过ACE_Thread_Mutex实现线程的访问互斥,下面的例
子演示ACE_Thread_Mutex类的使用。
#include "ace/Thread.h"
#include "ace/Synch.h"
#include <iostream>
using namespace 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()方法获取互斥体,从而获取对改互斥体所保护的共享资源
的唯一访问权限,访问结束时调用释放互斥体,使得其它线程能获取共享资源的访问权限。
在此例中,本来Thread2的打印消息在Thread1之前,但由于Thread1先获得互斥体,故Thread2只有待
Thread1结束后才能进入临界区。读者朋友们可以通过将ACE_Thread_Mutex替换为ACE_NULL_Mutex看一下
不加锁的执行结果。
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++包装类中实现信号量语义。
2.2、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要注意其对象生命
周期,否则容易造成死锁。
2.3、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>
using namespace 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,消费者线程执行比生产者快,
两个线程不加限制并发执行会导致先消费,后生产的情况(只是加互斥锁也不能很好的解决,以为无法
保证生产者一定先获得互斥体)。所以这里通过条件变量的通知方式保证线程的顺序执行:
a) 消费者线程获取互斥体,等待条件满足(生产者生产了食品)。同时释放互斥体,进入休眠状态。
b) 生产者获取互斥体(虽然是消费者先获取的互斥体,但消费者调用的wait函数会释放消费者的互斥体
),生产商品后,通过条件变量发送信号(调用signal函数)通知消费者生产完成,结束生产过程,释
放互斥体。
c) 消费者收到信号后,重新获取互斥体,完成消费过程。
使用条件变量的注意事项:
l 条件变量必须和互斥体一起使用,也就是说使用前必须加锁(调用互斥体acquire函数),使用完后需
释放互斥体。
条件变量中的wait()和signal()成对使用的话,必须保证wait()函数在signal()之前执行,这样才能保
证wait()能收到条件满足通知,不至于一直等待下去,形成死锁(worker线程中的第一句话就是起的这
个作用)。
3. ACE的线程管理机制
2.1、ACE Lock类属
不同的操作系统下用c++进行过多线程编程的朋友对那些线程处理的API可能深有体会,这些API提供了相
同或是相似的功能,但是它们的API的差别却极为悬殊。
ACE_Thread提供了对不同OS的线程调用的简单包装,通过一个通用的接口进行处理线程创建、挂起、取
消和删除等问题。
一. 线程入口函数
所有线程必须从一个指定的函数开始执行,该函数称为线程函数,它必须具有下列原型:
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还提供了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"
#include <iostream>
using namespace 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()函数。然后阻塞主函数,待线
程结束后退出程序。
4. ACE的网络通讯机制
4.1、TCP通讯
传输控制协议TCP(Transmission Control Protocol):TCP提供可靠的、面向连接的运输服务,用于高
可靠性数据的传输。TCP协议的可靠性是指保证每个tcp报文能按照发送顺序到达客户端。
Tcp通信过程一般为如下步骤:
a) 服务器绑定端口,等待客户端连接。
b) 客户端通过服务器的ip和服务器绑定的端口连接服务器。
c) 服务器和客户端通过网络建立一条数据通路,通过这条数据通路进行数据交互。
常用API:
1. ACE_INET_Addr类。
ACE"地址"类ACE_Addr的子类,表示TCP/IP和UDP/IP的地址。它通常包含机器的ip和端口信息,通过它可
以定位到所通信的进程。
定义方式:
ACE_INET_Addr addInfo(3000,"192.168.1.100");
常用方法:
l get_host_name 获取主机名
l get_ip_address 获取ip地址
l get_port_number 获取端口号
2. ACE_SOCK_Acceptor类。
服务期端使用,用于绑定端口和被动地接受连接。
常用方法:
l open 绑定端口
l accept建立和客户段的连接
3. ACE_SOCK_Connector类。
客户端使用,用于主动的建立和服务器的连接。
常用方法:
l connect() 建立和服务期的连接。
4. ACE_SOCK_Stream类。
客户端和服务器都使用,表示客户段和服务器之间的数据通路。
常用方法:
l send () 发送数据
l recv () 接收数据
l 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>
using namespace 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>
using namespace 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;
}
下表给出了服务器端和客户端的传输过程的比较:
操作
客户端
服务器端
初始化
不需要
调用acceptor.open()绑定端口
建立连接
调用connector.connect()方法
调用acceptor.accept()方法
传输数据
发送:调用peer.recv()方法
接收:调用peer.send()方法
关闭连接
调用peer.close()方法
4.2、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通信过程如下:
l 服务器端绑定一固定udp端口,等待接收客户端的通信。
l 客户端通过服务器的ip和地址信息直接对服务器端发送消息。
l 服务器端收到客户端发送的消息后获取客户端的ip和端口信息,通过该地址信息和客户端通信。
下面代码为EchoServer的udp版:
//server.cpp
#include <ace/SOCK_Dgram.h>
#include <ace/INET_Addr.h>
#include <ace/Time_Value.h>
#include <string>
#include <iostream>
using namespace 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>
using namespace 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的实例对象中无法保存远端地址信息(保存了
本地地址信息),故通信的时候需要加上远端地址信息。
5. ACE的设计模式
5.1、主动对象模式
主动对象模式用于降低方法执行和方法调用之间的耦合。该模式描述了另外一种更为透明的任务间通信
方法。
传统上,所有的对象都是被动的代码段,对象中的代码是在对它发出方法调用的线程中执行的,当方法
被调用时,调用线程将阻塞,直至调用结束。而主动对象却不一样。这些对象具有自己的命令执行线程
,主动对象的方法将在自己的执行线程中执行,不会阻塞调用方法。
例如,设想对象"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,const string& 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(const string& msg)
{
cout<<endl<<msg<<endl;
ACE_OS::sleep(2);
}
LogMsgCmd *LogMsgActive(const string& 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>
using namespace std;
class Logger: public ACE_Task<ACE_MT_SYNCH>
{
public:
Logger()
{
this->activate();
}
int svc();
void LogMsg(const string& msg);
void LogMsgActive (const string& msg);
private:
ACE_Activation_Queue cmdQueue; //命令队列
};
class LogMsgCmd: public ACE_Method_Object
{
public:
LogMsgCmd(Logger *plog,const string& msg)
{
this->log=plog;
this->msg=msg;
}
int call()
{
this->log->LogMsg(msg);
return 0;
}
private:
Logger *log;
string msg;
};
void Logger::LogMsg(const string& msg)
{
cout<<endl<<msg<<endl;
ACE_OS::sleep(2);
}
//以主动的方式记录日志
void Logger::LogMsgActive(const string& 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对象,它是线程安全的,使用方法比较简单,这里
我也不多介绍了。
主动对象的基本结构就是这样,然而,由于主动对象是异步调用的,又引出了如下两个新问题:
l 方法调用线程如何知道该方法已经执行完成?
l 如何或得方法的返回值?
要解决这两个问题,首先得介绍一下ACE_Future对象,ACE_Future是表示一个会在将来被赋值的"期货"
对象,可以通过ready()函数查询它是否已经被赋值。该对象创建的时候是未赋值的,后期可以通过set
()函数来进行赋值,所赋的值可以通过get()函数来获取。
下面代码演示了它的基本用法:
#include "ace/Future.h"
#include <string>
#include <iostream>
using namespace std;
void get_info(ACE_Future<string> &fu)
{
string state = fu.ready()?"ready":"not ready";
cout<<endl<<state<<endl;
if(fu.ready())
{
string value;
fu.get(value);
cout<<"value:\t"<<value<<endl;
}
}
int main(int argc, char *argv[])
{
ACE_Future<string> fu;
get_info(fu);
fu.set("12345");
get_info(fu);
return 0;
}
通过ACE_Future对象来解决上述两个问题的方法如下:
l 首先创建ACE_Future对象用以保留返回值。
l 调用主动命令时将ACE_Future对象作为参数传入,生成的命令对象中保存ACE_Future对象的指针。
l 命令执行线程执行完命令后,将返回值通过set()函数设置到ACE_Future对象中。
l 调用线程可以通过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>
using namespace std;
class Logger: public ACE_Task<ACE_MT_SYNCH>
{
public:
Logger()
{
this->activate();
}
int svc();
string LogMsg(const string& msg);
void LogMsgActive (const string& msg,ACE_Future<string> *result);
private:
ACE_Activation_Queue cmdQueue; //命令队列
};
class LogMsgCmd: public ACE_Method_Object
{
public:
LogMsgCmd(Logger *plog,const string& 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(const string& msg)
{
ACE_OS::sleep(2);
cout<<endl<<msg<<endl;
return msg;
}
//以主动的方式记录日志
void Logger::LogMsgActive(const string& 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())
{
string value;
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>
using namespace std;
class MyObserver:public ACE_Future_Observer<string>
{
virtual void update (const ACE_Future<string> &future)
{
string value;
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;
}
通过观察者模式,可以更有效,及时的获取异步命令的返回值,但同时也增加了程序结构的复杂度并且难
以调试,使用的时候应该根据需要选取合适的方式。
5.2、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的反应器,只需如下几步:
l 创建事件处理器,以处理他所感兴趣的某事件。
l 在反应器上登记,通知说他有兴趣处理某事件,同时传递他想要用以处理此事件的事件处理器的指针
给反应器。
随后反应器框架将自动地:
l 在内部维护一些表,将不同的事件类型与事件处理器对象关联起来。
l 在用户已登记的某个事件发生时,反应器发出对处理器中相应方法的回调。
反应器模式在ACE中被实现为ACE_Reactor类,它提供反应器框架的功能接口。
如上面所提到的,反应器将事件处理器对象作为服务提供者使用。反应器内部记录某个事件处理器的特
定事件的相关回调方法。当这些事件发生时,反应器会创建这种事件和相应的事件处理器的关联。
l 事件处理器
事件处理器就是需要通过轮询发生事件改变的对象列表中的对象,如在上面的例子中就是连接的客户端
,每个客户端都可以看成一个事件处理器。
l 回调事件
就是反应器支持的事件,如Socket读就绪,写就绪。拿上面的例子来说,如果某个客户端(事件处理器
)在反应器中注册了读就绪事件,当客户端给服务器发送一条消息的时候,就会触发这个客户端的数据
可读的回调函数。
在反应器框架中,所有应用特有的事件处理器都必须由ACE_Event_Handler的抽象接口类派生。可以通过
重载相应的"handle_"方法实现相关的回调方法。
使用ACE_Reactor基本上有三个步骤:
l 创建ACE_Event_Handler的子类,并在其中实现适当的"handle_"方法,以处理你想要此事件处理器为
之服务的事件类型。
l 通过调用反应器对象的register_handler(),将你的事件处理器登记到反应器。
l 在事件发生时,反应器将自动回调相应的事件处理器对象的适当的handle_"方法。
下面我就以一个Socket客户端的例子为例简单的说明反应器的基本用法。
#include <ace/OS.h>
#include <ace/Reactor.h>
#include <ace/SOCK_Connector.h>
#include <string>
#include <iostream>
using namespace 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";
return false;
}
ACE_Reactor::instance()->register_handler(this,ACE_Event_Handler::READ_MASK);
cout<<endl<<"connecetd ";
return true;
}
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通信中使用反应器做进一步的介绍。
5.3、接收者-连接(Reactor-Connect)者模式
接受器-连接器设计模式(Acceptor-Connector)使分布式系统中的连接建立及服务初始化与一旦服务
初始化后所执行的处理去耦合。
这样的去耦合通过三种组件来完成:acceptor、connector 和 servicehandler(服务处理器)。
l 连接器主动地建立到远地接受器组件的连接,并初始化服务处理器来处理在连接上交换的数据。
l 接受器被动地等待来自远地连接器的连接请求,在这样的请求到达时建立连接,并初始化服务处理器
来处理在连接上交换的数据。
l 初始化的服务处理器执行应用特有的处理,并通过连接器和接受器组件建立的连接来进行通信。
5.3.1. 服务处理器(Service Handler):
Service Handler 实现应用服务,通常扮演客户角色、服务器角色,或同时扮演这两种角色。它提供挂
钩方法,由 Acceptor 或 Connector 调用,以在连接建立时启用应用服务。此外,Service Handler 还
提供数据模式传输端点,其中封装了一个 I/O 句柄。一旦连接和初始化后,该端点被 Service Handler
用于与和其相连的对端交换数据。
5.3.2. 接受器(Acceptor):
Acceptor 是一个工厂,实现用于被动地建立连接并初始化与其相关联的 Service Handler 的策略。此
外,Acceptor 包含有被动模式的传输端点工厂,它创建新的数据模式端点,由 Service Handler 用于
在相连的对端间传输数据。通过将传输端点工厂绑定到网络地址,比如 Acceptor 在其上侦听的 TCP 端
口号,Acceptor的 open 方法对该工厂进行初始化。
一旦初始化后,被动模式的传输端点工厂侦听来自对端的连接请求。当连接请求到达时,Acceptor 创建
Service Handler,并使用它的传输端点工厂来将新连接接受进Service Handler 中。
5.3.3. 连接器(Connector):
Connector 是一个工厂,实现用于主动地建立连接并初始化与其相关联的 Service Handler 的策略。它
提供方法,由其发起到远地 Acceptor 的连接。同样地,它还提供另一个方法,完成对 Service
Handler 的启用;该处理器的连接是被同步或异步地发起的。Connector 使用两个分开的方法来透 明地
支持异步连接建立。
5.3.4. 分派器(Dispatcher):
为 Acceptor,Dispatcher 将在一或多个传输端点上接收到的连接请求多路分离给适当的 Acceptor。
Dispatcher允许多个 Acceptor 向其登记,以侦听同时在不同端口上从不同对端而来的连接。 为
Connector,Dispatcher 处理异步发起的连接的完成。在这种情况下,当异步连接被建立时,
Dispatcher 回调 Connector。Dispatcher 允许多个 Service Handler 通过一个 Connector 来异步地
发起和完成它们 的连接。注意对于同步连接建立,Dispatcher 并不是必需的,因为发起连接的线程控
制也完成服务服务处 理器的启用。
Dispatcher 通常使用事件多路分离模式来实现,这些模式由反应器(Reactor)或前摄器(Proactor)
来提供,它们分别处理同步和异步的多路分离。同样地,Dispatcher 也可以使用主动对象(Active Obj
ect)模式来实现为单独的线程或进程。
Acceptor 组件协作
Acceptor 和 Service Handler 之间的协作。这些协作被划分为三个阶段:
1. 端点初始化阶段:
为被动地初始化连接,应用调用 Acceptor 的 open 方法。该方法创建被动模式的传 输端点,将其绑定
到网络地址,例如,本地主机的 IP 地址和 TCP 端口号,并随后侦听来自对端 Connector 的连接请求
。其次,open 方法将 Acceptor 对象登记到 Dispatcher,以使分派器能够在连接事件 到达时回调
Acceptor。最后,应用发起 Dispatcher 的事件循环,等待连接请求从对端 Connector 到来。
2. 服务初始化阶段:
当连接请求到达时,Dispatcher 回调 Acceptor 的accept 方法。该方法装配以下活动 所必需的资源:
l 创建新的 Service Handler,
l 使用它的被动模式传输端点工厂来将连接接受进 该处理器的数据模式传输端点中,
l 通过调用 Service Handler 的 open 挂钩将其启用。Servic e Handler 的 open 挂钩可以执行服务
特有的初始化,比如分配锁、派生线程、打开日志文件,和/或将 该 Service Handler 登记到
Dispatcher。
3. 服务处理阶段:
在连接被动地建立和 Service Handler 被初始化后,服务处理阶段开始了。在此阶段, 应用级通信协
议,比如 HTTP 或 IIOP,被用于在本地 Service Handler 和与其相连的远地 Peer 之间、 经由前者的
peer_stream_端点交换数据。当交换完成,可关闭连接和 Service Handler,并释放资源。
Connector 组件协作
Connector 组件可以使用同步和异步两种方式来初始化它的 Service Handle,这里仅介绍一下同步时的
协作情况。
同步的 Connector 情况中的参与者之间的协作可被划分为以下三个阶段:
l 连接发起阶段:
为在 Service Handler 和它的远地 Peer 之间发起连接,应用调用 Connector 的 connect 方法。该方
法阻塞调用线程的线程控制、直到连接同步完成,以主动地建立连接。
l 服务初始化阶段:
在连接完成后,Connector 的 connect 方法调用 complete 方法来启用 Service Handl er。complete
方法通过调用 Service_Handler 的 open 挂钩方法来完成启用;open 方法执行服务特有的 初始化。
l 服务处理阶段:
此阶段与 Service Handler 被 Acceptor 创建后所执行的服务处理阶段相类似。特别地, 一旦
Service Handler 被启用,它使用与和其相连接的远地 Service Handler 交换的数据来执行应用特 有
的服务处理。
实现及运行一般步骤:
l 创建 Service Handler;
l 被动地或主动地将 Service Handler 连接到它们的远地对端;以及
l 一旦连接,启用 Service Handler。
主要角色:Service Handler(服务处理器)、Acceptor 和 Connector。
服务处理器:该抽象类继承自 Event_Handler,并为客户、服务器或同时扮演两种角色的组件所提供 的
服务处理提供通用接口。应用必须通过继承来定制此类,以执行特定类型的服务。Service Handler 接
口如下所示:
template <class PEER_STREAM>
class Service_Handler : public Event_Handler
{
public:
//连接成功后的初始化入口函数 (子类定义).
virtual int open (void) = 0;
//返回通信流的引用
PEER_STREAM &peer (void)
{
return peer_stream_;
}
};
一旦 Acceptor 或 Connector 建立了连接,它们调用 Service Handler 的 open 挂钩。该纯虚方法必
须被 Concrete Service Handler 子类定义;后者执行服务特有的初始化和后续处理。
连接器:该抽象类实现主动连接建立和初始化 Service Handler 的通用策略。它的接口如下所示:
template <class SERVICE_HANDLER,class PEER_CONNECTOR>
class Connector : public Event_Handler
{
public:
enum Connect_Mode
{
SYNC, //以同步方式连接
ASYNC //以异步方式连接
};
// 主动连接并激活服务处理器
int connect (SERVICE_HANDLER *sh,
const PEER_CONNECTOR::PEER_ADDR &addr,
Connect_Mode mode);
protected:
//定义连接激活策略
virtual int connect_service_handler(SERVICE_HANDLER *sh,
const PEER_CONNECTOR::PEER_ADDR &addr,
Connect_Mode mode);
// Defines the handler's concurrency strategy.
virtual int activate_service_handler(SERVICE_HANDLER *sh);
// 当以异步方式连接完成时激活服务处理器
virtual int complete (HANDLE handle);
private:
// IPC mechanism that establishes
// connections actively.
PEER_CONNECTOR connector_;
};
Conncetor 通过特定类型的 PEER CONNECTOR 和 SERVICE HANDLER 被参数化。PEER CONNECTO R 提供的
传输机制被 Connector 用于主动地建立连接,或是同步地、或是异步地。SERVICE HANDLER提供的服务
对与相连的对端交换的数据进行处理。C++参数化类型被用于使(1)连接建立策略与(2)服务处理器类
型、网络编程接口和传输层连接协议去耦合。
参数化类型是有助于提高可移植性的实现决策。例如,它们允许整体地替换 Connector 所用的 IPC 机
制。这使得 Connector 的连接建立代码可在含有不同网络编程接口(例如,有 socket,但没有 TLI;
反之 亦然)的平台间进行移植。
Service Handler 的 open 挂钩在连接成功建立时被调用。
接受器(Acceptor):该抽象类为被动连接建立和初始化 Service Handler 实现通用的策略。Acceptor
的接 口如下所示:
template <class SERVICE_HANDLER,
class PEER_ACCEPTOR>
class Acceptor : public Event_Handler
{
public:
// Initialize local_addr transport endpoint factory
// and register with Initiation_Dispatcher Singleton.
virtual int open(const PEER_ACCEPTOR::PEER_ADDR &local_addr);
// Factory Method that creates, connects, and
// activates SERVICE_HANDLER's.
virtual int accept (void);
protected:
//定义服务处理器的创建策略
virtual SERVICE_HANDLER *make_service_handler (void);
// 定义服务处理器的连接策略
virtual int accept_service_handler(SERVICE_HANDLER *);
//定义服务处理器的激活策略
virtual int activate_service_handler(SERVICE_HANDLER *);
// Demultiplexing hooks inherited from Event_Handler,
// which is used by Initiation_Dispatcher for
// callbacks.
virtual HANDLE get_handle (void) const;
virtual int handle_close (void);
private:
// IPC mechanism that establishes
// connections passively.
PEER_ACCEPTOR peer_acceptor_;
};
Acceptor 通过特定类型的 PEER ACCEPTOR 和 SERVICE HANDLER 被参数化。PEER ACCEPTOR 提供的传输
机制被 Acceptor 用于被动地建立连接。SERVICE HANDLER 提供的服务对与远地对端交换的 数据进行处
理。注意 SERVICE HANDLER 是由应用层提供的具体的服务处理器。
参数化类型使 Acceptor 的连接建立策略与服务处理器的类型、网络编程接口及传输层连接发起协议去
耦合。就如同 Connector 一样,通过允许整体地替换 Acceptor 所用的机制,参数化类型的使用有助于
提高可移植性。这使得连接建立代码可在含有不同网络编程接口(比如有 socket,但没有 TLI;反之亦
然)的平台间移植。
make_service_handler 工厂方法定义 Acceptor 用于创建 SERVICE HANDLER 的缺省策略。如下所示:
template <class SH, class PA> SH *
Acceptor<SH, PA>::make_service_handler (void)
{
return new SH;
}
缺省行为使用了"请求策略"(demand strategy),它为每个新连接创建新的 SERVICE HANDLER。但是,
Acceptor 的子类可以重定义这一策略,以使用其他策略创建 SERVICE HANDLE,比如创建单独的单体 (
Singleton)[10]或从共享库中动态链接 SERVICE HANDLER。
accept_service_handler 方法在下面定义 Acceptor 所用的 SERVICE HANDLER 连接接受策略:
template <class SH, class PA> int
Acceptor<SH, PA>::accept_service_handler(SH *handler)
{
peer_acceptor_->accept (handler->peer ());
}
缺省行为委托 PEER ACCEPTOR 所提供的 accept 方法。子类可以重定义 accept_service_handler 方法
,以 执行更为复杂的行为,比如验证客户的身份,以决定是接受还是拒绝连接。
Activate_service_handler 定义 Acceptor 的 SERVICE HANDLER 并发策略:
程序示例:
在ACE中,默认的服务处理器是ACE_Svc_Handler,这也是一个模版类,可以通过相关的参数特化。由于
ACE_Svc_Handler继承自ACE_Task和ACE_Event_Handler,功能相当强大,同时也存在一定开销,如果需
要减小开销可以自己写一个仅继承自ACE_Event_Handler的服务处理器。
为了演示简单,我这里就以一个EchoServer的服务器端和客户端为例,其中接收器和连接器都采用缺省
策略,并没有进行重载。
服务器端:
#include "ace/Reactor.h"
#include "ace/Svc_Handler.h"
#include "ace/Acceptor.h"
#include "ace/Synch.h"
#include "ace/SOCK_Acceptor.h"
class My_Svc_Handler;
typedef ACE_Acceptor<My_Svc_Handler,ACE_SOCK_ACCEPTOR> MyAcceptor;
class My_Svc_Handler:
public ACE_Svc_Handler <ACE_SOCK_STREAM,ACE_NULL_SYNCH>
{
public:
int open(void*)
{
ACE_OS::printf("\nConnection established\n");
//注册相应事件
ACE_Reactor::instance()->register_handler(this,
ACE_Event_Handler::READ_MASK);
return 0;
}
int handle_input(ACE_HANDLE)
{
int rev = peer().recv(data,1024);
if(rev == 0)
{
delete this;
}
else
{
data[rev]='\0';
ACE_OS::printf("<<rev:\t %s\n",data);
peer().send(data,rev+1);
return 0;
}
}
private:
char data[1024];
};
int main(int argc, char* argv[])
{
ACE_INET_Addr addr(3000);
MyAcceptor acceptor(addr,ACE_Reactor::instance());
while(1)
ACE_Reactor::instance()->handle_events();
}
客户端:
#include "ace/Reactor.h"
#include "ace/Svc_Handler.h"
#include "ace/Connector.h"
#include "ace/Synch.h"
#include "ace/SOCK_Connector.h"
class My_Svc_Handler;
typedef ACE_Connector<My_Svc_Handler,ACE_SOCK_CONNECTOR> MyConnector;
class My_Svc_Handler:
public ACE_Svc_Handler <ACE_SOCK_STREAM,ACE_NULL_SYNCH>
{
public:
int open(void*)
{
ACE_OS::printf("\nConnection established\n");
//注册相应事件
ACE_Reactor::instance()->register_handler(this,
ACE_Event_Handler::READ_MASK);
return 0;
}
int handle_input(ACE_HANDLE)
{
int rev = peer().recv(data,1024);
if(rev == 0)
{
delete this;
}
else
{
data[rev]='\0';
ACE_OS::printf("<<rev:\t %s\n",data);
return 0;
}
}
int sendData(char *msg)
{
ACE_OS::printf("<<send:\t %s\n",msg);
return peer().send(msg,strlen(msg));
}
private:
char data[1024];
};
int main(int argc, char* argv[])
{
ACE_INET_Addr addr(3000,"192.168.1.142");
My_Svc_Handler *svchandler = new My_Svc_Handler();
MyConnector connector;
if(connector.connect(svchandler,addr)==-1)
{
ACE_OS::printf("Connect fail");
}
svchandler->sendData("hello wrold");
while(1)
ACE_Reactor::instance()->handle_events();
}
5.4、Proactor模式
当 OS 平台支持异步操作时,一种高效而方便的实现高性能 Web 服务器的方法是使用前摄式事件分派。
使用前摄式事件分派模型设计的 Web 服务器通过一或多个线程控制来处理异步操作的完成。这样,通过
集成完成事件多路分离(completion event demultiplexing)和事件处理器分派,前摄器模式简化了异
步的 Web 服务器。
异步的 Web 服务器将这样来利用前摄器模式:首先让 Web 服务器向 OS 发出异步操作,并将回调方法
登记到 Completion Dispatcher(完成分派器),后者将在操作完成时通知 Web 服务器。于是 OS 代表
Web 服务器执行操作,并随即在一个周知的地方将结果排队。Completion Dispatcher 负责使完成通知
出队,并执行适当的、含有应用特有的 Web 服务器代码的回调。
使用前摄器模式的主要优点是可以启动多个并发操作,并可并行运行,而不要求应用必须拥有多个线程
。操作被应用异步地启动,它们在 OS 的 I/O 子系统中运行直到完成。发起操作的线程现在可以服务
另外的请求了。
在ACE中,可以通过ACE_Proactor实现前摄器模式。实现方式如下。
5.4.1、创建服务处理器:
Proactor框架中服务处理器均派生自ACE_Service_Handler,它和Reactor框架的事件处理器非常类似。
当发生IO操作完成事件时,会触发相应的事件完成会调函数。
5.4.2、实现服务处理器IO操作
Proactor框架中所有的IO操作都由相应的异步操作类来完成,这些异步操作类都继承自
ACE_Asynch_Operation。常用的有以下几种。
l ACE_Asynch_Read_Stream, 提供从TCP/IP socket连接中进行异步读操作.
l ACE_Asynch_Write_Stream, 提供从TCP/IP socket连接中进行异步写操作.
使用这些操作类的一般方式如下:
l 初始化
将相关的操作注册到服务处理器中,一般可通过调用其open方法实现。
l 发出IO操作
发出异步IO操作请求,该操作不会阻塞,具体的IO操作过程由操作系统异步完成。
l IO操作完成回调处理
异步IO操作完成后,OS会触发服务处理器中的相应回调函数,可通过该函数的ACE_Asynch_Result参数获
取相应的返回值。
5.2.3、使用连接器或接受器和远端进行连接
ACE为Proactor框架提供了两个工厂类来建立TCP/IP连接。
l ACE_Asynch_Acceptor, 用于被动地建立连接
l ACE_Asynch_Connector 用于主动地建立连接
当远端连接建立时,连接器或接受器便会创建相应的服务处理器,从而可以实现服务处理。
5.2.4、启动Proactor事件分发处理
启动事件分发处理只需如下调用:
while(true)
ACE_Proactor::instance()->handle_events();
2.4.5、程序示例
服务器端:
服务器端简单的实现了一个EchoServer,流程如下:当客户端建立连接时,首先发出一个异步读的异步
请求,当读完成时,将所读的数据打印出来,并发出一个新的异步请求。
#include "ace/Message_Queue.h"
#include "ace/Asynch_IO.h"
#include "ace/OS.h"
#include "ace/Proactor.h"
#include "ace/Asynch_Acceptor.h"
class HA_Proactive_Service : public ACE_Service_Handler
{
public:
~HA_Proactive_Service ()
{
if (this->handle () != ACE_INVALID_HANDLE)
ACE_OS::closesocket (this->handle ());
}
virtual void open (ACE_HANDLE h, ACE_Message_Block&)
{
this->handle (h);
if (this->reader_.open (*this) != 0 )
{
ACE_ERROR ((LM_ERROR, ACE_TEXT ("%p\n"),
ACE_TEXT ("HA_Proactive_Service open")));
delete this;
return;
}
ACE_Message_Block *mb = new ACE_Message_Block(buffer,1024);
if (this->reader_.read (*mb, mb->space ()) != 0)
{
ACE_OS::printf("Begin read fail\n");
delete this;
return;
}
return;
}
//异步读完成后会调用此函数
virtual void handle_read_stream
(const ACE_Asynch_Read_Stream::Result &result)
{
ACE_Message_Block &mb = result.message_block ();
if (!result.success () || result.bytes_transferred () == 0)
{
mb.release ();
delete this;
return;
}
mb.copy(""); //为字符串添加结束标记'\0'
ACE_OS::printf("rev:\t%s\n",mb.rd_ptr());
mb.release();
ACE_Message_Block *nmb = new ACE_Message_Block(buffer,1024);
if (this->reader_.read (*nmb, nmb->space ()) != 0)
return;
}
private:
ACE_Asynch_Read_Stream reader_;
char buffer[1024];
};
int main(int argc, char *argv[])
{
int port=3000;
ACE_Asynch_Acceptor<HA_Proactive_Service> acceptor;
if (acceptor.open (ACE_INET_Addr (port)) == -1)
return -1;
while(true)
ACE_Proactor::instance ()->handle_events ();
return 0;
}
客户端:
客户端代码比较简单,就是每隔1秒钟将当前的系统时间转换为字符串形式通过异步形式发送给服务器,
发送完成后,释放时间字符的内存空间。
#include "ace/Message_Queue.h"
#include "ace/Asynch_IO.h"
#include "ace/OS.h"
#include "ace/Proactor.h"
#include "ace/Asynch_Connector.h"
class HA_Proactive_Service : public ACE_Service_Handler
{
public:
~HA_Proactive_Service ()
{
if (this->handle () != ACE_INVALID_HANDLE)
ACE_OS::closesocket (this->handle ());
}
virtual void open (ACE_HANDLE h, ACE_Message_Block&)
{
this->handle (h);
if (this->writer_.open (*this) != 0 )
{
ACE_ERROR ((LM_ERROR, ACE_TEXT ("%p\n"),
ACE_TEXT ("HA_Proactive_Service open")));
delete this;
return;
}
ACE_OS::printf("connceted");
for(int i=0;i<10;i++) //每隔秒中发送时间至服务器
{
ACE_OS::sleep(1);
time_t now = ACE_OS::gettimeofday().sec();
char *time = ctime(&now); //获取当前时间的字符串格式
ACE_Message_Block *mb = new ACE_Message_Block(100);
mb->copy(time);
if (this->writer_.write(*mb,mb->length()) !=0)
{
ACE_OS::printf("Begin read fail\n");
delete this;
return;
}
}
return;
}
//异步写完成后会调用此函数
virtual void handle_write_dgram
(const ACE_Asynch_Write_Stream::Result &result)
{
ACE_Message_Block &mb = result.message_block ();
mb.release();
return;
}
private:
ACE_Asynch_Write_Stream writer_;
};
int main(int argc, char *argv[])
{
ACE_INET_Addr addr(3000,"192.168.1.142");
HA_Proactive_Service *client = new HA_Proactive_Service();
ACE_Asynch_Connector<HA_Proactive_Service> connector;
connector.open();
if (connector.connect(addr) == -1)
return -1;
while(true)
ACE_Proactor::instance ()->handle_events ();
return 0;
}
6. ACE的消息存放对象
2.1、ACE Lock类属
锁类属包含的类包装简单的锁定机制,比如互斥体、信号量、读/写互斥体和令牌等。这里我就以互斥
体为例简单的介绍一下其使用方法,对其它的锁类进行一些简单的说明。
ACE_Message_Block在Ace中用来表示消息的存放空间,可用做网络通信中的消息缓冲区,使用非常频繁
,下面将在如下方简单的介绍一下ACE_Message_Block相关功能。
l 创建消息块
l 释放消息块
l 从消息块中读写数据
l 数据的拷贝
l 其它常用函数
6.1、创建消息块
创建消息块的方式比较灵活,常用的有以下几种方式 :
1、直接给消息块分配内存空间创建。
ACE_Message_Block *mb = new ACE_Message_Block (30);
2、共享底层数据块创建。
char buffer[100];
ACE_Message_Block *mb = new ACE_Message_Block (buffer,30);
这种方式共享底层的数据块,被创建的消息块并不拷贝该数据,也不假定自己拥有它的所有权。在消息
块mb被销毁时,相关联的数据缓冲区data将不会被销毁。这是有意义的:消息块没有拷贝数据,因此内
存也不是它分配的,这样它也不应该负责销毁它。
3、通过duplicate()函数从已有的消息块中创建副本。
ACE_Message_Block *mb = new ACE_Message_Block (30);
ACE_Message_Block *mb2 = mb->duplicate();
这种方式下,mb2和mb共享同一数据空间,使用的是ACE_Message_Block的引用计数机制。它返回指向要
被复制的消息块的指针,并在内部增加内部引用计数。
4、通过clone()函数从已有的消息块中复制。
ACE_Message_Block *mb = new ACE_Message_Block (30);
ACE_Message_Block *mb2 = mb->clone();
clone()方法实际地创建整个消息块的新副本,包括它的数据块和附加部分;也就是说,这是一次"深拷
贝"。
6.2、释放消息块
一旦使用完消息块,程序员可以调用它的release()方法来释放它。
l 如果消息数据内存是由该消息块分配的,调用release()方法就也会释放此内存。
l 如果消息块是引用计数的,release()就会减少计数,直到到达0为止;之后消息块和与它相关联的数
据块才从内存中被移除。
l 如果消息块是通过共享已分配的底层数据块创建的,底层数据块不会被释放。
无论消息块是哪种方式创建的,只要在使用完后及时调用release()函数,就能确保相应的内存能正确的
释放。
6.3、从消息块中读写数据
ACE_Message_Block提供了两个指针函数以供程序员进行读写操作,rd_ptr()指向可读的数据块地址,
wr_ptr()指向可写的数据块地址,默认情况下都执行数据块的首地址。下面的例子简单了演示它的使用
方法。
#include "ace/Message_Queue.h"
#include "ace/OS.h"
int main(int argc, char *argv[])
{
ACE_Message_Block *mb = new ACE_Message_Block (30);
ACE_OS::sprintf(mb->wr_ptr(),"%s","hello");
ACE_OS::printf("%s\n",mb->rd_ptr ());
mb->release();
return 0;
}
注意:这两个指针所指向的位置并不会自动移动,在上面的例子中,函数执行完毕后,执行的位置仍然
是最开始的0,而不是最新的可写位置5,程序员需要通过wr_ptr(5)函数手动移动写指针的位置。
6.4、数据的拷贝
一般的数据的拷贝可以通过函数来实现数据的拷贝,copy()还会保证wr_ptr()的更新,使其指向缓冲区
的新末尾处。
下面的例子演示了copy()函数的用法。
mb->copy("hello");
mb->copy("123",4);
注意:由于c++是以'\0'作为字符串结束标志的,对于上面的例子,底层数据块中保存的是"hello
\0123\0",而用ACE_OS::printf("%s\n",mb->rd_ptr ());打印出来的结果是"hello",使用copy函数进
行字符串连接的时候需要注意。
6.5、其它常用函数
length() 返回当前的数据长度
next() 获取和设置下一个ACE_Message_Block的链接。(用来建立消息队列非常有用)
space() 获取剩余可用空间大小
size() 获取和设置数据存储空间大小。
注意:
这里说一下ACE::read_n 的行为:
ACE::read_n 会试图读取buf长度的数据.如果遇到文件结束(EOF)或者错误则返回 0 或 -1;如果先到达
了buf长度则返回数据区长度;问题来了:如果数据读取成功,但是没有到达buf长度怎么办? 如何拿到已
读数据的长度? 这就要用到ACE::read_n的第4个参数,这个参数记录了实际读取的数据长度.
在上面的code里还用到了几个函数:
ACE_Message_Block::size 指数据区的长度, 就是初始化时指定的长度,这里是10;
ACE_Message_Block::length 指数据的长度, 是 wr_ptr() - rd_ptr()的结果.
注意数据区和数据的区别....
ACE_Message_Block::cont ACE_Message_Block还实现了内存的链表结构;
7. 总结
一般文章整理的只是ACE的基础部分,如果需要深入了解ACE还需要通过查看源代码以进一步了解。可分
为如下模块:
1. 并发和同步
2. 进程间通信(IPC)
3. 内存管理
4. 定时器
5. 信号
6. 文件系统管理
7. 线程管理
8. 事件多路分离和处理器分派
9. 连接建立和服务初始化
10. 软件的静态和动态配置、重配置
11. 分层协议构建和流式框架
12. 分布式通信服务:名字、日志、时间同步、事件路由和网络锁定。
========
ACE学习
ACE的配置(window)
(使用VC++)安装:
1. 从网上下载相应源码――――根据提示编辑config.h文件,并放置在ACE_ROOT\ace 目录下。
2. 用VC打开ACE_ROOT\ace\ace.dsw ,并编译,编译后会在ACE_ROOT\lib 目录下生成两个库:
ACEd.dll(动态库)和ACEd.lib(静态库)。
3. 链接:把ACEd.dll(动态库)和ACEd.lib(静态库)复制到目录ACE_ROOT\ace下,因为这是默认的静态
库链接路径。或者可以修改静态库链接路径:project->setting->link->object/library 填入
ACE_ROOT\lib.
4.执行:If you use the dynamic libraries, make sure you include ACE_ROOT\bin in your PATH
whenever you run programs that
uses ACE. Otherwise you may experience problems finding ace.dll or aced.dll.
就是在程序执行时,如果使用ACE的动态库,必须修改系统环境变量PATH的值,把包含ACEd.dll 的路径
添加到PATH中。修改如下:电脑属性――高级――环境变量――系统变量――Path(修改)。或者把
ACEd.dll 添加到系统默认的搜索路径之中,如添加到c:/window/system32 中。
在VC++下使用ACE
新建工程:使用new菜单,选中project。选取win32 console Applitation(控制台应用程序),建立
一个空项目。同时新建一个workspace。
一个workspace可以对应几个项目,一个项目对应一个程序。当然如何在一个workspace管理多个工
程现在还没搞清楚。Workspace保存空间中的相应配置,而一个项目保存,自己项目下的项目配置。
添加头文件的搜索路径:tools->option->directories,添加ace头文件路径。当然,按照同样的办
法,也可以修改库文件、执行文件、源文件的搜索路径,只需做相应选择就可以了。
修改项目的setting:通过project->setting进入,或是直接在项目名字上点击右键,选择setting
即可。把C/C++中的MLd修改为MDd(多线程);把link中input中的库改为aced.lib,同时additional
path 中 http://www.cnblogs.com/../ace。这样设置就基本完成。
注:以下摘抄自《ACE 程序员教程》
ACE简介
ACE自适配通信环境 (Adaptive Communication Environment)是面向对象的构架和工具包,它为通
信软件实现了核心的并发和分布式模式。ACE中的组件可用于以下几种目的:
· 并发和同步
· 进程间通信(IPC)
· 内存管理
· 定时器
· 信号
· 文件系统管理
· 线程管理
· 事件多路分离和处理器分派
· 连接建立和服务初始化
· 软件的静态和动态配置、重配置
· 分层协议构建和流式构架
· 分布式通信服务:名字、日志、时间同步、事件路由和网络锁定,等等。
目前ACE适用的OS平台包括:实时OS(VxWorks、Chorus、LynxOS和pSoS)、大多数版本的UNIX
(SunOS 4.x和5.x; SGI IRIX 5.x和6.x; HP-UX 9.x, 10.x和11.x; DEC UNIX 3.x和4.x; AIX 3.x和
4.x; DG/UX; Linux; SCO; UnixWare; NetBSD和FreeBSD)、Win32(使用MSVC++和Borland C++的WinNT
3.5.x、4.x、Win95和WinCE)以及MVS OpenEdition。
在ACE构架中有三个基本层次:
· 操作系统(OS)适配层
· C++包装层
· 构架和模式层
第2章 IPC SAP:进程间通信服务访问点包装
ACE_IPC_SAP类提供的一些函数是所有IPC接口公有的。有四个不同的类由此类派生而出,每个类各
自代表ACE包含的一种IPC SAP包装类属。这些类封装适用于特定IPC接口的功能。例如,ACE_SOCK类包含
的功能适用于BSD socket编程接口,而ACE_TLI包装TLI编程接口。ACE_FIFO类和 ACE_SPIPE类。
socket类属(ACE_SOCK)
类名
职责
ACE_SOCK_Acceptor
用于被动的连接建立,基于BSD accept()和listen()调用。
ACE_SOCK_Connector
用于主动的连接建立,基于BSD connect()调用。
ACE_SOCK_Dgram
用于提供基于UDP(用户数据报协议)的无连接消息传递服务。封装了sendto()和receivefrom()等调用
,并提供了简单的send()和recv()接口。
ACE_SOCK_IO
用于提供面向连接的消息传递服务。封装了send()、recv()和write()等调用。该类是ACE_SOCK_Stream
和ACE_SOCK_CODgram类的基类。
ACE_SOCK_Stream
用于提供基于TCP(传输控制协议)的面向连接的消息传递服务。派生自ACE_SOCK_IO,并提供了更多的
包装方法。
ACE_SOCK_CODgram
用于提供有连接数据报(connected datagram)抽象。派生自ACE_SOCK_IO;它包含的open()方法使用
bind()来绑定到指定的本地地址,并使用UDP连接到远地地址。
ACE_SOCK_Dgram_Mcast
用于提供基于数据报的多点传送(multicast)抽象。包括预订多点传送组,以及发送和接收消息的方法
ACE_SOCK_Dgram_Bcast
用于提供基于数据报的广播(broadcast)抽象。包括在子网中向所有接口广播数据报消息的方法
表2-1 ACE_SOCK中的类及其职责
第3章 ACE的内存管理
ACE含有两组不同的类用于内存管理。
第一组是那些基于ACE_Allocator的类。这组类使用动态绑定和策略模式来提供灵活性和可扩展性。
它们只能用于局部的动态内存分配。
第二组类基于ACE_Malloc模板类。这组类使用C++模板和外部多态性 (External Polymorphism)来
为内存分配机制提供灵活性。在这组类中的类不仅包括了用于局部动态内存管理的类,也包括了管理进
程间共享内存的类。这些共享内存类使用底层OS(OS)共享内存接口。
3.1 分配器(Allocator)
分配器用于在ACE中提供一种动态内存管理机制。在ACE中有若干使用不同策略的分配器可用。这些
不同策略提供相同的功能,但是具有不同的特性。所有的分配器都支持ACE_Allocator接口,因此无论是
在运行时还是在编译时,它们都可以很容易地相互替换。这也正是灵活性之所在。
分配器
描述
ACE_Allocator
ACE中的分配器类的接口类。这些类使用继承和动态绑定来提供灵活性。
ACE_Static_Allocator
该分配器管理固定大小的内存。每当收到分配内存的请求时,它就移动内部指针、以返回内存chunk(“
大块”)。它还假定内存一旦被分配,就再也不会被释放。
ACE_Cached_Allocator
该分配器预先分配内存池,其中含有特定数目和大小的内存chunk。这些chunk在内部空闲表(free list
)中进行维护,并在收到内存请求(malloc())时被返回。当应用调用free()时,chunk被归还到内部空
闲表、而不是OS中。
ACE_New_Allocator
为C++ new和delete操作符提供包装的分配器,也就是,它在内部使用new和delete操作符,以满足动态
内存请求。
表3-1 ACE中的分配器
使用如下:
typedef ACE_Cached_Allocator<MEMORY_BLOCK,ACE_SYNCH_MUTEX> Allocator;
3.2 ACE_Malloc
Malloc类集使用模板类ACE_Malloc来提供内存管理。ACE_Malloc模板需要两个参数(一个是内存池
,一个是池锁),以产生我们的分配器类。当应用发出free()调用时,ACE_Malloc不会把所释放的内存
返还给内存池,而是由它自己的空闲表进行管理。当ACE_Malloc收到后续的内存请求时,它会使用空闲
表来查找可返回的空block。因而,在使用ACE_Malloc时,如果只发出简单的malloc()和free()调用,从
OS分配的内存数量将只会增加,不会减少。ACE_Malloc类还含有一个remove()方法,用于发出请求给内
存池,将内存返还给OS。该方法还将锁也返还给OS。
3.2.2 使用ACE_Malloc
ACE_Malloc类的使用很简单。首先,用你选择的内存池和锁定机制实例化ACE_Malloc,以创建分配
器类。随后用该分配器类实例化一个对象,这也就是你的应用将要使用的分配器。当你实例化分配器对
象时,传给构造器的第一个参数是一个字符串,它是你想要分配器对象使用的底层内存池的“名字”。
将正确的名字传递给构造器非常重要,特别是如果你在使用共享内存的话。否则,分配器将会为你创建
一个新的内存池。如果你在使用共享内存池,这当然不是你想要的,因为你根本没有获得共享。
为了方便底层内存池的共享(重复一次,如果你在使用共享内存的话,这是很重要的),
ACE_Malloc类还拥有一个映射(map)类型接口:可被给每个被分配的内存block一个名字,从而使它们
可以很容易地被在内存池中查找的另一个进程找到。该接口含有bind()和find()调用。bind()调用用于
给由malloc()调用返回给ACE_Malloc的block命名。find()调用,如你可能想到的那样,用于查找与某个
名字相关联的内存。
表3-2列出了各种可用的内存池:
池名
宏
描述
ACE_MMAP_Memory_Pool
ACE_MMAP_MEMORY_POOL
使用<mmap(2)>创建内存池。这样内存就可在进程间共享了。每次更新时,内存都被更新到后备存储
(backing store)。
ACE_Lite_MMAP_Memory_Pool
ACE_LITE_MMAP_MEMORY_POOL
使用<mmap(2)>创建内存池。不像前面的映射,它不做后备存储更新。代价是较低可靠性。
ACE_Sbrk_Memory_Pool
ACE_SBRK_MEMORY_POOL
使用<sbrk(2)>调用创建内存池。
ACE_Shared_Memory_Pool
ACE_SHARED_MEMORY_POOL
使用系统V <shmget(2)>调用创建内存池。
Memory_Pool
内存可在进程间共享。
ACE_Local_Memory_Pool
ACE_LOCAL_MEMORY_POOL
通过C++的new和delete操作符创建局部内存池。该池不能在进程间共享。
第4章 线程管理:ACE的同步和线程管理机制
ACE_Thread提供了对OS的线程调用的简单包装,这些调用处理线程创建、挂起、取消和删除等问题
。它提供给应用程序员一个简单易用的接口,可以在不同的线程API间移植。ACE_Thread是非常“瘦”的
包装,有着很少的开销。其大多数方法都是内联的,因而等价于对底层OS专有线程接口的直接调用。
ACE_Thread中的所有方法都是静态的,而且该类一般不进行实例化。
线程是通过使用ACE_Thread::spawn_n()调用创建的。要作为线程的执行启动点调用的函数的指针(
在此例中为worker()函数)被作为参数传入该调用中。要注意的重点是ACE_Thread::spawn_n()要求所有
的线程启动函数(方法)必须是静态的或全局的(就如同直接使用OS线程API时所要求的一样)。
等待是通过使用ACE_Thread::join()调用来完成的。该方法的参数是你想要主线程与之联接的线程
的句柄(ACE_hthread_t)。
4.2 ACE同步原语
ACE有若干可用于同步目的的类。这些类可划分为以下范畴:
· ACE Lock类属
· ACE Guard类属
· ACE Condition类属
· 杂项ACE Synchronization类
名字
描述
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接口。
表4-1 ACE锁类属中的类
表4-1中描述的类都支持同样的接口。但是,在任何继承层次中,这些类都是互不关联的。在ACE中
,锁通常用模板来参数化,因为,在大多数情况下,使用虚函数调用的开销都是不可接受的。使用模板
使得程序员可获得相当程度的灵活性。他可以在编译时(但不是在运行时)选择他想要使用的的锁定机
制的类型。然而,在某些情形中,程序员仍可能需要使用动态绑定和替换(substitution);对于这些
情况,ACE提供了ACE_Lock和ACE_Lock_Adapter类。
在临界区内完成的工作使用ACE_Thread_Mutex互斥体对象进行保护。该对象由主线程作为参数传给
工作者线程。临界区控制是通过在ACE_Thread_Mutex对象上发出acquire()调用,从而获取互斥体的所有
权来完成的。一旦互斥体被获取,没有其他线程能够再进入这一代码区。临界区控制是通过使用
release()调用来释放的。一旦互斥体的所有权被放弃,就会唤醒所有其他在等待的线程。这些线程随即
相互竞争,以获得互斥体的所有权。第一个试图获取所有权的线程会进入临界区。
使用示例:
struct Args
{
public:Args(int iterations): mutex_(),iterations_(iterations){}
ACE_Thread_Mutex mutex_;
int iterations_;
};
4.2.1.3 使用令牌(Token)
如表4-1中所提到的,ACE_Token类提供所谓的“递归互斥体”,它可以被最初获得它的同一线程进
行多次重新获取。ACE_Token类还确保所有试图获取它的线程按严格的FIFO(先进先出)顺序排序。
递归锁允许同一线程多次获取同一个锁。线程不会因为试图获取它已经拥有的锁而死锁。这些类型
的锁能在各种不同的情况下派上用场。例如,如果你用一个锁来维护跟踪流的一致性,你可能希望这个
锁是递归的,因为某个方法可以调用一个跟踪例程,获取锁,被信号中断,然后再尝试获取这个跟踪锁
。如果锁是非递归的,线程将会在这里锁住它自己。你会发现很多其他需要递归锁的有趣应用。重要的
是要记住,你获取递归锁多少次,就必须释放它多少次。
在SunOS 5.x上运行例4-3,释放锁的线程常常也是重新获得它的线程(大约90%的情况是这样)。但
是如果你采用ACE_Token类作为锁定机制来运行这个例子,每个线程都会轮流获得令牌,然后有序地把机
会让给下一个线程。
尽管ACE_Token作为所谓的递归锁非常有用,它们实际上是更大的“令牌管理”构架的一部分。该构
架允许你维护数据存储中数据的一致性。
4.2.2 ACE守卫(Guard)类属
ACE中的守卫用于自动获取和释放锁。守卫类的对象定义一个代码块,在其上获取一个锁。在退出此
代码块时,锁被自动释放。
ACE中的守卫类是一种模板,它通过所需锁定机制的类型来参数化。底层的锁可以是ACE Lock类属中
的任何类,也就是,任何互斥体或锁类。它是这样工作的:对象的构造器获取锁,析构器释放锁。表4-2
列出了ACE中可用的守卫:
名字
描述
ACE_Guard
自动在底层锁上调用acquire()和release()。任何ACE Lock类属中的锁都可以作为它的模板参数传入。
ACE_Read_Guard
自动在底层锁上调用acquire()和release()。
ACE_Write_Guard
自动在底层锁上调用acquire()和release()。
表4-2 ACE中的守卫
4.2.3 ACE条件(Condition)类属
ACE_Condition类是针对OS条件变量原语的包装类。线程常常需要特定条件被满足才能继续它的操作
。条件变量不是被用作互斥原语,而是用作特定条件已经满足的指示器。在使用条件变量时,你的程序
应该完成以下步骤:
· 获取全局资源(例如,消息队列)的锁(互斥体)。
· 检查条件(例如,消息队列里有空间吗?)。
· 如果条件失败,调用条件变量的wait()方法。等待在未来条件变为真。
· 当另一线程在全局资源上执行操作时,它发信号(signal())给所有其他在此资源上测试条件的线程
(例如,另一线程从消息队列中取出一个消息,然后通过条件变量发送信号,以使阻塞在wait()上的线
程能够再尝试将它们的消息插入队列)。
· 在醒来之后,重新检查条件现在是否为真。如为真,则在全局资源上执行操作(例如,将消息插入全
局消息队列)
需要特别注意的是,在阻塞wait调用中之前,条件变量机制(也就是ACE_Cond)负责释放全局资源
上的互斥体。如果没有进行此操作,将没有其他的线程能够在此资源上工作(该资源是条件改变的原因
)。同样,一旦阻塞线程收到信号、重又醒来,它在检查条件之前会在内部重新获取锁。
注意主线程首先获取一个互斥体,然后对条件进行测试。如果条件不为真,主线程就等待在此条件
变量上。条件变量随即自动释放互斥体,并使主线程进入睡眠。条件变量总是像这样与互斥体一起使用
。这是一种可如下描述的一般模式[1]:
while( expression NOT TRUE ) wait on condition variable;
记住条件变量不是用于互斥,而是用于我们所描述的发送信号功能。
4.2.4 杂项同步类
除了上面描述的同步类,ACE还包括其他一些同步类,比如ACE_Barrier和ACE_Atomic_Op。
4.2.4.1 ACE中的栅栏(Barrier)
栅栏有一个好名字,因为它恰切地描述了栅栏应做的事情。一组线程可以使用栅栏来进行共同的相
互同步。组中的每个线程各自执行,直到到达栅栏,就阻塞在那里。在所有相关线程到达栅栏后,它们
就全部继续它们的执行。就是说,它们一个接一个地阻塞,等待其他的线程到达栅栏;一旦所有线程都
到达了它们的执行路径中的“栅栏点”,它们就一起重新启动。
在ACE中,栅栏在ACE_Barrier类中实现。在栅栏对象被实例化时,它将要等待的线程的数目会作为
参数传入。一旦到达执行路径中的“栅栏点”,每个线程都在栅栏对象上发出wait()调用。它们在这里
阻塞,直到其他线程到达它们各自的“栅栏点”,然后再一起继续执行。当栅栏从相关线程那里接收了
适当数目的wait()调用时,它就同时唤醒所有阻塞的线程。
4.2.4.2 原子操作(Atomic Op)
ACE_Atomic_Op类用于将同步透明地参数化进基本的算术运算中。ACE_Atomic_Op是一种模板类,锁
定机制和需要参数化的类型被作为参数传入其中。ACE是这样来实现此机制的:重载所有算术操作符,并
确保在操作前获取锁,在操作后释放它。运算本身被委托给通过模板传入的的类。
4.3 使用ACE_THREAD_MANAGER进行线程管理
我们可以使用ACE_Thread包装类来创建和销毁线程。但是,该包装类的功能比较有限。
ACE_Thread_Manager提供了ACE_Thread中的功能的超集。特别地,它增加了管理功能,以使启动、取消
、挂起和恢复一组相关线程变得更为容易。它用于创建和销毁成组的线程和任务(ACE_Task是一种比线
程更高级的构造,可在ACE中用于进行多线程编程。我们将在后面再来讨论任务)。它还提供了这样的功
能:发送信号给一组线程,或是在一组线程上等待,而不是像我们在前面的例子中所看到的那样,以一
种不可移植的方式来调用join()。
4.4 线程专有存储(Thread Specific Storage)
对于各个线程来说,可能需要不同的全局或静态数据。可使用线程专有存储来满足此需求。像输入
端口这样的结构可放在线程专有存储中,并可像逻辑上的静态或全局变量一样被访问;而实际上它对线
程来说是私有的。
传统上,线程专有存储通过让人迷惑的底层操作系统API来实现。在ACE中,TSS通过使用ACE_TSS模
板类来实现。需要成为线程专有的类被传入ACE_TSS模板,然后可以使用C++的->操作符来调用它的全部
公共方法。
第5章 任务和主动对象(Active Object):并发编程模式(多线程)
5.1 主动对象
那么到底什么是主动对象呢?传统上,所有的对象都是被动的代码段,对象中的代码是在对它发出
方法调用的线程中执行的。也就是,调用线程(calling threads)被“借出”,以执行被动对象的方法
。
而主动对象却不一样。这些对象持有它们自己的线程(甚或多个线程),并将这个线程用于执行对
它们的任何方法的调用。因而,如果你想象一个传统对象,在里面封装了一个线程(或多个线程),你
就得到了一个主动对象。
例如,设想对象“A”已在你的程序的main()函数中被实例化。当你的程序启动时,OS创建一个线程
,以从main()函数开始执行。如果你调用对象A的任何方法,该线程将“流过”那个方法,并执行其中的
代码。一旦执行完成,该线程返回调用该方法的点并继续它的执行。但是,如果”A”是主动对象,事情
就不是这样了。在这种情况下,主线程不会被主动对象借用。相反,当”A”的方法被调用时,方法的执
行发生在主动对象持有的线程中。另一种思考方法:如果调用的是被动对象的方法(常规对象),调用
会阻塞(同步的);而另一方面,如果调用的是主动对象的方法,调用不会阻塞(异步的)。
5.2 ACE_Task
ACE_Task是ACE中的任务或主动对象“处理结构”的基类。在ACE中使用了此类来实现主动对象模式
。所有希望成为“主动对象”的对象都必须从此类派生。你也可以把ACE_TASK看作是更高级的、更为面
向对象的线程类。
当我们在前一章中使用ACE_Thread包装时,你一定已经注意到了一些“不好”之处。那一章中的大
多数程序都被分解为函数、而不是对象。这是因为ACE_Thread包装需要一个全局函数名、或是静态方法
作为参数。随后该函数(静态方法)就被用作所派生的线程的“启动点”。这自然就使得程序员要为每
个线程写一个函数。如我们已经看到的,这可能会导致非面向对象的程序分解。
相反,ACE_Task处理的是对象,因而在构造OO程序时更便于思考。因此,在大多数情况下,当你需
要构建多线程程序时,较好的选择是使用ACE_Task的子类。这样做有若干好处。首要的是刚刚所提到的
,这可以产生更好的OO软件。其次,你不必操心你的线程入口是否是静态的,因为ACE_Task的入口是一
个常规的成员函数。而且,我们会看到ACE_Task还包括了一种用于与其他任务进行通信的易于使用的机
制。
重申刚才所说的,ACE_Task可用作:
· 更高级的线程(我们称之为任务)。
· 主动对象模式中的主动对象。
ACE_Task的结构:每个任务都含有一或多个线程,以及一个底层消息队列。各个任务通过这些消息
队列进行通信。但是,消息队列并非是程序员需要关注的对象。发送任务可以使用putq()调用来将消息
插入到另一任务的消息队列中。随后接收任务就可以通过使用getq()调用来从它自己的消息队列里将消
息提取出来。
5.2.2 创建和使用任务
要创建任务或主动对象,必须从ACE_Task类派生子类。在子类派生之后,必须采取以下步骤:
· 实现服务初始化和终止方法:open()方法应该包含所有专属于任务的初始化代码。其中可能包括诸如
连接控制块、锁和内存这样的资源。close()方法是相应的终止方法。
· 调用启用(Activation)方法:在主动对象实例化后,你必须通过调用activate()启用它。要在主动
对象中创建的线程的数目,以及其他一些参数,被传递给activate()方法。activate()方法会使svc()方
法成为所有它生成的线程的启动点。
· 实现服务专有的处理方法:如上面所提到的,在主动对象被启用后,各个新线程在svc()方法中启动
(如何区分并调用不同线程)。应用开发者必须在子类中定义此方法。
5.2.3 任务间通信
如前面所提到的,ACE中的每个任务都有一个底层消息队列。这个消息队列被用作任务间通信的一种
方法。当一个任务想要与另一任务“谈话”时,它创建一个消息,并将此消息放入(putq())它想要与
之谈话的任务的消息队列。接收任务通常用getq()从消息队列里获取消息。如果队列中没有数据可用,
它就进入休眠状态。如果有其他任务将消息插入它的队列,它就会苏醒过来,从队列中拾取数据并处理
它。因而,在这种情况下,接收任务将从发送任务那里接收消息,并以应用特定的方式作出反馈。
5.3 主动对象模式(Active Object Pattern)
主动对象模式用于降低方法执行和方法调用之间的耦合。该模式描述了另外一种更为透明的任务间
通信方法。
该模式使用ACE_Task类作为主动对象。在这个对象上调用方法时,它就像是常规对象一样。就是说
,方法调用是通过同样的->操作符来完成的,其不同在于这些方法的执行发生于封装在ACE_Task中的线
程内。在使用被动或主动对象进行编程时,客户程序看不到什么区别,或仅仅是很小的区别。对于构架
开发者来说,这是非常有用的,因为开发者需要使构架客户与构架的内部结构屏蔽开来。这样构架用户
就不必去担心线程、同步、会合点(rendezvous),等等。
5.3.1 主动对象模式工作原理
主动对象模式是ACE实现的较为复杂的模式中的一个。该模式有如下参与者:
1. 主动对象(基于ACE_Task)。
2. ACE_Activation_Queue。
3. 若干ACE_Method_Object(主动对象的每个方法都需要有一个方法对象)。
4. 若干ACE_Future对象(每个要返回结果的方法都需要这样一个对象)。
我们已经看到,ACE_Task是怎样创建和封装线程的。要使ACE_Task成为主动对象,需要完成一些额
外的工作:
必须为所有要从客户异步调用的方法编写方法对象。每个方法对象都派生自ACE_Method_Object,并
会实现它的call()方法。每个方法对象还维护上下文信息(比如执行方法所需的参数,以及用于获取返
回值的ACE_Future对象。这些值作为私有属性维护)。你可以把方法对象看作是方法调用的“罩
子”(closure)。客户发出方法调用,使得相应的方法对象被实例化,并被放入启用队列(activation
queue)中。方法对象是命令(Command)模式的一种形式(参见有关设计模式的参考文献)。
ACE_Activation_Queue是一个队列,方法对象在等待执行时被放入其中。因而启用队列中含有所有
等待调用的方法(以方法对象的形式)。封装在ACE_Task中的线程保持阻塞,等待任何方法对象被放入
启用队列。一旦有方法对象被放入,任务就将该方法对象取出,并调用它的call()方法。call()方法应
该随即调用该方法在ACE_Task中的相应实现。在方法实现返回后,call()方法在ACE_Future对象中设置
(set())所获得的结果。
客户使用ACE_Future对象获取它在主动对象上发出的任何异步操作的结果。一旦客户发出异步调用
,立即就会返回一个ACE_Future对象。于是客户就可以在任何它喜欢的时候去尝试从ACE_Future对象中
获取结果。如果客户试图在结果被设置之前从ACE_Future对象中提取结果,客户将会阻塞。如果客户不
希望阻塞,它可以通过使用ready()调用来轮询(poll)ACE_Future对象。如果结果已被设置,该方法返
回1;否则就返回0。ACE_Future对象基于“多态期货”(polymorphic futures)的概念。
call()方法的实现应该将返回的ACE_Future对象的内部值设置为从调用实际的方法实现所获得的结
果(这个实际的方法实现在ACE_Task中编写)。
第6章 反应堆(Reactor):用于事件多路分离和分派的体系结构模式(事件驱动-异步事件)
反应堆本质上提供一组更高级的编程抽象,简化了事件驱动的分布式应用的设计和实现。除此而外
,反应堆还将若干不同种类的事件的多路分离集成到易于使用的API中。特别地,反应堆对基于定时器的
事件、信号事件、基于I/O端口监控的事件和用户定义的通知进行统一地处理。
ACE中的反应堆与若干内部和外部组件协同工作。其基本概念是反应堆构架检测事件的发生(通过在
OS事件多路分离接口上进行侦听),并发出对预登记事件处理器(event handler)对象中的方法的“回
调”(callback)。该方法由应用开发者实现,其中含有应用处理此事件的特定代码。于是用户(也就
是,应用开发者)必须:
1. 创建事件处理器,以处理他所感兴趣的某事件。
2. 在反应堆上登记,通知说他有兴趣处理某事件,同时传递他想要用以处理此事件的事件处理器的指针
给反应堆。
随后反应堆构架将自动地:
1. 在内部维护一些表,将不同的事件类型与事件处理器对象关联起来。
2. 在用户已登记的某个事件发生时,反应堆发出对处理器中相应方法的回调。
6.2 事件处理器
反应堆模式在ACE中被实现为ACE_Reactor类,它提供反应堆构架的功能接口。
如上面所提到的,反应堆将事件处理器对象作为服务提供者使用。一旦反应堆成功地多路分离和分
派了某事件,事件处理器对象就对它进行处理。因此,反应堆会在内部记住当特定类型的事件发生时,
应该回调哪一个事件处理器对象。当应用在反应堆上登记它的处理器对象,以处理特定类型的事件时,
反应堆会创建这种事件和相应的事件处理器的关联。
因为反应堆需要记录哪一个事件处理器将被回调,它需要知道所有事件处理器对象的类型。这是通
过替换模式(Substitution Pattern)的帮助来实现的(或者换句话说,通过“是……类型”(is a
type of)变种继承)。该构架提供名为ACE_Event_Handler的抽象接口类,所有应用特有的事件处理器
都必须由此派生(这使得应用特有的处理器都具有相同的类型,即ACE_Event_Handler,所以它们可以相
互替换)。
ACE_Event_Handler类拥有若干不同的“handle”(处理)方法,每个处理方法被用于处理不同种类的事
件。当应用程序员对特定事件感兴趣时,他就对ACE_Event_Handler类进行子类化,并实现他感兴趣的处
理方法。如上面所提到的,随后他就在反应堆上为特定事件“登记”他的事件处理器类。于是反应堆就
会保证在此事件发生时,自动回调在适当的事件处理器对象中的适当的”handle”方法。
使用ACE_Reactor基本上有三个步骤:
· 创建ACE_Event_Handler的子类,并在其中实现适当的“handle_”方法,以处理你想要此事件处理器
为之服务的事件类型。(参看表6-1来确定你需要实现哪一个“handle_”方法。注意你可以使用同一个
事件处理器对象处理多种类型的事件,因而可以重载多于一个的“handle_”方法。)
· 通过调用反应堆对象的register_handler(),将你的事件处理器登记到反应堆。
· 在事件发生时,反应堆将自动回调相应的事件处理器对象的适当的“handle_”方法。
ACE_Event_Handler中的处理方法
在子类中重载,所处理事件的类型:
handle_signal()
信号。当任何在反应堆上登记的信号发生时,反应堆自动回调该方法。
handle_input()
来自I/O设备的输入。当I/O句柄(比如UNIX中的文件描述符)上的输入可用时,反应堆自动回调该方法
。
handle_exception()
异常事件。当已在反应堆上登记的异常事件发生时(例如,如果收到SIGURG(紧急信号)),反应堆自
动回调该方法。
handle_timeout()
定时器。当任何已登记的定时器超时的时候,反应堆自动回调该方法。
handle_output()
I/O设备输出。当I/O设备的输出队列有可用空间时,反应堆自动回调该方法。
表6-1 ACE_Event_Handler中的处理方法及其对应事件
6.2.1 事件处理器登记
登记事件处理器、以处理特定事件,是在反应堆上调用register_handler()方法来完成的。
register_handler()方法是重载方法,就是说,实际上有若干方法可用于登记不同的事件类型,每个方
法都叫做register_handler()。但是它们有着不同的特征:它们的参数各不相同。基本上,
register_handler()方法采用handle/event_handle元组或signal/event_handler元组作为参数,并将它
们加入反应堆的内部分派表。当有事件在handle上发生时,反应堆在它的内部分派表中查找相应的
event_handler,并自动在它找到的event_handler上回调适当的方法。
6.2.2 事件处理器的拆除和生存期管理
一旦所需的事件被处理后,可能就无需再让事件处理器登记在反应堆上。因而,反应堆提供了从它
的内部分派表中拆除事件处理器的技术。一旦事件处理器被拆除,它就不再会被反应堆回调。把这样的
死掉的句柄从反应堆里拆除是很重要的,因为,如果不这样做,反应堆将会把此句柄标记为“读就绪”
,并会持续不断地回调此事件处理器的handle_方法。
6.2.2.1 从反应堆内部分派表中隐式拆除事件处理器
隐式拆除是更为常用的从反应堆中拆除事件处理器的技术。事件处理器的每个“handle_”方法都会
返回一个整数给反应堆。如果此整数为0,在处理器方法完成后、事件处理器将保持在反应堆上的登记。
但是,如果“handle_”方法返回的整数<0,反应堆将自动回调此事件处理器的handle_close()方法,并
将它从自己的内部分派表中拆除。handle_close()方法用于执行处理器特有的任何清除工作,它们需要
在事件处理器被拆除前完成;其中可以包括像删除处理器申请的动态内存、或关闭日志文件这样的工作
6.2.2.2从反应堆内部分派表中显式拆除事件处理器
另一种从反应堆的内部表中拆除事件处理器的方法是显式地调用反应堆的remove_handler()方法集
。该方法也是重载方法,就像register_handler()一样。它采用需要拆除的处理器的句柄或信号号码作
为参数,并将该处理器从反应堆的内部分派表中拆除。在remove_handler()被调用时,反应堆还自动调
用事件处理器的handle_close()方法。可以这样来对其进行控制:将ACE_Event_Handler::DONT_CALL掩
码传给remove_handler(),从而使得handle_close()方法不会被调用。
6.3 通过反应堆进行事件处理
6.3.1 I/O事件多路分离
通过在具体的事件处理器类中重载handle_input()方法,反应堆可用于处理基于I/O设备的输入事件
。这样的I/O可以发生在磁盘文件、管道、FIFO或网络socket上。为了进行基于I/O设备的事件处理,反
应堆在内部使用从操作系统获取的设备句柄(在基于UNIX的系统中,该句柄是在文件或socket打开时,
OS返回的文件描述符。在Windows中该局柄是由Windows返回的设备句柄)。网络应用显然是最适于这样
的多路分离的应用之一。下面的例子演示反应堆是怎样与具体接受器一起使用来构造一个服务器的。
show sourceview sourceprint?
001
#include "ace/Reactor.h"
002
#include "ace/SOCK_Acceptor.h"
003
004
#define PORT_NO 1024
005
006
typedef
ACE_SOCK_Acceptor Acceptor;
007
008
009
//forward declaration
010
class
My_Accept_Handler;
011
012
class
My_Input_Handler: public
ACE_Event_Handler
013
{
014
public:
015
//Constructor
016
My_Input_Handler()
017
{
018
ACE_DEBUG((LM_DEBUG,"Constructor\n"));
019
}
020
021
//Called back to handle any input received
022
int handle_input(ACE_HANDLE)
023
{
024
//receive the data
025
peer_.recv_n(data,12);
026
ACE_DEBUG((LM_DEBUG,"%s\n",data));
027
028
// do something with the input received.
029
// ...
030
//keep yourself registered with the reactor
031
032
return 0;
033
}
034
035
//Used by the reactor to determine the underlying handle
036
ACE_HANDLE get_handle()
const
037
{
038
return this->peer_.get_handle();
039
//return this->peer_i().get_handle();
040
041
}
042
043
//Returns a reference to the underlying stream.
044
ACE_SOCK_Stream &peer_i()
045
{
046
return this->peer_;
047
}
048
049
050
ACE_SOCK_Stream peer_;
//public
051
private:
052
//ACE_SOCK_Stream peer_;
053
char data [12];
054
};
055
056
class
My_Accept_Handler: public
ACE_Event_Handler
057
{
058
public:
059
//Constructor
060
My_Accept_Handler(ACE_Addr &addr)
061
{
062
this->open(addr);
063
}
064
065
//Open the peer_acceptor so it starts to ”listen”
066
//for incoming clients.
067
068
int open(ACE_Addr &addr)
069
{
070
peer_acceptor.open(addr);
071
return 0;
072
}
073
074
//Overload the handle input method
075
int handle_input(ACE_HANDLE handle)
076
{
077
//Client has requested connection to server.
078
//Create a handler to handle the connection
079
080
My_Input_Handler *eh =
new My_Input_Handler();
081
082
//Accept the connection "into" the Event Handler
083
if (this->peer_acceptor.accept (eh->peer_,
// stream
084
0, // remote address
085
0, // timeout
086
1) ==-1) //restart if interrupted
087
{
088
ACE_DEBUG((LM_ERROR,"Error in connection\n"));
089
}
090
091
ACE_DEBUG((LM_DEBUG,"Connection established\n"));
092
093
//Register the input event handler for reading
094
ACE_Reactor::instance()->register_handler(eh,ACE_Event_Handler::READ_MASK);
095
096
//Unregister as the acceptor is not expecting new clients
097
098
return -1;
099
}
100
101
//Used by the reactor to determine the underlying handle
102
ACE_HANDLE get_handle(void)
const
103
{
104
return this->peer_acceptor.get_handle();
105
}
106
107
private:
108
Acceptor peer_acceptor;
109
};
110
111
112
int
main(int argc,
char * argv[])
113
{
114
//Create an address on which to receive connections
115
ACE_INET_Addr addr(PORT_NO);
116
117
//Create the Accept Handler which automatically begins to “listen”
118
//for client requests for connections
119
120
My_Accept_Handler *eh=new My_Accept_Handler(addr);
121
122
//Register the reactor to call back when incoming client connects
123
ACE_Reactor::instance()->register_handler(eh,ACE_Event_Handler::ACCEPT_MASK);
124
125
//Start the event loop
126
while(1)
127
{
128
ACE_Reactor::instance()->handle_events();
129
}
130
return 0;
131
}
第一个具体事件处理器My_Accept_Handler用于接受和建立从客户到来的连接。另一个事件处理器是
My_Input_Handler,它用于在连接建立后对连接进行处理。因而,My_Accept_Handler接受连接,并将实
际的处理委托给My_Input_Handler。
我们首先创建了一个ACE_INET_Addr地址对象,将我们希望在其上接受连接的端口作为参数传给它。
其次,实例化一个类型为My_Accept_Handler的对象。随后地址对象通过My_Accept_Handler的构造器传
递给它。My_Accept_Handler有一个用于连接建立的底层“具体接受器”(在讲述“IPC”的一章中有与
具体接受器相关的内容)。My_Accept_Handler的构造器将对新连接的“侦听”委托给该具体接受器的
open()方法。在处理器开始侦听连接后,它在反应堆上登记,通知说在接收到新连接请求时,它需要被
回调。为完成此操作,我们采用ACE_Event_Handler::ACCEPT_MASK掩码调用register_handler()。
当反应堆被告知要登记处理器时,它执行“双重分派”来确定事件处理器的底层句柄。为完成此操
作,它调用get_handler()方法。因为反应堆使用get_handle()方法来确定底层流的句柄,在
My_Accept_Handler中必须实现get_handle()方法。在此例中,我们简单地调用具体接受器的
get_handle(),它会将适当的句柄返回给反应堆。
一旦在该句柄上接收到新的连接请求,反应堆会自动地回调My_Accept_Handler的handle_input()方
法。随后Accept Handler(接受处理器)实例化一个新的Input Handler(输入处理器),并调用具体接
受器的accept()方法来实际地建立连接。注意Input Handler底层的流是作为accept()调用的第一个参数
传入的。这使得新实例化的Input Handler中的流被设置为在连接建立(由accept()完成)后立即创建的
新流。随后Accept Handler将Input Handler登记到反应堆,通知它如果有任何可读的输入就进行回调(
使用ACE_Event_Handler::READ_MASK)。随后接受处理器返回-1,使自己从反应堆的内部事件分派表中
被拆除。
现在,如果有任何输入从客户到达,反应堆将自动回调My_Input_Handler::handle_input()。注意
在My_Input_Handler的handle_input()方法中,返回给反应堆是0。这指示我们希望保持它的登记;反之
在My_Accept_Handler中我们在它的handle_input()中返回-1,以确保它被注销。
除了在上面的例子中使用的READ_MASK和ACCEPT_MASK而外,还有若干其他的掩码,可在登记或是拆
除处理器时使用。这些掩码如表6-2所示,它们可与register_handler()和remove_handler()方法一起使
用。每个掩码保证反应堆回调事件处理器时的不同行为方式,通常这意味着不同的“handle”方法会被
回调。
掩码
回调方法
何时
和……一起使用
ACE_Event_Handler::READ_MASK
handle_input()
在句柄上有数据可读时。
register_handler()
ACE_Event_Handler::WRITE_MASK
handle_output()
在I/O设备输出缓冲区上有可用空间、并且新数据可以发送给它时。
register_handler()
ACE_Event_Handler::TIMER_MASK
handle_close()
传给handle_close()以指示调用它的原因是超时。
接受器和连接器的handle_timeout方法。反应堆不使用此掩码。
ACE_Event_Handler::ACCEPT_MASK
handle_input()
在OS内部的侦听队列上收到了客户的新连接请求时。
register_handler()
ACE_Event_Handler::CONNECT_MASK
handle_input()
在连接已经建立时。
register_handler()
ACE_Event_Handler::DONT_CALL
None.
在反应堆的remove_handler()被调用时保证事件处理器的handle_close()方法不被调用。
remove_handler()
表6-2 反应堆中的掩码
6.4 定时器(Timer)
反应堆还包括了调度定时器的方法,它们在超时的时候回调适当的事件处理器的handle_timeout()
方法。为调度这样的定时器,反应堆拥有一个schedule_timer()方法。该方法接收事件处理器(该事件
处理器的handle_timeout()方法将会被回调)、以及以ACE_Time_value对象形式出现的延迟作为参数。
此外,还可以指定时间间隔,使定时器在它超时后自动被复位。
反应堆在内部维护ACE_Timer_Queue,它以定时器要被调度的顺序对它们进行维护。实际使用的用于
保存定时器的数据结构可以通过反应堆的set_timer_queue()方法进行改变。反应堆有若干不同的定时器
结构可用,包括定时器轮(timer wheel)、定时器堆(timer heap)和哈希式定时器轮(hashed timer
wheel)。这些内容将在后面的部分详细讨论。
6.4.1 ACE_Time_Value
ACE_Time_Value是封装底层OS平台的日期和时间结构的包装类。它基于在大多数UNIX操作系统上都
可用的timeval结构;该结构存储以秒和微秒计算的绝对时间。
其他的OS平台,比如POSIX和Win32,使用略有不同的表示方法。该类封装这些不同,并提供了可移
植的C++接口。
ACE_Time_Value类使用运算符重载,提供简单的算术加、减和比较。该类中的方法会对时间量进行
“规范化”(normalize)。所谓规范化,是将timeval结构中的两个域调整为规范化的编码方式;这种
编码方式可以确保精确的比较
首先通过实现事件处理器Time_Handler的handle_timeout()方法,将其设置用以处理超时。主函数实例
化Time_Handler类型的对象,并使用反应堆的schedule_timer()方法调度多个定时器(10个)。
handle_timeout方法需要以下参数:指向将被回调的处理器的指针、定时器超时时间,以及一个将在
handle_timeout()方法被回调时发送给它的参数。每次调用schedule_timer(),它都返回一个唯一的定
时器标识符,并随即存储在timer_id[]数组里。这个标识符可用于在任何时候取消该定时器。在上面的
例子中也演示了定时器的取消:在所有定时器被初始调度后,程序通过调用反应堆的cancel_timer()方
法(使用相应的timer_id作为参数)取消了第五个定时器。
6.4.3 使用不同的定时器队列
不同的环境可能需要不同的调度和取消定时器的方法。在下面的任一条件为真时,实现定时器的算
法的性能就会成为一个问题:
· 需要细粒度的定时器。
· 在某一时刻未完成的定时器的数目可能会非常大。
· 算法使用过于昂贵的硬件中断来实现。
ACE允许用户从若干在ACE中已存在的定时器中进行选择,或是根据为定时器定义的接口开发他们自
己的定时器。表6-3详细列出了ACE中可用的各种定时器:
定时器
数据结构描述
性能
ACE_Timer_Heap
定时器存储在优先级队列的堆实现中。
schedule_timer()的开销=O(lg n)
cancel_timer()的开销=O(lg n)
查找当前定时器的开销=O(1)
ACE_Timer_List
定时器存储在双向链表中。
schedule_timer()的开销=O(n)
cancel_timer()的开销=O(1)
查找当前定时器的开销=O(1)
ACE_Timer_Hash
在这里使用的这种结构是定时器轮算法的变种。性能高度依赖于所用的哈希函数。
schedule_timer()的开销=最坏=O(n) 最佳=O(1)
cancel_timer()的开销=O(1)
查找当前定时器的开销=O(1)
ACE_Timer_Wheel
定时器存储在“数组指针”(pointers to arrays)的数组中。每个被指向的数组都已排序。
schedule_timer()的开销=最坏=O(n)
cancel_timer()的开销=O(1)
查找当前定时器的开销=O(1)
表6-3 ACE中的定时器
6.5 处理信号(Signal)
如我们在例6-1中所看到的,反应堆含有进行信号处理的方法。处理信号的事件处理器应重载
handle_signal()方法,因为该方法将在信号发生时被回调。要为信号登记处理器,可以使用多个
register_handler()方法中的一个,就如同例6-1中所演示的那样。如果对特定信号不再感兴趣,通过调
用remove_handler(),处理器可以被拆除,并恢复为先前安装的信号处理器。反应堆在内部使用
sigaction()系统调用来设置和恢复信号处理器。通过使用ACE_Sig_Handlers类和与其相关联的方法,无
需反应堆也可以进行信号处理。
使用反应堆进行信号处理和使用ACE_Sig_Handlers类的重要区别是基于反应堆的机制只允许应用给
每个信号关联一个事件处理器,而ACE_Sig_Handlers类允许在信号发生时,回调多个事件处理器。
6.6 使用通知(Notification)
反应堆不仅可以在系统事件发生时发出回调,也可以在用户定义的事件发生时回调处理器。这是通
过反应堆的“通知”接口来完成的;该接口由两个方法组成:notify()和max_notify_iterations()。
通过使用notify()方法,可以明确地指示反应堆对特定的事件处理器对象发出回调。在反应堆与消
息队列、或是协作任务协同使用时,这是十分有用的。可在ASX构架组件与反应堆一起使用时找到这种用
法的一些好例子。
max_notify_iterations()方法通知反应堆,每次只完成指定次数的“迭代”(iterations)。也就是说
,在一次handle_events()调用中只处理指定数目的“通知”。因而如果使用max_notify_iterations()
将迭代的次数设置为20,而又有25个通知同时到达,handle_events()方法一次将只处理这些通知中的20
个。剩下的五个通知将在handle_events()下一次在事件循环中被调用时再处理。
事件处理循环中值得注意的一个主要区别是,程序传递给handle_events()一个ACE_Time_Value。如
果在此时间内没有事件发生,handle_events()方法就会结束。在handle_events()结束后,
perform_notification()被调用,它使用反应堆的notify()方法来请求反应堆通知处理器(它是在事件
发生时被作为参数传入的)。随后反应堆就使用所收到的掩码来执行对处理器的适当“handle”方法的
调用。在此例中,通过传递ACE_Event_Handler::READ_MASK,我们使用notify()来通知我们的事件处理
器有输入,从而使得反应堆回调该处理器的handle_input()方法。因为我们已将max_notify_iterations
设为5,所以在一次handle_events()调用过程中反应堆实际上只会发出5个通知。
第7章 接受器(Acceptor)和连接器(Connector):连接建立模式
接受器/连接器模式设计用于降低连接建立与连接建立后所执行的服务之间的耦合。因为该模式降低
了服务和连接建立方法之间的耦合,非常容易改动其中一个,而不影响另外一个,从而也就可以复用以
前编写的连接建立机制和服务例程的代码。
7.1 接受器模式
在ACE中,接收器模式借助名为ACE_Acceptor的“工厂”(Factory)实现。工厂(通常)是用于对
助手对象的实例化过程进行抽象的类。在面向对象设计中,复杂的类常常会将特定功能委托给助手类。
复杂的类对助手的创建和委托必须很灵活。这种灵活性是在工厂的帮助下获得的。工厂允许一个对象通
过改变它所委托的对象来改变它的底层策略,而工厂提供给应用的接口却是一样的,这样,可能根本就
无需对客户代码进行改动(有关工厂的更多信息,请阅读“设计模式”中的参考文献)。
ACE_Acceptor工厂允许应用开发者改变“助手”对象,以用于:
· 被动连接建立
· 连接建立后的处理
同样地,ACE_Connector工厂允许应用开发者改变“助手”对象,以用于:
· 主动连接建立
· 连接建立后的处理
ACE_Acceptor被实现为模板容器,通过两个类作为实参来进行实例化。第一个参数实现特定的服务
(类型为ACE_Event_Handler。因为它被用于处理I/O事件,所以必须来自事件处理类层次),应用在建
立连接后执行该服务;第二个参数是“具体的”接受器(可以是在IPC_SAP一章中讨论的各种变种)。
特别要注意的是ACE_Acceptor工厂和底层所用的具体接受器是非常不同的。具体接受器可完全独立
于ACE_Acceptor工厂使用,而无需涉及我们在这里讨论的接受器模式(独立使用接受器已在IPC_SAP一章
中讨论和演示)。ACE_Acceptor工厂内在于接受器模式,并且不能在没有底层具体接受器的情况下使用
。ACE_Acceptor使用底层的具体接受器来建立连接。如我们已看到的,有若干ACE的类可被用作
ACE_Acceptor工厂模板的第二个参数(也就是,具体接受器类)。但是服务处理类必须由应用开发者来
实现,而且其类型必须是ACE_Event_Handler。ACE_Acceptor工厂可以这样来实例化:
typedef ACE_Acceptor<My_Service_Handler,ACE_SOCK_ACCEPTOR> MyAcceptor;
这里,名为My_Service_Handler的事件处理器和具体接受器ACE_SOCK_ACCEPTOR被传给MyAcceptor。
ACE_SOCK_ACCEPTOR是基于BSD socket流家族的TCP接受器(各种可传给接受器工厂的不同类型的接受器
,见表7-1和IPC一章)。请再次注意,在使用接受器模式时,我们总是处理两个接受器:名为
ACE_Acceptor的工厂接受器,和ACE中的某种具体接受器,比如ACE_SOCK_ACCEPTOR(你可以创建自定义
的具体接受器来取代ACE_SOCK_ACCEPTOR,但你将很可能无需改变ACE_Acceptor工厂类中的任何东西)。
重要提示:ACE_SOCK_ACCEPTOR实际上是一个宏,其定义为:
#define ACE_SOCK_ACCEPTOR ACE_SOCK_Acceptor, ACE_INET_Addr
我们认为这个宏的使用是必要的,因为在类中的typedefs在某些平台上无法工作。如果不是这样的
话,ACE_Acceptor就可以这样来实例化:
typedef ACE_Acceptor<My_Service_Handler,ACE_SOCK_Acceptor>MyAcceptor;
7.1.1 组件
如上面的讨论所说明的,在接受器模式中有三个主要的参与类:
· 具体接受器:它含有建立连接的特定策略,连接与底层的传输协议机制系在一起。下面是在ACE
中的各种具体接受器的例子:ACE_SOCK_ACCEPTOR(使用TCP来建立连接)、ACE_LSOCK_ACCEPTOR(使用
UNIX域socket来建立连接),等等。
· 具体服务处理器:由应用开发者编写,它的open()方法在连接建立后被自动回调。接受器构架假
定服务处理类的类型是 ACE_Event_Handler,这是ACE定义的接口类(该类已在反应堆一章中详
细讨论过)。另一个特别为接受器和连接器模式的服务处理而创建的类是ACE_Svc_Handler。该类不仅基
于ACE_Event_Handler接口(这是使用反应堆所必需的),同时还基于在ASX流构架中使用的ACE_Task类
。ACE_Task类提供的功能有:创建分离的线程、使用消息队列来存储到来的数据消息、并发地处理它们
,以及其他一些有用的功能。如果与接受器模式一起使用的具体服务处理器派生自ACE_Svc_Handler、而
不是ACE_Event_Handler,它就可以获得这些额外的功能。对ACE_Svc_Handler中的额外功能的使用,在
这一章的高级课程里详细讨论。在下面的讨论中,我们将使用ACE_Svc_Handler作为我们的事件处理器。
在简单的ACE_Event_Handler和ACE_Svc_Handler类之间的重要区别是,后者拥有一个底层通信流组件。
这个流在ACE_Svc_Handler模板被实例化的时候设置。而在使用ACE_Event_Handler的情况下,我们必须
自己增加I/O通信端点(也就是,流对象),作为事件处理器的私有数据成员。因而,在这样的情况下,
应用开发者应该将他的服务处理器创建为ACE_Svc_Handler类的子类,并首先实现将被构架自动回调的
open()方法。此外,因为ACE_Svc_Handler是一个模板,通信流组件和锁定机制是作为模板参数被传入的
。
· 反应堆:与ACE_Acceptor协同使用。如我们将看到的,在实例化接受器后,我们启动反应堆的事
件处理循环。反应堆,如先前所解释的,是一个事件分派类;而在此情况下,它被接受器用于将连接建
立事件分派到适当的服务处理例程。
接受器类型
所用地址
所用流
具体接受器
TCP流接受器
ACE_INET_Addr
ACE_SOCK_STREAM
ACE_SOCK_ACCEPTOR
UNIX域本地流socket接受器
ACE_UNIX_Addr
ACE_LSOCK_STREAM
ACE_LSOCK_ACCEPTOR
管道作为底层通信机制
ACE_SPIPE_Addr
ACE_SPIPE_STREAM
ACE_SPIPE_ACCEPTOR
表7-1 ACE中的连接建立机制
7.2 连接器
连接器与接受器非常类似。它也是一个工厂,但却是用于主动地连接远程主机。在连接建立后,它
将自动回调适当的服务处理对象的open()方法。连接器通常被用在你本来会使用BSD connect()调用的地
方。在ACE中,连接器,就如同接受器,被实现为名为ACE_Connector的模板容器类。如先前所提到的,
它需要两个参数,第一个是事件处理器类,它在连接建立时被调用;第二个是“具体的”连接器类。
你必须注意,底层的具体连接器和ACE_Connector工厂是非常不一样的。ACE_Connector工厂使用底
层的具体连接器来建立连接。随后ACE_Connector工厂使用适当的事件或服务处理例程(通过模板参数传
入)来在具体的连接器建立起连接之后处理新连接。如我们在IPC一章中看到的,没有ACE_Connector工
厂,也可以使用这个具体的连接器。但是,没有具体的连接器类,就会无法使用ACE_Connector工厂(因
为要由具体的连接器类来实际处理连接建立)。
下面是对ACE_Connector类进行实例化的一个例子:
typedef ACE_Connector<My_Svc_Handler,ACE_SOCK_CONNECTOR> MyConnector;
这个例子中的第二个参数是具体连接器类ACE_SOCK_CONNECTOR。连接器和接受器模式一样,在内部使用
反应堆来在连接建立后回调服务处理器的open()方法。我们可以复用我们为前面的接受器例子所写的服
务处理例程。
7.3 高级课程
下面的部分更为详细地解释接受器和连接器模式实际上是如何工作的。如果你想要调谐服务处理和
连接建立策略(其中包括调谐底层具体连接器将要使用的服务处理例程的创建和并发策略,以及连接建
立策略),对该模式的进一步了解就是必要的。此外,还有一部分内容解释怎样使用通过
ACE_Svc_Handler类自动获得的高级特性。最后,我们说明怎样与接受器和连接器模式一起使用简单的轻
量级ACE_Event_Handler。
7.3.1 ACE_SVC_HANDLER类
如上面所提到的,ACE_Svc_Handler类基于ACE_Task(它是ASX流构架的一部分)和
ACE_Event_Handler接口类。因而ACE_Svc_Handler既是任务,又是事件处理器。这里我们将简要介绍
ACE_Task和ACE_Svc_Handler的功能。
7.3.1.1 ACE_Task
ACE_Task被设计为与ASX流构架一起使用;ASX基于UNIX系统V中的流机制。在设计上ASX与Larry
Peterson构建的X-kernel协议工具非常类似。
ASX的基本概念是:到来的消息会被分配给由若干模块(module)组成的流。每个模块在到来的消息
上执行某种固定操作,然后把它传递给下一个模块作进一步处理,直到它到达流的末端为止。模块中的
实际处理由任务来完成。每个模块通常有两个任务,一个用于处理到来的消息,一个用于处理外出的消
息。在构造协议栈时,这种结构是非常有用的。因为每个模块都有固定的简单接口,所创建的模块可以
很容易地在不同的应用间复用。例如,设想一个应用,它处理来自数据链路层的消息。程序员会构造若
干模块,每个模块分别处理不同层次的协议。因而,他会构造一个单独的模块,进行网络层处理;另一
个进行传输层处理;还有一个进行表示层处理。在构造这些模块之后,它们可以(在ASX的帮助下)被“
串”成一个流来使用。如果后来创建了一个新的(也许是更好的)传输模块,就可以在不对程序产生任
何影响的情况下、在流中替换先前的传输模块。注意模块就像是容纳任务的容器。这些任务是实际的处
理元件。一个模块可能需要两个任务,如同在上面的例子中;也可能只需要一个任务。如你可能会猜到
的,ACE_Task是模块中被称为任务的处理元件的实现。
7.3.1.2任务通信的体系结构
每个ACE_Task都有一个内部的消息队列,用以与其他任务、模块或是外部世界通信。如果一个
ACE_Task想要发送一条消息给另一个任务,它就将此消息放入目的任务的消息队列中。一旦目的任务收
到此消息,它就会立即对它进行处理。
所有ACE_Task都可以作为0个或多个线程来运行。消息可以由多个线程放入ACE_Task的消息队列,或是从
中取出,程序员无需担心破坏任何数据结构。因而任务可被用作由多个协作线程组成的系统的基础构建
组件。各个线程控制都可封装在ACE_Task中,与其他任务通过发送消息到它们的消息队列来进行交互。
这种体系结构的唯一问题是,任务只能通过消息队列与在同一进程内的其他任务相互通信。
ACE_Svc_Handler解决了这一问题,它同时继承自ACE_Task和ACE_Event_Handler,并且增加了一个私有
数据流。这种结合使得ACE_Svc_Handler对象能够用作这样的任务(并发,同一进程);它能够处理事件
(异步,不同进程)、并与远地主机的任务间发送和接收数据。
ACE_Task被实现为模板容器,它通过锁定机制来进行实例化。该锁用于保证内部的消息队列在多线程环
境中的完整性。如先前所提到的,ACE_Svc_Handler模板容器不仅需要锁定机制,还需要用于与远地任务
通信的底层数据流来作为参数。
7.3.1.3 创建ACE_Svc_Handler
ACE_Svc_Handler模板通过锁定机制和底层流来实例化,以创建所需的服务处理器。如果应用只是单
线程的,就不需要使用锁,可以用ACE_NULL_SYNCH来将其实例化。但是,如果我们想要在多线程应用中
使用这个模板,可以这样来进行实例化:
class MySvcHandler:
public ACE_Svc_Handler<ACE_SOCK_STREAM,ACE_MT_SYNCH>
{
}
7.3.1.4 在服务处理器中创建多个线程
在上面的例7-5中,我们使用ACE_Thread包装类和它的静态方法spawn(),创建了单独的线程来发送
数据给远地对端。但是,在我们完成此工作时,我们必须定义使用C++ static修饰符的文件范围内的静
态send_data()方法。结果当然就是,我们无法访问我们实例化的实际对象的任何数据成员。换句话说,
我们被迫使send_data()成员函数成为class-wide的函数,而这并不是我们所想要的。这样做的唯一原因
是,ACE_Thread::spawn()只能使用静态成员函数来作为它所创建的线程的入口。另一个有害的副作用是
到对端流的引用也必须成为静态的。简而言之,这不是编写这些代码的最好方式。
ACE_Task提供了更好的机制来避免发生这样的问题。每个ACE_Task都有activate()方法,可用于为
ACE_Task创建线程。所创建的线程的入口是非静态成员函数svc()。因为svc()是非静态函数,它可以调
用任何对象实例专有的数据或成员函数。ACE对程序员隐藏了该机制的所有实现细节。activate()方法有
着非常多的用途,它允许程序员创建多个线程,所有这些线程都使用svc()方法作为它们的入口。还可以
设置线程优先级、句柄、名字,等等。activate()方法的原型是:
// = Active object activation method.
virtual int activate (long flags = THR_NEW_LWP,
int n_threads = 1,
int force_active = 0,
long priority = ACE_DEFAULT_THREAD_PRIORITY,
int grp_id = -1,
ACE_Task_Base *task = 0,
ACE_hthread_t thread_handles[] = 0,
void *stack[] = 0,
size_t stack_size[] = 0,
ACE_thread_t thread_names[] = 0);
第一个参数flags描述将要创建的线程所希望具有的属性。在线程一章里有详细描述。可用的标志有
:
THR_CANCEL_DISABLE, THR_CANCEL_ENABLE, THR_CANCEL_DEFERRED,
THR_CANCEL_ASYNCHRONOUS, THR_BOUND, THR_NEW_LWP, THR_DETACHED,
THR_SUSPENDED, THR_DAEMON, THR_JOINABLE, THR_SCHED_FIFO,
THR_SCHED_RR, THR_SCHED_DEFAULT
第二个参数n_threads指定要创建的线程的数目。第三个参数force_active用于指定是否应该创建新
线程,即使activate()方法已在先前被调用过、因而任务或服务处理器已经在运行多个线程。如果此参
数被设为false(0),且如果activate()是再次被调用,该方法就会设置失败代码,而不会生成更多的线
程。
第四个参数用于设置运行线程的优先级。缺省情况下,或优先级被设为
ACE_DEFAULT_THREAD_PRIORITY,方法会使用给定的调度策略(在flags中指定,例如,
THR_SCHED_DEFAULT)的“适当”优先级。这个值是动态计算的,并且是在给定策略的最低和最高优先级
之间。如果显式地给定一个值,这个值就会被使用。注意实际的优先级值极大地依赖于实现,最好不要
直接使用。在线程一章中,可读到更多有关线程优先级的内容。
还可以传入将要创建的线程的线程句柄、线程名和栈空间,以在线程创建过程中使用。如果它们被
设置为NULL,它们就不会被使用。但是如果要使用activate创建多个线程,就必须传入线程的名字或句
柄,才能有效地对它们进行使用。
7.3.1.5使用服务处理器中的消息队列机制
如前面所提到的,ACE_Svc_Handler类拥有内建的消息队列。这个消息队列被用作在
ACE_Svc_Handler和外部世界之间的主要通信接口。其他任务想要发给该服务处理器的消息被放入它的消
息队列中。这些消息会在单独的线程里(通过调用activate()方法创建)处理。随后另一个线程就可以
把处理过的消息通过网络发送给另外的远地目的地(很可能是另外的ACE_Svc_Handler)。
如先前所提到的,在这种多线程情况下,ACE_Svc_Handler会自动地使用锁来确保消息队列的完整性
。所用的锁即通过实例化ACE_Svc_Handler模板类创建具体服务处理器时所传递的锁。之所用通过这样的
方式来传递锁,是因为这样程序员就可以对他的应用进行“调谐”。不同平台上的不同锁定机制有着不
同程度的开销。如果需要,程序员可以创建他自己的优化的、遵从ACE的锁接口定义的锁,并将其用于服
务处理器。这是程序员通过使用ACE可获得的灵活性的又一范例。重要的是程序员必须意识到,在此服务
处理例程中的额外线程将带来显著的锁定开销。为将此开销降至最低,程序员必须仔细地设计他的程序
,确保使这样的开销最小化。特别地,上面描述的例子有可能导致过度的开销,在大多数情况下可能并
不实用。
ACE_Task,进而是ACE_Svc_Handler(因为服务处理器也是一种任务),具有若干可用于对底层队列
进行设置、操作、入队和出队操作的方法。这里我们将只讨论这些方法中的一部分。因为在服务处理器
中(通过使用msg_queue()方法)可以获取指向消息队列自身的指针,所以也可以直接调用底层队列(也
就是,ACE_Message_Queue)的所有公共方法。(有关消息队列提供的所有方法的更多细节,请参见后面
的“消息队列”一章。)
如上面所提到的,服务处理器的底层消息队列是ACE_Message_Queue的实例,它是由服务处理器自动
创建的。在大多数情况下,没有必要调用ACE_Message_Queue的底层方法,因为在ACE_Svc_Handler类中
已对它们的大多数进行了包装。ACE_Message_Queue是用于使ACE_Message_Block进队或出队的队列。每
个ACE_Message_Block都含有指向“引用计数”(reference-counted)的ACE_Data_Block的指针,
ACE_Data_Block依次又指向存储在块中的实际数据(见“消息队列”一章)。这使得ACE_Message_Block
可以很容易地进行数据共享。
ACE_Message_Block的主要作用是进行高效数据操作,而不带来许多拷贝开销。每个消息块都有一个
读指针和写指针。无论何时我们从块中读取时,读指针会在数据块中向前增长。类似地,当我们向块中
写的时候,写指针也会向前移动,这很像在流类型系统中的情况。可以通过ACE_Message_Block的构造器
向它传递分配器,以用于分配内存(有关Allocator的更多信息,参见“内存管理”一章)。例如,可以
使用ACE_Cached_Allocation_Strategy,它预先分配内存并从内存池中返回指针,而不是在需要的时候
才从堆中分配内存。这样的功能在需要可预测的性能时十分有用,比如在实时系统中。
7.4 接受器和连接器模式工作原理
接受器和连接器工厂(也就是ACE_Connector和ACE_Acceptor)有着非常类似的运行结构。它们的工
作可大致划分为三个阶段:
· 端点或连接初始化阶段
· 服务初始化阶段
· 服务处理阶段
7.4.1 端点或连接初始化阶段
在使用接受器的情况下,应用级程序员可以调用ACE_Acceptor工厂的open()方法,或是它的缺省构
造器(它实际上会调用open()方法),来开始被动侦听连接。当接受器工厂的open()方法被调用时,如
果反应堆单体还没有被实例化,open()方法就首先对其进行实例化。随后它调用底层具体接受器的open
()方法。于是具体接受器会完成必要的初始化来侦听连接。例如,在使用ACE_SOCK_Acceptor的情况中,
它打开socket,将其绑定到用户想要在其上侦听新连接的端口和地址上。在绑定端口后,它将会发出侦
听调用。open方法随后将接受器工厂登记到反应堆。因而在接收到任何到来的连接请求时,反应堆会自
动回调接受器工厂的handle_input()方法。注意正是因为这一原因,接受器工厂才从ACE_Event_Handler
类层次派生;这样它才可以响应ACCEPT事件,并被反应堆自动回调。
在使用连接器的情况中,应用程序员调用连接器工厂的connect()方法或connect_n()方法来发起到
对端的连接。除了其他一些选项,这两个方法的参数包括我们想要连接到的远地地址,以及我们是想要
同步还是异步地完成连接。我们可以同步或异步地发起NUMBER_CONN个连接:
//Synchronous
OurConnector.connect_n(NUMBER_CONN,ArrayofMySvcHandlers,Remote_Addr,0,
ACE_Synch_Options::synch);
//Asynchronous
OurConnector.connect_n(NUMBER_CONN,ArrayofMySvcHandlers,Remote_Addr,0,
ACE_Synch_Options::asynch);
如果连接请求是异步的,ACE_Connector会在反应堆上登记自己,等待连接被建立(ACE_Connector也
派生自ACE_Event_Handler类层次)。一旦连接被建立,反应堆将随即自动回调连接器。但如果连接请求
是同步的,connect()调用将会阻塞,直到连接被建立、或是超时到期为止。超时值可通过改变特定的
ACE_Synch_Options来指定。详情请参见参考手册。
7.4.2 接受器的服务初始化阶段
在有连接请求在指定的地址和端口上到来时,反应堆自动回调ACE_Acceptor工厂的handle_input()
方法。
该方法是一个“模板方法”(Template Method)。模板方法用于定义一个算法的若干步骤的顺序,
并允许改变特定步骤的执行。这种变动是通过允许子类定义这些方法的实现来完成的。(有关模板方法
的更多信息见“设计模式”参考指南)。
在我们的这个案例中,模板方法将算法定义如下:
· make_svc_handler():创建服务处理器。
· accept_svc_handler():将连接接受进前一步骤创建的服务处理器。
· activate_svc_handler():启动这个新服务处理器。
这些方法都可以被重新编写,从而灵活地决定这些操作怎样来实际执行。
这样,handle_input()将首先调用make_svc_handler()方法,创建适当类型的服务处理器(如我们
在上面的例子中所看到的那样,服务处理器的类型由应用程序员在ACE_Acceptor模板被实例化时传入)
。在缺省情况下,make_svc_handler()方法只是实例化恰当的服务处理器。但是,make_svc_handler()
是一个“桥接”(bridge)方法,可被重载以提供更多复杂功能。(桥接是一种设计模式,它使类层次
的接口与实现去耦合。参阅“设计模式”参考文献)。例如,服务处理器可创建为进程级或线程级的单
体,或者从库中动态链接,从磁盘中加载,甚或通过更复杂的方式创建,如从数据库中查找并获取服务
处理器,并将它装入内存。
在服务处理器被创建后,handle_input()方法调用accept_svc_handler()。该方法将连接“接受进
”服务处理器;缺省方式是调用底层具体接受器的accept()方法。在ACE_SOCK_Acceptor被用作具体接受
器的情况下,它调用BSD accept()例程来建立连接(“接受”连接)。在连接建立后,连接句柄在服务
处理器中被自动设置(接受“进”服务处理器);这个服务处理器是先前通过调用make_svc_handler()
创建的。该方法也可被重载,以提供更复杂的功能。例如,不是实际创建新连接,而是“回收利用”旧
连接。在我们演示各种不同的接受和连接策略时,将更为详尽地讨论这一点。
7.4.3 连接器的服务初始化阶段
应用发出的connect()方法与接受器工厂中的handle_input()相类似,也就是,它是一个“模板方法
”。
在我们的这个案例中,模板方法connect()定义下面一些可被重定义的步骤:
· make_svc_handler():创建服务处理器。
· connect_svc_handler():将连接接受进前一步骤创建的服务处理器。
· activate_svc_handler():启动这个新服务处理器。
每一方法都可以被重新编写,从而灵活地决定这些操作怎样来实际执行。
这样,在应用发出connect()调用后,连接器工厂通过调用make_svc_handler()来实例化恰当的服务
处理器,一如在接受器的案例中所做的那样。其缺省行为只是实例化适当的类,并且也可以通过与接受
器完全相同的方式重载。进行这样的重载的原因可以与上面提到的原因非常类似。
在服务处理器被创建后,connect()调用确定连接是要成为异步的还是同步的。如果是异步的,在继
续下一步骤之前,它将自己登记到反应堆,随后调用connect_svc_handler()方法。该方法的缺省行为是
调用底层具体连接器的connect()方法。在使用ACE_SOCK_Connector的情况下,这意味着将适当的选项设
置为阻塞或非阻塞式I/O,然后发出BSD connect()调用。如果连接被指定为同步的,connect()调用将会
阻塞、直到连接完全建立。在这种情况下,在连接建立后,它将在服务处理器中设置句柄,以与它现在
连接到的对端通信(该句柄即是通过在服务处理器中调用peer()方法获得的在流中存储的句柄,见上面
的例子)。在服务处理器中设置句柄后,连接器模式将进行到最后阶段:服务处理。
如果连接被指定为异步的,在向底层的具体连接器发出非阻塞式connect()调用后,对
connect_svc_handler()的调用将立即返回。在使用ACE_SOCK_Connector的情况中,这意味着发出非阻塞
式BSD connect()调用。在连接稍后被实际建立时,反应堆将回调ACE_Connector工厂的handle_output()
方法,该方法在通过make_svc_handler()方法创建的服务处理器中设置新句柄。然后工厂将进行到下一
阶段:服务处理。
与accept_svc_handler()情况一样,connect_svc_handler()是一个“桥接”方法,可进行重载以提
供变化的功能。
7.4.4 服务处理(是不是有问题???)
一旦服务处理器被创建、连接被建立,以及句柄在服务处理器中被设置,ACE_Acceptor的
handle_input()方法(或者在使用ACE_Connector的情况下,是handle_output()或
connect_svc_handler())将调用activate_svc_handler()方法。该方法将随即启用服务处理器。其缺省
行为是调用作为服务处理器的入口的open()方法。如我们在上面的例子中所看到的,在服务处理器开始
执行时,open()方法是第一个被调用的方法。是在open()方法中,我们调用activate()方法来创建多个
线程控制;并在反应堆上登记服务处理器,这样当新的数据在连接上到达时,它会被自动回调。该方法
也是一个“桥接”方法,可被重载以提供更为复杂的功能。特别地,这个重载的方法可以提供更为复杂
的并发策略,比如,在另一不同的进程中运行服务处理器。
7.5 调谐接受器和连接器策略
如上面所提到的,因为使用了可以重载的桥接方法,很容易对接受器和连接器进行调谐。桥接方法
允许调谐:
· 服务处理器的创建策略:通过重载接受器或连接器的make_svc_handler()方法来实现。例如,这可以
意味着复用已有的服务处理器,或使用某种复杂的方法来获取服务处理器,如上面所讨论的那样。
· 连接策略:连接创建策略可通过重载connect_svc_handler()或accept_svc_handler()方法来改变。
· 服务处理器的并发策略:服务处理器的并发策略可通过重载activate_svc_handler()方法来改变。例
如,服务处理器可以在另外的进程中创建。
如上所示,调谐是通过重载ACE_Acceptor或ACE_Connector类的桥接方法来完成的。ACE的设计使得
程序员很容易完成这样的重载和调谐。
7.5.1 ACE_Strategy_Connector和ACE_Strategy_Acceptor类
为了方便上面所提到的对接受器和连接器模式的调谐方法,ACE提供了两种特殊的“可调谐”接受器
和连接器工厂,那就是ACE_Strategy_Acceptor和ACE_Strategy_Connector。它们和ACE_Acceptor与
ACE_Connector非常类似,同时还使用了“策略”模式。
策略模式被用于使算法行为与类的接口去耦合。其基本概念是允许一个类(称为Context Class,上
下文类)的底层算法独立于使用该类的客户进行变动。这是通过具体策略类的帮助来完成的。具体策略
类封装执行操作的算法或方法。这些具体策略类随后被上下文类用于执行各种操作(上下文类将“工作
”委托给具体策略类)。因为上下文类不直接执行任何操作,当需要改变功能时,无需对它进行修改。
对上下文类所做的唯一修改是使用另一个具体策略类来执行改变了的操作。(要阅读有关策略模式的更
多信息,参见“设计模式”的附录)。
在ACE中,ACE_Strategy_Connector和ACE_Strategy_Acceptor使用若干具体策略类来改变算法,以
创建服务处理器,建立连接,以及为服务处理器设置并发方法。如你可能已经猜到的一样,
ACE_Strategy_Connector和ACE_Strategy_Acceptor利用了上面提到的桥接方法所提供的可调谐性。
7.5.1.1 使用策略接受器和连接器
在ACE中已有若干具体的策略类可用于“调谐”策略接受器和连接器。当类被实例化时,它们作为参
数被传入策略接受器或连接器。表7-2显示了可用于调谐策略接受器和连接器类的一些类。
需要修改
具体策略类
描述
创建策略
(重定义make_svc_handler())
ACE_NOOP_Creation_Strategy
这个具体策略并不实例化服务处理器,而只是一个空操作。
ACE_Singleton_Strategy
保证服务处理器被创建为单体。也就是,所有连接将有效地使用同一个服务处理例程。
ACE_DLL_Strategy
通过从动态链接库中动态链接服务处理器来对它进行创建。
连接策略
(重定义connect_svc_handler())
ACE_Cached_Connect_Strategy
检查是否有已经连接到特定的远地地址的服务处理器没有在被使用。如果有这样一个服务处理器,就对
它进行复用。
并发策略
(重定义activate_svc_handler())
ACE_NOOP_Concurrency_Strategy
一个“无为”(do-nothing)的并发策略。它甚至不调用服务处理器的open()方法。
ACE_Process_Strategy
在另外的进程中创建服务处理器,并调用它的open()方法。
ACE_Reactive_Strategy
先在反应堆上登记服务处理器,然后调用它的open()方法。
ACE_Thread_Strategy
先调用服务处理器的open()方法,然后调用它的activate()方法,以让另外的线程来启动服务处理器的
svc()方法。
表7-2 用于调谐策略接受器和连接器类的类
7.5.1.2 使用ACE_Cached_Connect_Strategy进行连接缓存
在许多应用中,客户会连接到服务器,然后重新连接到同一服务器若干次;每次都要建立连接,执
行某些工作,然后挂断连接(比如像在Web客户中所做的那样)。不用说,这样做是非常低效而昂贵的,
因为连接建立和挂断是非常昂贵的操作。在这样的情况下,连接者可以采用一种更好的策略:“记住”
老连接并保持它,直到确定客户不会再重新建立连接为止。ACE_Cached_Connect_Strategy就提供了这样
一种缓存策略。这个策略对象被ACE_Strategy_Connector用于提供基于缓存的连接建立。如果一个连接
已经存在,ACE_Strategy_Connector将会复用它,而不是创建新的连接。
当客户试图重新建立连接到先前已经连接的服务器时,ACE_Cached_Connect_Strategy确保对老的连
接和服务处理器进行复用,而不是创建新的连接和服务处理器。因而,实际上,
ACE_Cached_Connect_Strategy不仅管理连接建立策略,它还管理服务处理器创建策略。因为在此例中,
用户不想创建新的服务处理器,我们将ACE_Null_Creation_Strategy传递给ACE_Strategy_Connector。
如果连接先前没有建立过,ACE_Cached_Connect_Strategy将自动使用内部的创建策略来实例化适当的服
务处理器,它是在这个模板类被实例化时传入的。这个策略可被设置为用户想要使用的任何一种策略。
除此而外,也可以将ACE_Cached_Connect_Strategy自己在其构造器中使用的创建、并发和recycling策
略传给它。
第8章 服务配置器(Service Configurator):用于服务动态配置的模式
如果服务可以被动态地启动、移除、挂起和恢复,那将会方便得多。这样,服务开发者就不必再担
心配置的服务。他所需关心的是服务如何完成工作。管理员就可以在应用中增加或替换新服务,而不用
重新编译或关闭服务进程。
服务配置器模式可以完成所有这些任务。它使服务的实现与配置去耦合。无需关闭服务器,就可以
在应用中增加新服务和移除旧服务。在大多数情况下,提供服务的服务器都被实现为看守(daemon)进
程。
8.1 构架组件
ACE中的服务配置器由以下组件组成:
· 名为ACE_Service_Object的抽象类。应用开发者必须从它派生出子类,以创建他自己的应用特有的具
体服务对象(Service Object)。
· 应用特有的具体服务对象。
· 服务仓库ACE_Service_Repository。它记录服务器所运行的和所知道的服务。
· ACE_Service_Config。它是整个服务配置器框架的应用开发接口。
· 服务配置文件。该文件含有所有服务对象的配置信息。其缺省的名字是svc.conf。当你的应用对
ACE_Service_Config发出open()调用时,服务配置器框架会读取并处理你写在此文件中的所有配置信息
,随后相应地配置应用。
ACE_Service_Object包括了一些由框架调用的方法,用于服务要启动(init())、停止(fini())
、挂起(suspend())或是恢复(resume())时。ACE_Service_Object派生自ACE_Shared_Object和
ACE_Event_Handler。ACE_Shared_Object在应用想要使用操作系统的动态链接机制来进行加载时被用作
抽象基类。ACE_Event_Handler已在对反应堆的讨论中进行了介绍。当开发者想要他的类响应来自反应堆
的事件时,他就从ACE_Event_Handler派生他的子类。
为什么服务对象要从ACE_Event_Handler继承?用户发起重配置的一种方法是生成一个信号;当这样
的信号事件发生时,反应堆被用于处理信号,并向ACE_Service_Config发出重配置请求。除此而外,软
件的重配置也可能在某事件产生后发生。因而所有的服务对象都被构造为能对事件进行处理。
服务配置文件有它自己的简单脚本,用于描述你想要服务怎样启动和运行。你可以定义你是想要增
加新服务,还是挂起、恢复或移除应用中现有的服务。另外还可以给服务发送参数。服务配置器还允许
进行基于ACE的流(stream)的重配置。我们将在讨论了ACE流构架之后再来更多地讨论这一点。
8.2 定义配置文件
服务配置文件指定在应用中哪些服务要被加载和启动。此外,你可以指定哪些服务要被停止、挂起
或恢复。还可以发送参数给你的服务对象的init()方法。
8.2.1 启动服务
服务可以被静态或动态地启动。如果服务要动态启动,服务配置器实际上会从共享对象库(也就是
,动态链接库)中加载服务对象。为此,服务配置器需要知道哪个库含有此对象,并且还需要知道对象
在该库中的名字。因而,在你的代码文件中你必须通过你需要记住的名字来实例化服务对象。于是动态
服务会这样被配置:
dynamic service_name type_of_service * location_of_shared_lib:name_of_object “parameters”
而静态服务这样被初始化:
static service_name “parameters_send_to_service_object”
8.2.2 挂起或恢复服务
如刚才所提到的,你在启动服务时分配给它一个名字。这个名字随后被用于挂起或恢复该服务。于
是要挂起服务,你所要做的就是在svc.conf文件中指定:
suspend service_name
这使得服务对象中的suspend()方法被调用。随后你的服务对象就应该挂起它自己(基于特定服务不
同的“挂起”含义)。
如果你想要恢复这个服务,你所要做的就是在svc.conf文件中指定:
resume service_name
这使得服务对象中的resume()方法被调用。随后你的服务对象就应该恢复它自己(基于特定服务不
同的“恢复”含义。)
8.2.3 停止服务
停止并移除服务(如果服务是动态加载的)同样是很简单的操作,可以通过在你的配置文件中指定
以下指令来完成:
remove service_name
这使得服务配置器调用你的应用的fini()方法。该方法应该使此服务停止。服务配置器自己会负责
将动态对象从服务器的地址空间里解除链接。
8.3 编写服务
为服务配置器编写你自己的服务相对比较简单。你可以让这个服务做任何你想做的事情。唯一的约
束是它应该是ACE_Service_Object的子类。所以它必须实现init()和fini()方法。在
ACE_Service_Config被打开(open())时,它读取配置文件(也就是svc.conf)并根据这个文件来对服
务进行初始化。一旦服务被加载,它会调用该服务对象的init()方法。类似地,如果配置文件要求移除
服务,fini()方法就会被调用。这些方法负责分配和销毁服务所需的任何资源,比如内存、连接、线程
等等。在svc.conf文件中指定的参数通过服务对象的init()方法来传入。
下面的例子演示一个派生自ACE_Task_Base的服务。ACE_Task_Base类含有activate()方法,用于在
对象里创建线程。(在“任务和主动对象”一章中讨论过的ACE_Task派生自ACE_Task_Base,并包括了用
于通信目的的消息队列。因为我们不需要我们的服务与其它任务通信,我们仅仅使用ACE_Task_Base来帮
助我们完成工作。)更多详细信息,请阅读“任务和主动对象”一章。该服务是一个“无为”(do-
nothing)的服务,一旦启动,它只是周期性地广播当天的时间。
相应的实现如下所述:在时间服务接收到init()调用时,它在任务中启用(activate())一个线程
。这将会创建一个新线程,其入口为svc()方法。在svc()方法中,该线程将会进行循环,直到它看到
canceled_标志被设置为止。此标志在服务配置构架调用fini()时设置。但是,在fini()方法返回底层的
服务配置框架之前,它必须确定在底层的线程已经终止。因为服务配置器将要实际地卸载含有
TimeService的共享库,从而将TimeService对象从应用进程中删除。如果在此之前线程并未终止,它将
会对已经被服务配置器“蒸发”的代码发出调用!我们当然不需要这个。为了确保线程在服务配置器“
蒸发”TimeService对象之前终止,程序使用了条件变量。(要更多地了解怎样使用条件变量,请阅读有
关线程的章节)。
下面是一个简单的、只是用于启用时间服务的配置文件。可以去掉注释#号来挂起、恢复和移除服务
。
例8-1c
# To configure different services, simply uncomment the appropriate
#lines in this file!
#resume TimeService
#suspend TimeService
#remove TimeService
#set to dynamically configure the TimeService object and do so without
#sending any parameters to its init method
dynamic TimeService Service_Object * ./Server:time_service ""
最后,下面是启动服务配置器的代码段。这些代码还设置了一个信号处理器对象,用于发起重配置
。该信号处理器已被设置成响应SIGWINCH信号(在窗口发生变化时产生的信号)。在启动服务配置器之
后,应用进入一个反应式循环,等待SIGWINCH信号事件发生。一旦事件发生,就会回调事件处理器,由
它调用ACE_Service_Config的reconfigure()方法。如先前所讲述的,在此调用发生时,服务配置器重新
读取配置文件,并处理用户放在其中的任何新指令。例如,在动态启动TimeService后,在这个例子中你
可以改变svc.conf文件,只留下一个挂起命令在里面。当配置器读取它时,它将调用TimeService的挂起
方法,从而使它挂起它的底层线程。类似地,如果稍后你又改变了svc.conf,要求恢复服务,配置器就
会调用TimeService::resume()方法,从而恢复先前被挂起的线程。
8.4 使用服务管理器
ACE_Service_Manager是可用于对服务配置器进行远程管理的服务。它目前可以接受两种类型的请求
。其一,你可以向它发送“help”消息,列出当前被加载进应用的所有服务。其二,你可以向服务管理
器发送“reconfigure”消息,从而使得服务配置器重新配置它自己。
第9章 消息队列(Message Queue)
现代的实时应用通常被构建成一组相互通信、但又相互独立的任务。这些任务可以通过若干机制来
与对方进行通信,其中常用的一种就是消息队列。在这一情况下,基本的通信模式是:发送者(或生产
者)任务将消息放入消息队列,而接收者(或消费者)任务从此队列中取出消息。这当然只是消息队列
的使用方式之一。在接下来的讨论中,我们将看到若干不同的使用消息队列的例子。
ACE中的消息队列是仿照UNIX系统V的消息队列设计的,如果你已经熟悉系统V的话,就很容易掌握
ACE的消息队列的使用。在ACE中有多种不同类型的消息队列可用,每一种都使用不同的调度算法来进行
队列的入队和出队操作。
9.1 消息块
在ACE中,消息作为消息块(Message Block)被放入消息队列中。消息块包装正被存储的实际消息
数据,并提供若干数据插入和处理操作。每个消息块“包含”一个头和一个数据块。注意在这里“包含
”是在宽松的意义上使用的。消息块可以不对与数据块(Data Block)或是消息头(Message Header)
相关联的内存进行管理(尽管你可以让消息块进行这样的管理)。它仅仅持有指向两者的指针。所以包
含只是逻辑上的。数据块持有指向实际的数据缓冲区的指针。如图9-1所示,这样的设计带来了多个消息
块之间的数据的灵活共享。注意在图中两个消息块共享一个数据块。这样,无需带来数据拷贝开销,就
可以将同一数据放入不同的队列中。
消息块类名为ACE_Message_Block,而数据块类名为ACE_Data_Block。ACE_Message_Block的构造器
是实际创建消息块和数据块的方便办法。
9.1.1 构造消息块
ACE_Message_Block类包含有若干不同的构造器。你可以使用这些构造器来帮助你管理隐藏在消息和
数据块后面的消息数据。ACE_Message_Block类可用于完全地隐藏ACE_Data_Block,并为你管理消息数据
;或者,如果你需要,你可以自己创建和管理数据块及消息数据。下一部分将考查怎样使用
ACE_Message_Block来管理消息内存和数据块。然后我们将考查怎样独立地进行这样的管理,而不用依赖
ACE_Message_Block的管理特性。
9.1.1.1 ACE_Message_Block分配和管理数据内存
要创建消息块,最容易的方法是将底层数据块的大小传给ACE_Message_Block的构造器,从而创建
ACE_Data_Block,并为消息数据分配空的内存区。在创建消息块后,你可以使用rd_ptr()和wr_ptr()方
法来在消息块中插入和移除数据。让ACE_Message_Block来为数据和数据块创建内存区的主要优点是,它
会为你正确地管理所有内存,从而使你免于在将来为许多内存泄漏而头疼。
ACE_Message_Block的构造器还允许程序员指定ACE_Message_Block在内部分配内存时所应使用的分
配器。如果你传入一个分配器,消息块将用它来为数据块和消息数据区的创建分配内存。
ACE_Message_Block的构造器为:
ACE_Message_Block (size_t size,
ACE_Message_Type type = MB_DATA,
ACE_Message_Block *cont = 0,
const char *data = 0,
ACE_Allocator *allocator_strategy = 0,
ACE_Lock *locking_strategy = 0,
u_long priority = 0,
const ACE_Time_Value & execution_time = ACE_Time_Value::zero,
const ACE_Time_Value & deadline_time = ACE_Time_Value::max_time);
上面的构造器的参数为:
1. 要与消息块相关联的数据缓冲区的大小。注意消息块的大小是size,但长度将为0,直到wr_ptr被设
置为止。这将在后面进一步解释。
2. 消息的类型。(在ACE_Message_Type枚举中有若干类型可用,其中包括缺省的数据消息)。
3. 指向“片段链”(fragment chain)中的下一个消息块的指针。消息块可以实际地链接在一起来形成
链。随后链可被放入消息队列中,就好像它是单个数据块一样。该参数缺省为0,意味着此块不使用链
。
4. 指向要存储在此消息块中的数据缓冲区的指针。如果该参数的值为零,就会创建缓冲区(大小由第一
个参数指定),并由该消息块进行管理。当消息块被删除时,相应的数据缓冲区也被删除。但是,如果
在此参数中指定了数据缓冲区,也就是,参数不为空,当消息块被销毁时它就不会删除数据缓冲区。这
是一个重要特性,必须牢牢记住。
5. 用于分配数据缓存(如果需要)的allocator_strategy,在第四个参数为空时使用(如上面所解释的
)。任何ACE_Allocator的子类都可被用作这一参数。(关于ACE_Allocator的更多信息,参见“内存管
理”一章)。
6. 如果locking_strategy不为零,它就将用于保护访问共享状态(例如,引用计数)的代码区,以避免
竞争状态。
7. 这个参数以及后面两个参数用于ACE中的实时消息队列的调度,目前应保留它们的缺省值。
9.1.1.2 用户分配和管理消息内存
如果你正在使用ACE_Message_Block,你并不一定要让它来为你分配内存。消息块的构造器允许你:
· 创建并传入你自己的指向消息数据的数据块。
· 传入指向消息数据的指针,消息块将创建并设置底层的数据块。消息块将为数据块、而不是消息数据
管理内存。
下面的例子演示怎样将指向消息数据的指针传给消息块,以及ACE_Message_Block怎样创建和管理底
层的ACE_Data_Block。
//The data
char data[size];
data = ”This is my data”;
//Create a message block to hold the data
ACE_Message_Block *mb = new ACE_Message_Block (data, // data that is stored
// in the newly created data
//
blocksize); //size of the block that
//is to be stored.
该构造器创建底层数据块,并将它设置为指向传递给它的数据的开头。被创建的消息块并不拷贝该
数据,也不假定自己拥有它的所有权。这就意味着在消息块mb被销毁时,相关联的数据缓冲区data将不
会被销毁。这是有意义的:消息块没有拷贝数据,因此内存也不是它分配的,这样它也不应该负责销毁
它。
9.1.2 在消息块中插入和操作数据
除了构造器,ACE_Message_Block还提供若干方法来直接在消息块中插入数据。另外还有一些方法可
用来操作已经在消息块中的数据。
每个ACE_Message_Block都有两个底层指针:rd_ptr和wr_ptr,用于在消息块中读写数据。它们可以
通过调用rd_ptr()和wr_ptr()方法来直接访问。rd_ptr指向下一次读取数据的位置,而wr_ptr指向下一
次写入数据的位置。程序员必须小心地管理这些指针,以保证它们总是指向正确的位置。在使用这些指
针读写数据时,程序员必须自己来增加它们的值,它们不会魔法般地自动更新。大多数内部的消息块方
法也使用这两个指针,从而使它们能够在你调用消息块方法时改变指针状态。程序员必须保证自己了解
这些指针的变化。
9.1.2.1 拷贝与复制(Copying and Duplicating)
可以使用ACE_Message_Block的copy()方法来将数据拷贝进消息块。
int copy(const char *buf, size_t n);
copy方法需要两个参数,其一是指向要拷贝进消息块的缓冲区的指针,其二是要拷贝的数据的大小
。该方法从wr_pt指向的位置开始往前写,直到它到达参数n所指示的数据缓冲区的末尾。copy()还会保
证wr_ptr的更新,使其指向缓冲区的新末尾处。注意该方法将实际地执行物理拷贝,因而应该小心使用
。
base()和length()方法可以联合使用,以将消息块中的整个数据缓冲区拷贝出来。base()返回指向
数据块的第一个数据条目的指针,而length()返回队中数据的总大小。将base和length相加,可以得到
指向数据块末尾的指针。合起来使用这些方法,你就可以写一个例程来从消息块中取得数据,并做一次
外部拷贝。
duplicate()和clone()方法用于制作消息块的“副本”。如它的名字所暗示的,clone()方法实际地
创建整个消息块的新副本,包括它的数据块和附加部分;也就是说,这是一次“深度复制”。而另一方
面,duplicate()方法使用的是ACE_Message_Block的引用计数机制。它返回指向要被复制的消息块的指
针,并在内部增加内部引用计数。
9.1.2.2 释放消息块
一旦使用完消息块,程序员可以调用它的release()方法来释放它。如果消息数据内存是由该消息块
分配的,调用release()方法就也会释放此内存。如果消息块是引用计数的,release()就会减少计数,
直到到达0为止;之后消息块和与它相关联的数据块才从内存中被移除。
9.2 ACE的消息队列
如先前所提到的,ACE有若干不同类型的消息队列,它们大体上可划分为两种范畴:静态的和动态的
。静态队列是一种通用的消息队列(ACE_Message_Queue),而动态消息队列
(ACE_Dynamic_Message_Queue)是实时消息队列。这两种消息队列的主要区别是:静态队列中的消息具
有静态的优先级,也就是,一旦优先级被设定就不会再改变;而另一方面,在动态消息队列中,基于诸
如执行时间和最终期限等参数,消息的优先级可以动态地改变。
例子由一个Qtest类组成,它通过ACE_NULL_SYNCH锁定来实例化缺省大小的消息队列。锁(互斥体和条件
变量)被消息队列用来:
· 保证由消息块维护的引用计数的安全,防止在有多个线程访问时的竞争状态。
· “唤醒”所有因为消息队列空或满而休眠的线程。
在此例中,因为只有一个线程,消息队列的模板同步参数被设置为空(ACE_NULL_SYNCH,意味着使
用ACE_Null_Mutex和ACE_Null_Condition)。随后Qtest的enq_msgs()方法被调用,它进入循环,创建消
息、并将其放入消息队列中。消息数据的大小作为参数传给ACE_Message_Block的构造器。使用该构造器
使得内存被自动地管理(也就是,内存将在消息块被删除时,即release()时被释放)。wr_ptr随后被获
取(使用wr_ptr()访问方法),且数据被拷贝进消息块。在此之后,wr_ptr向前增长。然后使用消息队
列的enqueue_prio()方法来实际地将消息块放入底层消息队列中。
在no_msgs_个消息块被创建、初始化和插入消息队列后,enq_msgs()调用deq_msgs()方法。该方法
使用ACE_Message_Queue的dequeue_head()方法来使消息队列中的每个消息出队。在消息出队后,就显示
它的数据,然后再释放消息。
9.3 水位标
水位标用于在消息队列中指示何时在其中的数据已过多(消息队列到达了高水位标),或何时在其
中的数据的数量不足(消息队列到达了低水位标)。两种水位标都用于流量控制。例如,
low_water_mark可用于避免像TCP中的“傻窗口综合症”(silly window syndrome)那样的情况,而
high_water_mark可用于“阻止“或减缓数据的发送或生产。
ACE中的消息队列通过维护已经入队的总数据量的字节计数来获得这些功能。因而,无论何时有新消
息块被放入消息队列中,消息队列都将先确定它的长度,然后检查是否能将此消息块放入队列中(也就
是,确认如果将此消息块入队,消息队列没有超过它的高水位标)。如果消息队列不能将数据入队,而
它又持有一个锁(也就是,使用了ACE_SYNC,而不是ACE_NULL_SYNCH作为消息队列的模板参数),它就
会阻塞调用者,直到有足够的空间可用,或是入队方法的超时(timeout)到期。如果超时已到期,或是
队列持有一个空锁,入队方法就会返回-1,指示无法将消息入队。
类似地,当ACE_Message_Queue的dequeue_head方法被调用时,它检查并确认在出队之后,剩下的数
据的数量高于低水位标。如果不是这样,而它又持有一个锁,它就会阻塞;否则就返回-1,指示失败(
和入队方法的工作方式一样)。
分别有两个方法可用于设置和获取高低水位标:
//get the high water mark
size_t high_water_mark(void)
//set the high water mark
void high_water_mark(size_t hwm);
//get the low water_mark
size_t low_water_mark(void)
//set the low water_mark
void low_water_mark(size_t lwm)
9.4 使用消息队列迭代器(Message Queue Iterator)
和其它容器类的常见情况一样,可将前进(forward)和后退(reverse)迭代器用于ACE中的消息队
列。这两个迭代器名为ACE_Message_Queue_Iterator和ACE_Message_Queue_Reverse_Iterator。它们都
需要一个模板参数,用于在遍历消息队列时进行同步。如果有多个线程使用消息队列,该参数就应设为
ACE_SYNCH;否则,就可设为ACE_NULL_SYNCH。在迭代器对象被创建时,必须将我们想要进行迭代的消息
队列的引用传给它的构造器。
9.5 动态或实时消息队列
如上面所提到的,动态消息队列是其中的消息的优先级随时间变化的队列。实时应用需要这样的行
为特性,因而这样的队列在实时应用中天生更为有用。
ACE目前提供两种动态消息队列:基于最终期限(deadline)的和基于松弛度(laxity)的(参见
[IX])动态消息队列。基于最终期限的消息队列通过每个消息的最终期限来设置它们的优先级。在使用
最早deadline优先算法来调用dequeue_head()方法时,队列中有着最早的最终期限的消息块将最先出队
。而基于松弛度的消息队列,同时使用执行时间和最终期限来计算松弛度,并将其用于划分各个消息块
的优先级。松弛度是十分有用的,因为在根据最终期限来调度时,被调度的任务有可能有最早的最终期
限,但同时又有相当长的执行时间,以致于即使它被立即调度,也不能够完成。这会消极地影响其它任
务,因为它可能阻塞那些可以调度的任务。松弛度把这样的长执行时间考虑在内,并保证任务如果不能
完成,就不会被调度。松弛度队列中的调度基于最小松弛度优先算法。
基于松弛度的消息队列和基于最终期限的消息队列都实现为ACE_Dynamic_Message_Queue。ACE使用
策略(STRATEGY)模式来为动态队列提供不同的调度特性。每种消息队列使用不同的“策略”对象来动
态地设置消息队列中消息的优先级。每个这样的“策略”对象都封装了一种不同的算法来基于执行时间
、最终期限,等等,计算优先级;并且无论何时消息入队或是出队,都会调用这些策略对象来完成前述
计算工作。(有关策略模式的更多信息,请参见“设计模式”)。消息策略模式派生自
ACE_Dynamic_Message_Strategy,目前有两种策略可用:ACE_Laxity_Message_Strategy和
ACE_Deadline_Message_Strategy。因此,要创建基于松弛度的动态消息队列,首先必须创建
ACE_Laxity_Message_Strategy对象。随后,应该对ACE_Dynamic_Message_Queue对象进行实例化,并将
新创建的策略对象作为参数之一传给它的构造器。
创建消息队列
为简化这些不同类型的消息队列的创建,ACE提供了名为ACE_Message_Queue_Factory的具体消息队
列工厂,它使用工厂方法(FACTORY METHOD,更多信息参见“设计模式”)模式的一种变种来创建适当
类型的消息队列。消息队列工厂有三个静态的工厂方法,可用来创建三种不同类型的消息队列:
static ACE_Message_Queue<ACE_SYNCH_USE> *
create_static_message_queue();
static ACE_Dynamic_Message_Queue<ACE_SYNCH_USE> *
create_deadline_message_queue();
static ACE_Dynamic_Message_Queue<ACE_SYNCH_USE> *
create_laxity_message_queue();
每个方法都返回指向刚创建的消息队列的指针。注意这些方法都是静态的,而
create_static_message_queue()方法返回的是ACE_Message_Queue,其它两个方法则返回
ACE_Dynamic_Message_Queue。
能看到这里对ACE估计感兴趣,下载学习ACE调试过的例程代码。
http://nvercongming.iteye.com/
========
ACE中网络通讯编程基本架构
ACE_INET_Addr类,包装了网络地址
ACE_SOCK_Connector类,扮演主动连接角色,发起通讯连接。连接到远端的服务。
ACE_SOCK_Acceptor类,扮演被动连接角色,等待连接。等待远端客户的请求。
ACE_SOCK_Stream类,扮演数据通讯角色,发送和接收数据。完成客户与服务之间的通讯。
利用ACE库来开发网络通讯程序是很简单的,一个基本程序只用到以上提到的几个类,就可以完成一个基
于客户端、服务器端模型的网络应用的开发。开发者无需了解Socket在不同平台上的实现,记忆众多并
相互关联的Socket APIs。
以下以一个Hello World程序为演示。
客户端程序。发送一个Hello World到远端的服务器,并接收服务器返回的信息,将信息打印在屏幕上。
01.
include < iostream >
02.
#include < string >
03.
04.
#include < ace/ACE.h >
05.
#include < ace/INET_Addr.h >
06.
#include < ace/SOCK_Connector.h >
07.
#include < ace/SOCK_Stream.h >
08.
09.
int main( int argc, char* argv[] )
10.
{
11.
ACE::init();//初始化ACE库,在windows下一定要
12.
13.
std::string str = "hello world";
14.
15.
//设置服务器地址
16.
//第一个参数是端口,第二个是ip地址,也可以是域名。
17.
//可以先定义一个地址对象,再用ACE_INET_Addr的set函数来设定。
18.
//地址的配置很多,具体的参照文档
19.
ACE_INET_Addr peer_addr( 5050, "127.0.0.1" );
20.
ACE_SOCK_Stream peer_stream;//定义一个通讯队形
21.
22.
ACE_SOCK_Connector peer_connector;//定义一个主动连接对象
23.
peer_connector.connect( peer_stream, peer_addr );//发起一个连接
24.
25.
peer_stream.send( str.c_str(), str.length() );//发送数据到服务器
26.
27.
str.erase();
28.
str.resize( sizeof( "hello world" ) );
29.
peer_stream.recv( (void*)str.c_str(), str.length() );//接收来自服务器的信息
30.
31.
std::cout << "from server message : " << str << std::endl;
32.
33.
ACE::fini();
34.
return 0;
35.
}
服务器端代码。接收一个远端的连接,将接收到的信息打印在屏幕上,并将接收到的信息返回给客户端
。
01.
#include < iostream >
02.
#include < string >
03.
04.
#include < ace/ACE.h >
05.
#include < ace/SOCK_Acceptor.h >
06.
#include < ace/SOCK_Stream.h >
07.
08.
int main( int argc, char* argv[] )
09.
{
10.
ACE::init();
11.
12.
std::string str;
13.
str.resize( sizeof( "hello world" ) );
14.
15.
//设置服务器地址
16.
ACE_INET_Addr peer_addr( 5050, "127.0.0.1" );
17.
18.
ACE_SOCK_Stream peer_stream;
19.
20.
//创建被动连接角色对象
21.
ACE_SOCK_Acceptor peer_acceptor;
22.
//开启被动连接对象,将对象绑定到一个地址上
23.
peer_acceptor.open( peer_addr );
24.
25.
//等待连接
26.
peer_acceptor.accept( peer_stream );
27.
28.
//数据通讯
29.
peer_stream.recv( (void*)str.c_str(), str.length() );
30.
std::cout << "from client message : " << str << std::endl;
31.
peer_stream.send( str.c_str(), str.length() );
32.
33.
ACE::fini();
34.
return 0;
35.
}
××××以上代码需要ACE库才能运转××××
利用ACE编程的基本框架。
客户端
1 创建地址对象。(ACE_INET_Addr)
2 创建主动连接对象。(ACE_SOCK_Connector)
3 创建数据通讯对象。(ACE_SOCK_Stream)
4 设置服务器地址。(ACE_INET_Addr::set)
5 将数据通讯对象和地址作为参数传给主动连接对象,发起主动连接(ACE_SOCK_Connector::connect)
6 利用通讯对象接收和发送数据。(ACE_SOCK_Stream::recv和ACE_SOCK_Stream::send)
服务器端
1 创建地址对象。(ACE_INET_Addr)
2 创建被动连接对象。(ACE_SOCK_Connector)
3 创建数据通讯对象。(ACE_SOCK_Stream)
4 设置服务器地址。(ACE_INET_Addr::set)
5 将地址作为参数传给被动连接对象,启动接收(ACE_SOCK_Acceptor::open)
6 将数据通讯对象传给被动连接对象,启动接收,接受连接(ACE_SOCK_Connector::accept)
7 利用通讯对象接收和发送数据。(ACE_SOCK_Stream::recv和ACE_SOCK_Stream::send)
http://www.vckbase.com/index.php/wv/1545
========