Java Tip 103: Send HTTP requests for serialized objects

Implement Web object tunneling to transport Java objects through firewalls

My Web Object Tunneling utility evolved when I needed to develop an applet that would display dynamic server-side data stored as JavaBeans. I needed to consider how to detect their modifications, and how to download the beans.

I decided to design a transport layer that would allow the applet to request the JavaBeans directly from the server in a serialized form. Moreover, since the applet would be used outside the corporate firewall, I decided to use HTTP to build the layer.

A standard RMI solution would have incurred extra delays when run with proxy servers, making the applet appear slower. Additionally, I would have had to modify my existing servlets into remotable objects, then deal with an RMI registry. Other common problems with the RMI solution include:

  • Although all Netscape browsers, since 4.03 with JDK 1.1, patch support RMI, they have a limited capacity for implementing applet-to-server communication. For example, some security managers disallow the creation of ServerSockets. Instead, RMI multiplexes on the established socket, so communication remains (virtually) bidirectional, but slows down because of the additional layer that manages the protocol.
  • Microsoft claims to have “Java support in MSIE,” but despite being part of 1.1 specifications, RMI is not supported by Microsoft’s browsers (Internet Explorer 4.0 and 5.0) by default. Users must install an RMI patch (from Microsoft or IBM, for example) to run RMI applets on their MS browsers. For more details, see Resources.

Basically, the straight RMI solution was too slow and required too much additional coding.

With my custom Web Object Tunneling, I only had to modify my existing servlets to extend a new base class and override a single method.

Model overview

Web Object Tunneling is implemented inside a set of classes, which enables the creation of Java object channels between clients and servlets. The client party originally creates the channel, since the implementation is based on HTTP (request/response protocol). (See Figure 1.)

Figure 1 Click on thumbnail to view full image (15 KB)

The client-side objects involved are:

  • HttpObjectChannel, the client-side channel — the entry point for any communication with server-side channels. It makes it possible to initiate HTTP requests (HttpObjectRequest) to any given Web server URL (HttpObjectChannelServlet).
  • HttpObjectRequest, which represents an HTTP request. It handles the HTTP communication exchanges with the Web server. It sends POST requests (along with any desired argument) to the Web server and waits for Java objects to be returned.
  • HttpObjectVarg, a variable arguments container. Any argument required in a request (HttpObjectRequest) should be SET inside this object.

The server-side objects involved are:

  • HttpObjectChannelServlet, the server-side channel. You can extend this class to construct a servlet that can respond to HTTP requests with serialized Java objects.
  • HttpObject: This interface can be implemented by any (serializable) object that should be “freed” right after HttpObjectChannelServlet sends it to the client. That is, if you want to help the garbage collector clean the object, implement HttpObject with a customized free() method (for example, to close sockets or break circular references).

How do you use it?

