Following the previous post about a HTTP server based on the Simple Framework library, here’s the Websocket use case. The example gets a bit more complicated with both a HTTP handler, Websocket handler and client, but still a lot simpler than the original example code.

Two interfaces have to be implemented to have a meaningful Websocket server: First, the Service interface, which gets invoked when a new connection and Session is established. From here it is possible to register a FrameListener on the session channel, which is the second interface to implement. The FrameListener methods will get invoked when new Frames, whether it is data or control codes is received. This listener also gets notified about errors and when the session or socket is closed. Two minimal examples of these implementations are shown below, followed by the setup() method which ties it all together and starts the server. Notice how RouterContainer takes both the HTTP and Websocket handlers, and routes the incoming requests appropriately.

  class WebsocketService implements Service {
    @Override
    public void connect(Session session) {
      System.out.println("Session: " + session);
      try {
        FrameChannel channel = session.getChannel();
        channel.register(new WebsocketFrameListener());
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }

  class WebsocketFrameListener implements FrameListener {

    @Override
    public void onFrame(Session session, Frame frame) {
      System.out.println("onFrame " + frame.getType() + ": " + frame);
    }

    @Override
    public void onError(Session session, Exception cause) {
      System.out.println("onError: " + cause);
    }

    @Override
    public void onClose(Session session, Reason reason) {
      System.out.println("onClose: " + reason);
    }
  }

  public void setup() throws IOException, InterruptedException {
    HttpContainer httpContainer = new HttpContainer();
    Router websocketRouter = new DirectRouter(new WebsocketService());
    RouterContainer routerContainer = new RouterContainer(httpContainer, websocketRouter, 1);

    ContainerSocketProcessor server = new ContainerSocketProcessor(routerContainer, 1);
    serverSocket = new SocketConnection(server);

    serverSocket.connect(new InetSocketAddress(PORT));
  }

To test the core functionality of the Websocket server, a bit more is needed on the client side. First, a HTTP like handshake has to be requested and the response read, then the client must respond to ping control frames, and finally, it can send frames of data itself.

Below is the method which sends the handshake to the server as specified in the Websocket RFC 6455. There’s a few things to note: Currently, details like target path, the host, origin are hard-coded. In a normal client, they should be resolved and set in a proper way. The security key is also hard-coded, but should be generated according to the specification. Finally, note that the line ending has to be “\r\n” or 0xd, 0xa in hex. Therefore, when using a PrintWriter to wrap the raw OutputStream, be careful not to use the println() methods as they might append the incorrect line termination.

What’s useful about a minimal test client like this, is that’s it becomes easy to try out these kind of details in isolation, without distractions from other functionality. Although this example does not go as far as asserting the behaviour, it could easily be extended to an in-memory integration test of the server.

  private void sendHandshake(OutputStream rawOut) {
    String handshake[] = {
      "GET /test HTTP/1.1",
      "Host: loclahost",
      "Upgrade: websocket",
      "Connection: Upgrade",
      "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==",
      "Origin: http://localhost:" + PORT,
      "Sec-WebSocket-Protocol: test",
      "Sec-WebSocket-Version: 13",
      ""
    };

    PrintWriter pwOut = new PrintWriter(rawOut, true);
    for (String str : handshake) {
      pwOut.print(str + "\r\n");
    }
    pwOut.flush();
  }

  private void receiveHandshake(InputStream rawIn) throws IOException {
    BufferedReader bufIn = new BufferedReader(new InputStreamReader(rawIn));

    System.out.println("Response: ");
    String line;
    do {
      line = bufIn.readLine();
      System.out.println("R: '" + line + "'");
    } while (line != null && !line.isEmpty());
  }

Finally, this test method ties the client together: it first opens the socket connection to the server; gets the IO streams; sends and receives the handshake; starts the ping response handler on a separate thread; sends one frame of its own with a ping, waits for some seconds to observe the server in operation, and finally sends a close operation code before closing the connection.

  public void testWebsocketRequest() throws IOException, InterruptedException {
    Socket socket = new Socket("localhost", PORT);

    OutputStream rawOut = socket.getOutputStream();
    InputStream rawIn = socket.getInputStream();

    sendHandshake(rawOut);
    receiveHandshake(rawIn);

    pingPongHandler(rawIn, rawOut);

    sendPing(rawOut);

    Thread.sleep(20000);

    clientClosed = true;
    sendClose(rawOut);

    socket.close();
  }

Here’s the full test case listing, which also includes a HTTP request test. A dependency on the Simple Framework library is needed, for example through the Gradle dependency:

repositories {
  mavenCentral()
}

dependencies {
  compile 'org.simpleframework:simple-http:6.0.1'
}
SfWebsocketServerTest.java
GitHub Raw
/* Copyright rememberjava.com. Licensed under GPL 3. See http://rememberjava.com/license */
package com.rememberjava.http;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.Arrays;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.simpleframework.http.Request;
import org.simpleframework.http.Response;
import org.simpleframework.http.core.Container;
import org.simpleframework.http.core.ContainerSocketProcessor;
import org.simpleframework.http.socket.Frame;
import org.simpleframework.http.socket.FrameChannel;
import org.simpleframework.http.socket.FrameListener;
import org.simpleframework.http.socket.Reason;
import org.simpleframework.http.socket.Session;
import org.simpleframework.http.socket.service.DirectRouter;
import org.simpleframework.http.socket.service.Router;
import org.simpleframework.http.socket.service.RouterContainer;
import org.simpleframework.http.socket.service.Service;
import org.simpleframework.transport.connect.SocketConnection;

/**
 * Tests a combined HTTP and Websocket server using the SimpleFramework API.
 * 
 * See http://www.simpleframework.org/
 */
public class SfWebsocketServerTest {

  private static final int PORT = 48889;

  /**
   * Handles incoming HTTP requests.
   */
  class HttpContainer implements Container {

    @Override
    public void handle(Request req, Response resp) {
      System.out.println("HTTP Request: \n" + req);

      resp.setCode(200);
      try {
        resp.getByteChannel().write(ByteBuffer.wrap("Hello".getBytes()));
        resp.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }

  /**
   * Handles incoming Websocket requests and establishes the frame channel
   * listener.
   */
  class WebsocketService implements Service {
    @Override
    public void connect(Session session) {
      System.out.println("Session: " + session);
      try {
        FrameChannel channel = session.getChannel();
        channel.register(new WebsocketFrameListener());
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }

  /**
   * Handles incoming Frames from an established channel.
   */
  class WebsocketFrameListener implements FrameListener {

    @Override
    public void onFrame(Session session, Frame frame) {
      System.out.println("onFrame " + frame.getType() + ": " + frame);
    }

    @Override
    public void onError(Session session, Exception cause) {
      System.out.println("onError: " + cause);
    }

    @Override
    public void onClose(Session session, Reason reason) {
      System.out.println("onClose: " + reason);
    }
  }

  private SocketConnection serverSocket;

  private boolean clientClosed;

  @Before
  public void setup() throws IOException, InterruptedException {
    HttpContainer httpContainer = new HttpContainer();
    Router websocketRouter = new DirectRouter(new WebsocketService());
    RouterContainer routerContainer = new RouterContainer(httpContainer, websocketRouter, 1);

    ContainerSocketProcessor server = new ContainerSocketProcessor(routerContainer, 1);
    serverSocket = new SocketConnection(server);

    serverSocket.connect(new InetSocketAddress(PORT));
  }

  @After
  public void close() throws IOException {
    serverSocket.close();
  }

  /**
   * Tests the HTTP part only
   */
  @Test
  public void testHttpRequest() throws Exception {
    URL url = new URL("http://localhost:" + PORT);
    BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));
    in.lines().forEach(System.out::println);
    in.close();
  }

  /**
   * Sends a HTTP Websocket handshake to the server; prints its response;
   * listens for pings and sends pongs; sends a ping; sends a close and closes
   * the client socket after 20 seconds.
   */
  @Test
  public void testWebsocketRequest() throws IOException, InterruptedException {
    Socket socket = new Socket("localhost", PORT);

    OutputStream rawOut = socket.getOutputStream();
    InputStream rawIn = socket.getInputStream();

    sendHandshake(rawOut);
    receiveHandshake(rawIn);

    pingPongHandler(rawIn, rawOut);

    sendPing(rawOut);

    Thread.sleep(20000);

    clientClosed = true;
    sendClose(rawOut);

    socket.close();
  }

  private void sendClose(OutputStream out) throws IOException {
    sendOpcode(out, 0x88);
  }

  private void sendPing(OutputStream out) throws IOException {
    sendOpcode(out, 0x89);
    System.out.println("Sent ping");
  }

  private void sendPong(OutputStream out) throws IOException {
    sendOpcode(out, 0x8a);
    System.out.println("Sent pong");
  }

  private void sendOpcode(OutputStream out, int code) throws IOException {
    out.write(new byte[] { (byte) code, (byte) 0 });
    out.flush();
  }

  /**
   * Client side. Starts a separate thread which reads the InputStream stream
   * and checks for ping frames. If one is detected, sends a pong on the
   * OutputStream.
   */
  private void pingPongHandler(InputStream in, OutputStream out) {
    final int pingOpCode = 0x89;

    new Thread(() -> {
      while (!clientClosed) {
        try {
          byte[] buf = readInput(in);

          if (buf.length >= 2 && (buf[0] & 0xff) == pingOpCode) {
            sendPong(out);
          } else if (buf.length > 0) {
            System.out.println("From server:");
            for (int i = 0; i < buf.length; i++) {
              System.out.print(Integer.toHexString(buf[i]) + " ");
            }
            System.out.println();
          }

          Thread.sleep(50);
        } catch (IOException | InterruptedException e) {
          e.printStackTrace();
        }
      }
    }).start();
  }

  /**
   * Client side. Reads available bytes from the InputStream.
   */
  private byte[] readInput(InputStream in) throws IOException {
    byte[] buf = new byte[2000];
    int i = 0;
    while (in.available() > 0) {
      buf[i++] = (byte) in.read();
    }

    return Arrays.copyOf(buf, i);
  }

  /**
   * Client side. Sends the Websocket handshake request to the server.
   */
  private void sendHandshake(OutputStream rawOut) {
    String handshake[] = {
      "GET /test HTTP/1.1",
      "Host: loclahost",
      "Upgrade: websocket",
      "Connection: Upgrade",
      "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==",
      "Origin: http://localhost:" + PORT,
      "Sec-WebSocket-Protocol: test",
      "Sec-WebSocket-Version: 13",
      ""
    };

    PrintWriter pwOut = new PrintWriter(rawOut, true);
    for (String str : handshake) {
      pwOut.print(str + "\r\n");
    }
    pwOut.flush();
  }

  /**
   * Client side. Reads the Websocket handshake response from the server.
   */
  private void receiveHandshake(InputStream rawIn) throws IOException {
    BufferedReader bufIn = new BufferedReader(new InputStreamReader(rawIn));

    System.out.println("Response: ");
    String line;
    do {
      line = bufIn.readLine();
      System.out.println("R: '" + line + "'");
    } while (line != null && !line.isEmpty());
  }
}