7.5 Implementing the PPP Daemon
Now on to the business of making our "remote data logger" truly remote. We'll accomplish this by adding support for establishing dial-up networking connections to our logger using the PPP network interface and supporting API classes. At this point we're going to bring a second server into the picture, which could become confusing. The top-level network server is what we implemented in the DataLogger class in Section 7.2. It blocks on accept, waiting for a connection over any network interface. It doesn't really care if the connection is established over an Ethernet network or a serial line using PPP. The server we'll implement in this section is a "dial-up" server that allows clients to establish TCP/IP connections to TINI using a PPP interface. For the sake of brevity, we'll just refer to the dial-up server as the "server." However, when both servers are a part of the discussion context, we'll explicitly refer to the "dial-up server."
We'll implement our dial-up server in a class named PPPDaemon. A portion of the PPPDaemon class is shown in Listing 7.10. PPPDaemon implements two interfaces: PPPEventListener to receive PPP event notification and DataLinkListener to receive notification about errors that occur with the physical data link. In this section, we won't get too concerned about the details of the underlying physical link and whether the connection is established over a hard-wired serial link or using modems. Then next section will deal with the low-level data link handling issues.
On construction PPPDaemon requires an instance of a class that implements the PPPDaemonListener interface shown in Listing 7.11. The daemonError interface method is invoked by PPPDaemon to provide asynchronous notification of a PPP or data link error to the listener. The isValidUser method is invoked after the server has received the client's login information. This gives the listener the final say on whether a PPP connection is accepted or rejected.
Listing 7.10 PPPDaemon
import java.io.*; import com.dalsemi.tininet.ppp.*; public class PPPDaemon implements PPPEventListener, DataLinkListener { private PPP ppp; private PPPDataLink dataLink; private int maxRetries; private PPPDaemonListener listener; ... public PPPDaemon(PPPDaemonListener listener, String portName, int speed) throws PPPException { this(listener, portName, speed, 3, true); } public PPPDaemon(PPPDaemonListener listener, String portName, int speed, int maxRetries, boolean modemLink) throws PPPException { this.listener = listener; this.maxRetries = maxRetries; try { if (modemLink) { dataLink = new PPPModemLink(portName, speed, this); } else { dataLink = new PPPSerialLink(portName, speed, this); } } catch (DataLinkException dle) { throw new PPPException("Unable to initialize PPPDaemon:" + dle.getMessage()); } ppp = new PPP(); ppp.setLocalAddress(new byte[] {(byte) 192, (byte) 168, 1, 1}); ppp.setRemoteAddress(new byte[] {(byte) 192, (byte) 168, 1, 2}); ppp.setAuthenticate(true); } ... public void dataLinkError(String error) { System.err.println("Error in data link:"+error); ppp.close(); } }
After initializing the listener and maxRetries fields, PPPDaemon's constructor creates an object to manage the physical data link. It creates either a PPPSerialLink or a PPPModemLink object, depending on the modemLink boolean passed to the constructor. Both of these classes and the PPPDataLink interface they implement will be covered in detail in the next section. For now it's sufficient to know that by using the PPPDataLink object, the daemon can initialize the link and obtain a reference to its underlying serial port. From this point forward the daemon doesn't care if the physical link is over a hard-wired serial connection or a modem.
Next, a new PPP object is created and the IP addresses for both the local interface and the remote peer are set.
Listing 7.11 PPPDaemonListener interface
public interface PPPDaemonListener { public void daemonError(String error); public boolean isValidUser(String name, String password); }
It is easiest to understand the operation of PPPDaemon as a Finite State Machine (FSM). The state diagram for the FSM implemented by the PPPDaemon class is shown in Figure 7.1. The solid lines represent state transitions caused by PPPDaemon invoking methods on its PPP object. The dashed lines represent transition caused by errors detected by the native PPP implementation.
Note that there are actually two finite state machines at work here: the true PPP state machine3 that is implemented as a part of the network stack beneath the IP module (see Figure 5.1) and the high-level state machine implemented by PPPDaemon, whose state transitions are driven by events generated by the PPP daemon thread and method invocations on a PPP object. The low-level PPP state machine is very complex and has several additional states. For the most part, the arcane details of its implementation are hidden from the application developer by the PPP class. The purpose of the PPPEventListener interface is to provide a mechanism to drive a much simpler, higher-level state machine that gives the application an opportunity to control the physical data link, user authentication, and the handling of error information.
Figure 7.1 PPP daemon FSM
After creating a new PPP object, PPPDaemon is in the INIT state. At this point, there is no PPP traffic traveling across the physical data link. To transition to the STARTING state, the owner of the PPPDaemon object invokes the startDaemon method shown in Listing 7.12. startDaemon adds its own object (this) as a listener for PPP events and invokes the open method on its PPP object.
Listing 7.12 StartDaemon
public void startDaemon() throws PPPException { retryCount = 0; try { // Add PPP event listener to driver state machine ppp.addEventListener(this); } catch (java.util.TooManyListenersException le) { throw new PPPException("Unable to add event listener"); } ppp.open(); } public void stopDaemon() { // Don't receive any more PPP events ppp.removeEventListener(this); ppp.close(); }
The bulk of the FSM is implemented in the pppEvent method shown in Listing 7.13. pppEvent is invoked by a daemon thread that is created during construction of the PPP object. It is passed a PPPEvent object that is used to determine the event type. The pppEvent method switches on the event type to determine the next appropriate action. The event processing usually completes by invoking a method on a PPP object forcing another state transition. The possible events were described in the previous chapter and are listed here for convenience.
STARTING
AUTHENTICATION_REQUEST
UP
STOPPED
CLOSED
The STARTING state provides the application with a chance to initialize the physical data link. Our sample PPP daemon implementation does so using the initializeLink method defined in the PPPDataLink (Listing 7.14) interface. If initializeLink returns normally, the server invokes the up method on its PPP object, passing it a reference to the serial port. All PPP traffic flows over this port. This is really a handoff of serial port ownership. Once the port reference is passed to PPP, it assumes exclusive access to the serial port. If initializeLink fails to bring up the link successfully for any reason, it throws a DataLinkException, which is caught, and close is invoked on the PPP object. This will cause the notifier thread to generate a CLOSED event transitioning PPPDaemon to the CLOSED state.
At this point, PPP waits for a client to begin LCP (Line Control Protocol) negotiation. Once a client successfully completes the line negotiation, PPP requests login information and the remote peer replies with a user name and password. This generates an AUTHENTICATION_REQUESTED event (the AUTH state in Figure 7.1), and pppEvent gets the user name and password for the PPP object and passes them to the listener's isValidUser method. If the listener likes the login information, PPP completes its negotiation with the client, establishing the IP addresses for both the local and remote peer, and generates an UP event. pppEvent then invokes addInterface on the PPP object, which adds a new network interface to the OS.
Now the communication link is fully established and ready for IP traffic. If the listener didn't like the login information, a STOPPED event is generated, and the retryCount, which is used to track errors, is incremented. A STOPPED event can also be generated by the remote peer breaking the connection. Regardless of how we transitioned to the STOPPED state, we'll invoke close on the PPP object to generate a CLOSED event. This gives both the underlying PPP object and our daemon a chance to perform an orderly shutdown of the connection. If the connection had been fully established (that is, it had at some point transitioned to the UP state), then we'll invoke down on the PPP object and remove the network interface that was added during the UP state processing.
Listing 7.13 pppEvent
... private int retryCount; public void pppEvent(PPPEvent ev) { switch (ev.getEventType()) { case PPPEvent.STARTING: try { // Now we need to bring up the physical link dataLink.initializeLink(); ppp.up((SerialPort) dataLink.getPort()); } catch (DataLinkException dle) { listener.serverError("Data link error:"+ dle.getMessage()); ppp.close(); } break; case PPPEvent.AUTHENTICATION_REQUEST: ppp.authenticate(listener.isValidUser(ppp.getPeerID(), ppp.getPeerPassword())); break; case PPPEvent.UP: // Reset error count after successfully bringing // up connection retryCount = 0; ppp.addInterface("ppp0"); isUp = true; break; case PPPEvent.STOPPED: ppp.close(); if (++retryCount < maxRetries) { ppp.close(); } else { listener.serverError( "Unable to establish PPP connection"); } break; case PPPEvent.CLOSED: if (isUp) { ppp.removeInterface("ppp0"); ppp.down(); isUp = false; } try { // Sleep before recycling ppp connection Thread.sleep(1000); } catch (InterruptedException ie) {} ppp.open(); break; default: break; } }
The state machine as implemented in Listing 7.13 is designed to run continuously, retrying if transient errors occur. Every time a connection is successfully established (the UP state is reached), the error count is reset to 0. Unless a maximum retry count (maxRetries) is reached, the daemon continues to run. Once the error count threshold is reached, the listener is notified that a persistent problem is preventing the daemon from establishing PPP connections. The listener can choose to either stop the daemon entirely by invoking stopDaemon or take some action to fix the problem and recycle the server by stopping and restarting it. The problem may be with the modem or phone line and may require some human intervention.