Ru-Brd |
MotivationEvent-driven networked applications have historically been programmed using native OS mechanisms, such as the Socket API and the select() synchronous event demultiplexer . Applications developed this way, however, are not only nonportable, they are inflexible because they tightly couple low-level event detection, demultiplexing, and dispatching code together with application event processing code. Developers must therefore rewrite all this code for each new networked application, which is tedious , expensive, and error prone. It's also unnecessary because much of event detection, demultiplexing , and dispatching can be generalized and reused across many networked applications. One way to address these problems is to combine skilled object-oriented design with networked application domain experience to produce a set of framework classes that separates application event handling code from the reusable event detection, demultiplexing, and dispatching code in the framework. Sections 3.2 through 3.4 laid the groundwork for this framework by describing reusable time value and timer queue classes, and by defining the interface between framework and application event processing code with the ACE_Event_Handler class. This section describes how the ACE_Reactor class at the heart of the ACE Reactor framework defines how applications can register for, and be notified about, events from multiple sources. Class CapabilitiesACE_Reactor implements the Facade pattern [GoF] to define an interface that applications can use to access the various ACE Reactor framework features. This class provides the following capabilities:
The interface for ACE_Reactor is shown in Figure 3.6 (page 72). This class has a rich interface that exports all the features in the ACE Reactor framework. We therefore group its method descriptions into the six categories described below. 1. Reactor initialization and destruction methods. The following methods initialize and destroy an ACE_Reactor :
The ACE_Reactor class isolates a variety of demultiplexing mechanisms behind the stable interface discussed in this chapter. To partition the different mechanisms in an easy-to-use and easy-to-maintain way, the ACE_Reactor class uses the Bridge pattern [GoF] to separate its implementations from its class interface. This design allows users to substitute a specific reactor implementation when the default isn't appropriate. The ACE_Reactor constructor can optionally be passed a pointer to the implementation used to detect and demultiplex events and to dispatch the methods on the appropriate event handlers. The ACE_Select_Reactor described in Section 4.2 is the default implementation of the ACE_Reactor on most platforms. The exception is Windows, which defaults to ACE_WFMO_Reactor for the reasons described in Sidebar 25 (page 105). The ACE_Reactor::open() method can be passed:
Although ACE offers " full-featured " class constructors, they're best used in error-free or prototype situations where error checking is not important (see Item 10 in [Mey96]). The preferred usage in ACE is a separate call to open() (and close() in the case of object cleanup) methods. This preference stems from the ability of open() and close() Figure 3.6 The ACE_Reactor ClassACE_Reactor # reactor_ : ACE_Reactor * # implementation_ : ACE_Reactor_Impl * + ACE_Reactor (implementation : ACE_Reactor_Impl * = 0, delete_implementation : int = 0) + open (max_handles : int, restart : int = 0, sig_handler : ACE_Sig_Handler * = 0, timer_queue : ACE_Timer_Queue * = 0) : int + close () : int + register_handler (handler : ACE_Event_Handler *, mask : ACE_Reactor_Mask) : int + register_handler (io : ACE_HANDLE, handler : ACE_Event_Handler *, mask : ACE_Reactor_Mask) : int + remove_handler (handler : ACE_Event_Handler *, mask : ACE_Reactor_Mask) : int + remove_handler (io : ACE_HANDLE, mask : ACE_Reactor_Mask) : int + remove_handler (hs : const ACE_Handle_Set&, m : ACE_Reactor_Mask) : int + suspend_handler (handler : ACE_Event_Handler *) : int + resume_handler (handler : ACE_Event_Handler *) : int + mask_ops (handler : ACE_Event_handler *, mask : ACE_Reactor_Mask, ops : int) : int + schedule_wakeup (handler : ACE_Event_Handler *, masks_to_be_added : ACE_Reactor_Mask ) : int + cancel_wakeup (handler : ACE_Event_Handler *, masks_to_be_cleared : ACE_Reactor_Mask) : int + handle_events (max_wait_time : ACE_Time_Value * = 0) : int + run_reactor_event_loop (event_hook : int (*)(void *) = 0) : int + end_reactor_event_loop () : int + reactor_event_loop_done () : int + schedule_timer (handler : ACE_Event_Handler *, arg : void *, delay : ACE_Time_Value &, repeat : ACE_Time_Value & = ACE_Time_Value::zero) : int + cancel_timer (handler : ACE_Event_Handler *, dont_call_handle_close : int = 1) : int + cancel_timer (timer_id : long, arg : void ** = 0, dont_call_handle_close : int = 1) : int + notify (handler : ACE_Event_Handler * = 0, mask : ACE_Reactor_Mask = ACE_Event_Handler::EXCEPT_MASK, timeout : ACE_Time_Value * = 0) : int + max_notify_iterations (iterations : int) : int + purge_pending_notifications (handler : ACE_Event_Handler *, mask : ACE_Reactor_Mask = ALL_EVENTS_MASK) : int + instance () : ACE_Reactor * + owner (new_owner : ACE_thread_t, old_owner : ACE_thread_t * = 0) : intto return error indications , whereas ACE constructors and destructors don't throw native C++ exceptions. The motivation for avoiding native C++ exceptions in ACE's design is discussed in Section A.6 of C++NPv1. The ACE_Svc_Handler class discussed in Section 7.2 of this book closes the underlying socket handle automatically. The ACE_Reactor destructor and close() methods release all the resources used by a reactor. This shutdown process involves calling the handle_close() hook method on all event handlers associated with handles that remain registered with a reactor. Any scheduled timers are deleted without notice and any notifications that are buffered in the reactor's notification mechanism (page 77) are lost when a reactor is closed. 2. Event handler management methods. The following methods register and remove event handlers from an ACE_Reactor :
The ACE_Reactor 's registration and removal methods offer multiple overloaded signatures to facilitate their use in many different situations. For example, the register_handler() methods can be used with any of the following signatures:
The ACE_Reactor::remove_handler() methods can be used to remove event handlers from a reactor so that they are no longer registered for one or more types of I/O events or signals. There are variants with and without an explicit handle specification (just like the first two register_handler() method variants described above). One variant accepts an ACE_Handle_Set to remove a number of handles at once; the other accepts an ACE_Sig_Set to remove signals from reactor handling. The ACE_Reactor::cancel_timer() method (page 76) must be used to remove event handlers that are scheduled for timer events. When an application calls one of the ACE_Reactor::remove_handler() methods for I/O event removal, it can pass a bit mask consisting of the enumeration literals defined in the table on page 50. This bit mask indicates which I/O event types are no longer of interest. The event handler's handle_close() method is subsequently called to notify it of the removal. After handle_close() returns and the event handler is no longer registered to handle any I/O events, the ACE_Reactor removes the event handler from its internal I/O event demultiplexing data structures. An application can prevent handle_close() from being called back by adding the ACE_Event_Handler::DONT_CALL flag to remove_handler() 's mask parameter. This flag instructs a reactor not to dispatch the handle_close() method when removing an event handler, as shown in the Service_Reporter::fini() method (page 135). To ensure a reactor won't invoke handle_close() in an infinite recursion, the DONT_CALL flag should always be passed to remove_handler() when it's called from within the handle_close() hook method itself. By default, the handle_close() hook method is not called when canceling timers via the cancel_timer() method. However, an optional final argument can be supplied to request that handle_close() be called. The handle_close() method is not called when removing an event handler from signal handling. The suspend_handler() method can be used to remove a handler or set of handlers temporarily from the reactor's handle-based event demultiplexing activity. The resume_handler() method reverts the actions of suspend_handler() so that the handle(s) are included in the set of handles waited upon by the reactor's event demultiplexer. Since suspend_handler() and resume_handler() affect only I/O handle-based dispatching, they have no effect on timers, signal handling, or notifications. The mask_ops() method performs operations that get, set, add, or clear the event type(s) associated with an event handler's dispatch mask. The mask_ops() method assumes that an event handler is already present and doesn't try to register or remove it. It's therefore more efficient than using register_handler() and remove_handler() . The schedule_wakeup() and cancel_wakeup() methods are simply "syntactic sugar" for common operations involving mask_ops() . They help prevent subtle errors, however, such as replacing a mask when adding bits was intended. For example, the following mask_ops() calls enable and disable the ACE_Event_Handler::WRITE_MASK : ACE_Reactor::instance ()->mask_ops (handler, ACE_Event_Handler::WRITE_MASK, ACE_Reactor::ADD_MASK); // ... ACE_Reactor::instance ()->mask_ops (handler, ACE_Event_Handler::WRITE_MASK, ACE_Reactor::CLR_MASK); These calls can be replaced by the following more concise and informative method calls: ACE_Reactor::instance ()->schedule_wakeup (handler, ACE_Event_Handler::WRITE_MASK); // ... ACE_Reactor::instance ()->cancel_wakeup (handler, ACE_Event_Handler::WRITE_MASK); 3. Event-loop management methods. Inversion of control is a key capability offered by the ACE Reactor framework. Similar to other frameworks, such as the X Windows Toolkit or Microsoft Foundation Classes (MFC), ACE_Reactor implements the event loop that controls when application event handlers are dispatched. After registering its initial event handlers, an application can manage its event loop via methods in the following table:
The handle_events() method gathers the handles of all registered event handlers, passes them to the reactor's event demultiplexer, and blocks for up to an application-specified time interval awaiting the occurrence of an event, such as I/O activity or timer expiration. When an event occurs, this method dispatches the appropriate preregistered event handlers by invoking their handle_*() hook method(s) defined by the application to process the event(s). If more than one event occurs, they are all dispatched before returning. The return value indicates the number of events processed , 0 if no events occurred before the caller-specified timeout, or -1 if an error occurred. The run_reactor_event_loop() method is a simple wrapper around handle_events() . It runs the event loop continually, calling handle_events() until either
Applications without specialized event handling needs often use run_reactor_event_loop() and end_reactor_event_loop() to handle their event loops because these methods detect and handle errors automatically. Many networked applications run a reactor's event loop in a single thread of control. Sections 4.3 and 4.4 describe how the ACE_TP_Reactor and ACE_WFMO_Reactor classes allow multiple threads to call their event loop methods concurrently. 4. Timer management methods. By default, ACE_Reactor uses the ACE_Timer_Heap timer queue mechanism described in Section 3.4 to schedule and dispatch event handlers in accordance to their timeout deadlines. The timer management methods exposed by the ACE_Reactor include:
The ACE_Reactor codifies the proper usage of the ACE timer queue functionality in the context of handling a range of event types including I/O and timers. In fact, most users interact with the ACE timer queues only via the ACE_Reactor , which integrates the ACE timer queue functionality into the Reactor framework as follows :
Together, these actions effectively integrate timers into the ACE Reactor framework in an easy-to-use way that allows applications to reuse the ACE timer queue capabilities without interacting with timer queue methods directly. Sidebar 16 describes how to minimize dynamic memory allocations in ACE timer queues.
5. Notification methods. A reactor has a notification mechanism that applications can use to insert events and event handlers into a reactor's dispatching engine. The following methods manage various aspects of a reactor's notification mechanism:
The ACE_Reactor::notify() method can be used for several purposes:
By default, a reactor dispatches all event handlers in its notification mechanism after detecting a notification event. The max_notify_iterations() method can change the number of event handlers dispatched. Setting a low value improves fairness and prevents starvation , though it increases dispatching overhead somewhat.
Notifications to the reactor are queued internally while waiting for the reactor to dispatch them (Sidebar 17 discusses how to avoid deadlock on a queue). If an event handler associated with a notification is invalidated before the notification is dispatched, a catastrophic failure can occur when the reactor tries to dispatch an invalid event handler pointer. The purge_pending_notifications() method can therefore be used to remove any notifications associated with an event handler from the queue. The ACE Reactor framework assists users by calling purge_pending_notifications() from the ACE_Event_Handler destructor. This behavior is inherited by all application event handlers because the destructor is declared virtual . Notifications remain in a queue until they are dispatched or purged by an event handler, which ensures that a notification will be processed even if the reactor is busy processing other events at the time that notify() is called. However, if the reactor ceases to detect and dispatch events (e.g., after run_reactor_event_loop() returns), any queued notifications remain and will not be dispatched unless and until the reactor is directed to detect and dispatch events again. Notifications will therefore be lost if the reactor is closed or deleted before dispatching the queued notifications. Applications are responsible for deciding when to terminate event processing, and no events from any source will be detected , demultiplexed, or dispatched after that time. 6. Utility methods. The ACE_Reactor class also defines the following utility methods:
The ACE_Reactor can be used in two ways:
Some reactor implementations, such as the ACE_Select_Reactor described in Section 4.2, only allow one thread to run their handle_events() method. The owner() method changes the identity of the thread that owns the reactor to allow this thread to run the reactor's event loop. Sidebar 18 (page 80) describes how to avoid deadlock when using a reactor in multithreaded applications. Figure 3.8 (page 85) presents a sequence diagram of the interactions among classes in the ACE Reactor framework. Additional coverage of the ACE Reactor framework's design appears in the Reactor pattern's Implementation section in Chapter 3 of POSA2. Figure 3.8. UML Sequence Diagram for the Reactive Logging Server
ExampleBefore we show the rest of the reactive networked logging server, let's quickly review the external behavior of the logging server and client developed in C++NPv1. The logging server listens on a TCP port number specified on the command line, defaulting to the port number specified as ace_logger in the OS network services file. For example, the following line might appear in the UNIX /etc/services file:
ace_logger 9700/tcp # Connection-oriented Logging Service Client applications can optionally specify the TCP port and the IP host name or address where the client application and logging server should rendezvous to exchange log records. If this information isn't specified, however, the port number is located in the services database, and the hostname is assumed to be the ACE _ DEFAULT _ SERVER _ HOST , which is defined as " localhost " on most OS platforms. The version of the logging server shown below offers the same capabilities as the Reactive_Logging_Server_Ex version in Chapter 7 of C++NPv1. Both servers run in a single thread of control in a single process, handling log records from multiple clients reactively. The main difference is that the version described here reuses the event detection, demultiplexing, and dispatching capabilities from the ACE Reactor framework. This refactoring removes the following application-independent code from the original Reactive_Logging_Server_Ex implementation: Handle-to-object mapping. Two data structures in Reactive_Logging_Server_Ex performed the following mappings:
Since the ACE Reactor framework now provides and maintains the code that manages handle-to-object mappings, the resulting application is smaller, faster, and makes much better use of the reusable software artifacts available in ACE. Event detection, demultiplexing, and dispatching. To detect both connection and data events, the Reactive_Logging_Server_Ex server used the ACE::select() synchronous event demultiplexer method. This design had the following drawbacks, however:
The new logging server reuses the ACE Reactor framework's ability to portably and efficiently detect, demultiplex, and dispatch I/O- and time-based events. This framework also allows the application to integrate signal handling if the need arises. With the application-independent code described above removed, an important maintenance problem with the original code is revealed. Although the code worked correctly, the Reactive_Logging_Server_Ex , Logging_Handler , handle-to- ACE_FILE_IO map and ACE_FILE_IO objects were loosely cohesive and tightly coupled . Changing the event handling mechanism therefore also required changes to all of the application-specific event handling code, which illustrates the negative effects of tangling application-specific code with (what should be) application-independent code. This resulted in a design that was hard to extend and maintain, which would add considerable cost to the logging server as it evolved over time. In contrast, the Logging_Event_Handler class (page 56) shows how the new reactive logging server separates concerns more effectively by combining ACE_FILE_IO with a Logging_Handler and registering the socket's handle with the reactor. This example show the following steps that developers can use to integrate applications with the ACE Reactor framework:
Figure 3.7 illustrates the reactive logging server architecture that builds on our earlier implementations from C++NPv1. This architecture enhances reuse and extensibility by decoupling the following aspects of the logging server: Figure 3.7. Architecture of the ACE_Reactor Logging Server
Our implementation begins in a header file called Reactor_Logging_Server.h , which includes several header files that provide the various capabilities we'll use in our ACE_Reactor -based logging server. #include "ace/ACE.h" #include "ace/Reactor.h" We next define the Reactor_Logging_Server class, which forms the basis for many subsequent logging server examples in this book: template <class ACCEPTOR> class Reactor_Logging_Server : public ACCEPTOR { public: Reactor_Logging_Server (int argc, char *argv[], ACE_Reactor *); }; This class inherits from its ACCEPTOR template parameter. To vary certain aspects of Reactor_Logging_Server 's connection establishment and logging behavior, subsequent examples will instantiate it with various types of acceptors, such as the Logging_Acceptor_Ex (pages 67, 96, and 101), the Logging_Acceptor_WFMO (page 113), the TP_Logging_Acceptor (page 193), and the TPC_Logging_Acceptor (page 227). Reactor_Logging_Server also contains a pointer to the ACE_Reactor that it uses to detect, demultiplex, and dispatch I/O- and time-based events to their event handlers. Reactor_Logging_Server differs from the Logging_Server class defined in Chapter 4 of C++NPv1 since Reactor_Logging_Server uses the ACE_Reactor::handle_events() method to process events via callbacks to instances of Logging_Acceptor and Logging_Event_Handler . Thus, the handle_connections() , handle_data() , and wait_for_multiple_events() , hook methods used in the reactive logging servers from C++NPv1 are no longer needed. The Reactor_Logging_Server template implementation resides in Reactor_Logging_Server_T.cpp . Its constructor performs the steps necessary to initialize the reactive logging server: 1 template <class ACCEPTOR> 2 Reactor_Logging_Server<ACCEPTOR>::Reactor_Logging_Server 3 (int argc, char *argv[], ACE_Reactor *reactor) 4 : ACCEPTOR (reactor) { 5 u_short logger_port = argc > 1 ? atoi (argv[1]) : 0; 6 ACE_TYPENAME ACCEPTOR::PEER_ADDR server_addr; 7 int result; 8 9 if (logger_port != 0) 10 result = server_addr.set (logger_port, INADDR_ANY); 11 else 12 result = server_addr.set ("ace_logger", INADDR_ANY); 13 if (result != -1) 14 result = ACCEPTOR::open (server_addr); 15 if (result == -1) reactor->end_reactor_event_loop (); 16 } Line 5 Set the port number that we'll use to listen for client connections. Line 6 Use the PEER_ADDR trait class that's part of the ACCEPTOR template parameter to define the type of server_addr . The use of traits simplifies the wholesale replacement of IPC classes and their associated addressing classes. Sidebar 19 explains the meaning of the ACE _ TYPENAME macro. Lines 9 “12 Set the local server address server_addr . Line 14 Pass server_addr to ACCEPTOR::open() to initialize the passive-mode endpoint and register this object with the reactor for ACCEPT events. Line 15 If an error occured, instruct the reactor to shut its event loop down so the main() function doesn't hang.
We conclude with the logging server's main() function, which resides in Reactor_Logging_Server.cpp : 1 typedef Reactor_Logging_Server<Logging_Acceptor_Ex> 2 Server_Logging_Daemon; 3 4 int main (int argc, char *argv[]) { 5 ACE_Reactor reactor; 6 Server_Logging_Daemon *server = 0; 7 ACE_NEW_RETURN (server, 8 Server_Logging_Daemon (argc, argv, &reactor), 9 1); 10 11 if (reactor.run_reactor_event_loop () == -1) 12 ACE_ERROR_RETURN ((LM_ERROR, "%p\n", 13 "run_reactor_event_loop()"), 1); 14 return 0; 15 } Lines 1 “2 Instantiate the Reactor_Logging_Server template with the Logging_Acceptor_Ex class (page 67) to create the Server_Logging_Daemon typedef . Lines 6 “9 Dynamically allocate a Server_Logging_Daemon object. Lines 11 “13 Use the local instance of ACE_Reactor to drive all subsequent connection and data event processing until an error occurs. The ACE _ ERROR _ RETURN macro and other ACE debugging macros are described in Sidebar 10 on page 93 of C++NPv1. Line 15 When the local reactor 's destructor runs at the end of main() it calls the Logging_Acceptor::handle_close() method (page 58) to delete the dynamically allocated Server_Logging_Daemon object. The destructor also calls Logging_Event_Handler_Ex::handle_close() (page 70) for each registered event handler to clean up the handler and shut the server down gracefully. Figure 3.8 illustrates the interactions in the example above. Since all event detection, demultiplexing, and dispatching is handled by the ACE Reactor framework, the reactive logging server implementation is much shorter than the equivalent ones in C++NPv1. In fact, the work involved in moving from the C++NPv1 reactive servers to the current server largely involves deleting code that's no longer needed, such as handle set and handle set management, handle-to- ACE_FILE_IO mapping, synchronous event demultiplexing, and event dispatching. The remaining application-defined functionality is isolated in classes inherited from the ACE Reactor framework. |
Ru-Brd |