HomeDigital EditionSys-Con RadioSearch Java Cd
Advanced Java AWT Book Reviews/Excerpts Client Server Corba Editorials Embedded Java Enterprise Java IDE's Industry Watch Integration Interviews Java Applet Java & Databases Java & Web Services Java Fundamentals Java Native Interface Java Servlets Java Beans J2ME Libraries .NET Object Orientation Observations/IMHO Product Reviews Scalability & Performance Security Server Side Source Code Straight Talking Swing Threads Using Java with others Wireless XML
 

Consider an Internet client that wants to connect to a site which allows access only to trusted clients. Consider a trusted client that has access to the site. Wouldn't it be great if the trusted client could relay the Internet client's data to the restricted-access site? In other words, it could act as a "channel", or a "router", for a restricted site.

This article describes JTRouter - a multi-threaded Java program that acts as a tunnel for socket communication between an Internet client and a remote server. JTRouter allows a machine to initiate as well as accept Internet connections in independent threads. It is a unique implementation in that both the client and the server roles are implemented in the same process.

JDJ has published several articles that provide an excellent introduction to client/server programming in Java using sockets and threads and there are several texts that cover these topics in detail. This article won't focus on re-introducing these concepts. I assume here that you are familiar with basic networking and threading in Java. A table of the main Java API classes used in this program is shown in Table 1. In subsequent sections, we will examine the JTRouter program which uses these classes to develop a practical application. Later, we will see how to run the program and what kind of applications it may be used in.

Table 1

Before diving into the nuts and bolts of JTRouter, I would like to mention how much easier it has been to develop this program in Java than in a language like C or C++. The entire JTRouter program consists of less than 350 lines of code! Some of its original error-checking capabilities and functionality were stripped down to a version that could fit in this article, but even the full-fledged program has less than 500 lines of code. I shudder to think of how many painful hours of debugging I would have spent if I had to develop JTRouter in a language that did not have the rich support for threading and networking that Java does.

Program Description
This program was written for a client who wanted to provide Internet access to his customer's site. The customer wanted to provide the access but did not want to run a Web site directly since he didn't have any security measures in place. The solution we decided on was to provide an intermediate server written in Java that would selectively route data from this customer to Internet clients and vice versa.

The "server" does little more than just play the role of a traditional server in a client/server paradigm. It also initiates connections to the customer site. In that sense, it is playing the role of a client. To avoid confusion, let us define the entities in this network. The Internet client is referred to as the Client. The intermediate server is the JTRouter and the customer is the Remote Host. Thus, JTRouter is a server for the Client but acts as a client for Remote host.

The relationship between the three entities is illustrated in Figure 1. JTRouter has a ServerSocket that listens for connection requests. Client 1 and Client 2 make connection requests to JTRouter. JTRouter provides a Java utility for "piping" the output of one socket to the input of the other socket and vice versa. This is called a JTunnel. The JTRouter ServerSocket accepts the connections and creates JTunnels for routing information to and from the Remote Host.

Figure 1
Figure 1:

JTRouter is implemented as a single process that listens for Internet connection requests from clients and, upon receiving one, opens a socket connection with the Remote Host. It then routes the data from the client to the Remote Host and back from the Remote Host to the client. At the same time, it continues to listen for other Internet requests. JTRouter is also responsible for detecting lost or disconnected channels and cleaning up after them.

JTRouter defines four Java classes to achieve its purpose. These are PipedSocketStream, JTunnel. JTServer, and JTListener. Detailed descriptions of these classes and the JTRouter program follow.

PipedSocketStream
Java provides a set of stream-based and socket-based classes in the java.io and java.net packages respectively for writing networking applications. Among the stream-based classes are the PipedInputStream and PipedOutputStream classes. The purpose of these two classes is to get input from one stream (PipedInputStream) and write it to another stream (PipedOutputStream). The two streams are connected together to form a "pipe".

The Socket class in Java uses two methods, getInputStream() and getOutputStream(), that provide the basic objects for stream-based socket communications. The Socket reads from one stream (e.g., InputStream returned by getInputStream()) and writes out via the other stream (e.g., OutputStream returned by getOutputStream()).

The PipedSocketStream class combines the functionality of the "piped-stream" classes and the Socket class. Listing 1 shows PipedSocketStream. The constructor takes two Socket arguments. It obtains an input stream from s1 and an output stream from s2 as shown in the code snippet below:

