10 服务配置器(Service Configurator)模式:通过服务配置器模式动态配置通信服务

 

Prashant Jain   Douglas C. Schmidt

10.1 介绍

 

一系列迅速增长的通信服务正出现在Internet上。通信服务是为客户提供功能的服务器中的组件。在Internet可用的服务包括:WWW浏览和内容获取服务(例如,Alta VistaApacheNetscapeHTTP服务器)、软件发布服务(例如,Castinet)、电子邮件和网络新闻传输代理(例如,sendmailnntpd)、远程文件访问(例如,ftpd)、远程终端访问(例如,rlogindtelnetd)、路由表管理(例如,gatedrouted)、主机和用户活动报告(例如,fingerdrwhod)、网络时间协议(例如,ntpd),以及请求代理服务(例如,orbixdRPC portmapper),等等。

实现这些服务的常用方法是将每个服务开发成为单独的程序,随后编译、链接,并在单独的进程中执行每个程序。但是,这样的“静态”配置服务方法生成不灵活、常常也是低效的应用和软件体系结构。静态配置的主要问题是它相对于应用中的其他服务,将特定服务的实现和服务的配置紧耦合在一起。

本文描述服务配置器Service Configurator)模式,它通过使服务的行为与服务实现被配置进应用的时间点去耦合来增强应用的灵活性(常常还有性能)。本文使用用C++编写的分布式时间服务作为例子来演示服务配置器模式。但是,服务配置器已经以许多方式被实现,范围从现代操作系统(像SolarisWindows NT)中的设备驱动程序,到Internet超级服务器(像inetdWindows NT服务控制管理器),以及Java applets

 

10.2 服务配置器模式

 

10.2.1 意图

 

使服务的行为与服务实现被配置进应用或系统的时间点去耦合。

 

10.2.2 别名

 

超级服务器。

 

10.2.3 动机

 

服务配置器模式使服务的实现与服务被配置进应用或系统的时间点去耦合。这样的去耦改善了服务的模块性,并允许服务独立于配置问题(比如两个服务是否必须驻留在一起,或是采用何种并发模型执行服务)而持续发展。

此外,服务配置器模式使它配置的服务的管理集中化。这便利了服务的自动初始化和终止,并且可以通过将常用的服务初始化和终止模式分解进高效的可复用组件而提高性能。

这一部分使用分布式时间服务作为例子来说明服务配置器模式的动机。

 

10.2.3.1 上下文

 

当服务需要进行动态发起、挂起、恢复和终止时,应该采用服务配置器模式。此外,如果服务配置决策必须被推迟至运行时,也应该采用服务配置器模式。

 

 

10-1 分布式时间服务

 

为说明该模式的动机,考虑图10-1中所示的分布式时间服务。该服务为在局域网和广域网中协作的计算机提供准确、容错的时钟同步。在要求多个主机维护精确的全局时间的分布式系统中,同步时间服务是很重要的。例如,大型分布式医学成像系统[1]需要全局同步的时钟,以确保病人的检查被精确地记录时间,并由放射科医生通过健康保健递送系统迅捷地加以分析。

如图10-1所示,该分布式时间服务的体系结构含有下列时间服务器(Time Server)、事务员(Clerk)和客户(Client)组件:

 

 

10.2.4 常用解决方案

 

一种实现分布式时间服务的方法是将时间服务器、事务员和客户的逻辑功能静态地配置进分离的物理进程。在该方法中,可有一或多个主机运行时间服务器进程;后者处理来自事务员进程的时间更新请求。下面的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.5 常用解决方案的陷阱和缺陷

 

尽管像反应堆、接受器和连接器这样的模式的使用改善了上面所示的分布式时间服务器的模块性和可移植性,使用静态方法来配置通信服务有以下缺点:

 

 

10.2.6 更好的解决方案

 

更为方便和灵活的实现分布式服务的方法常常是使用服务配置器模式。该模式使通信服务的行为与这些服务被配置进应用或系统的时间点去耦合。服务配置器模式消除了以下需求的压力:

 

 

10-2使用OMT表示法来演示根据服务配置器模式设计的分布式时间服务的结构。

 

 

10-2 分布式时间服务的结构

 

Service基类为配置和控制服务(比如时间服务或事务员)提供标准接口。基于服务配置器的应用使用该接口来发起、挂起、恢复和终止服务,以及获取关于服务的运行时信息(比如它的IP地址和端口号)。服务自身驻留在Service Repository(服务仓库)中,可由基于服务配置器的应用来在Service Repository中增加或移除。

