7.3 The ACE_Acceptor Class

Ru-Brd

Motivation

Many 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 Capabilities

ACE_Acceptor is a factory that implements the Acceptor role in the Acceptor-Connector pattern [POSA2]. This class provides the following capabilities:

  • It decouples the passive connection establishment and service initialization logic from the processing performed by a service handler after it's connected and initialized .

  • It provides a passive-mode IPC endpoint used to listen for and accept connections from peers. The type of this IPC endpoint can be parameterized with many of ACE's IPC wrapper facade classes, thereby separating lower-level connection mechanisms from application-level service initialization policies.

  • It automates the steps necessary to connect the IPC endpoint passively and create/activate its associated service handler.

  • Since ACE_Acceptor is derived from ACE_Service_Object , it inherits the event-handling and configuration capabilities described in Chapters 3 and 5.

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

  • An SVC_HANDLER class, which provides an interface for processing services defined by clients , servers, or both client and server roles in peer-to-peer services. This parameter is instantiated by a subclass of the ACE_Svc_Handler class described in Section 7.2.

  • A PEER_ACCEPTOR class, which is able to accept client connections passively. This parameter is often specified as one of the ACE IPC wrapper facades, such as the ACE_SOCK_Acceptor described in Chapter 3 of C++NPv1.

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 :

Method

Description

ACE_Acceptor() open ()

Bind an acceptor's passive-mode IPC endpoint to a particular address, such as a TCP port number and IP host address, then listen for the arrival of connection requests .

ACE_Acceptor() close()

Close the acceptor's IPC endpoint and release its resources.

acceptor()

Returns a reference to the underlying PEER_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:

Method

Description

handle_input()

This template method is called by a reactor when a connection request arrives from a peer connector. It can use the three methods outlined below to automate the steps necessary to connect an IPC endpoint passively, and to create and activate its associated service handler.

make_svc_handler()

This factory method creates a service handler to process data requests emanating from its peer service handler via its connected IPC endpoint.

accept_svc_handler()

This hook method uses the acceptor's passive-mode IPC endpoint to create a connected IPC endpoint and encapsulate the endpoint with an I/O handle that's associated with the service handler.

activate_svc_handler()

This hook method invokes the service handler's open() hook method, which allows the service handler to finish initializing itself.

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

  1. It calls the make_svc_handler() factory method to create a service handler dynamically.

  2. It calls the accept_svc_handler() hook method to accept a connection and store it in the service handler.

  3. It calls the activate_svc_handler() hook method to allow the service handler to finish initializing itself.

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:

  • Creating service handlers based on some criteria, such as the number of CPUs available, a stored configuration parameter, calculated historical load average, or the current host workload.

  • Always returning a singleton service handler (page 247) or

  • Dynamically linking the handler from a DLL by using the ACE_Service_Config or ACE_DLL classes described in Chapter 5.

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.

Example

This 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);  }; 

Sidebar 51: An Overview of Authentication and Encryption Protocols

To protect against potential attacks or third-party discovery, many networked applications must authenticate the identities of their peers and encrypt sensitive data sent over a network. To provide these capabilities, various cryptography packages, such as OpenSSL [Ope01], and security protocols, such as Transport Layer Security (TLS) [DA99], have been developed. These packages and protocols provide library calls that ensure authentication, data integrity, and confidentiality between two communicating applications. For example, the TLS protocol can encrypt/decrypt data sent/received across a TCP / IP network. TLS is based on an earlier protocol named the Secure Sockets Layer (SSL), which was developed by Netscape.

The OpenSSL toolkit used by the examples in this chapter is based on the SSLeay library written by Eric Young and Tim Hudson. It is open source, under active development, and runs on multiple platforms, including most platforms ACE supports, such as Linux, FreeBSD, OpenBSD, NetBSD, Solaris, AIX, IRIX, HP-UX, OpenUNIX, DG/UX, ReliantUNIX, UnixWare, Cray T90 and T3E, SCO Unix, Microsoft Windows, and MacOS. OpenSSL is a highly successful open-source package, as evidenced by its extensive commercial and noncommerical user community.

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;  } 

Sidebar 52: ACE Wrapper Facades for OpenSSL

Although the OpenSSL API provides a useful set of functions, it suffers from the usual problems incurred by native OS APIs written in C (see Chapter 2 in C++NPv1). To address these problems, ACE provides classes that encapsulate OpenSSL using an API similar to the ACE C++ Socket wrapper facades. For example, the ACE_SOCK_Acceptor , ACE_SOCK_Connector , and ACE_SOCK_Stream classes described in Chapter 3 of C++NPv1 have their SSL-enabled counterparts: ACE_SSL_SOCK_Acceptor , ACE_SSL_SOCK_Connector , and ACE_SSL_SOCK_Stream .

The ACE SSL wrapper facades allow networked applications to ensure the integrity and confidentiality of data exchanged across a network. They also follow the same structure and APIs as their Socket API counterparts, which makes it easy to replace them wholesale using C++ parameterized types and the ACE_Svc_Handler template class. For example, to apply the ACE wrapper facades for OpenSSL to our networked logging server we can simply remove all the OpenSSL API code and instantiate the ACE_Acceptor , ACE_Connector , and ACE_Svc_Handler with the ACE_SSL_SOCK_Acceptor , ACE_SSL_SOCK_Connector , and ACE_SSL_SOCK_Stream , respectively.

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:

  1. It dynamically links the TPCLS DLL into the address space of the process.

  2. It uses the ACE_DLL class to extract the _make_TPC_Logging_Server() factory function from the TPCLS DLL symbol table.

  3. This function is called to allocate a TPC_Logging_Server dynamically and return a pointer to it.

  4. The ACE Service Configurator framework then calls TPC_Logging_Server::init() through this pointer, passing as its argc / argv argument an expansion of the TPC_LOGGING_SERVER_PORT environment variable that designates the port number where the logging server listens for client connection requests. The port number is ultimately passed down to the Reactor_Logging_Server constructor (page 83).

  5. If init() succeeds, the TPC_Logging_Server pointer is stored in the ACE_Service_Repository under the name "TPC_Logging_Server" .

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

  • Listen for and accept connections

  • Create and activate new service handlers

Our thread-per-connection logging server also reused classes from earlier solutions that provided the following capabilities:

  • Initialize the network listener address using the Reactor_Logging_Server_Adapter template defined in Section 5.2 (page 122).

  • Dynamically configure the logging server and run the ACE Reactor's event loop in the main() function.

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


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