7.2 The ACE_Svc_Handler Class

Ru-Brd

Motivation

Chapter 2 defined a service as a set of functionality offered to a client by a server. A service handler is the portion of a networked application that either implements or accesses (or both, in the case of a peer-to-peer arrangement) a service. Connection-oriented networked applications require at least two communicating service handlersone for each end of every connection. Incidentally, applications using multicast or broadcast communication may have multiple service handlers. Although these connectionless communication protocols don't cleanly fit the Acceptor-Connector model, the ACE_Svc_Handler class is often a good choice for implementing a service handler and should be considered .

When designing the service handlers involved in a service, developers should also take into account the communication design dimensions discussed in Chapter 1 of C++NPv1. In general, the application functionality defined by a service handler can be decoupled from the following design aspects:

  • How the service handler was connected (actively or passively ) and initialized

  • The protocols used to connect, authenticate, and exchange messages between two service handlers

  • The network programming API used to access the OS IPC mechanisms

In general, connection/authentication protocols and service initialization strategies change less frequently than the service handler functionality implemented by an application. To separate these concerns and allow developers to focus on the functionality of their service handlers, the ACE Acceptor-Connector framework defines the ACE_Svc_Handler class.

Class Capabilities

ACE_Svc_Handler is the basis of ACE's synchronous and reactive data transfer and service processing mechanisms. This class provides the following capabilities:

  • It provides the basis for initializing and implementing a service in a synchronous and/or reactive networked application, acting as the target of the ACE_Connector and ACE_Acceptor connection factories.

  • It provides an IPC endpoint used by a service handler to communicate with its peer service handler(s). The type of this IPC endpoint can be parameterized with many of ACE's IPC wrapper facade classes, thereby separating lower-level communication mechanisms from application-level service processing policies.

  • Since ACE_Svc_Handler derives from ACE_Task (and ACE_Task from ACE_ Event_Handler ), it inherits the concurrency, synchronization, dynamic configuration, and event handling capabilities described in Chapters 3 through 6.

  • It codifies the most common practices of reactive network services, such as registering with a reactor when a service is opened and closing the IPC endpoint when unregistering a service from a reactor.

The interface for ACE_Svc_Handler is shown in Figure 7.2 (page 208). As shown in the figure, this class template is parameterized by:

Figure 7.2. The ACE_Svc_Handler Class

  • A PEER_STREAM traits class, which is able to transfer data between connected peer service handlers. It also defines an associated PEER_STREAM::PEER_ADDR trait that represents the address class for identifying the peers to the service. The PEER_ STREAM parameter is often instantiated by one of the ACE IPC wrapper facades, such as the ACE_SOCK_Stream described in Chapter 3 of C++NPv1.

  • A SYNCH_STRATEGY traits class, which applies the Strategized Locking pattern [POSA2] to parameterize the synchronization traits of an ACE_Message_Queue in the parent ACE_Task class. This parameter is often instantiated by either the ACE_NULL_SYNCH or ACE_MT_SYNCH traits classes (page 163).

Sidebar 40 (page 165) describes the C++ traits and traits class idioms.

Since ACE_Svc_Handler is a descendant of ACE_Event_Handler , an instance of it can be registered with the ACE Reactor framework for various types of events. For example, it can be registered to handle READ and WRITE events. Its handle_input() and handle_output() hook methods will then be dispatched automatically by a reactor when its data-mode socket handle is ready to receive or send data, respectively.

The ACE_Svc_Handler class has a rich interface that exports both its capabilities and the capabilities of its parent classes. We therefore group the description of its methods into the three categories described below.

1. Service creation and activation methods. The ACE Acceptor-Connector framework can modify various service handler creation and initialization aspects at compile time and at run time. By default, an ACE_Svc_Handler subclass is allocated dynamically by an acceptor or connector factory, which use the following methods to create and activate it:

Method

Description

ACE_Svc_Handler()

Constructor called by an acceptor or connector when it creates a service handler

open ()

Hook method called automatically by an acceptor or connector to initialize a service handler

Sidebar 47 explains why the ACE Acceptor-Connector framework decouples service handler creation from activation.

