4.4 The Initial Logging Server

I l @ ve RuBoard

Our first networked logging service implementation defines the initial design and the core reusable class implementations . The logging server listens on a TCP port number defined in the operating system's network services file as ace_logger , which is a practice used by many networked servers. For example, the following line might exist 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 address where the client application and logging server should rendezvous to exchange log records. If this information is not specified, however, the port number is located in the services database, and the host name is assumed to be the ACE_DEFAULT_SERVER_HOST, which is defined as " localhost " on most OS platforms. Once it's connected, the client sends log records to the logging server, which writes each record to a file. This section presents a set of reusable classes that handle passive connection establishment and data transfer for all the logging servers shown in this book.

4.4.1 The Logging_Server Base Class

Now that previous sections described ACE_Message_Block , ACE_Output_CDR , and ACE_InputCDR , we'll use these classes in a new base class that will simplify our logging server implementations throughout the book. Figure 4.4 illustrates our Logging_Server abstract base class, the Logging_Handler class we'll develop in Section 4.4.2, and the concrete logging server classes that we'll develop in future chapters. We put the definition of the Logging_Server base class into the following header file called Logging_Server.h :

Figure 4.4. Logging Server Example Classes

 #include "ace/FILE_IO.h" #include "ace/SOCK_Acceptor.h" // Forward declaration. class ACE_SOCK_Stream; class Logging_Server { public:   // Template Method that runs logging server's event loop.   virtual int run (int argc, char *argv[]); protected:   // The following four methods are ''hooks'' that can be   // overridden by subclasses.   virtual int open (u_short port = 0);   virtual int wait_for_multiple_events () { return 0; }   virtual int handle_connections () = 0;   virtual int handle_data (ACE_SOCK_Stream * = 0) = 0;   // This helper method can be used by the hook methods.   int make_log_file (ACE_FILE_IO &, ACE_SOCK_Stream * = 0);   // Close the socket endpoint and shutdown ACE.   virtual ~Logging_Server () {     acceptor_.close ();   }   // Accessor.   ACE_SOCK_Acceptor &acceptor () {     return acceptor_;   } private:   // Socket acceptor endpoint.   ACE_SOCK_Acceptor acceptor_; }; 

All the subsequent networked logging service examples will include this header file, subclass Logging_Server , and override and reuse its methods, each of which we describe below. The implementation file Logging_Server.cpp includes the following ACE header files:

 #include "ace/FILE_Addr.h" #include "ace/FILE_Connector.h" #include "ace/FILE_IO.h" #include "ace/INET_Addr.h" #include "ace/SOCK_Stream.h" #include "Logging_Server.h" 
The Logging_Server::run() Template Method

This public method performs the canonical initialization and event loop steps used by most of the logging servers in this book.

 int Logging_Server::run (int argc, char *argv[]) {   if (open (argc > 1 ? atoi (argv[1]) : 0) == -1)     return -1;   for (;;) {     if (wait_for_multiple_events () == -1) return -1;     if (handle_connections () == -1) return -1;     if (handle data () == -1) return -1;   }   return 0; } 

The code above is an example of the Template Method pattern [GHJV95], which defines the skeleton of an algorithm in an operation, deferring some steps to hook methods that can be overridden by subclasses. The calls to open(), wait_for_multiple_events(), handle_data() , and handle_connections() in the run() template method are all hook methods that can be overridden by subclasses.

The Logging_Server Hook Methods

Each hook method in the Logging_Server class has a role and a default behavior that's described briefly below.

