Ru-Brd |
MotivationMany connection-oriented server applications tightly couple their connection establishment and service initialization code in ways that make it hard to reuse existing code. For example, if you examine the Logging_Acceptor (page 58), Logging_Acceptor_Ex (page 67), Logging_Acceptor_WFMO (page 112), CLD_Acceptor (page 176), and TP_Logging_Acceptor (page 193) classes, you'll see that the handle_input() method was rewritten for each logging handler, even though the structure and behavior of the code was nearly identical. The ACE Acceptor-Connector framework defines the ACE_Acceptor class so that application developers needn't rewrite this code repeatedly. Class CapabilitiesACE_Acceptor is a factory that implements the Acceptor role in the Acceptor-Connector pattern [POSA2]. This class provides the following capabilities:
The interface for ACE_Acceptor is shown in Figure 7.4. As shown in the figure, this class template is parameterized by: Figure 7.4. The ACE_Acceptor Class
Since ACE_Acceptor is a descendant of ACE_Event_Handler , an instance of it can be registered with the ACE Reactor framework to process ACCEPT events. Its handle_input() method will be dispatched automatically by a reactor when a new connection request arrives from a client. The ACE_Acceptor class has a flexible interface that can be customized extensively by application developers. We therefore group the description of its methods into the two categories described below. 1. Acceptor initialization, destruction, and accessor methods. The following methods are used to initialize and destroy an ACE_Acceptor :
A portion of ACE_Acceptor::open() is shown below: 1 template <class SVC_HANDLER, class PEER_ACCEPTOR> 2 int ACE_Acceptor<SVC_HANDLER, PEER_ACCEPTOR>::open 3 (const ACE_TYPENAME PEER_ACCEPTOR::PEER_ADDR &addr, 4 ACE_Reactor * = ACE_Reactor::instance (), 5 int flags = 0, 6 /* ... Other parameters omitted ... */) 7 { /* ... */ } Line 3 To designate the proper type of IPC addressing class, a PEER_ACCEPTOR template parameter must define a PEER_ADDR trait. Sidebar 5 in Chapter 3 in C++NPv1 illustrates how the ACE_SOCK_Acceptor class meets this criteria. Line 4 By default, the open() method uses the singleton ACE_Reactor to register the acceptor to handle ACCEPT events. This reactor can be changed on a per-instance basis, which is useful when a process uses multiple reactors, for example, one per thread. Line 5 The flags parameter indicates whether a service handler's IPC endpoint initialized by the acceptor should start in blocking mode (the default) or in nonblocking mode ( ACE _ NONBLOCK ). 2. Connection establishment and service handler initialization methods. The following ACE_Acceptor methods can be used to establish connections passively and initialize their associated service handlers:
Figure 7.5 (page 220) shows the default steps that an ACE_Acceptor performs in its handle_input() template method: Figure 7.5. Steps in ACE_Acceptor Connection Acceptance
Sidebar 47 (page 209) explains why the ACE_Acceptor::handle_input() template method decouples service handler creation from activation. ACE_Acceptor::handle_input() uses the Template Method pattern [GoF] to allow application designers to change the behavior of any of the three steps outlined above. The default behaviors of make_svc_handler() , accept_svc_handler() , and activate_svc_handler() can therefore be overridden by subclasses. This design allows a range of behavior modification and customization to support many use cases. The three primary variation points in ACE_Acceptor::handle_input() are described and illustrated in more detail below. 1. Service handler creation. The ACE_Acceptor::handle_input() template method calls the make_svc_handler() factory method to create a new service handler. The default implementation of make_svc_handler() is shown below: 1 template <class SVC_HANDLER, class PEER_ACCEPTOR> int 2 ACE_Acceptor<SVC_HANDLER, PEER_ACCEPTOR>::make_svc_handler 3 (SVC_HANDLER *&sh) { 4 ACE_NEW_RETURN (sh, SVC_HANDLER, -1); 5 sh->reactor (reactor ()); 6 return 0; 7} Line 4 Allocate an instance of SVC_HANDLER dynamically. The SVC_HANDLER constructor should initialize any pointer data members to NULL to avoid subtle run-time problems that can otherwise arise if failures are detected in ACE_Acceptor::handle_input() template method and the SVC_HANDLER must be closed. Line 5 Set the reactor of the newly created service handler to the same reactor associated with the acceptor. Subclasses can override make_svc_handler() to create service handlers in any way that they like, such as:
2. Connection establishment. The ACE_Acceptor::handle_input() template method calls the accept_svc_handler() hook method to passively accept a new connection from a peer connector. The default implementation of this method delegates to the PEER_ACCEPTOR::accept() method, as shown below: template <class SVC_HANDLER, class PEER_ACCEPTOR> int ACE_Acceptor<SVC_HANDLER, PEER_ACCEPTOR>::accept_svc_handler (SVC_HANDLER *sh) { if (acceptor ().accept (sh->peer ()) == -1) { sh->close (0); return -1; } return 0; } For this accept_svc_handler() implementation to compile, the PEER_ACCEPTOR template parameter must have a public accept() method. ACE_SOCK_Acceptor (Chapter 3 of C++NPv1) meets this requirement, as do most ACE IPC wrapper facades. Subclasses can override accept_svc_handler() to add extra processing required before or after the connection is accepted, but before it can be used. For example, this method can authenticate a new connection before activating the service. The authentication process could validate the peer's host and/or port number, perform a login sequence, or set up an SSL session on the new socket. The Example section on page 222 illustrates how to implement authentication by overriding the accept_svc_handler() hook method to perform SSL authentication before activating the service. Use caution, however, when performing exchanges with the peer in this situation. If activate_svc_handler() is called via a reactor callback, the application's entire event dispatching loop may block for an unacceptably long amount of time. 3. Service handler activation. The ACE_Acceptor::handle_input() template method calls the activate_svc_handler() hook method to activate a new service after it has been created and a new connection has been accepted on its behalf . The default behavior of this method is shown below: 1 template <class SVC_HANDLER, class PEER_ACCEPTOR> int 2 ACE_Acceptor<SVC_HANDLER, PEER_ACCEPTOR>::activate_svc_handler 3 (SVC_HANDLER *sh) { 4 int result = 0; 5 if (ACE_BIT_ENABLED (flags_, ACE_NONBLOCK)) { 6 if (sh->peer ().enable (ACE_NONBLOCK) == -1) 7 result = -1; 8 } else if (sh->peer ().disable (ACE_NONBLOCK) == -1) 9 result = -1; 10 11 if (result == 0 && sh->open (this) == -1) 12 result = -1; 13 if (result == -1) sh->close (0); 14 return result; 15 } Lines 59 The blocking/nonblocking status of a service handler is set to reflect the flags stored by the acceptor in its constructor. If the acceptor's flag_ designates nonblocking mode, we enable nonblocking I/O on the service handler's peer stream. Otherwise, the peer stream is set to blocking mode. Lines 1114 The service handler's open() hook method is called to activate the handler (see page 209 for an example). If service activation fails, the service handler's close() hook method is called to release any resources associated with the service handler. Subclasses can override activate_svc_handler() to activate the service in some other way, such as associating it with a thread pool or spawning a new process or thread to process data sent by clients. Since the ACE_Acceptor::handle_input() method is virtual it's possible (though rare) to change the sequence of steps performed to accept a connection and initialize a service handler. Regardless of whether the default ACE_Acceptor steps are used or not, the functionality of the service handler itself is decoupled completely from the steps used to passively connect and initialize it. This design maintains the modularity and separation of concerns that are so important to minimize the cost of the service's future maintenance and development. ExampleThis example is another variant of our server logging daemon. It uses the ACE_Acceptor instantiated with an ACE_SOCK_Acceptor to listen on a passive-mode TCP socket handle defined by the " ace_logger " service entry in the system's services database, which is usually /etc/services . This revision of the server uses the thread-per-connection concurrency model to handle multiple clients simultaneously . As shown in Figure 7.6, the main thread uses a reactor to wait for new connection requests from clients. When a connection arrives, the acceptor uses the OpenSSL [Ope01] authentication protocol outlined in Sidebar 51 (page 224) to ensure that the client logging daemon is permitted to connect with the server. If the client is legitimate , the acceptor dynamically creates a TPC_Logging_Handler from the Example part of Section 7.2 to handle the connection. The TPC_Logging_Handler::open() method (page 214) spawns a thread to process log records sent by the client over the connection. Figure 7.6. Architecture of the Thread-per-Connection Logging Server
Since much of the code is reused from the ACE Acceptor-Connector framework and OpenSSL library, this example mostly extends, instantiates, and uses existing capabilities. We subclass ACE_Acceptor and override its open() method and accept_svc_handler() hook method to define the server end of its authentication protocol. The TPC_Logging_Acceptor class and its protected data members are declared as follows : #include "ace/SOCK_Acceptor.h" #include <openssl/ssl.h> class TPC_Logging_Acceptor : public ACE_Acceptor<TPC_Logging_Handler, ACE_SOCK_Acceptor> { protected: // The SSL ''context'' data structure. SSL_CTX *ssl_ctx_; // The SSL data structure corresponding to authenticated // SSL connections. SSL *ssl_; The ssl_ctx_ and ssl_ data members are passed to the OpenSSL API calls made by the following public methods in TPC_Logging_Acceptor . public: typedef ACE_Acceptor<TPC_Logging_Handler, ACE_SOCK_Acceptor> PARENT; typedef ACE_SOCK_Acceptor::PEER_ADDR PEER_ADDR; TPC_Logging_Acceptor (ACE_Reactor *) : PARENT (r), ssl_ctx_ (0), ssl_ (0) {} // Destructor frees the SSL resources. virtual TPC_Logging_Acceptor (void) { SSL_free (ssl_); SSL_CTX_free (ssl_ctx_); } // Initialize the acceptor instance. virtual int open (const ACE_SOCK_Acceptor::PEER_ADDR &local_addr, ACE_Reactor *reactor = ACE_Reactor::instance (), int flags = 0, int use_select = 1, int reuse_addr = 1); // <ACE_Reactor> close hook method. virtual int handle_close (ACE_HANDLE = ACE_INVALID_HANDLE, ACE_Reactor_Mask = ACE_Event_Handler::ALL_EVENTS_MASK); // Connection establishment and authentication hook method. virtual int accept_svc_handler (TPC_Logging_Handler *sh); };
TPC_Logging_Acceptor::open() (in TPC_Logging_Server.cpp ) initializes itself using its base class implementation and establishes the server's identity as follows: 1 #include "ace/OS.h" 2 #include "Reactor_Logging_Server_Adapter.h" 3 #include "TPC_Logging_Server.h" 4 #include "TPCLS_export.h" 5 6 #if !defined (TPC_CERTIFICATE_FILENAME) 7 # define TPC_CERTIFICATE_FILENAME "tpc-cert.pem" 8 #endif /* !TPC_CERTIFICATE_FILENAME */ 9 #if !defined (TPC_KEY_FILENAME) 10 # define TPC_KEY_FILENAME "tpc-key.pem" 11 #endif /* !TPC_KEY_FILENAME */ 12 13 int TPC_Logging_Acceptor::open 14 (const ACE_SOCK_Acceptor::PEER_ADDR &local_addr, 15 ACE_Reactor *reactor, 16 int flags, int use_select, int reuse_addr) { 17 if (PARENT::open (local_addr, reactor, flags, 18 use_select, reuse_addr) != 0) 19 return -1; 20 OpenSSL_add_ssl_algorithms (); 21 ssl_ctx_ = SSL_CTX_new (SSLv3_server_method ()); 22 if (ssl_ctx_ == 0) return -1; 23 24 if (SSL_CTX_use_certificate_file (ssl_ctx_, 25 TPC_CERTIFICATE_FILENAME, 26 SSL_FILETYPE_PEM) <= 0 27 SSL_CTX_use_PrivateKey_file (ssl_ctx_, 28 TPC_KEY_FILENAME, 29 SSL_FILETYPE_PEM) <= 0 30 !SSL_CTX_check_private_key (ssl_ctx_)) 31 return -1; 32 ssl_ = SSL_new (ssl_ctx_); 33 return ssl_ == 0 ? -1 : 0; 34 } Lines 611 Since our logging server doesn't have a user interface, its server certificate and accompanying key are assumed to exist in a default set of files. However, the application may override the default filenames by defining the specified preprocessor macros. Lines 1718 Initialize the ACE_Acceptor using the default open() implementation. Line 20 Initialize the OpenSSL library. For brevity, we place the OpenSSL_add_ssl_algorithms() call in TPC_Logging_Acceptor::open() . Although this function can be called safely multiple times per process, ideally this call should be made only once per process. Initialization must be synchronized if multiple threads can call the function, however, since this OpenSSL function isn't thread safe. Lines 2122 Set up for SSL version 3connection and create the SSL structure corresponding connections to be authenticated. Lines 2431 Set up the certificate and accompanying private key used to identify the server when establishing connections and then verify that the private key matches the correct certificate. This code assumes that the certificate and key are encoded in Privacy Enhanced Mail (PEM) format within the specified files. Lines 3233 Initialize a new SSL data structure, which is used in the TPC_Logging_Acceptor::accept_svc_handler() hook method when establishing SSL connections via the OpenSSL API, as follows: 1 int TPC_Logging_Acceptor::accept_svc_handler 2 (TPC_Logging_Handler *sh) { 3 if (PARENT::accept_svc_handler (sh) == -1) return -1; 4 SSL_clear (ssl_); // Reset for new SSL connection. 5 SSL_set_fd 6 (ssl_, ACE_reinterpret_cast (int, sh->get_handle ())); 7 8 SSL_set_verify 9 (ssl_, 10 SSL_VERIFY_PEER SSL_VERIFY_FAIL_IF_NO_PEER_CERT, 11 0); 12 if (SSL_accept (ssl_) == -1 13 SSL_shutdown (ssl_) == -1) return -1; 14 return 0; 15 } Line 3 Accept the TCP connection using the default accept_svc_handler() implementation. Lines 46 Reset the SSL data structures for use with a new SSL connection. Lines 811 Configure the SSL data structures so that client authentication is performed and enforced when accepting an SSL connection. Line 12 Perform the actual SSL connection and negotiation. If authentication of the client fails, the SSL_accept() call will fail. Line 13 Shut down the SSL connection if authentication succeeds. Since we don't actually encrypt the log record data we simply communicate via the TCP stream from here on. If data encryption were required, we could use the ACE wrapper facades for OpenSSL described in Sidebar 52. By overriding the open() and accept_svc_handler() hook methods, we added authentication to our server logging daemon without affecting any other part of its implementation. This extensibility illustrates the power of the Template Method pattern used in the ACE_Acceptor class design. When our example service is shut down via the ACE Service Configurator framework, Reactor_Logging_Server_Adapter::fini() (page 123) will end up calling the following handle_close() method: int TPC_Logging_Acceptor::handle_close (ACE_HANDLE h, ACE_Reactor_Mask mask) { PARENT::handle_close (h, mask); delete this; return 0; }
This method calls ACE_Acceptor::handle_close() to close the listening acceptor socket and unregister it from the reactor framework. To avoid memory leaks, the method then deletes this object, which was allocated dynamically when the service was initialized. We finally create the TPC_Logging_Server type definition: typedef Reactor_Logging_Server_Adapter<TPC_Logging_Acceptor> TPC_Logging_Server; ACE_FACTORY_DEFINE (TPCLS, TPC_Logging_Server) We also use the ACE _ FACTORY _ DEFINE macro described in Sidebar 32 (page 136) to automatically generate the _make_TPC_Logging_Server() factory function, which is used in the following svc.conf file: dynamic TPC_Logging_Server Service_Object * TPCLS:_make_TPC_Logging_Server() "$TPC_LOGGING_SERVER_PORT" This file directs the ACE Service Configurator framework to configure the thread-perconnection logging server via the following steps:
The various *Logging_Acceptor* classes written by hand for our previous logging server examples are no longer needed. Their purpose has been subsumed by TPC_Logging_Acceptor , which inherits from ACE_Acceptor . The first template argument to the ACE_Acceptor base class is TPC_Logging_Handler , which derives from ACE_Svc_Handler . In the TPC_Logging_Server service, the ACE Acceptor-Connector framework performs most of the basic work of authenticating a client connection, as well as initializing and operating a service handler. Since the ACE_Acceptor refactors the functionality of accepting connections, authenticating clients, and activating service handlers into well-defined steps in its handle_input() template method, the source code for our logging server daemon shrank considerably. In particular, ACE_Acceptor supplied all of the code we previously had to write manually to
Our thread-per-connection logging server also reused classes from earlier solutions that provided the following capabilities:
Therefore, the only code of any consequence we had to write was the TPC_Logging_Handler class (page 213), which spawned a thread per connection to receive and process Zlog records, and the TPC_Logging_Acceptor::accept_svc_handler() method (page 222), which implemented the server end of the protocol that authenticates the identity of the client and server logging daemons. Once again, due to the flexibility offered by the ACE Service Configurator framework, we simply reuse the Configurable_Logging_Server main program from Chapter 5. |
Ru-Brd |