Ru-Brd |
MotivationChapter 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:
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 CapabilitiesACE_Svc_Handler is the basis of ACE's synchronous and reactive data transfer and service processing mechanisms. This class provides the following capabilities:
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
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:
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:
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:
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:
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:
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 .
ExampleThis 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 .
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.
|
Ru-Brd |