| By Ajit Sagar | Article Rating: |
|
| October 1, 1997 12:00 AM EDT | Reads: |
12,107 |
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.
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.
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
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.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 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.
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
.
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.
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.
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();
}
Published October 1, 1997 Reads 12,107
Copyright © 1997 SYS-CON Media, Inc. — All Rights Reserved.
Syndicated stories and blog feeds, all rights reserved by the author.
More Stories By Ajit Sagar
Ajit Sagar is a principal architect with Infosys Technologies, Ltd., a global consulting and IT services company. Ajit has been working with Java since 1997, and has more than 15 years experience in the IT industry. During this tenure, he's been a programmer, lead architect, director of engineering, and product manager for companies from 15 to 25,000 people in size. Ajit has served as JDJ's J2EE editor, was the founding editor of XML Journal, and has been a frequent speaker at SYS-CON's Web Services Edge series of conferences, JavaOne, and international conference. He has published more than 125 articles.
- Cloud CEOs, CTOs & SVPs to Speak at 4th International Cloud Computing Expo
- Kindle 2 vs Nook
- Why IBM’s Server Chief Got Busted
- The Difference Between Web Hosting and Cloud Computing
- Cloud Computing Journal Opens "Readers' Choice Awards" Nominations
- Cloud Computing Expo: Exclusive Q&A with Yahoo! SVP Cloud Computing
- Industry Experts Discuss the State of Cloud Computing
- Ajax in RichFaces 3.3, JSF 2 and RichFaces 4
- It's the Java vs. C++ Shootout Revisited!
- The End of IT 1.0 As We Know It Has Begun
- An Introduction to Abbot
- Java Kicks Ruby on Rails in the Butt
- Interviewing Java Developers With Tears in My Eyes
- Cloud CEOs, CTOs & SVPs to Speak at 4th International Cloud Computing Expo
- 1st Annual Government IT Expo: Call for Papers Deadline July 15
- How to Diagnose Java Resource Starvation
- REA Is Where RIA Becomes the Norm
- Kindle 2 vs Nook
- Anatomy of a Java Finalizer
- Why IBM’s Server Chief Got Busted
- A Cup of AJAX? Nay, Just Regular Java Please
- Java Developer's Journal Exclusive: 2006 "JDJ Editors' Choice" Awards
- The i-Technology Right Stuff
- JavaServer Faces (JSF) vs Struts
- Rich Internet Applications with Adobe Flex 2 and Java
- Java vs C++ "Shootout" Revisited
- Bean-Managed Persistence Using a Proxy List
- Reporting Made Easy with JasperReports and Hibernate
- Creating a Pet Store Application with JavaServer Faces, Spring, and Hibernate
- What's New in Eclipse?


