Pointers to ACE_Thread_Manager, ACE_Message_Queue , and ACE_Reactor objects can be passed to the ACE_Svc_Handler constructor to override its defaults. The open() hook method can perform activities that initialize a service handler, such as:

  • Spawning a thread (or pool of threads) that will perform service processing via the svc() hook method

  • Registering one or more event sources, such as input events or timeouts, with a reactor

    Sidebar 47: Decoupling Service Handler Creation from Activation

    The motivations for decoupling service activation from service creation in the ACE Acceptor-Connector framework include:

    • To make service handler creation flexible. ACE allows for wide flexibility in the way an application creates (or reuses) service handlers. Many applications create new handlers dynamically as needed, but some may recycle handlers or use a single handler for all connections, as discussed in Sidebar 53 (page 240).

    • To simplify error handling. ACE doesn't rely on native C++ exceptions for the reasons described in Appendix A.6 of C++NPv1. The constructor used to create a service handler therefore shouldn't perform any operations that can fail. Instead, any such operations should be placed in the open() hook method, which must return -1 if activation fails.

    • To ensure thread safety. If a thread is spawned in a constructor it's not possible to ensure that the object has been initialized completely before the thread begins to run. To avoid this potential race condition, the ACE Acceptor-Connector framework decouples service handler creation from activation.

  • Opening log files and initializing usage statistics

  • Initializing locks or other resources

If these initialization activities complete successfully, open() returns 0. If a failure occurs and the service cannot or should not continue, however, open() must report this event to its caller by returning -1. Since the service handler doesn't control how it was instantiated, a failure in open() must be reported back to the caller so that cleanup activities can be performed. By default, the service handler is deleted automatically if open() returns -1, as shown in the various activate_svc_handler() methods of the acceptor and connector factories (page 221).

The ACE_Svc_Handler defines a default implementation of open() that performs the common set of operations shown below:

 template <class PEER_STREAM, class SYNCH_STRATEGY> int  ACE_Svc_Handler<PEER_STREAM, SYNCH_STRATEGY>::open      (void *factory) {    if (reactor () && reactor ()->register_handler        (this, ACE_Event_Handler::READ_MASK) == -1) return -1;    else return 0;  } 

The void * parameter to open() is a pointer to the acceptor or connector factory that created the service handler. By default, a service handler registers itself with a reactor and processes incoming events reactively. The Example part of this section (page 214) illustrates a service handler that activates itself in its open() method to become an active object and process incoming events concurrently. Since a service handler is responsible for its life cycle management after being activated successfully, it rarely interacts with the acceptor that created and activated it. As shown in the Example part of Section 7.4, however, a service handler often uses a connector to reestablish connections if failures occur.

2. Service processing methods. As outlined above, a service handler can perform its processing in several ways. For example, it can process events reactively using a reactor or it can process them concurrently via one or more processes or threads. The following methods inherited from ACE_Svc_Handler 's ancestors can be overridden by its subclasses and used to perform service handler processing:

Method

Description

svc()

ACE_Svc_Handler inherits the svc() hook method from the ACE_Task class described in Section 6.3. After a service handler's activate() method is invoked, much of its subsequent processing can be performed concurrently within its svc() hook method.

handle_*()

ACE_Svc_Handler inherits the handle_*() methods from the ACE_ Event_Handler class described in Section 3.3. A service handler can therefore register itself with a reactor to receive callbacks, such as handle_ input() , when various events of interest occur, as described in Chapter 3.

peer()

Returns a reference to the underlying PEER_STREAM . A service handler's PEER_STREAM is ready for use when its open() hook method is called. Any of the service processing methods can use this accessor to obtain a reference to the connected IPC mechanism.

Although the ACE_Svc_Handler SYNCH_STRATEGY template argument parameterizes the ACE_Message_Queue inherited from ACE_Task , it has no effect on the PEER_STREAM IPC endpoint. It's inappropriate for the ACE Acceptor-Connector framework to unilaterally serialize use of the IPC endpoint since it's often not accessed concurrently. For example, a service handler may run as an active object with a single thread or be driven entirely by callbacks from a reactor in a single-threaded configuration.