This example makes an HTTP request to a servlet, sends it some variables (var1, var2, and var3), waits for an object to be returned, and prints the returned object.

  1:import java.net.URL;
  2:
  3:import jjv.net.HttpObjectChannel;
  4:import jjv.net.HttpObjectRequest;
  5:import jjv.net.HttpVarg;
  6:
  7:// JVHttpObjectChannelTester
  8:public class JVHttpObjectChannelTester implements Runnable {
  9:
 10:    /* constants */
 11:    public static final String    COMMAND_NAME  = new String("JVHttpObjectChannelTester");
 12:    public static final String    COMMAND_DESC  = new String("Web Tunneling Application Tester");
 13:    public static final String    COMMAND_VER   = new String("1.0");
 14:    public static final String    COMMAND_CR    = new String("Copyright (c) 2000 by JV - All Rights Reserved");
 15:
 16:    /* globals */
 17:    protected boolean  _trace;
 18:    protected String   _url;
 19:
 20:    // utilities
 21:    private void TRACE(String msg) { if (_trace) System.out.println("TRACE JVHttpObjectChannelTester " + msg); }
 22:    private void ERROR(String msg) { System.out.println("ERROR JVHttpObjectChannelTester " + msg); }
 23:
 24:    // Main
 25:    public static final void main(String args[]) { new JVHttpObjectChannelTester(args); }
 26:
 27:    // Constructor
 28:    public JVHttpObjectChannelTester(String args[]) {
 29:
 30:        /* init */
 31:        _trace = false;
 32:        _url = "
 33:
 34:        /* try */
 35:        try {
 36:            int  i;
 37:
 38:            /* scan */
 39:            i = 0;
 40:            while (true) {
 41:              char  c;
 42:
 43:              /* set */
 44:              c = args[i].charAt(0);
 45:
 46:              /* check */
 47:              if ((c == '/') || (c == '-'))
 48:                c = args[i].charAt(1);
 49:
 50:              /* check */
 51:              switch(c) {
 52:                case 'v': /* version */
 53:                    System.out.println(COMMAND_NAME + " : " + COMMAND_DESC + " - Version : " + COMMAND_VER);
 54:                    System.out.println(COMMAND_CR);
 55:                    exit(0);
 56:
 57:                case 'T': /* trace */
 58:                    _trace = true;
 59:                    break;
 60:
 61:                case '?': /* bad option */
 62:                    this.usage();
 63:                    exit(1);
 64:              }
 65:
 66:              /* next */
 67:              i++;
 68:            }
 69:        } catch(ArrayIndexOutOfBoundsException excp) {;};
 70:
 71:        /* check */
 72:        if (this.init() < 0) {
 73:            ERROR("Cannot init...");
 74:            exit(1);
 75:        }
 76:
 77:        /* run */
 78:        this.run();
 79:    }
 80:
 81:    // Usage
 82:    public void usage() {
 83:        System.out.println("Usage: " + COMMAND_NAME + " [-v] [-T]");
 84:        System.out.println("Where:");
 85:        System.out.println("t-vtprint command version");
 86:        System.out.println("t-Tttrace");
 87:        return;
 88:    }
 89:
 90:    // Init
 91:    public int init() { return(0); }
 92:
 93:    // Run
 94:    public void run() {
 95:        String              url;
 96:        HttpObjectChannel   channel;
 97:        HttpObjectRequest   request;
 98:        HttpVarg            varg;
 99:        Object              response;
100:
101:        /* check */
102:        if ((url = _url) == null) {
103:            ERROR("run: Invalid URL, _url="" + _url + """);
104:            usage();
105:            exit(1);
106:        }
107:
108:        /* try */
109:        try {
110:            channel = new HttpObjectChannel(this, new URL(url));
111:        } catch(Exception excp) {
112:            channel = null;
113:            ERROR("run: Exception caught while creating channel, excp='" + excp + "'");
114:            excp.printStackTrace();
115:            exit(1);
116:        }
117:
118:        /* create */
119:        request = channel.createRequest();
120:
121:        /* set */
122:        varg = new HttpVarg();
123:        varg.set("var1", "val1");
124:        varg.set("var2", "val2");
125:        varg.set("var3", "val3");
126:
127:        /* check */
128:        if ((response = request.exec(varg)) == null) {
129:            ERROR("run: Cannot exec request");
130:            exit(1);
131:        }
132:
133:        /* */
134:        TRACE("The Response is '" + response + "'");
135:
136:        /* exit */
137:        exit(0);
138:    }
139:
140:    // Exit
141:    public void exit(int code) { System.exit(code); }
142:}

First, you need to create a channel (line 110) between the client (this program) and the servlet (the URL). Then you can initiate an HTTP request (HttpObjectRequest) by calling channel.createRequest() (line 119). Some variables can be set inside an HttpVarg object, to be sent to the servlet and processed there (from line 122, where the HttpVarg object is created, to line 125). The request is performed at line 128 by calling the exec(varg) method. The response object is returned at the same time (line 128) and is finally printed by the client.

How does it work?

Now I will explore the “internals” of this simple HTTP Object Tunneling.