Service基类的两个子类出现在分布式时间服务中:Time ServerClerk。每个子类表示一个在分布式时间服务中有着特定功能的具体ServiceTime Server服务负责接收和处理来自Clerk的时间更新请求。Clerk服务是连接器(Connector[6]工厂,负责(1)为每个服务器创建新连接,(2)动态分配新的处理器,以向相连的服务器发送时间更新请求,(3)通过处理器接收来自所有服务器的回复,以及(4)随后更新本地系统时间。

通过管理时间服务中的服务组件的配置,服务配置器模式使分布式时间服务变得更为灵活,并从而使它与实现问题分离开来。此外,服务配置器提供了构架来将其他通信服务的配置和管理合并在一个管理单元中。

 

10.3 适用性

 

当有以下情况时使用服务配置器模式:

 

 

当有以下情况时不要使用服务配置器模式:

 

 

10.4 结构和参与者

在图10-3中使用OMT表示法演示了服务配置器模式的结构:

 

 

10-3 服务配置器模式的结构

 

服务配置器模式中的关键参与者包括:

 

 

10.5 协作

 

10-4描述下面的服务配置器模式三个阶段中的组件间协作:

 

 

10-4 服务配置器模式的交互图

 

 

10.6 效果

 

10.6.1 好处

 

服务配置器模式提供以下好处:

 

 

10.6.2 缺点

 

服务配置器模式有以下缺点:

 

 

10.7 实现

 

服务配置器模式可以通过许多方式实现。这一部分解释实现该模式时所涉及的步骤和可选方案。表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的执行。suspendresume方法是调度挂钩,由服务配置器用于挂起和恢复Service的执行。info方法允许服务配置器获取与Service相关的信息(比如它的名字和网络地址)。这些方法一起给出了服务配置器和由它管理的Service之间的统一约定。

 

Windows NT服务控制管理器(SCM)使用这种方案。每个Windows NT主机都拥有主SCM进程,通过向系统服务传递多种控制信息(比如PAUSERESUMETERMINATE)来自动对其进行初始化和管理。每个SCM管理的服务的开发者都要负责编写代码来处理这些消息。

 

 

 

10.8 示例代码

 

下面的代码介绍一个用C++写成的服务配置器模式的例子。该例子聚焦于10.2.3中介绍的分布式时间服务的与配置有关的方面。此外,它还演示了其他模式(比如反应堆模式[4]、接受器[5]和连接器[6]模式)的使用,它们通常被用于开发通信服务和对象请求代理(Object Request Broker)。

在下面的例子中,图10-3所示的OMT类图中的Concrete Service类由Time Server类以及Clerk类来表示。这一部分中的C++代码实现Time ServerClerk类。两个类都继承自Service,使得它们可被动态配置进应用中。此外,该方法使用基于显式动态链接[7] 和配置文件的配置机制来动态地配置分布式时间服务的事务员和服务器部分。服务执行机制则基于在单线程控制中的反应式事件处理模型。

下面的例子显示事务员组件可以怎样改变它用于计算本地系统时间的算法、而又影响服务配置器配置的其他组件的执行。一旦算法被修改,事务员组件就由服务配置器进行动态重配置。

下面所示的代码还包括main驱动函数,它提供任何基于服务配置器的应用的通用入口。使用ACE[7],该实现可运行在UNIX/POSIXWin32平台上;ACE可通过WWWhttp://www.cs.wustl.edu/~schmidt/ACE.html获取。

 

10.8.1 Time Server

 

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 Serversuspendresume挂钩来将其挂起和恢复。

 

10.8.2 Clerk

 

Clerk使用Connector类来建立和维护与一或多个Time Server的连接。Connector类使用连接器模式[6]来为每个到时间服务器的连接创建处理器。处理器接收并处理来自Time Server的时间更新。

Clerk类继承自Service基类。因此,像Time Service一样,它也可以被服务配置器动态地配置。服务配置器可以分别通过调用Clerkinitsuspendresumefini挂钩来将其初始化、挂起、恢复和终止。

 

// 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的响应,它就重新计算它的本地系统时间。因而,当客户向事务员请求当前时间时,它们会接收到全局同步的时间值。

 

10.8.3 配置应用

 

10.8.3.1 共驻式配置

 

下面的代码演示应用的动态配置和执行,使用配置文件来使Time ServerClerk共同驻留在同一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