dis = new DataInputStream(s1.getInputStream());
dos = new DataOutputStream(s2.getOutputStream());

Notice that PipedSocketStream implements the Runnable interface. So it can start as an independent thread that relays data in one direction. The run() method in Listing 1 has a simple while loop that calls readAndWrite(). The method readAndWrite() reads a character from the Socket s1's input stream (dis) and writes it out to Socket s2's output stream. readAndWrite() returns a true if it successfully completes both operations and a false if it fails. On getting a false, the run() method returns; this ends the life of the PipedSocketStream Thread. Another way in which the thread could break out of the loop is if dis.read(), dos.write() or dos.flush() return an IOException.

Once a PipedSocketStream thread is started, it relays data from s1 to s2. It dies only if it loses the connection (or the other side disconnects) or it gets an Exception due to a failure in a system call.

JTunnel
Figure 2 shows a JTunnel. JTunnel contains and manages two PipedSocketStream objects. It starts each of these in an independent thread. The code for JTunnel is shown in Listing 2. The JTunnel constructor takes two Socket variables, in_cSocket (client Socket) and in_sSocket (server Socket), as arguments. These are copied into local Socket variables cSocket and sSocket respectively.

Figure 2
Figure 2:

JTunnel has three methods. jtopen() creates the PipedSocketStream threads:

clientServerPipe = new Thread(new PipedSocketStream(cSocket, sSocket));
serverClientPipe = new Thread(new PipedSocketStream(sSocket, cSocket));

Note that the order of the Socket arguments passed to PipedSocketStream is reversed in the second call. As a result, the clientServerThread thread obtains its input stream from cSocket and its output stream from sSocket. serverClientThread does the opposite. The result is that the output of cSocket is piped to the input of sSocket and vice versa. Hence, we get our two one-way pipes. The communication is started by calling the start() method on both the threads.

The isActive() method returns a boolean value that is true if both the threads are alive and false if either is dead. It does this by calling the Thread isAlive() method. jTclose() makes sure that both threads are stopped and the associated sockets are closed.

JTunnel uses two one-way pipes to simulate a full duplex connection. That is why when one thread dies, it has to kill the other and cleans up by closing the associated sockets. A one-way socket connection does not make any sense in this scenario.

JTServer
JTServer is the main routine in the JTRouter program. It listens for connection requests from the Internet, creates and manages JTunnels and keeps track of open connections. Listing 3 shows the code for JTServer. Four variables, shown in Table 2 are defined for creating and maintaining JTunnels.

Table 2

Let us look at the main() routine. When JTServer is started by the Java interpreter, it uses the System DataInputStream to get three values from the user - in_clientPort, in_remoteHostPort and in_remoteHostAddr. These are passed to the JTServer constructor for creating the JTServer object. JTServer implements the Runnable interface and is started as a thread. This gives the JTServer program the flexibility to spawn multiple JTServer threads. Thus, you could have several JTServers, each acting as a router and all this in a single process! In fact, that is the way JTServer has been implemented in one application.

The constructor for the JTServer takes three arguments - in_clientPort (the socket for Internet Client requests), in_remoteHostPort (the socket port on which JTServer sends out data to the Remote Host) and in_remoteHostAddr (the Internet address of the Remote Host). These are copied to the local variables clientPort, remoteHostPort and remoteHostAddr respectively.

Let us now look at the run() method for JTServer. The first thing the run() method does is to create and start a JTListener thread. JTListener listens for connections on the Client port. Note that JTServer passes itself to JTListener as the "this" argument. The boolean jtChanged is set to false, indicating that no JTunnel has recently changed state. The run() method now waits for 100 ms. Note that the JTServer object waits on itself (it calls this.wait()). That is why the run() method is synchronized.

