Servlet adapters can solve your socket problems

We like security when our data is protected, but we hate it when our application, or applet, doesn't work because security mechanisms are interfering. This article demonstrates how restrictions on TCP/IP's socket communication can be avoided without breaking any security rules.

Many of us have been in situations where we needed more freedom and flexibility than provided by HTTP or HTTPs (ports 80 or 443), but had access denied by firewall and security features. One way to get around this, without breaking any permission, is by creating servlet adapters.

Simple Socket Communications
Let's look at a simple client/server application (see Figure 1) where the client asks the server, over a TCP/IP connection, for the time of day. The client opens a socket, reads a java.util.Date object from the socket's input stream, prints it out and exits.

Figure 1
Figure 1. Simple TCP/IP connection.


class Client1 {
  final static String SVR_ADDRESS = "localhost";
  final static int    SVR_PORT = 2050;

  static public void main(String[] args) {
    try {
      final ObjectInputStream in = new ObjectInputStream(
                                   new Socket(SVR_ADDRESS, SVR_PORT).getInputStream());
      System.out.println((Date)in.readObject());
      in.close();
    }catch(Exception e) { e.printStackTrace(); }
  }
}
The server opens a server socket, listens for a client's connection request, accepts it, sends back a java.util.Date object with current time to the client, and then exits.


class Server1 {
  final static int    PORT = 2050;

  static public void main(String[] args) {
    try {
      ObjectOutputStream out = new ObjectOutputStream(
                                  new ServerSocket(PORT).
accept().getOutputStream());
      out.writeObject(new Date());
      out.flush();
      out.close();
    }catch(IOException e) { e.printStackTrace(); }
  }
}
Of course, in real life, a server must handle multiple clients. It must start separate threads for each client connection, appropriately handle connection errors, and so on. However, I shall ignore these complexities for the sake of emphasizing the techniques I will present.

Servlet Adapters
The application in the previous client code snippet will run perfectly well in a non-restricted environment. But if we position a firewall between the client and the server, we get an exception with a "connection refused" message. How can we remedy the situation? Our problem is exacerbated by the fact that we can't make changes to the server implementation because the server is out of our control.

We could perhaps get around the firewall problem by rewriting our server as a Servlet and having the client use HTTP to communicate. However, we generally can't do this because existing clients are already expecting to communicate with the server as it is currently written (over port 2050), and because we may not have access to the server's implementation. For example, a real-time brokerage application may use a Data Feed Server with data from a third party. Moreover, we may have restrictions on the client side as well. For example, we don't have control over our site's firewall and restriction policies. We may be required to go through HTTP.

