Pushlets in Java
Back in the olden days, there was this thing called push. Push rode a wave of hype all to familiar to those us who have been around the block, before dropping off the face of the earth.
The protocols to support it still exist, however. They occured to me when thinking about a multicast publish/subscribe system in HTTP. How would one go about implementing such a system, for Java, today?
There are three transport strategies that exist. Two of which are “push” strategies, using “push” protocols.
First, there is “Server Push”. This is the original push mechanism concocted by Netscape way back in the days of Navigator 1.1. The orginal document is still the best reference.
In “Server Push” you do not send a Content-Length. You send a multipart/mime document. This is the format used to send an HTML e-mail messages, with the HTML and the images in one e-mail body. The document has “parts” and the “parts” are divided by boundries.
In “Server Push” the server sends information in parts. When the client sees a boundry, it assembles the part that has just arrived and does something with it.
The transaction ends when either the server or the client closes the socket.
Second, there is HTTP 1.1 chunked transfer encoding. HTTP 1.1 requires a Content-Length.
Some applications may generate and won’t know the size of the document until generation is complete. They’ll have to buffer the document to get it’s size. If the document is very large, this is going to cost memory, and it is going to delay the response to the client.
Chunking allows the server to attach a Content-Length to a chunk of data, and send a chunk, instead of sending the Content-Length for the entire document. A chunk is a chunk of data, with it’s own set of HTTP headers.
Java has transparent support for HTTP chunking. All a Servlet has to do is call flush method of the ServletOutputStream.flush() or ServletHttpResponse.flushBuffer() and a chunk is on the way.
Here’s the Servlet I wrote for a distributed clock.
package com.agtrz.swag.pushlet;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.SocketException;
import java.util.Date;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class Pushlet
extends HttpServlet
{
private final static long serialVersionUID = 20051117L;
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException
{
try
{
ObjectOutputStream output = new ObjectOutputStream(response.getOutputStream());
for (;;)
{
output.writeObject(new Date());
output.flush();
try
{
Thread.sleep(1000);
}
catch (InterruptedException e)
{
}
}
}
catch (SocketException e)
{
if (!e.getMessage().equals("Socket closed"))
{
throw e;
}
}
}
}
The client will end the transaction by closing the connection, which will produce an IOException of one sort or another. In the case of Jetty, this will be a SocketException and the message will be “Socket closed”, but others containers will have different messages.
Here I catch the exception to make a point, but in production, it would be better to let it find it’s way to the application log, where you can choose to ignore it using log filters.
The chunk reassembly on the client side is done by the HTTP protocol, so there is no need to look for boundries. There is no application level mime support needed.
The client simply reads its input normally. Assuming that the connection is read from the InputStream returned by HttpURLConnection.getInputStream(), the client will block until the bytes are available. The chunk headers are not available to the application.
package com.agtrz.swag.pushlet;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.HttpURLConnection;
import java.net.URL;
public class Pullet
implements Runnable
{
private final URL url;
private final int count;
public Pullet(URL url, int count)
{
this.url = url;
this.count = count;
}
private void tryRun()
throws IOException, ClassNotFoundException
{
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
ObjectInputStream objects = new ObjectInputStream(connection.getInputStream());
for (int i = 0; i < count; i++)
{
Object object = objects.readObject();
System.out.println(object.toString());
}
objects.close();
}
public void run()
{
try
{
tryRun();
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
}
When the client asks for an object, and one is not avaiable, it will block until one become available.
When the client has had enough objects, it closes the connection to the server.
This works out quite nicely in testing. Flushing the buffer on the server side sends an object to the client.
package com.agtrz.swag.pushlet;
import java.net.URL;
import junit.framework.TestCase;
import org.mortbay.http.HttpContext;
import org.mortbay.http.HttpServer;
import org.mortbay.jetty.servlet.ServletHandler;
import org.mortbay.util.InetAddrPort;
public class PushletTestCase
extends TestCase
{
private HttpServer server;
private void startJetty(String path, Class servletClass)
throws Exception
{
server = new HttpServer();
server.addListener(new InetAddrPort(8008));
HttpContext context = server.addContext("/");
ServletHandler handler = new ServletHandler();
handler.addServlet(path, servletClass.getName());
context.addHandler(handler);
server.start();
}
private void stopJetty()
throws InterruptedException
{
server.stop();
}
public void testPushlet()
throws Exception
{
startJetty("/pushlet/", Pushlet.class);
Thread one = new Thread(new Pullet(new URL("http://localhost:8008/pushlet/"), 3));
Thread two = new Thread(new Pullet(new URL("http://localhost:8008/pushlet/"), 3));
one.start();
two.start();
one.join();
two.join();
stopJetty();
}
}
Running the above unit test gives me the following.
log4j:WARN No appenders could be found for logger (org.mortbay.util.Container).
log4j:WARN Please initialize the log4j system properly.
Thu Nov 17 13:12:44 EST 2005
Thu Nov 17 13:12:44 EST 2005
Thu Nov 17 13:12:45 EST 2005
Thu Nov 17 13:12:45 EST 2005
Thu Nov 17 13:12:46 EST 2005
Thu Nov 17 13:12:46 EST 2005
The third way to implement push would be to forget about all this push non-sense and trust that keep-alive will save the connetion overhead, and that the Servlet engine will quickly get a Servlet to work on your request.
The client could send a normal HTTP request, get a response that has a list of objects and a version number. Deal with the response. Then send the next request for objects created since the last version number. If at any point their are not objects available, the server can hold onto the connection until the objects are available.
The loop in the client would be the to the loop for “chunked” client, except that the input stream would be drained, and a new one reopened.
The Problem with Push
The problem with the push solutions is that they don’t work well with proxies. Proxies may choose to gather the chunks and resend them as larger chunks. If the chunk is not large enough to send, the proxy will wait until more chunks arrive. Our scheme to push small objects to the client in a timely fashion is thus thwarted.
This is the problem I’m facing because I like to put my servlet engines behind mod_proxy, and mod_proxy likes to send buffers when they are full. It does not seem that the motivation for chunked transfer was open simplex connections, and mod_proxy is probably doing the right thing, or not doing anything that offends the HTTP 1.1 spec.
Thus, the keep-alive strategy, which I’ll code up later, is probably the best way to go.
November 19th, 2005 at 8:32 pm
Server push has a disadvantage of leaving a connection open - and the browser has a limited number of these connections that it uses (2 for IE for example). I write about it here with some discussion of the options:
http://www.bluishcoder.co.nz/2005/11/more-on-ajax-and-server-push.html
I mention there the possibility of using a hidden flash applet to hold a perssitent connection and have the javascript code communicate with it to send and receive events. The author of the AFLAX library has since implemented an example of doing this:
http://www.bluishcoder.co.nz/2005/11/aflax-makes-server-push-easy.html