libASSA Programmer's Manual | ||
---|---|---|
<<< Previous | Chapter 2. Writing Network Applications | Next >>> |
assa-logd accepts connection from client applications with the help of Acceptor class. For in-depth discussion of Acceptor pattern, please, refer to the article "Acceptor and Connector: A Family of Object Creational Patterns for Initializing Communication Services" by Douglas C. Schmidt [MartinBuschmannRiehle97].
An abstract class Acceptor implements the generic strategy for passively initializing communication services. This parameterized class separates cleanly data communication code from connection acceptance code.
Example 2-1. Class Acceptor definition.
template<class SERVICE_HANDLER, class PEER_ACCEPTOR> class Acceptor : public EventHandler { public: Acceptor (Reactor* r_); virtual ~Acceptor (); virtual int open (const Address& local_addr_); // ... other methods ... private: PEER_ACCEPTOR m_listenSocket; }; |
Acceptor is parameterized both by a particular type of SERVICE_HANDLER and PEER_ACCEPTOR. Parameterized types are used to decouple the connection establishment strategy from the type of service handler, network programming interface, and transport layer connection initiation protocol.
The PEER_ACCEPTOR provides the transport mechanism used by the Acceptor to passively establish the connection. In case of TCP/IP communication, the value of PEER_ACCEPTOR is IPv4Socket.
This class defines a generic interface for a service. The ServiceHandler contains a communication endpoint (m_peerStream) that encapsulates an I/O handle (i.e. BSD socket file descriptor). This endpoint can be initialized by either Acceptor or Connector and is subsequently used by the ServiceHandler to exchange data with the peer. Both Acceptor and Connector activate ServiceHandler by calling its open() hook when connection is established. Once a ServiceHandler is completely initialized, it typically does not interact with its initializer.
001 template<class PEER_STREAM> 002 class ServiceHandler : public EventHandler 003 { 004 public: 005 ServiceHandler (PEER_STREAM* ps_); 006 ServiceHandler (); 007 008 virtual ~ServiceHandler (); 009 010 virtual int open (void) = 0; 011 virtual void close (void); 012 013 operator PEER_STREAM& (); 014 PEER_STREAM& get_stream (); 015 // ... 016 017 protected: 018 PEER_STREAM* m_peerStream; 019 }; |
Line 5 declares a constructor used by derived class on a server side. It is connected using Acceptor class. Acceptor creates ServiceHandler object passing a newly created peer connection endpoint to it.
Line 6 declares a defalult constructor used by derived class on a client side. It is connected using Connector class.
Line 10 declares pure virtual function open() used to notify derived class that connection to the peer has been established.
Line 11 declares virtual function close() used to notify derived class that asynchronous connection establishment by Connector has failed (see Section 2.3 for details).
Parameter PEER_STREAM can be any class derived from the base class Socket. In particular, for TCP/IP communication, it takes the form of IPv4Socket class for communicating with TCP peers over IP.
To communicate with a peer, you are expected to derive your class from ServiceHandler, overload pure virual function open(). The derived class can be used by either Acceptor or Connector to establish a connection with a peers.
The client-side constructor instantiates an object of type PEER_STREAM on the heap. The PEER_STREAM object is passed as a parameter to the server-side constructor. Either way, ServiceHandler owns the PEER_STREAM object, and its destructor deletes the object. Never delete PEER_STREAM object yourself!
Deleting twice in C++ is a serious error; the behavior is undefined and most likely disastrous. |
We are going to introduce a new class, Conn, to represent a connection with the peer. After connection establishment by Acceptor, handling actual data transfer is delegated to this class.
// logserver/server/Conn.h 0023 #include <assa/ServiceHandler.h> 0024 #include <assa/IPv4Socket.h> 0025 0026 using ASSA::ServiceHandler; 0027 using ASSA::IPv4Socket; 0028 0029 class Conn : public ServiceHandler<IPv4Socket> 0030 { 0031 public: 0032 Conn (IPv4Socket* stream_) : 0033 ServiceHandler<IPv4Socket> (stream_) { /* no-op */ } 0034 0035 ~Conn () { /* no-op */ } 0036 0037 virtual int open (); 0038 0039 virtual int handle_read (int fd_); 0040 virtual int handle_close (int fd_); 0041 }; |
Our Conn class is derived from ServiceHandler, which in its own turn is derived from EventHandler.
Line 32 defines constructor that initializes the parent class, ServiceHandle, with the socket object.
Line 37 overloads pure virtual function open() derived from ServiceHandler.
Lines 39 and 40 overload two virtual functions derived from EventHandler. They enable class Conn to exchange data with a peer on the other end of connection.
First, we are going to show how class Conn is used in our logserver application. Then we will examine step-by-step its implementation broken down by functionality phases.
This step creates a passive mode endpoint (listening socket) that is bound to the network address (such as an IP address and a port number). This passive-mode endpoint is then listens for new connection requests from the peers.
// logserver/server/LogServer.h #include <assa/Acceptor.h> #include <assa/IPv4Socket.h> class Conn; class LogServer { ... private: ASSA::Acceptor<Conn, ASSA::IPv4Socket>* m_acceptor; }; |
We added necessary header files and a data member, m_acceptor to hold a pointer to the Acceptor object.
Acceptor object is instantiated and registered with Reactor for event notification during the server's initialization phase.
// logserver/server/LogServer.cpp 0020 #include <assa/INETAddress.h> 0021 #include "Conn.h" ... 0033 LogServer:: 0034 LogServer () : 0035 m_exit_value (0), 0036 m_acceptor (NULL) 0037 { 0038 .... 0070 } ... 0072 void 0073 LogServer:: 0074 initServer () 0075 { 0076 trace("LogServer::initServer"); 0077 0078 m_acceptor = new ASSA::Acceptor<Conn, ASSA::IPv4Socket> (getReactor ()); 0079 ASSA::INETAddress lport (getPortName ().c_str ()); 0080 Assert_exit (!lport.bad ()); 0081 Assert_exit (m_acceptor->open (lport) == 0); 0082 0083 DL((ASSA::APP,"Service has been initialized\n")); 0084 } |
Lines 20 and 21 add appropriate header files.
Line 36 in the constructor initalizes the pointer to Acceptor.
Line 78 instantiates an Acceptor object. Its first template parameter is class Conn which would handle connection once it has been established.
We create listening port address with INETAddress and test its validity on lines 79-80.
Line 81 opens listening socket for incoming connection requests from peers. We are ready to accept connection requests.
Always instantiate Acceptor<> object on a heap with operator new() and never delete it yourself. Reactor object owns all of your Acceptor<> objects. |
The collaboration diagram illustrates the message passing between classes involved in initServer().
Service initialization phase activates ServiceHandler associated with the passive-mode endpoint. When new connection is requested, the Reactor notifies Acceptor about incoming connection request. Acceptor performs the strategy for initializing ServiceHandler. The default steps involved are:
Instantiate new concrete ServiceHandler object.
Accept new connection.
Activate ServiceHandler object by calling its open() hook to let it execute service-specific initialization.
Graphically these steps can be represented as:
We provide implementation of open() function according the diagram.
Example 2-2. Registering EventHandler with Reactor
// logserver/server/Conn.cpp 0005 #include "LogServer.h" ... 0010 int Conn::open () 0011 { 0012 trace("Conn::open"); 0013 0014 ASSA::IPv4Socket& s = *this; 0015 REACTOR->registerIOHandler (this, s.getHandler (), ASSA::READ_EVENT); 0016 0017 return 0; 0018 } |
Line 5 includes LogServer header file to get definition of REACTOR.
Line 14 presents libassa idiom of getting a hold of PEER_STREAM object of ServiceHandler (you can also use get_stream() instead).
On line 15, Conn object registers itself with Reactor to be notified of READ_EVENT events (bytes arrived and ready to be read) on the socket PEER_STREAM.
Line 17 returns 0 to the fuction caller indicating that Conn accepted connection. You can reject connection acceptance by returning -1 here as well.
Once the passive connection has been established and service has been initialized, service processing phase begins. In this phase, ServiceHandler communicates with its peer by exchanging chunks of bytes that hopefully carry meaningful data.
As was mentioned before, during initialization phase Conn object registers with Reactor for notification of READ_EVENT event. When data is detected on the endpoint, Conn::handle_read() function is called by Reactor. We have more to say on the subject of data exchange between peers in the next chapter. For now, our evolving logging server gets a message from its peer and writes it to the log file.
// logserver/server/Conn.cpp 0048 int Conn::handle_read (int fd_) 0049 { 0050 trace("Conn::handle_read"); 0051 0052 ASSA::IPv4Socket& s = *this; 0053 if (s.getHandler () != fd_) { return (-1); } 0054 0055 int ret = 0; 0056 char buf [256]; 0057 0058 if ((ret = s.read (buf, 256)) < 0) { 0059 DL((ASSA::ERROR,"Error reading from socket\n")); 0060 return (-1); 0061 } 0062 0063 if (ret > 0) { 0064 /* 0065 * Process data received 0066 */ 0067 ASSA::MemDump::dump_to_log (ASSA::APP, "=> Got new message", buf, ret); 0068 } 0069 0070 return s.eof () ? -1 : s.in_avail (); 0071 } |
Line 53 checks to see if we are serving the right socket file descriptor.
Line 55 reads up to 256 bytes from the socket. If an error occurs on read, we report an error here and bail out.
Lines 63 through 68 dump data we've received to the log file both in ASCII and HEX format.
Line 70 is an idiom for proper returning from handle_read(). We always test for EOF condition of the socket and also report number of unread bytes still available in the internal socket buffer left.
UML diagram illustrates these steps:
As would be explained later (see Figure 2-10 for details), N >= M.
When remote peer drops connection, the process goes in reverse, and ServiceHandler object eventually destroys itself. This condition occurs when handle_read() returns 0 bytes read. Reactor marks ServiceHandler for destruction by calling its handle_close() function.
// logserver/server/Conn.cpp 0075 int Conn::handle_close (int /* fd */) 0076 { 0077 trace("Conn::handle_close"); 0078 0079 delete (this); 0080 return 0; 0081 } |
Line 79 self-destructs the objet Conn.
To illustrate with the diagram:
We have finished writing the minimal amount of code to get data from the peer. Our logging server can now handle multiple peers, each represented by Conn object that desroys itself upon peer disconnect. The inner class dependency is illustrated by the class diagram:
We are ready to build and test our first version of assa-logd. To test, have two xterm windows open: one for monitoring the server; another for the client. We haven't written a client yet, so we are going to use telnet to connect to our server and send ASCII messages instead.
Before you attempt to run the daemon, make sure your /etc/services has appropriate service names bound to the listening port numbers. We use ports 1000 and 1001, but they can be any other numbers so long that they are not used by some other daemons and their values is greater then 1024.
// /etc/services # Local services assalogd 10000/tcp # libASSA log server (clients) assalmon 10001/tcp # libASSA log server (monitors) |
In one window, start the daemon and monitor its log file:
% assa-logd --daemon --port=assalogd % tail -f LogServer.log [GenServer::initInternals] || Server configuration settings || [GenServer::initInternals] ================================================== [GenServer::initInternals] cmd_line_name = 'assa-logd' [GenServer::initInternals] name = 'lt-assa-logd' [GenServer::initInternals] std cfg fname = '/home/vlg/.assa-logd.conf' [GenServer::initInternals] alt cfg fname = '' [GenServer::initInternals] debug_mask = 0x22 [GenServer::initInternals] ================================================== [GenServer::initInternals] [LogServer::initServer] Service has been initialized |
From another terminal window, connect to the server and send it an ASCII string:
% telnet dedalus assalogd Trying 192.168.1.2... Connected to dedalus. Escape character is '^]'. It works! |
At this point, you should see in your first terminal window something along these lines:
| | | | [Conn::handle_read] (11 bytes) => Got new message | | | | [Conn::handle_read] 4974 2077 6f72 6b73 210d 0a It works!\r\n |
To terminate the server:
% kill `cat ~/.assa-logd.pid` |
You can alleviate the pain of typing repetevive commands to start and stop server with appropriate shell scripts. See (Section 4.15) for details.
<<< Previous | Home | Next >>> |
Writing Network Applications | Up | Connecting with Connector |