第10章 服务配置器(Service Configurator)模式:通过服务配置器模式动态配置通信服务
Prashant Jain Douglas C. Schmidt
一系列迅速增长的通信服务正出现在Internet上。通信服务是为客户提供功能的服务器中的组件。在Internet可用的服务包括:WWW浏览和内容获取服务(例如,Alta Vista、Apache、Netscape的HTTP服务器)、软件发布服务(例如,Castinet)、电子邮件和网络新闻传输代理(例如,sendmail和nntpd)、远程文件访问(例如,ftpd)、远程终端访问(例如,rlogind和telnetd)、路由表管理(例如,gated和routed)、主机和用户活动报告(例如,fingerd和rwhod)、网络时间协议(例如,ntpd),以及请求代理服务(例如,orbixd和RPC portmapper),等等。
实现这些服务的常用方法是将每个服务开发成为单独的程序,随后编译、链接,并在单独的进程中执行每个程序。但是,这样的“静态”配置服务方法生成不灵活、常常也是低效的应用和软件体系结构。静态配置的主要问题是它相对于应用中的其他服务,将特定服务的实现和服务的配置紧耦合在一起。
本文描述服务配置器(Service Configurator)模式,它通过使服务的行为与服务实现被配置进应用的时间点去耦合来增强应用的灵活性(常常还有性能)。本文使用用C++编写的分布式时间服务作为例子来演示服务配置器模式。但是,服务配置器已经以许多方式被实现,范围从现代操作系统(像Solaris和Windows NT)中的设备驱动程序,到Internet超级服务器(像inetd和Windows NT服务控制管理器),以及Java applets。
使服务的行为与服务实现被配置进应用或系统的时间点去耦合。
超级服务器。
服务配置器模式使服务的实现与服务被配置进应用或系统的时间点去耦合。这样的去耦改善了服务的模块性,并允许服务独立于配置问题(比如两个服务是否必须驻留在一起,或是采用何种并发模型执行服务)而持续发展。
此外,服务配置器模式使它配置的服务的管理集中化。这便利了服务的自动初始化和终止,并且可以通过将常用的服务初始化和终止模式分解进高效的可复用组件而提高性能。
这一部分使用分布式时间服务作为例子来说明服务配置器模式的动机。
当服务需要进行动态发起、挂起、恢复和终止时,应该采用服务配置器模式。此外,如果服务配置决策必须被推迟至运行时,也应该采用服务配置器模式。

