Skip to main content

Protocol Upgrade in Servlet 3.1 By Example

Posted by swchan2 on May 7, 2013 at 1:15 PM PDT

Update: Invoke WebConnection#close when there is an error.

Servlet 3.1 Specification (JSR 340) is almost ready for the release. One of the new features is the support for protocol upgrade.

HTTP protocol upgrade was introduced in HTTP 1.1 (RFC 2616):

The Upgrade general-header allows the client to specify what additional communication protocols it supports and would like to use if the server finds it appropriate to switch protocols. The server MUST use the Upgrade header field within a 101 (Switching Protocols) response to indicate which protocol(s) are being switched.

And Web Socket is an example of protocol upgrade. In Servlet 3.1, javax.servlet.http.HttpUpgradeHandler is introduced to allow the protocol upgrade processing.
In this blog, I will illustrate the use of the new API with a hypothetical protocol of checking ISBN number as follows:

Client:
(a_isbn_to_be_verified)*|EXIT
and tokens can be separated by " \t\n\r\f".
Server:
(a_previous_isbn (true|false) CRLF)*

In this example, TestServlet decides to upgrade to a "isbn" protocol.

@WebServlet("/test")
public class TestServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse res)
            throws IOException, ServletException {

        // checking the upgrade header and decide to upgrade
        if ("isbn".equals(req.getHeader("Upgrade"))) {
            // send 101 status code and corresponding headers
            res.setStatus(101);
            res.setHeader("Upgrade", "isbn");
            res.setHeader("Connection", "Upgrade");

            ISBNHttpUpgradeHandler handler = req.upgrade(ISBNHttpUpgradeHandler.class);
            // we can custom the http upgrade handler
            handler.setDebug(true);
        } else {
            res.getWriter().println("No upgrade: " + req.getHeader("Upgrade"));
        }
    }
}

Note that we can use CDI injection and JNDI lookup in methods of HttpUpgradeHandler. The implementation of ISBNHttpUpgradeHandler is as follows:

public class ISBNHttpUpgradeHandler implements HttpUpgradeHandler {
    // we can use CDI injection in HttpUpgradeHandler!
    @Inject
    private ISBNValidator isbnValidator;

    private boolean debug = false;

    public ISBNHttpUpgradeHandler() {
    }