A POST request to an URL is quite easy to write in Java. This task is handled inside the HttpObjectRequest exec() method. The main routine is shown here:

 1:public Object exec(HttpVarg varg) {
 2:   URL     url;
 3:   Object  o;
 4:
 5:   /* check */
 6:   if ((url = _channel.getURL()) == null) {
 7:      System.out.println("ERROR HttpObjectRequest exec: Cannot getURL");
 8:      return(null);
 9:   }
10:
11:   /* check */
12:   if (open(url) < 0) {
13:      System.out.println("ERROR HttpObjectRequest exec: Cannot open, url="" + url + """);
14:      return(null);
15:   }
16:
17:   /* check */
18:   if (send(varg) < 0) {
19:      System.out.println("ERROR HttpObjectRequest exec: Cannot send command, command='" + command + "'");
20:      close();
21:      return(null);
22:   }
23:
24:   /* try */
25:   try {
26:      o = getResponse();
27:   } finally {
28:      close();
29:   }
30:
31:   /* done */
32:   return(o);
33:}

You first open the HTTP socket by calling the HttpObjectRequest‘s private method open(url) on line 12. Then the POST HTTP request is sent by calling the HttpObjectRequest‘s private method send(varg) on line 18. The varg object provides the content of the POST request and carries any data the client wants the server to process. There, the response is read from the HttpObjectRequest‘s private method getResponse() on line 26. Finally, the socket is closed (line 28).

While the functions of the open(url) and close() methods are obvious (how to open and close a socket), the send(varg) and getResponse() methods require a more in-depth look:

 1:private int send(HttpVarg varg) {
 2:   int  res;
 3:
 4:   /* init */
 5:   res = 0;
 6:
 7:   /* try */
 8:   try {
 9:      URL           url;
10:      String        path;
11:      String        content;
12:      int           contentLength;
13:      StringBuffer  buffer;
14:
15:      /* set */
16:      url = _channel.getURL();
17:
18:      /* set */
19:      path = url.getFile();
20:
21:      /* set */
22:      content = varg.toURLEncodedString();
23:      contentLength = content.length();
24:
25:      /* set */
26:      buffer = new StringBuffer(128 + path.length() + contentLength);
27:      buffer.append("POST ");
28:      buffer.append(path);
29:      buffer.append(" HTTP/1.0nUser-Agent: NonenContent-Type: application/x-www-form-urlencodednContent-Length: ");
30:      buffer.append(Integer.toString(contentLength));
31:      buffer.append("nn");
32:      buffer.append(content);
33:
34:      /* write */
35:      _out.write(buffer.toString().getBytes());
36:      _out.write((new String("rn")).getBytes());
37:
38:      /* flush */
39:      _out.flush();
40:   } catch(Exception excp) {
41:      System.out.println("ERROR HttpObjectRequest send: Exception caught, excp='" + excp + "'");
42:      res = -1;
43:   }
44:
45:   /* done */
46:   return(res);
47:}

The varg data are converted to an URL-encoded string, to be inserted into the content of the HTTP request (line 22). This method could have also been written with the JDK’s URLConnection object.

 1:private Object getResponse() {
 2:   Object   o;
 3:
 4:   /* try */
 5:   try {
 6:      _in = new ObjectInputStream(getContentInputStream());
 7:   } catch(Exception excp) {
 8:      System.out.println("ERROR HttpObjectRequest getResponse: Cannot create InputStream, excp='" + excp + "'");
 9:      return(null);
10:   }
11:
12:   /* try */
13:   try {
14:      o = _in.readObject();
15:   } catch(Exception excp) {
16:      System.out.println("ERROR HttpObjectRequest getResponse: Exception caught, excp='" + excp + "'");
17:      o = null;
18:   }
19:
20:   /* done */
21:   return(o);
22:}

This method gets the InputStream from the opened socket to create an ObjectInputStream (line 6) and read the incoming response object (line 14). It’s important to understand why the ObjectInputStream cannot be created directly from the socket’s InputStream without additional coding.