图10-1 分布式时间服务
为说明该模式的动机,考虑图10-1中所示的分布式时间服务。该服务为在局域网和广域网中协作的计算机提供准确、容错的时钟同步。在要求多个主机维护精确的全局时间的分布式系统中,同步时间服务是很重要的。例如,大型分布式医学成像系统[1]需要全局同步的时钟,以确保病人的检查被精确地记录时间,并由放射科医生通过健康保健递送系统迅捷地加以分析。
如图10-1所示,该分布式时间服务的体系结构含有下列时间服务器(Time Server)、事务员(Clerk)和客户(Client)组件:
一种实现分布式时间服务的方法是将时间服务器、事务员和客户的逻辑功能静态地配置进分离的物理进程。在该方法中,可有一或多个主机运行时间服务器进程;后者处理来自事务员进程的时间更新请求。下面的C++代码段演示静态配置的时间服务器进程的结构:
// The
Clerk_Handler processes time requests
// from Clerks.
class
Clerk_Handler :
public
Svc_Handler <SOCK_Stream>
{
public:
// This method
is called by the Reactor
// when
requests arrive from Clerks.
virtual int
handle_input (void)
{
// Read request from Clerk and
reply
// with the current time.
}
// ...
};
// The
Clerk_Acceptor is a factory that
// accepts connections
from Clerks and creates
//
Clerk_Handlers.
typedef
Acceptor<Clerk_Handler, SOCK_Acceptor> Clerk_Acceptor;
int main (int
argc, char *argv[])
{
// Parse
command-line arguments.
Options::instance
()->parse_args (argc, argv);
// Set up
Acceptor to listen for Clerk connections.
Clerk_Acceptor
acceptor(Options::instance ()->port ());
// Register
with the Reactor Singleton.
Reactor::instance
()->register_handler(&acceptor, ACCEPT_MASK);
// Run the
event loop waiting for Clerks to
// connect and
perform time queries.
for (;;)
Reactor::instance
()->handle_events ();
/* NOTREACHED
*/
}
该程序使用反应堆(Reactor)模式[4]和接受器(Acceptor)模式[5]来实现静态配置的时间服务器进程。
每个需要全局时间同步的主机都可以运行事务员进程。事务员基于接收自一或多个时间服务器的值周期性地更新它们的本地系统时间。下面的C++代码段演示静态配置的事务员进程的结构:
// This class communicates
with the Time_Server.
class
Time_Server_Handler { /* ... */ };
// This class
establishes connections with the
// Time Servers
and periodically queries them for
// their latest
time values.
class Clerk :
public Svc_Handler <SOCK_Stream>
{
public:
// Initialize
the Clerk.
Clerk (void)
{
Time_Server_Handler **handler = 0;
// Use the Iterator pattern and the
// Connector pattern to set up the
// connections to the Time Servers.
for (ITERATOR iterator
(handler_set_);
iterator.next (handler) != 0;
iterator.advance ())
{
connector_.connect (*handler);
Time_Value timeout_interval (60);
// Register a timer that will
expire
// every 60 seconds. This will
trigger
// a call to the handle_timeout()
method,
// which will query the Time
Servers and
// retrieve the current time of
day.
Reactor::instance
()->schedule_timer
(this, timeout_interval);
}
}
// This method implements the Clock
Synchronization
// algorithm
that computes local system time. It
// is called
periodically by the Reactor’s timer
// mechanism.
int
handle_timeout (void)
{
// Periodically query the servers
by iterating
// over the handler set and
obtaining time
// updates from each Time Server.
Time_Server_Handler **handler = 0;
// Use the Iterator pattern to
query all
// the Time Servers
for (ITERATOR iterator
(handler_set_);
iterator.next (handler) != 0;
iterator.advance ())
{
Time_Value server_time =
(*handler)->get_server_time ();
// Compute the local system time
and
// store this in shared memory that
// is accessible to the Client
processes.
}
}
private:
typedef
Unbounded_Set <Time_Server_Handler *>HANDLER_SET;
typedef
Unbounded_Set_Iterator<Time_Server_Handler *> ITERATOR;
// Set of
Clerks and iterator over the set.
HANDLER_SET
handler_set_;
// The connector_
is a factory that
// establishes
connections with Time Servers
// and creates
Time_Server_Handlers.
Connector<Time_Server_Handler,
SOCK_Connector>connector_;
};
int main (int
argc, char *argv[])
{
// Parse
command-line arguments.
Options::instance
()->parse_args (argc, argv);
// Initialize
the Clerk.
Clerk clerk;
// Run the
event loop, periodically
// querying the
Time Servers to determine
// the global
time.
for (;;)
Reactor::instance
()->handle_events ();
/* NOTREACHED
*/
}
该程序使用反应堆(Reactor)模式[4]和连接器(Connector)模式[6]来实现静态配置的事务员进程。
客户进程可以使用它们的本地事务员报告的同步的时间。为最少化通信开销,当前时间可被存储在共享内存中,后者被映射到事务员和同一主机上所有客户的地址空间中。除了时间服务,这些主机提供的其他通信服务(比如文件传输、远程登录和HTTP服务器)也可以在分离的静态配置的进程中执行。
尽管像反应堆、接受器和连接器这样的模式的使用改善了上面所示的分布式时间服务器的模块性和可移植性,使用静态方法来配置通信服务有以下缺点:
更为方便和灵活的实现分布式服务的方法常常是使用服务配置器模式。该模式使通信服务的行为与这些服务被配置进应用或系统的时间点去耦合。服务配置器模式消除了以下需求的压力:
图10-2使用OMT表示法来演示根据服务配置器模式设计的分布式时间服务的结构。

图10-2 分布式时间服务的结构
Service基类为配置和控制服务(比如时间服务或事务员)提供标准接口。基于服务配置器的应用使用该接口来发起、挂起、恢复和终止服务,以及获取关于服务的运行时信息(比如它的IP地址和端口号)。服务自身驻留在Service Repository(服务仓库)中,可由基于服务配置器的应用来在Service Repository中增加或移除。
Service基类的两个子类出现在分布式时间服务中:Time
Server和Clerk。每个子类表示一个在分布式时间服务中有着特定功能的具体Service。Time Server服务负责接收和处理来自Clerk的时间更新请求。Clerk服务是连接器(Connector)[6]工厂,负责(1)为每个服务器创建新连接,(2)动态分配新的处理器,以向相连的服务器发送时间更新请求,(3)通过处理器接收来自所有服务器的回复,以及(4)随后更新本地系统时间。
通过管理时间服务中的服务组件的配置,服务配置器模式使分布式时间服务变得更为灵活,并从而使它与实现问题分离开来。此外,服务配置器提供了构架来将其他通信服务的配置和管理合并在一个管理单元中。
当有以下情况时使用服务配置器模式:
当有以下情况时不要使用服务配置器模式:
在图10-3中使用OMT表示法演示了服务配置器模式的结构:

图10-3 服务配置器模式的结构
服务配置器模式中的关键参与者包括:
图10-4描述下面的服务配置器模式三个阶段中的组件间协作:

图10-4 服务配置器模式的交互图
服务配置器模式提供以下好处:
服务配置器模式有以下缺点:
服务配置器模式可以通过许多方式实现。这一部分解释实现该模式时所涉及的步骤和可选方案。表10-1中总结了这些步骤和可选方案。
|
步骤 |
常用可选方案 |
|
定义服务控制接口 |
|
|
定义服务仓库 |
|
|
选择配置机制 |
|
|
确定服务执行机制 |
|
表10-1 实现服务配置器模式所涉及的步骤
定义服务控制接口有两种基本的方法,基于继承的和基于消息的:
class Service
{
public:
// =
Initialization and termination hooks.
virtual int init
(int argc, char *argv[]) = 0;
virtual int
fini (void) = 0;
// = Scheduling
hooks.
virtual int
suspend (void);
virtual int
resume (void);
// =
Informational hook.
virtual int
info (char **, size_t) = 0;
};
init方法用作Service的入口。它被服务配置器用于初始化Service的执行。fini方法允许服务配置器终止Service的执行。suspend和resume方法是调度挂钩,由服务配置器用于挂起和恢复Service的执行。info方法允许服务配置器获取与Service相关的信息(比如它的名字和网络地址)。这些方法一起给出了服务配置器和由它管理的Service之间的统一约定。
Windows NT服务控制管理器(SCM)使用这种方案。每个Windows NT主机都拥有主SCM进程,通过向系统服务传递多种控制信息(比如PAUSE、RESUME和TERMINATE)来自动对其进行初始化和管理。每个SCM管理的服务的开发者都要负责编写代码来处理这些消息。
下面的代码介绍一个用C++写成的服务配置器模式的例子。该例子聚焦于10.2.3中介绍的分布式时间服务的与配置有关的方面。此外,它还演示了其他模式(比如反应堆模式[4]、接受器[5]和连接器[6]模式)的使用,它们通常被用于开发通信服务和对象请求代理(Object Request Broker)。
在下面的例子中,图10-3所示的OMT类图中的Concrete Service类由Time Server类以及Clerk类来表示。这一部分中的C++代码实现Time Server和Clerk类。两个类都继承自Service,使得它们可被动态配置进应用中。此外,该方法使用基于显式动态链接[7] 和配置文件的配置机制来动态地配置分布式时间服务的事务员和服务器部分。服务执行机制则基于在单线程控制中的反应式事件处理模型。
下面的例子显示事务员组件可以怎样改变它用于计算本地系统时间的算法、而又不影响服务配置器配置的其他组件的执行。一旦算法被修改,事务员组件就由服务配置器进行动态重配置。
下面所示的代码还包括main驱动函数,它提供任何基于服务配置器的应用的通用入口。使用ACE[7],该实现可运行在UNIX/POSIX和Win32平台上;ACE可通过WWW在http://www.cs.wustl.edu/~schmidt/ACE.html获取。
Time Server使用Acceptor类来接受来自一或多个事务员的连接。Acceptor类使用接受器模式[5]来为所有来自事务员的连接创建处理器;这些事务员想要接收时间更新请求。该设计使Time Server的实现与它的配置去耦合,因此,开发者可以独立于Time Server的配置改变它的实现。这为改进时间服务器的实现提供了灵活性。
Time Server类继承自在10.7中定义的Service基类。这使得服务配置器能够动态地链接Timer
Server,以及解除其链接。在将Time Server服务装载进Service Repository之前,服务配置器调用它的init挂钩。该方法执行Time Server特有的初始化代码。同样地,当服务不再被需要时,fini挂钩方法被服务配置器自动调用,以将其终止。
// The
Clerk_Handler processes time requests
// from Clerks.
class
Clerk_Handler :
public
Svc_Handler <SOCK_Stream>
{
// This is
identical to the Clerk_Handler
// defined in
the second section.
};
class
Time_Server : public Service
{
public:
// Initialize
the service when linked dynamically.
virtual int
init (int argc, char *argv[])
{
// Parse command line arguments to
get
// port number to listen on.
parse_args (argc, argv);
// Set the connection acceptor
endpoint into
// listen mode (using the Acceptor
pattern).
acceptor_.open (port_);
// Register with the Reactor
Singleton.
Reactor::instance
()->register_handler
(&acceptor_, ACCEPT_MASK);
}
// Terminate
the service when dynamically unlinked.
virtual int
fini (void)
{
// Close down the connection.
acceptor_.close ();
}
// Other
methods (e.g., info(), suspend(), and
// resume())
omitted.
private:
// Parse
command line arguments or those
// specified by
the configuration file.
int parse_args
(int argc, char *argv[]);
// Acceptor is
a factory that accepts
// connections
from Clerks and creates
//
Clerk_Handlers.
Acceptor<Clerk_Handler,
SOCK_Acceptor>acceptor_;
// Port the
Time Server listens on.
int port_;
};
注意服务配置器也可以分别通过调用Time
Server的suspend和resume挂钩来将其挂起和恢复。
Clerk使用Connector类来建立和维护与一或多个Time Server的连接。Connector类使用连接器模式[6]来为每个到时间服务器的连接创建处理器。处理器接收并处理来自Time Server的时间更新。
Clerk类继承自Service基类。因此,像Time Service一样,它也可以被服务配置器动态地配置。服务配置器可以分别通过调用Clerk的init、suspend、resume和fini挂钩来将其初始化、挂起、恢复和终止。
// This class
communicates with the Time_Server.
class
Time_Server_Handler
: public
Svc_Handler <SOCK_Stream>
{
public:
// Get the
current time from a Time Server.
Time_Value
get_server_time (void);
// ...
};
// This class
establishes and maintains connections
// with the
Time Servers and also periodically queries
// them to
calculate the current time.
class Clerk :
public Service
{
public:
// Initialize
the service when linked dynamically.
virtual int
init (int argc, char *argv[])
{
// Parse command line arguments and
for
// every host:port specification of
server,
// create a Clerk instance that
handles
// connection to the server.
parse_args (argc, argv);
Time_Server_Handler **handler = 0;
// Use the Iterator pattern and the
// Connector pattern to set up the
// connections to the Time Servers.
for (ITERATOR iterator
(handler_set_);
iterator.next (handler) != 0;
iterator.advance ())
{
connector_.connect (*handler);
Time_Value timeout_interval (60);
// Register a timer that will
expire
// every 60 seconds. This will
trigger
// a call to the handle_timeout()
method,
// which will query the Time
Servers and
// retrieve the current time of
day.
Reactor::instance
()->schedule_timer
(this, timeout_interval);
}
}
// Terminate
the service when dynamically unlinked.
virtual int
fini (void)
{
Time_Server_Handler **handler = 0;
// Disconnect from all the time
servers.
for (ITERATOR iterator
(handler_set_);
iterator.next (handler) != 0;
iterator.advance ())
(*handler)->close ();
// Remove the timer.
Reactor::instance
()->cancel_timer (this);
}
// info(),
suspend(), and resume() methods omitted.
// The
handle_timeout method implements the
// Clock
Synchronization algorithm that computes
// local system
time. It is called periodically
// by the
Reactor’s timer mechanism.
int
handle_timeout (void)
{
// Periodically query the servers
by iterating
// over the handler set and
obtaining time
// updates from each Time Server.
Time_Server_Handler **handler = 0;
// Use the Iterator pattern to
query all
// the Time Servers
for (ITERATOR iterator
(handler_set_);
iterator.next (handler) != 0;
iterator.advance ())
{
Time_Value server_time =
(*handler)->get_server_time ();
// Compute the local system time
and
// store this in shared memory that
// is accessible to the Client
processes.
}
}
private:
// Parse
command line arguments or those
// specified by
the configuration file and
// create
Clerks for every specified server.
int parse_args
(int argc, char *argv[]);
typedef
Unbounded_Set <Time_Server_Handler *>HANDLER_SET;
typedef
Unbounded_Set_Iterator<Time_Server_Handler *> ITERATOR;
// Set of
Clerks and iterator over the set.
HANDLER_SET
handler_set_;
// Connector
used to set up connections
// to all
servers.
Connector<Time_Server_Handler,
SOCK_Connector>connector_;
};
通过遍历其处理器列表,Clerk周期性地发送时间更新请求给所有与其相连的Time
Server。一旦Clerk接收到来自所有与其相连的Time Server的响应,它就重新计算它的本地系统时间。因而,当客户向事务员请求当前时间时,它们会接收到全局同步的时间值。
下面的代码演示应用的动态配置和执行,使用配置文件来使Time Server和Clerk共同驻留在同一OS进程中:
int main (int
argc, char *argv[])
{
// Configure
the daemon.
Service_Config
daemon (argc, argv);
// Perform
daemon services updates.
daemon.run_event_loop
();
/* NOTREACHED
*/
}
这个完全通用的main程序在Service Config对象的构造器中动态地配置通信服务。该方法查询下面的svc.conf