It is possible, however, for a service handler's open() hook method to spawn multiple threads that access its IPC endpoint concurrently. In such cases, the application code in the service handler must perform any necessary synchronization. Chapter 10 of C++NPv1 describes ACE synchronization mechanisms that applications can use. For example, if more than one thread writes to the same socket handle, it's a good idea to serialize it with an ACE_Thread_Mutex to avoid interleaving data from different send() calls into the same TCP bytestream.

3. Service shutdown methods. A service handler can be used in many ways. For example, it can be dispatched by a reactor, run in its own thread or process, or form part of a thread pool. The ACE_Svc_Handler class therefore provides the following methods to shut a service handler down:

Method

Description

destroy()

Can be called to shut a service handler down directly

handle_close()

Calls destroy() via a callback from a reactor

close()

Calls handle_close() on exit from a service thread

Service handlers are often closed in accordance with an application-defined protocol, such as when a peer service handler closes a connection or when a serious communication error occurs. Regardless of the particular circumstance, however, a service handler's shutdown processing usually undoes the actions performed by the service handler's open() hook method, and deletes the service handler when needed. The shutdown methods listed in the table above can be divided into the following three categories:

  • Direct shutdown. An application can invoke the destroy() method directly to shut a service handler down. This method performs the following steps:

  1. Remove the handler from the reactor.

  2. Cancel any timers associated with the handler.

  3. Close the peer stream object to avoid handle leaks.

  4. If the object was allocated dynamically, delete it to avoid memory leaks.

Chapter 3 of C++NPv1 explained why the destruction of an ACE_SOCK -derived object doesn't close the encapsulated socket. ACE_Svc_Handler is at a higher level of abstraction, however, and, because it's part of a framework, it codifies common usage patterns. Since closing the socket is such a common part of shutting down a service handler, the ACE Acceptor-Connector framework performs this task automatically.

ACE_Svc_Handler uses the Storage Class Tracker C++ idiom described in Sidebar 48 (page 212) to check if it was allocated dynamically or statically. Its destroy() method can therefore tell if a service handler was allocated dynamically and, if so, delete it. If the service handler was not allocated dynamically, destroy() doesn't delete it.

If a service handler is registered with a reactor, it's best to not call destroy() from a thread that's not running the reactor event loop. Doing so could delete the service handler object out from under a reactor that's dispatching events to it, causing undefined (and undesired ) behavior (this is similar to the issue discussed in Sidebar 46 on page 196). Rather than calling destroy() directly, therefore, use the ACE_Reactor::notify() method (page 77) to transfer control to a thread dispatching reactor events, where destroy() is safer to use. An even better approach, however, is to alter the design to use the reactive shutdown technique described next .

  • Reactive shutdown. When an ACE_Svc_Handler is registered with the ACE Reactor framework, it often detects that the peer application has closed the connection and initiates shutdown locally. A reactor invokes a service handler's handle_close() method when instructed to remove the handler from its internal tables, usually when the service handler's handle_input() method returns -1 after a peer closes a connection. Reactive handlers should consolidate shutdown activities in the handle_close() method, as discussed in Sidebar 9 (page 51). As shown in Figure 7.3, the default handle_ close() method calls the destroy() method (page 211). The handle_close() method can be overridden in subclasses if its default behavior is undesirable.

    Figure 7.3. Reactive Shutdown of ACE_Svc_Handler

    Sidebar 48: Determining a Service Handler's Storage Class

    ACE_Svc_Handler objects are often allocated dynamically by the ACE_Acceptor and ACE_Connector factories in the ACE Acceptor-Connector framework. There are situations, however, when service handlers are allocated differently, such as statically or on the stack. To reclaim a handler's memory correctly, without tightly coupling it with the classes and factories that may instantiate it, the ACE_Svc_Handler class uses the C++ Storage Class Tracker idiom [vR96]. This idiom performs the following steps to determine automatically whether a service handler was allocated statically or dynamically and act accordingly :

    1. ACE_Svc_Handler overloads operator new , which allocates memory dynamically and sets a flag in thread-specific storage that notes this fact.

    2. The ACE_Svc_Handler constructor inspects thread-specific storage to see if the object was allocated dynamically, recording the result in a data member.

    3. When the destroy() method is eventually called, it checks the "dynamically allocated" flag. If the object was allocated dynamically, destroy() deletes it; if not, it will simply let the ACE_Svc_Handler destructor clean up the object when it goes out of scope.

  • Thread shutdown. As described on page 188, a service handler's close() hook method is called in each of a task's threads when its svc() method returns. Whereas a reactive service uses the reactor shutdown mechanism to initiate shutdown activity, an active thread that's handling a peer connection can simply return when the peer closes the connection. Since a single thread executing the service is a common use case, the default ACE_Svc_Handler::close() hook method implementation calls the handle_ close() method described above. This method can be overridden in subclasses to perform application-specific cleanup code if its default behavior is undesirable.