while (true) {
try {
this.wait(100);
}
catch (InterruptedException ie) {
ie.printStackTrace();
}

The code will break out of the wait if 100 ms expire or if it receives a notify() from the JTListener thread. In either case, the run() method goes on to call processQueueConnections() which opens a new JTunnel for any queued requests. This is followed by a call to cleanJTunnels(), which removes any JTunnels that are no longer active. The control then returns to the top of the while loop.

processQueuedConnections() copies the elements of jtPending Vector into newJTunnels. It does this in a block that is synchronized on jtPending (which is shared between the JTServer and JTListener threads). Synchronizing on jtPending ensures that the JTListener thread will not alter the contents of jtPending while the JTServer thread is using it. JTListener also synchronizes on jtPending when accessing it as shown in Listing 4:

synchronized(jts.jtPending) { jts.jtPending.addElement(clientSocket); }

The use of jtPending is further explained in the next section. The last step in the synchronized block is to remove the entries from jtPending Vector.

For each socket in newJTunnels, a connection to the Remote Host is opened:

Socket remoteHostSocket = connectToRemoteHost();

The method connectToRemoteHost() (which is the next method in Listing 3) obtains and returns a socket for the Remote Host:

sock = new Socket(remoteHostAddr, remoteHostPort);

Now that both the Client and Remote Host sockets are available, a new JTunnel is instantiated and opened. This is added to the JTunnels Vector and the number of activeConections is incremented.

The last method in Listing 3, cleanJTunnels(), checks the JTunnels Vector to see if any JTunnels are inactive (by calling the method isActive()). If so, it removes the JTunnel entry from the vector.

JTListener
JTListener is shown in Listing 4. This class actually plays the role of a traditional server. However, instead of creating a new thread to service each connection request, it just adds the associated sockets to the jtPending Vector and goes back to listen for new connections. JTServer takes care of servicing the request by opening a new JTunnel as seen earlier.

The constructor for JTListener takes a JTServer argument, in_jts. It copies this to a local variable, jts. Then it creates a new ServerSocket on jts.clientPort to listen for connection requests from the Client. JTListener implements the Runnable interface (recall that it is started as a thread by JTServer). The run() method listens for connection requests in a while loop:
while (true) {
...
if ((clientSocket = acceptConnection()) == null)
...
}

The method acceptConnection() (which is the next method in Listing 4) blocks on the accept() call on jtServerSocket. On getting a socket, it returns the socket to the run() method. When this new socket is obtained by the run() method (in the clientSocket variable), it is added to jtPending Vector. This is done in a synchronized block so that jtPending is not altered by the JTServer while it is being accessed here. Then the JTServer (jts) is sent a notify() in a synchronized block.

Program Files and Environment
The JTunnel program source consists of four files - PipedSocketStream.java, Jtunnel.java, JTListener.java and JTServer.java. These files were compiled using Symantec Café. The program was tested on NT 4.0 Workstation, NT 4.0 Server and Solaris 2.5.

In order to use the program, you will need the Client and the Remote Host. However, there is an easier way to test the program on a single machine. The source for a tester program, called JTunnelTester, is available on SYS-CON Interactive/JDJ On-line. The files you will need to run this program are:

  • JTunnelTester.class
  • JTFrame.class
  • JTunnelOutputViewer.class
  • JTunnel.cfg
JTunnelTester will simulate a Remote Host. The simplest way to simulate the Client is to use the telnet program. Windows NT comes with a telnet daemon. The steps to run the program are available on SYS-CON Interactive/JDJ On-line. The screen shots are given in Figures 3-6. Figure 3 shows the output of the JTServer program. Figure 4 shows the output of the JTunnelTester program. Figures 5 and 6 show the data sent from a telnet window to the JTunnelTester window. The top of the split window in Figure 5 shows a line of input from the user. This line is displayed in Figure 6 (the telnet window). The bottom of the split window in Figure 5 shows a line of text received by JTunnelTester from the telnet window associated with Figure 6.

Figure 3
Figure 3:
Figure 4
Figure 4:
Figure 5
Figure 5:
Figure 6
Figure 6:

Conclusion
In this article we examined a multi-threaded router program that may be used in several applications as shown below. We also examined a useful Java utility, called JTunnel, for piping sockets to create a channel for relaying client to server communication.

Some applications that JTRouter can be used in are:

  1. As a router/gateway way that relays data between two remote hosts
  2. Filtering/processing data between a data source and a data sink
  3. A line monitor between two hosts
  4. A push server for broadcasting data from a single client to multiple hosts
About the Author
Ajit Sagar is a Software Development Engineer at i2 Technologies, in Dallas, Texas. Ajit has seven years of programming experience in C, three years in C++ and one and a half years in Java. His focus is on networking, system and application software development. To reach Ajit, call 214 860-6906.

	

Listing 1: pipedSocketStream.java.
 
import java.net.Socket; 
import java.io.*; 
public class PipedSocketStream 
    implements Runnable { 
  private int ch; 
  private DataInputStream dis; 
  private DataOutputStream dos; 

  public PipedSocketStream(Socket s1, 
                           Socket s2) { 
    try { 
      dis = 
        new DataInputStream(s1.getInputStream()); 
      dos = 
        new DataOutputStream(s2.getOutputStream()); 
    } 
    catch(IOException ioe) { 
      ioe.printStackTrace(); 
      System.exit(1); 
    } 
  } // End PipedSocketStream() constructor 

  public void run() { 
    // Read and write from piped sockets 
    while(true) { 
      try { 
        if (!this.readAndWrite()) 
          return; 
      } 
      catch (IOException e) { 
        return; 
      } 
    } 
  }  // End run() method 

  private synchronized boolean readAndWrite() 
      throws IOException { 
    if ((ch = dis.read()) < 0) 
      return false; 
    dos.write(ch); 
    dos.flush(); 
    return true; 
  } // End readAndWrite() method 
} // End Class PipedSocketStream 

Listing 2: jtunnel.java.
 
import java.net.*; 
import java.io.*; 
import java.lang.Thread; 

public class JTunnel { 
  public Socket cSocket; 
  public Socket sSocket; 
  private Thread clientServerPipe; 
  private Thread serverClientPipe; 

  public JTunnel(Socket in_cSocket, 
                 Socket in_sSocket) { 
    cSocket = in_cSocket; 
    sSocket = in_sSocket; 
  } // End JTunnel constructor 

  // Create and activate PipedSocketStreams 
  public void JTopen() { 
    clientServerPipe = 
      new Thread(new PipedSocketStream(cSocket, 
                                       sSocket)); 
    serverClientPipe = 
      new Thread(new PipedSocketStream(sSocket, 
                                       cSocket)); 
    System.out.println("Starting socket pipes"); 
    clientServerPipe.start(); 
    serverClientPipe.start(); 
  } // End openJT() method 

  public boolean isActive() { 
    if (clientServerPipe.isAlive() && 
        serverClientPipe.isAlive()) 
      return true; 
    return false; 
  } // End isActive() method 

  public void JTclose() { 
    if (clientServerPipe.isAlive()) 
      clientServerPipe.stop(); 
    if (serverClientPipe.isAlive()) 
      serverClientPipe.stop(); 

    try { 
      if (cSocket != null) 
          cSocket.close(); 
      if (sSocket != null) 
          sSocket.close(); 
    } 
    catch (IOException ioe) { 
      ioe.printStackTrace(); 
    } 
  } // End closeJT() method 

  public void finalize() 
  { 
    JTclose(); 
  } // End finalize() method 
} // End class JTunnel 

Listing 3: jtserver.java.
 
import java.util.*; 
import java.lang.*; 
import java.io.*; 
import java.net.*; 

class JTServer implements Runnable{ 
  public int clientPort = 0; 
  private int remoteHostPort = 0; 
  private String remoteHostAddr = null; 

  private Vector jTunnels = new Vector(); 
  public Vector jtPending = new Vector(); 
  public int activeConnections = 0; 
  public boolean jtChanged = true; 

  public static void main(String[] args) { 
    int in_clientPort = 0; 
    int in_remoteHostPort = 0; 
    String in_remoteHostAddr = null; 
    DataInputStream dis = 
           new DataInputStream(System.in); 

    try { 
      System.out.println("Caller port: "); 
      in_clientPort = 
            Integer.parseInt(dis.readLine()); 
      System.out.println("Remote Host Port: "); 
      in_remoteHostPort = 
            Integer.parseInt(dis.readLine()); 
      System.out.println("Remote Host Address:"); 
      in_remoteHostAddr = dis.readLine(); 
    } 
    catch (IOException ioe) { 
      ioe.printStackTrace(); 
      System.exit(1); 
    } 
    // Create and start JTServer thread 
    Thread jts = new Thread ( 
      new JTServer(in_clientPort, 
        in_remoteHostPort, in_remoteHostAddr)); 
    jts.run(); 
  } // End main() 

  public JTServer(int in_clientPort, 
                  int in_remoteHostPort, 
                  String in_remoteHostAddr) { 
    clientPort = in_clientPort; 
    remoteHostPort = in_remoteHostPort; 
    remoteHostAddr = in_remoteHostAddr; 
  } 

  public synchronized void run() { 
    Thread jtListener = 
      new Thread(new JTListener(this)); 
    jtListener.start(); 
    jtChanged = false; 

    while (true) { 
      try { 
        this.wait(100); 
      } 
      catch (InterruptedException ie) { 
        ie.printStackTrace(); 
      } 

      // Clear JTunnels with dead threads 
      processQueuedConnections(); 
      cleanJTunnels(); 
      if (jtChanged) { 
        System.out.println("No. of JTunnels = " + 
                           activeConnections); 
        jtChanged = false; 
      } 
    } // End while loop 
  } // End run() method 

  void processQueuedConnections() { 
    Vector newJTunnels = new Vector(); 
    if (jtPending.size() == 0) 
      return; 

    synchronized (jtPending) { 
      for (int i = 0; 
        i < jtPending.size(); i++) { 
           newJTunnels.addElement((Socket) 
             (jtPending.elementAt(i))); 
      } 
      // Clear the JTunnels Pending queue 
      jtPending.removeAllElements(); 
    } 
    for (int i = 0; i < newJTunnels.size(); i++) { 
      Socket clientSocket = 
           (Socket)(newJTunnels.elementAt(i)); 

      // connect to remoteHost 
      Socket remoteHostSocket = connectToRemoteHost(); 
      if (remoteHostSocket == null) 
        continue; 

      // Create New JTunnel; add it to JTunnels 
      JTunnel jTunnel = null; 
      try { 
          jTunnel = new JTunnel(clientSocket, 
                                remoteHostSocket); 
          jTunnel.JTopen(); 
      } 
      catch(Exception e) { 
        e.printStackTrace(); 
        System.exit(1); 
      } 
      jTunnels.addElement(jTunnel); 
      activeConnections++; 
      jtChanged = true; 
    } // End for loop 
   } // End processQueuedConnections() method 

  // Try to connect to the remote host 
  public Socket connectToRemoteHost() { 
    Socket sock; 
    try { 
      sock = new Socket(remoteHostAddr, 
                        remoteHostPort); 
    } 
    catch(IOException ioe) { 
      ioe.printStackTrace(); 
      return null; 
    } 
    return sock; 
  } // End method connectToRemtoeHost() 

  // Close JTunnels associated with dead threads 
  void cleanJTunnels() { 
    for (int i = 0; i < jTunnels.size(); i++) { 
      JTunnel jTunnel = 
             (JTunnel)(jTunnels.elementAt(i)); 
      if (!(jTunnel.isActive())) { 
        jTunnel.JTclose(); 
        jTunnels.removeElementAt(i); 
        jtChanged = true; 
        activeConnections--; 
      } 
    } 
  } // End checkJTunnels() method 
} //End Class JTServer 

Listing 4: JTListener.java.
 
import java.io.*; 
import java.net.*; 
import java.lang.Thread; 

class JTListener implements Runnable { 
  private JTServer jts; 
  private ServerSocket jtServerSocket = null; 

  public JTListener(JTServer in_jts) {   
    jts = in_jts; 
    try { 
      jtServerSocket = 
          new ServerSocket(jts.clientPort); 
      System.out.println("Server started on port " 
                         + jts.clientPort); 
    } 
    catch(IOException ioe) { 
      ioe.printStackTrace(); 
      System.exit(2); 
    } 
  } // end method createJTServerSocket 

  public void run() { 
    while (true) { 
      System.out.println("Accepting connections"); 
      try { 
        Thread.sleep(100); 
      } 
      catch (InterruptedException ie) { 
        break; 
      } 

      // Accept internet connection requests 
      Socket clientSocket; 
      if ((clientSocket = acceptConnection()) 
                                    == null) { 
        System.out.println("Got null socket"); 
        System.exit(1); 
      } 
      System.out.println("Got request, " + 
      "clientSocket = " + clientSocket); 
      synchronized(jts.jtPending) { 
        jts.jtPending.addElement(clientSocket); 
      } 
      synchronized(jts) { 
        jts.notify(); 
      } 
    } // End while loop 
  } // End run() method 

  // accept client connections 
  private Socket acceptConnection() { 
    Socket sock = null; 
    try { 
      sock = jtServerSocket.accept(); 
      System.out.println("Connection from: " + 
              sock.getInetAddress().toString()); 
    } 
    catch(Exception e) { 
      e.printStackTrace(); 
      System.exit(4); 
    } 
    return sock; 
  } // End method acceptConnection() 
} // End Class JTListener

 

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.
  E-mail: [email protected]

Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.