    @Override
    public void init(WebConnection wc) {
        System.out.println("ISBNHttpUpgradeHandler.init");
        try {
            if (debug) {
                // testing: JNDI lookup works in here, too
                InitialContext initialContext = new InitialContext();
                String appName = (String)initialContext.lookup("java:app/AppName");
                System.out.println("--> appName: " + appName);
            }

            // set up a ReadListener to read the data in non-blocking way
            ServletInputStream input = wc.getInputStream();
            ReadListenerImpl readListener =
                new ReadListenerImpl(input, wc, isbnValidator, debug);
            input.setReadListener(readListener);

            // flush headers if any
            wc.getOutputStream().flush();
        } catch(Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    @Override
    public void destroy() {
        if (debug) {
            System.out.println("--> destroy");
        }
    }

    public void setDebug(boolean debug) {
        this.debug = debug;
    }
}

Non-blocking read is used in reading the incoming data. The #onDataAvailable method will be invoked when data is available for processing.

class ReadListenerImpl implements ReadListener {
    private static final String EXIT = "EXIT";
    private static final String DELIMITER = " \t\n\r\f";
    private static final String SPACE = " ";
    private static final String CRLF = "\r\n";

    private ServletInputStream input = null;
    private ServletOutputStream output = null;
    private WebConnection wc = null;
    private ISBNValidator isbnValidator = null;
    private boolean debug;

    private volatile String unprocessedData = "";

    ReadListenerImpl(ServletInputStream in, WebConnection c,
            ISBNValidator isbnV, boolean d) throws IOException {
        input = in;
        wc = c;
        isbnValidator = isbnV;
        debug = d;
        output = wc.getOutputStream();
    }

    @Override
    public void onDataAvailable() throws IOException {
        // put data into a StringBuilder
        StringBuilder sb = new StringBuilder(prevData);

        int len = -1;
        byte b[] = new byte[1024];
        while (input.isReady()
                && (len = input.read(b)) != -1) {
            String data = new String(b, 0, len);
            if (debug) {
                System.out.println("--> " + data);
            }
            sb.append(data);
        }

        try {
            processData(sb.toString());
        } catch(IOException ioe) {
            throw ioe;
        } catch(RuntimeException re) {
            throw re;
        } catch(Throwable t) {
            throw new IOException(t);
        }
    }

    @Override
    public void onAllDataRead() throws IOException {
        try {
            wc.close();
        } catch(Exception ex) {
            ex.printStackTrace();
        }
    }

    @Override
    public void onError(final Throwable t) {
        t.printStackTrace();
        try {
            wc.close();
        } catch(Exception ex) {
            ex.printStackTrace();
        }
    }

    // the following method illustrate how to parse the data
    private void processData(String data) throws Exception {
        String lastToken = null;
        StringTokenizer tokens = new StringTokenizer(data, DELIMITER, true);
        boolean isExit = false;
        OutputStream output = wc.getOutputStream();
        while (tokens.hasMoreTokens()) {
            String token = tokens.nextToken();
            if (debug) {
                System.out.println("--> token: " + token);
            }

            if (DELIMITER.contains(token)) {
                if (lastToken != null) {
                    if (EXIT.equals(lastToken)) {
                        isExit = true;
                        break;
                    } else {
                        boolean result = isbnValidator.isValid(lastToken);
                        output.write((lastToken + SPACE + result + CRLF).getBytes());
                        output.flush();
                   }
                }
                lastToken = null;
            } else {
                lastToken = token;
            }
        }

        // put unprocessed data
        // so that it can be processed when more data is available.
        // Note that a partial token, for instance "ISB", may be sent to listener.
        unprocessedData = ((lastToken != null) ? lastToken : "");

        if (isExit) {
            // closing the WebConnection
            wc.close();
            return;
        }
    }
}

The ISBNValidator has the logic to validate the ISBN numbers. For completeness, I will enclose the source code as follows.

@ApplicationScoped
public class ISBNValidator {
    /**
     * For convenience, we will ignore '-', ' '.
     */
    public boolean isValid(String isbnStr) {
        char[] isbnChars = isbnStr.toCharArray();
        if (isbnChars.length < 10) {
            return false;
        }

        boolean valid = true;
        boolean hasX = false;
        int[] is = new int[13];
        int len = 0;
        // read one more if there is
        for (int i = 0; i < isbnChars.length && len < 14; i++) {
            char c = isbnChars[i];
            if ((c >= '0' && c <= '9')) {
                is[len++] = c - '0';
            } else if (c == '-' || c == ' ') {
                //skip
            } else if ((c == 'X' || c == 'x') && len == 9) { // for isbn 10
                is[len++] = 10;
                hasX = true;
            } else {
                valid = false;
                break;
            }
        }

        if (!valid || (len != 10 && len != 13) || (len != 10 && hasX)) {
            return false;
        }

        if (len == 10) {
            return isISBN10(is);
        } else { // len == 13
            return isISBN13(is);
        }
    }

    // only look at the first 10 elements and 'X' has changed to 10
    // 10x_1 + 9x_2 + 8x_3 + ... + 2x_9 + x_10 = 0 (mod 11)
    private boolean isISBN10(int[] is) {
        int sum = 0;
        for (int i = 0; i < 10; i++) {
            sum += (10 - i) * is[i];
        }  
        return (sum % 11 == 0);
    }

    // x_13 = (10 - (x_1 + 3x_2 + x_3 + 3x_4 + ... + x_11 + 3x_12)) mod 10
    private boolean isISBN13(int[] is) {
        int sum = 0;
        for (int i = 0; i < 12; i += 2) {
            sum += is[i] + 3 * is[i + 1];
        }
        sum += is[12];
        return (sum % 10 == 0);
    }
}

Comments

What about the client code? I'm trying to run your example ...

What about the client code? I'm trying to run your example but instead of "isbn" as protocol I'm using "websocket", then I'm trying to connect via WebSocket from a JavaScript (new WebSocket("ws://localhost:8080/TestApp/test");) but I'm getting an EOFException.

It seems that TCPNIOTransport class from Grizzly is marking the connection as closed remotely and then calling tcpConnection.close0(), which I don't think should be happening.

If I remove the line wc.getOutputStream().flush(), the exception is not thrown, but nothing happens at the client side either. Any ideas here? Thanks!

This is not in websocket protocol. So, we cannot use the ...

This is not in websocket protocol. So, we cannot use the websocket javascript API here. In my testing, I use a standalone Java client with plain Socket.

The source codes for both client and server for this blog ...

The source codes for both client and server for this blog can be found in https://svn.java.net/svn/glassfish~svn/trunk/v2/appserv-tests/devtests/w...
where I use a Java client with Socket.

OK. Thanks for the info. I decided to implement my own and I ...

OK. Thanks for the info. I decided to implement my own and I would like to share it with whoever ends up here looking for the code for the complete example so they can also run it. My protocol was based on yours, but to validate Brazilian tax code numbers (CPF, in Portuguese). So I changed the client code back to ISBN numbers and translated it to English. But I haven't tried it, so if anyone finds a bug, please add a comment.

Here's the code:

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class ISBNClient {
public static void main(String[] args) throws Exception {
String server = "localhost";
int port = 8080;
String url = "/my-web-app/test";

// Opens the socket with the Java Web Server
Socket socket = new Socket(server, port);
PrintWriter out = new PrintWriter(socket.getOutputStream());
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));

// Sends the HTTP headers requesting an upgrade of protocol.
out.println("GET " + url + " HTTP/1.1\r");
out.println("Host: localhost\r");
out.println("Upgrade: isbn\r");
out.println("Connection: Upgrade\r");
out.println("\r");
out.flush();

// Receives the protocol upgrade back.
System.out.println("Response to the upgrade request:");
String line = in.readLine();
while (line != null) {
System.out.println("\t" + line);
if (line.trim().equals("Connection: Upgrade")) break;
line = in.readLine();
}

// Checks a few ISBNs.
String[] isbns = new String[] {"9788577260447", "9788577807918", "0000000000"};
System.out.println("\nSending:");
for (int i = 0; i < isbns.length; i++) {
System.out.println("\t" + i + ": " + isbns[i]);
out.print(isbns[i] + " ");
}
out.print("EXIT");
out.flush();

// Reads the responses.
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println("\nReceiving:");
for (int i = 0; i < isbns.length; i++) System.out.println("\t" + i + ": " + in.readLine());

System.out.println("\nEnd.");
out.close();
in.close();
}
}