Example

This example illustrates how to use the ACE_Svc_Handler class to implement a logging server based on the thread-per-connection concurrency model described in Chapter 5 of C++NPv1. The example code is in the TPC_Logging_Server.cpp and TPC_ Logging_Server.h files. The header file declares the example classes, and starts by including the necessary header files.

 #include "ace/Acceptor.h"  #include "ace/INET_Addr.h"  #include "ace/Reactor.h"  #include "ace/Svc_Handler.h"  #include "ace/FILE_IO.h"  #include "Logging_Handler.h" 

The TPC_Logging_Handler shown below inherits from ACE_Svc_Handler .

 class TPC_Logging_Handler    : public ACE_Svc_Handler<ACE_SOCK_Stream, ACE_NULL_SYNCH> { 

We parameterize the ACE_Svc_Handler template with an ACE_SOCK_Stream data transfer class and the ACE_NULL_SYNCH traits class, which designates a no-op synchronization strategy. Sidebar 49 (page 214) explains how ACE handles C++ compilers that don't support traits classes in templates.

TPC_Logging_Handler defines the following two data members that are initialized in its constructor.

 protected:    ACE_FILE_IO log_file_; // File of log records.    // Connection to peer service handler.    Logging_Handler logging_handler_;  public:    TPC_Logging_Handler (): logging_handler_ (log_file_) {} 

As usual, we reuse the Logging_Handler from Chapter 4 of C++NPv1 to read a log record out of the socket handle parameter and store it into an ACE_Message_Block .

Sidebar 49: Workarounds for Lack of Traits Class Support

If you examine the ACE Acceptor-Connector framework source code closely, you'll notice that the IPC class template argument to ACE_Acceptor, ACE_Connector , and ACE_Svc_Handler is a macro rather than a type parameter. Likewise, the synchronization strategy parameter to the ACE_Svc_Handler is a macro rather than a type parameter. ACE uses these macros to work around the lack of support for traits classes and templates in some C++ compilers. To work portably on those platforms, ACE class types, such as ACE_INET_Addr or ACE_Thread_Mutex , must be passed as explicit template parameters, rather than accessed as traits of traits classes, such as ACE_SOCK_Addr::PEER_ADDR or ACE_MT_SYNCH::MUTEX .

To simplify the efforts of application developers, ACE defines a set of macros that conditionally expand to the appropriate types. For example, the following table describes the ACE_SOCK* macros:

ACE Class

Description

ACE_SOCK_ACCEPTOR

Expands to either ACE_SOCK_Acceptor or ACE_ SOCK_Acceptor and ACE_INET_Addr

ACE_SOCK_CONNECTOR

Expands to either ACE_SOCK_Connector or to ACE_ SOCK_Connector and ACE_INET_Addr

ACE_SOCK_STREAM

Expands to either ACE_SOCK_Stream or to ACE_ SOCK_Stream and ACE_INET_Addr

These macros supply addressing classes that work properly with all C++ compilers supported by ACE. For example, they expand to a single class if template traits are supported and two classes if not.

ACE uses the ACE _ SOCK _ STREAM macro internally as the IPC class parameter to the ACE_Svc_Handler template macro rather than the ACE_SOCK_Stream class to avoid problems when porting code to older C++ compilers. Most modern C++ compilers no longer have these problems, so you needn't use these macros in your application code unless portability to legacy compilers is essential. For simplicity, the code in this book assumes that your C++ compiler fully supports template traits and traits classes, and therefore doesn't use the ACE macros. The C++NPv2 example code included with ACE, however, does use the macros to ensure portability to all ACE platforms.

Each instance of TPC_Logging_Handler is allocated dynamically by the TPC_ Logging_Acceptor (page 222) when a connection request arrives from a peer connector. TPC_Logging_Handler overrides the ACE_Svc_Handler::open() hook method to initialize the handler, as shown below:

 1 virtual int open (void *) {   2   static const ACE_TCHAR LOGFILE_SUFFIX[] = ACE_TEXT (".log");   3   ACE_TCHAR filename[MAXHOSTNAMELEN + sizeof (LOGFILE_SUFFIX)];   4   ACE_INET_Addr logging_peer_addr;   5   6   peer ().get_remote_addr (logging_peer_addr);   7   logging_peer_addr.get_host_name (filename, MAXHOSTNAMELEN);   8   ACE_OS_String::strcat (filename, LOGFILE_SUFFIX);   9  10   ACE_FILE_Connector connector;  11   connector.connect (log_file_,  12                      ACE_FILE_Addr (filename),  13                      0, // No timeout.  14                      ACE_Addr::sap_any, // Ignored.  15                      0, // Don't try to reuse the addr.  16                      O_RDWRO_CREATO_APPEND,  17                      ACE_DEFAULT_FILE_PERMS);  18  19   logging_handler_.peer ().set_handle (peer ().get_handle ());  20  21   return activate (THR_NEW_LWP  THR_DETACHED);  22 } 

Lines 217 Initialize a log file using the same logic described in the Logging_Event_ Handler::open() method (page 59).

Line 19 Borrow the socket handle from the service handler and assign it to logging_ handler_ , which is then used to receive and process client log records.

Line 21 Convert TPC_Logging_Handler into an active object. The newly spawned detached thread runs the following TPC_Logging_Handler::svc() hook method:

 virtual int svc () {      for (;;)        switch (logging_handler_.log_record ()) {        case -1: return -1; // Error.        case 0: return 0; // Client closed connection.        default: continue; // Default case.        }        /* NOTREACHED */          return 0;    }  }; 

This method focuses solely on reading and processing client log records. We break out of the for loop and return from the method when the log_record() method detects that its peer service handler has closed the connection or when an error occurs. Returning from the method causes the thread to exit, which in turn triggers ACE_Task::svc_run() to call the inherited ACE_Svc_Handler::close() method on the object. By default, this method closes the peer stream and deletes the service handler if it was allocated dynamically, as described in the table on page 210. Since the thread was spawned using the THR _ DETACHED flag, there's no need to wait for it to exit.

You may notice that TPC_Logging_Handler::svc() provides no way to stop the thread's processing if the server is somehow asked to shut down before the peer closes the socket. Adding this capability is left as an exercise for the reader. Some common techniques for providing this feature are described in Sidebar 50.

Sidebar 50: Techniques for Shutting Down Blocked Service Threads

Service threads often perform blocking I/O operations, as shown by the thread-per-connection concurrency model in TPC_Logging_Handler::svc() (page 215). If the service thread must be stopped before its normal completion, however, the simplicity of this model can cause problems. Some techniques for forcing service threads to shut down, along with their potential drawbacks, include:

  • Exit the server process, letting the OS abruptly terminate the peer connection, as well as any other open resources, such as files (a log file, in the case of this chapter's examples). This approach can result in lost data and leaked resources. For example, System V IPC objects are vulnerable in this approach.

  • Enable asynchronous thread cancellation and cancel the service thread. This design isn't portable and can also abandon resources if not programmed correctly.

  • Close the socket, hoping that the blocked I/O call will abort and end the service thread. This solution can be effective, but doesn't work on all platforms.

  • Rather than blocking I/O, use timed I/O and check a shutdown flag, or use the ACE_Thread_Manager cooperative cancellation mechanism, to cleanly shut down between I/O attempts. This approach is also effective, but may delay the shutdown by up to the specified timeout.

Ru-Brd


C++ Network Programming
C++ Network Programming, Volume I: Mastering Complexity with ACE and Patterns
ISBN: 0201604647
EAN: 2147483647
Year: 2002
Pages: 65

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net