Logging_Server::open(). This method initializes the server address and the Logging_Server 's acceptor endpoint to listen passively on a designated port number. Although it can be overridden by subclasses, the default implementation shown below suffices for all the examples in this book:

 int Logging_Server::open (u_short logger_port) {   // Raises the number of available socket handles to   // the maximum supported by the OS platform.   ACE::set_handle_limit ();   ACE_INET_Addr server_addr;   int result;   if (logger_port != 0)     result = server_addr.set (logger_port, INADDR_ANY);   else     result = server_addr.set ("ace_logger", INADDR_ANY);   if (result == -1)     return -1;   // Start listening and enable reuse of listen address   // for quick restarts.   return acceptor_.open (server_addr, 1); } 

Although an ACE_INET_Addr constructor accepts the port number and host name as arguments, we avoid it here for the following reasons:

  1. We allow the user to either pass a port number or use the default service name, which requires different calls

  2. Networking addressing functions such as gethostbyname() may be called, which can surprise programmers who don't expect a constructor to delay its execution until the server host address is resolved and

  3. We need to issue the proper diagnostic if there was an error.

Section A.6.3 on page 256 explains why ACE doesn't use native C++ exceptions to propagate errors.

Note the second argument on the acceptor_.open call. It causes the SO-REUSEADDR socket option to be set on the acceptor socket, allowing the program to restart soon after it has been stopped . This avoids any clashes with sockets in TIME_WAIT state left from the previous run that would otherwise prevent a new logging server from listening for logging clients for several minutes.

Logging_Server::wait_for_multiple_events(). The role of this method is to wait for multiple events to occur. It's default implementation is a no-op; that is, it simply returns 0. This default behavior is overridden by implementations of the logging server that use the select() -based synchronous event demultiplexing mechanisms described in Chapter 7.

Logging_Server::handle_connections(). The role of this hook method is to accept one or more connections from clients. We define it as a pure virtual method to ensure that subclasses implement it.

Logging_Server::handle_data(). The role of this hook method is to receive a log record from a client and write it to a log file. We also define it as a pure virtual method to ensure that subclasses implement it.

Sidebar 8: The ACE File Wrapper Facades

ACE encapsulates platform mechanisms for unbuffered file operations in accordance with the Wrapper Facade pattern. Like most of the ACE IPC wrapper facades families, the ACE File classes decouple the

  • Initialization factories, such as ACE_FILE_Connector , which open and/or create files, from

  • Data transfer classes, such as the ACE_FILE_IO , which applications use to read and write data to a file opened by ACE_FILE_Connector ,

The symmetry of the ACE IPC and file wrapper facades demonstrates the generality of ACE's design and provides the basis for strategizing IPC mechanisms into the higher-level ACE frameworks described in [SH].

The Logging_Server::make_log_file() Method

This helper method can be used by the hook methods to initialize a log file using the ACE File wrapper facades outlined in Sidebar 8.

 int Logging_Server::make_log_file (ACE_FILE_IO &logging_file,                                    ACE_SOCK_Stream *logging_peer) {   char filename[MAXHOSTNAMELEN + sizeof (".log")];   if (logging_peer != 0) { // Use client host name as file name.     ACE_INET_Addr logging_peer_addr;     logging_peer->get_remote_addr (logging_peer_addr);     logging_peer_addr.get_host_name (filename, MAXHOSTNAMELEN);     strcat (filename, ".log");   }   else     strcpy (filename, "logging_server.log");   ACE_FILE_Connector connector;   return connector.connect (logging_file,                             ACE_FILE_Addr (filename),                             0, // No time-out.                             ACE_Addr::sap_any, // Ignored.                             0, // Don't try to reuse the addr.                             O_RDWR  O_CREAT  O_APPEND,                             ACE_DEFAULT_FILE_PERMS); 

We name the log file " logging_server.log " by default. This name can be overridden by using the host name of the connected client, as we show on page 156 in Section 7.4.

Sidebar 9: The Logging Service Message Framing Protocol

Since TCP is a bytestream protocol, we need an application-level message framing protocol to delimit log records In the data stream. We use an 8-byte, CDR-encoded header that contains the byte-order Indication and payload length, followed by the log record contents, as shown below:



4.4.2 The Logging_Handler Class

This class is used in logging servers to encapsulate the I/O and processing of log records in accordance with the message framing protocol described in Sidebar 9. The sender side of this protocol implementation is shown in the Logging_Client::send() method on page 96, and the receiver side is shown in the Logging_Handler::recv_log_record() method on page 88.

The Logging_Handler class definition is placed in a header file called Logging_Handler.h .

 #include "ace/FILE_IO.h" #include "ace/SOCK_Stream.h" class ACE_Message_Block; // Forward declaration. class Logging_Handler { protected:   // Reference to a log file.   ACE_FILE_IO &log_file_;   // Connected to the client.   ACE_SOCK_Stream logging_peer_; public:   // Initialization and termination methods.   Logging_Handler (ACE_FILE IO &log_file)     : log_file_ (log_file) {}   Logging_Handler (ACE_FILE_IO &log_file,                    ACE_HANDLE handle)     : log_file_ (log_file) { logging_peer_.set_handle (handle); }   Logging_Handler (ACE_FILE_IO &log_file,                    const ACE_SOCK_Stream &logging_peer)     : log_file_ (log_file), logging_peer_ (logging_peer) {}   Logging_Handler (const ACE_SOCK_Stream &logging_peer)     : log_file_ (0), logging_peer_ (logging_peer) {}   int close () { return logging_peer_.close (); }   // Receive one log record from a connected client. Returns   // length of record on success and <mblk> contains the   // hostname, <mblk->cont()> contains the log record header   // (the byte order and the length) and the data. Returns -1 on   // failure or connection close.   int recv_log_record (ACE_Message_Block *&mblk);   // Write one record to the log file. The <mblk> contains the   // hostname and the <mblk->cont> contains the log record.   // Returns length of record written on success; -1 on failure.   int write_log_record (ACE_Message_Block *mblk);   // Log one record by calling <recv_log_record> and   // <write_log_record>. Returns 0 on success and -1 on failure.   int log_record ();   ACE_SOCK_Stream &peer () { return logging_peer_; } // Accessor. }; 

Below, we show the implementation of the recv_log_record(), write_ log_record() , and log_record() methods.

Logging_Handler::recv_log_record(). This method implements the receiving side of the networked logging service's message framing protocol (see Sidebar 9, page 86). It uses the ACE_SOCK_Stream, ACE_Message_Block , and ACE_InputCDR classes along with the extraction operator>> defined on page 79 to read one complete log record from a connected client. This code is portable and interoperable since we demarshal the contents received from the network using the ACE_InputCDR class.

 1 int Logging_Handler::recv_log_record (ACE_Message_Block *&mblk)  2 {  3   ACE_INET_Addr peer_addr;  4   logging_peer_.get_remote_addr (peer_addr);  5   mblk = new ACE_Message_Block (MAXHOSTNAMELEN + 1);  6   peer_addr.get_host_name (mblk->wr_ptr (), MAXHOSTNAMELEN);  7   mblk->wr_ptr (strlen (mblk->wr_ptr ()) + 1); // Go past name.  8  9   ACE_Message_Block *payload = 10     new ACE_Message_Block (ACE_DEFAULT_CDR_BUFSIZE); 11   // Align Message Block for a CDR stream. 12   ACE_CDR::mb_align (payload); 13 14   if (logging_peer_.recv_n (payload->wr_ptr (), 8) == 8) { 15     payload->wr_ptr (8);     // Reflect addition of 8 bytes. 16 17     ACE_InputCDR cdr (payload); 18 19     ACE_CDR::Boolean byte_order; 20     // Use helper method to disambiguate booleans from chars. 21     cdr  >> ACE_InputCDR::to_boolean (byte_order); 22     cdr.reset_byte_order (byte_order); 23 24     ACE_CDR::ULong length; 25     cdr  >>  length; 26 27     payload->size (length + 8); 28 29     if (logging_peer_.recv_n (payload->wr_ptr(), length) > 0) { 30       payload->wr_ptr (length);    // Reflect additional bytes. 31       mblk->cont (payload); 32       return length; // Return length of the log record. 33     } 34   } 35   payload->release (); 36   mblk->release (); 37   payload = mblk = 0; 38   return -1; 39 } 

Lines 37 We allocate a new ACE_Message_Block in which to store the logging_peer_ 's host name. We're careful to NUL-terminate the host name and to ensure that only the host name is included in the current length of the message block, which is defined as wr_ptr () rd_ptr() .

Lines 912 We create a separate ACE_Message_Block to hold the log record. The size of the header is known (8 bytes), so we receive that first. Since we'll use the CDR facility to demarshal the header after it's received, we take some CDR- related precautions with the new message block. CDR's ability to demarshal data portably requires the marshaled data start on an 8-byte alignment boundary. ACE_Message_Block doesn't provide any alignment guarantees , so we call ACE_CDR::mb_align() to force proper alignment before using payload->wr_ptr() to receive the header. The alignment may result in some unused bytes at the start of the block's internal buffer, so we must initially size payload larger than the 8 bytes it will receive ( ACE_DEFAULT_CDR_BUFSIZE has a default value of 512).

Lines 1415 Following a successful call to recv_n() to obtain the fixed- sized header, payload 's write pointer is advanced to reflect the addition of 8 bytes to the message block.

Lines 1725 We create a CDR object to demarshal the 8-byte header located in the payload message block. The CDR object copies the header, avoiding any alteration of the payload message block as the header is de-marshaled. Since the byte order indicator is a ACE_CDR::Boolean , it can be extracted regardless of the client's byte order. The byte order of the CDR stream is then set according to the order indicated by the input, and the length of the variable-sized log record payload is extracted.

Lines 2732 Now that we know the length of the log record payload, the payload message block is resized to hold the complete record. The second recv_n() call appends the remainder of the log record to the payload message block, immediately following the header. If all goes well, we update the write pointer in payload to reflect the added data, chain payload to mblk via its continuation field, and return the length of the log record.

Lines 3538 Error cases end up here, so we need to release the memory in mblk and payload to prevent leaks.

The recv_log_record() method makes effective use of a CDR stream and a message block to portably and efficiently receive a log record. It leverages the message block's continuation chain to store the host name of the client application, followed by the message block holding the log record.These two items are kept separate, yet are passed around the application as a single unit, as shown in Figure 4.5.

Figure 4.5. Message Block Chain of Log Record Information

Logging_Handler::write_log_record(). This method uses the ACE_FILE_IO, ACE Message_Block , and ACE_InputCDR classes together with the extraction operator>> defined on page 79 to format and write the host name and log record data received from a client into a log file.

 1 int Logging Handler::write_log_record (ACE Message Block *mblk)  2 {  3   if (log_file_->send_n (mblk) == -1) return -1;  4  5   if (ACE::debug ()) {  6     ACE_InputCDR cdr (mblk->cont ());  7     ACE_CDR::Boolean byte_order;  8     ACE_CDR::ULong length;  9     cdr >> ACE_InputCDR::to_boolean (byte_order); 10     cdr.reset_byte_order (byte_order); 11     cdr >> length; 12     ACE_Log_Record log_record; 13     cdr >> log_record; // Extract the <ACE_log_record>. 14     log_record.print (mblk->rd_ptr (), 1, cerr); 15   } 16 17   return mblk->total_length (); 18 } 

Line 3 The peer host name is in mblk and the log record is in mblk 's continuation chain. We use the send_n() method from ACE_FILE_IO to write all the message blocks chained through their cont() pointers. Internally, this method uses an OS gather-write operation to reduce the domain- crossing penalty . Since the log file is written out in CDR format, we'd need to write a separate program to read the file and display its values in a humanly readable format.

Lines 518 If we're in debugging mode, we build a CDR stream from the log record data and print its contents to cerr . Since the log record's header is still in the log record message block, the byte order must be extracted to ensure that the log record itself is demarshaled correctly. Although the header's length is not used, we extract it to ensure that the CDR stream is positioned correctly at the start of the log record when the extraction operator is called to demarshal the log record. The method returns the number of bytes written to the log file.

Logging_Handler::log_record(). This method uses both the recv_log_record() and the write_log_record() methods to read a log record from a socket and write it to a log file, respectively.

 int Logging_Handler::log_record () {   ACE_Message_Block *mblk = 0;   if (recv_log_record (mblk) == -1)     return -1;   else {     int result = write_log_record (mblk);     mblk->release (); // Free up the entire contents.     return result == -1 ? -1 : 0;   } } 

The release() call deletes the client host name in mblk and the log record in mblk->cont() .

4.4.3 The Iterative_Logging_Server Class

The following code shows how to implement an iterative logging server using the ACE Socket wrapper facades. This example subclasses the various hook methods in the Logging_Server base class to process client logging requests iteratively as follows :

  1. The handle_connections() hook method accepts a new connection from a client.

  2. The handle_data() hook method reads and processes log records from that client until the connection is closed or a failure occurs.

  3. Go back to step 1.

We first include the header files we'll need for this example.

 #include "ace/FILE_IO.h" #include "ace/INET_Addr.h" #include "ace/Log_Msg.h" #include "Logging_Server.h" #include "Logging_Handler.h" 

We next create an Iterative_Logging_Server class that inherits from Logging_Server and put this class into a header file called Iterative_Logging_Server.h . We define data members that receive log records from a client and write the data to a log file.

 class Iterative_Logging_Server : public Logging_Server { protected:   ACE_FILE_IO log_file_;   Logging_Handler logging_handler_; public:   Iterative_Logging_Server (): logging_handler_ (log_file_) {}   Logging_Handler &logging_handler () {     return logging_handler_;{   } protected:   // Other methods shown below... }; 

The open() hook method creates and/or opens a log file using the make_log_file() helper method defined on page 85. If the call to make_ log_file() fails, we use the ACE_ERROR_RETURN macro described in Sidebar 10 to print the reason and return a failure value. If it succeeds, we call our parent's open() hook to initialize the ACE_INET_Addr networking address where the logging server listens for client connections.

 virtual int open (u_short port) {   if (make_log_file (log_file_) == -1)     ACE_ERROR_RETURN ((LM_ERROR, "%p\n", "make_log_file()"),                       -l) ;   return Logging_Server::open (port); } 

The destructor closes down the log file, as shown below:

 virtual ~Iterative_Logging_Server () {   log_file_.close (); } 

Sidebar 10: ACE Debugging and Error Macros

ACE provides a set of macros that consolidate the printing of debugging and error messages via a printf() -like format. The most common macros are ACE_DEBUG, ACE_ERROR, and ACE_ERROR_RETURN, which encapsulate the ACE_Log_Msg::log() method. This method takes a variable number of arguments, so the first argument to all these macros is actually a compound argument wrapped in an extra set of parentheses to make it appear as one argument to the C++ preprocessor.

The first argument to the ACE_Log_Msg:: log() method is the severity code, for example, LM_ERROR for general errors and LM_DEBUG for diagnostic information. The next parameter is a format string that accepts most printf() conversion specifiers. ACE defines some additional format specifiers that are useful for tracing and logging operations, including

Format Action
%1 Displays the line number where the error occurred
%N Displays the filename where the error occurred
%n Displays the name of the program
%P Displays the current process ID
%p Takes a const char * argument and displays it and the error string corresponding to errno (like perror() )
%T Displays the current time
%t Displays the calling thread's ID

The ACE_ERROR_RETURN macro is a shortcut for logging an error message and returning a value from the current function, Hence, it takes two arguments: the first is the same as the other macros and the second is the value to return from the function after logging the message.

We can reuse the inherited wait_for_multiple_events() method since we're implementing an iterative server. Thus, the handle_connections() method simply blocks until it can accept the next client connection.

 virtual int handle_connections () {   ACE_INET_Addr logging_peer_addr;   if (acceptor ().accept (logging_handler_.peer (),                           &logging_peer_addr) == -1)     ACE_ERROR_RETURN ((LM_ERROR, "%p\n", "accept ()"), -1);   ACE_DEBUG ((LM_DEBUG, "Accepted  connection from %s\n",              logging_peer_addr.get_host_name ()));   return 0; } 

After the server accepts a new connection from a client, its handle_data() hook method continues to receive log records from that client until the connection closes or a failure occurs.

 virtual int handle_data (ACE_SOCK_Stream *) {   while (logging_handler_.log_record () != -1)     continue;   logging_handler_.close () ; // Close the socket handle.   return 0; } 

The main() function instantiates an Iterative_Logging_Server object and calls its run() method to process all client requests iteratively. This code is in file Iterative_Logging_Server.cpp .

 #include "ace/Log_Msg.h" #include "Iterative_Logging_Server.h" int main (int argc, char *argv[]) {   Iterative_Logging_Server server;   if (server.run (argc, argv) == -1)     ACE_ERROR_RETURN ((LM_ERROR, "%p\n", "server.run()"), 1);   return 0; } 

This example illustrates how much easier it is to program networked applications using ACE instead of the native C Socket API calls. All the Socket API's accidental complexities identified in Section 2.3 have been removed via the use of ACE wrapper facade classes. With this complexity removed, application developers are freed to focus on strategic application logic. Note that most of the source code we've shown is concerned with application-specific (de)marshaling and I/O code in the Logging_Handler methods shown in Section 4.4.1, and none of the "traditional" socket manipulation, buffer management, or byte ordering code found in applications programmed directly to the Socket API.

Despite its many improvements, however, our first logging server's implementation is limited by its iterative server design. Subsequent examples in this book illustrate how concurrency patterns and ACE wrapper facades help to overcome these problems. Since the logging server decouples the Logging_Server and Logging_Handler roles, we can modify its implementation details without changing its overall design.

I l @ ve RuBoard


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

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