The response object and the ObjectStream header are embedded in an HTTP response. Thus, the HTTP header added by the remote Web server is initially readable. Directly creating an ObjectInputStream from here would produce a StreamCorruptedException, which indicates that the stream doesn’t contain a serialized object. That’s why the HTTP header is read (in the getContentInputStream() method) before the ObjectInputStream is created to read the response object (line 6).

 1:private InputStream getContentInputStream() {
 2:   InputStream  in;
 3:
 4:   /* wait for data */
 5:   try {
 6:      /* get */
 7:      in = _socket.getInputStream();
 8:
 9:      /* skip HTTP header */
10:      int   i;
11:      int   i1 = -1;
12:      int   i2 = -1;
13:      int   i3 = -1;
14:      while ((i = in.read()) >= 0) {
15:         if ((i1 == 'r') && (i2 == 'n') && (i3 == 'r') && (i == 'n'))
16:            break;
17:         i1 = i2;
18:         i2 = i3;
19:         i3 = i;
20:      }
21:   } catch(Exception excp) {
22:      System.out.println("ERROR HttpObjectRequest getContentInputStream: Exception caught, excp=" + excp);
23:      in = null;
24:   }
25:
26:   /* done */
27:   return(in);
28:}

The goal here is to return the InputStream with the HTTP header discarded. To do so, a loop reads all incoming characters until the pattern “rnrn” (end of HTTP header) is reached.

From a server-side perspective, this implementation is based upon the Servlet API, and any servlet that responds to HTTP requests with Java serialized objects should extend HttpObjectChannelServlet. Here’s a look at its service(request, response) method:

 1:public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException {
 2:
 3:   /* try */
 4:   try {
 5:      Object              channelResponse;
 6:      ObjectOutputStream  out;
 7:
 8:      /* check */
 9:      if ((channelResponse = getChannelResponse(request, response)) == null)
10:         return;
11:
12:      /* create */
13:      out = new ObjectOutputStream(response.getOutputStream());
14:
15:      /* write & flush */
16:      out.writeObject(channelResponse);
17:      out.flush();
18:
19:      /* check */
20:      if (channelResponse instanceof HttpObject)
21:         ((HttpObject) channelResponse).free();
22:   } catch(Exception excp) {
23:      log_error("service: Exception, excp=" + excp);
24:   }
25:
26:   /* done */
27:   return;
28:}

The response object is returned by the getChannelResponse(request, response) (line 9). The ObjectOutputStream is then created (line 13) to write the object (line 16). This layer can also “free” the response object to optimize, for example, the garbage collection, by implementing the HttpObject interface (line 20). This method should not be overridden by a “custom” channel servlet; the getChannelResponse() must be overridden instead.

Conclusion

You have seen that building a simple implementation of Web Object Tunneling (without RMI) is quite an easy task. This technique is useful for any application framework that needs to respond efficiently to client requests with Java serialized objects through firewalls. This basic capability may be enough for many projects; however, you can build more complete implementations that would overcome some of its current limitations:

  • The client cannot send serialized objects to the servlet, only simple variables. This can be implemented inside the HttpVarg object, where all the arguments are encoded. Since a serializable object can be represented as a byte array, you can easily insert it into the HTTP request (like any other simple argument).
  • The servlet cannot deliver asynchronous notifications to clients (sockets are closed just after responses), forcing the clients to poll servers.
  • The arguments that a client sends to a server are neither typed nor controlled by this API. Again, you could handle this with the HttpVarg object, but now the API must be changed. The set() method should be modified by replacing the name (String) argument with a more generic one, which could be called HttpResource (holding the name of the resource, but also its type and its default value).
  • The API is very basic; you must deal with channels, requests, and responses. A more generic layer (like RPC) could allow users to deal with remote services and remote objects. This could be an intense, but interesting, task to perform.
Johann Vazquez is the principal software
engineer at Indra Sistemas
(Spain), where he works with Java technologies to build n-tier Web
and WAP services. He has also worked as a developer for Datamedia (France) and a Java
expert for Sema Group (France).
In the last four years, Vazquez has developed Java software (GUIs,
CTI servers, etc.) and Web/WAP sites, and has worked with a wide
range of technologies, including Swing, applets, JTAPI, JDBC, RMI,
RMI-IIOP, Corba, JNI, J2EE (EJB, Servlet/JSP, JNDI), and JavaMail.
Vazquez lives in Madrid, Spain with his wife, Izarne, and his
daughter, Naia.

Source: www.infoworld.com