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
ServerSocket
s. 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.)
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 sendsPOST
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 beSET
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 afterHttpObjectChannelServlet
sends it to the client. That is, if you want to help the garbage collector clean the object, implementHttpObject
with a customizedfree()
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. Theset()
method should be modified by replacing the name (String) argument with a more generic one, which could be calledHttpResource
(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.