One solution to these problems is to create servlet adapters. By servlet adapter, I mean a Java servlet that intercepts HTTP communications from a client to a server, and redirects the communication to the server over a TCP/IP socket. This allows the server to use a non-HTTP protocol while the client goes through the firewall to the Web server via HTTP (see Figure 2). (We are using the Web server's port to "tunnel" through the firewall.) This approach allows the server to listen on arbitrary port numbers—not just those opened by the firewall.

Figure 2
Figure 2. Simple servlet adapter.

Also, the servlet adapter does not have restrictions on making any calls back to the server, assuming the Web Server and the server are both inside the firewall. This lets your servlet make those TCP/IP calls to the server to receive data.

What you have to do is create a servlet adapter. Of course, you have to implement a different way of communicating on the client side. Instead of connecting to the server directly, clients must communicate through the servlet adapter. On the client side, therefore, you have to substitute the socket communication with servlet communication.


class RestrictedClient1 {
  //this line has to be modified according to you particular Web server
  final static String servletCodeBase = "http://localhost:8080/servlet/";

  static public void main(String[] args) {
    try {
      URL url = new URL(servletCodeBase + "Adapter");
      ObjectInputStream in = new ObjectInputStream
(url.openConnection().getInputStream());
      System.out.println((Date)in.readObject());
      in.close();
    }catch(Exception e) { e.printStackTrace(); }
  }
}
Let's now look at an altered version of the client that communicates with a servlet adapter. It looks almost the same as the "socket" client, except it does not know the server name and the port. Rather, it knows the servlet's URL. Also, it should wrap an ObjectOutputStream object around the input stream from the servlet's URL connection, because we are going to pass Serializable objects to the client.

The remainder of our new client code is the same. It will read the data from the ObjectInputStream and print it out to the standard output. As you can see, it's a simple change and will not impact the rest of the client's work.

The Servlet Adapter (see Listing 1) is very straightforward as well. On getting called by the client, this servlet opens a socket input stream with our original server, opens an output stream with the client, reads data from the server, and passes it back to the client.

As you can see, using such adapters enables you to work successfully in restricted environments without even touching the server-side code. And it only requires small changes on the client side.

Adding Asynchrony
Let's look at a more complex example that adds asynchronous capability. We'll first look at an example that uses socket communications, and then we'll enhance it to include a servlet adapter.

Assume that we have an application (or applet) that communicates with the server side over a TCP/IP socket and uses two channels: one for sending requests and another for receiving responses. We shall implement a simple publish-and-subscribe mechanism in which the client initiates requests to the server for change notifications (over the subscribe channel), and the server returns these notifications asynchronously to the client (over the publish channel).

In this example, a client sends a request (subscribes) to receive the current date and time from the server repeatedly after each interval. The length of the notification interval is sent as part of the request message, while date objects will be returned as the responses. You can see examples of implementations of the client and server in Listings 2 and 3, respectively.

As you can see from Listing 3, the server opens two ServerSockets—one for requests and another for responses. The server listens on the request socket, waiting for requests from the client. After receiving a request, the server starts a timer with the interval value that was sent in the client's request message. In an infinite loop, the timer starts sending java.util.Date objects containing the current time. In our example, a SocketException will ungracefully break this loop when the client exits.

The client work is even simpler (see Listing 2). First, it opens two sockets to the servlet adapter—a request socket and a response socket. Then it sends a subscription request on the request socket and starts an infinite loop that listens on the response socket for a response. Every time the loop receives a response, it prints out the Date object. You can observe the correctness of the server's operation by comparing the difference in Date object values with the requested interval value (see Figure 3).

Figure 3
Figure 3. Asynchronous TCP/IP connections.

Asynchronous Servlet Adapters
Now, let's add servlet adapters to our asynchronous example. Because we want these two channels to operate asynchronously, we need two servlet adapters rather than one. (We may eventually want to add or remove subscriptions over the request channel while the response channel is asynchronously sending notifications back to the client as a result of a previous subscription request.) Consequently, we have to create two adapters: a Request Adapter (see Listing 4) and a Response adapter (see Listing 5).

We also need to change our most recent client so that it connects to the servlet adapters instead of connecting to the server directly (see Listing 6). This new client establishes a URL connection with the RequestAdapter servlet and then sends the interval value as a request parameter. It also sets up an asynchronous URL connection with the ResponseAdapter servlet. And again, the rest of it stays the same: The client gets an ObjectInputStream, waits for the notifications to arrive, and prints out incoming java.util.Date objects.

Now let's look at the role of servlet adapters. On receiving the client request, the RequestAdapter (see Listing 4) opens a socket output stream with our original server and passes the interval value to it. If no exceptions have occurred, then a successful message has been sent back to the client. Depending on your application, if the client sends request messages very often, you can keep your output stream with the server open all the time, assuming, of course, that you have implemented the appropriate support mechanisms (i.e., communication failure recovery).

The ResponseAdapter requires a little bit more work because it does not work on incoming messages like the RequestAdapter. However, the ResponseAdapter is responsible for keeping communications up all the time (session base) until the client exits. As soon as the ResponseAdapter receives a call from a client, it opens an input stream with the server, obtains an output stream from the calling client, and starts an infinite loop. In this loop, it passes all received data from the server to the client. Again, in real life you have to add some extra code to take care of communication failures, choose the right size of buffer, determine how to buffer the transferred data, and so on.

Considerations
Even with a relatively sophisticated communication style such as publish and subscribe, using servlet adapters allows you to run your TCP/IP client/server applications in a restricted security environment without touching the server-side code and with making only small changes on the client side.

Figure 4
Figure 4. Asynchronous servlet adapters.

You can even implement a cascaded connection. First, have the client try to connect to the server directly by using a socket. If that fails, then connect through the servlet adapters. This approach will allow you to support clients that are both inside and outside of the firewall, but will allow those inside to connect directly with your servers.

You can also implement a "partial constant connection." This involves a constant TCP/IP connection between the servlet and a server, as well as a temporary HTTP connection between the servlet and the client. Even in this case, you don't have to change any code on the server side. You merely implement the appropriate buffering mechanism on the servlet side. But again, this is necessary only when you cannot keep a large number of clients and data pushed to clients simultaneously connected to the server.

With normal HTTP request/response behavior, the servlet sends a single response back to the client and then immediately closes the connection. This behavior is intended to keep HTTP connections short-lived, thus encouraging a large number of connections per unit of time.

However, notice that the ResponseAdapter servlet holds open the HTTP connection with the client for a relatively long time. It does this in order to repeatedly send "time change" notifications back to the client whenever they occur. Admittedly, this is contrary to normal HTTP request/response behavior, and does not scale to a large number of concurrently connected clients that are using my approach. However, if the application needs a limited number of such pub/sub clients, then my approach can be very useful.

Conclusion
The servlet adapter approach can be used for any client/server applications that currently use sockets, RMI or any type of communication, and that fail when trying to open connections through a firewall.

This approach can also be used for applets trying to communicate with servers. As you know, an untrusted applet inside of the applet sandbox can open sockets only to the machine from which it was actually downloaded—which is generally a Web server machine, and not the machine on which the server is running.

So, if your server runs on a different machine, or if the server's machine name is assigned dynamically, then you can use this approach to solve the problem. Of course, in real life you have to take care to use good distributed computing practices to solve bottleneck, concurrency, scalability, and other problems.

Trademarks
Copyright 2001 Sun Microsystems Inc. All Right Reserved. Sun, Sun Microsystems, the Sun logo, Java, Solaris and The Network is the Computer are trademarks or registered trademarks of Sun Microsystems Inc. in the United States and other countries. All SPARC trademarks are used under license and are trademarks or registered trademarks of SPARC International Inc. in the United States and other countries. Products bearing SPARC trademarks are based upon an architecture developed by Sun Microsystems Inc. Sun Microsystems Inc. may have intellectual property rights relating to implementations of the technology described in this article, and no license of any kind is given here. Please visit http://www.sun.com/software/communitysource/ for licensing information. The information in this article (the "Information") is provided "as is," for discussion purposes only. All express or implied conditions, representations and warranties, including any implied warranty of merchantability, fitness for a particular purpose, or non-infringement are disclaimed, except to the extent that such disclaimers are held to be legally invalid. Neither Sun nor the authors make any representations, warranties, or guaranties as to the quality, suitability, truth, accuracy or completeness of the Information. Neither Sun nor the authors shall be liable for any damages suffered as a result of using, modifying, contributing, copying, or distributing the Information.