Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Building Real-Time Web Applications With Cettia, Part 1

DZone's Guide to

Building Real-Time Web Applications With Cettia, Part 1

In this tutorial, we will take a look at the features required to create real-time oriented web applications with Cettia and build the Cettia starter kit.

· Web Dev Zone ·
Free Resource

Deploy code to production now. Release to users when ready. Learn how to separate code deployment from user-facing feature releases with LaunchDarkly.

I started Cettia’s predecessor’s predecessor (a jQuery plugin for HTTP streaming that I used to demonstrate Servlet 3.0’s Async Servlet with IE 6) in 2011. Since then, WebSocket and Asynchronous IO have come into wide use, and it has become easier to develop and maintain real-time web applications in both client and server environments. In the meantime, however, functional and non-functional requirements have become more sophisticated and difficult to meet, and it has become harder to estimate and control the accompanying technical debt as well.

Cettia is the result of projects that started out as an effort to address these challenges and is a framework to create real-time web applications without compromise:

  • It is designed to run on any I/O framework on the Java Virtual Machine (JVM) seamlessly.
  • It provides a simple, full duplex connection even if given proxy, firewall, anti-virus software or arbitrary Platform as a Service (PaaS).
  • It is designed not to share data between servers and can be scaled horizontally with ease.
  • It offers an event system to classify events which take place server-side and client-side and can exchange them in real-time.
  • It streamlines a set of sockets handling that is helpful to greatly improve the multi-device user experience.
  • It deals with temporary disconnection and permanent disconnection in an event-driven way.

In this tutorial, we will take a look at the features required to create real-time oriented web applications with Cettia and build the Cettia starter kit. The source code for the starter kit is available at https://github.com/cettia/cettia-starter-kit.

Setting Up the Project

Before getting started, be sure that you have Java 8+ and Maven 3+ installed. According to statistics from Maven Central, Servlet 3, and Java WebSocket API 1 are the most-used I/O frameworks in writing Cettia applications, so we will use them to build the Cettia starter kit. Of course, you can use other frameworks like Grizzly and Netty, as you will see later.

First, create a directory called starter-kit. We will write and manage only the following three files in the directory:

  1. pom.xml: the Maven project descriptor. With this POM configuration, we can start up the server without a pre-installed ‘servlet container’ which is an application server that implements Servlet specification.
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xmlns="http://maven.apache.org/POM/4.0.0"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>
      <groupId>io.cettia.starter</groupId>
      <artifactId>cettia-starter-kit</artifactId>
      <version>0.0.1-SNAPSHOT</version>
      <packaging>war</packaging>
      <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <failOnMissingWebXml>false</failOnMissingWebXml>
      </properties>
      <dependencies>
        <dependency>
          <groupId>io.cettia</groupId>
          <artifactId>cettia-server</artifactId>
          <version>1.0.0</version>
        </dependency>
        <dependency>
          <groupId>io.cettia.asity</groupId>
          <artifactId>asity-bridge-servlet3</artifactId>
          <version>1.0.0</version>
        </dependency>
        <dependency>
          <groupId>io.cettia.asity</groupId>
          <artifactId>asity-bridge-jwa1</artifactId>
          <version>1.0.0</version>
        </dependency>
        <dependency>
          <groupId>javax.servlet</groupId>
          <artifactId>javax.servlet-api</artifactId>
          <version>3.1.0</version>
          <scope>provided</scope>
        </dependency>
        <dependency>
          <groupId>javax.websocket</groupId>
          <artifactId>javax.websocket-api</artifactId>
          <version>1.0</version>
          <scope>provided</scope>
        </dependency>
      </dependencies>
      <build>
        <plugins>
          <plugin>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-maven-plugin</artifactId>
            <version>9.4.8.v20171121</version>
          </plugin>
        </plugins>
      </build>
    </project>
    To start up the server on port 8080, run mvn jetty:run. This Maven command is all we do with Maven in this tutorial. If you can achieve it with other build tools such as Gradle, it’s absolutely fine to do that.
  2. src/main/java/io/cettia/starter/CettiaConfigListener.java: a Java class to play with the Cettia server. ServletContext is a context object to represent a web application in a servlet container, and we can access it when the web application initialization process is starting by implementing a ServletContextListener#contextInitialized method. Within the method, we will set up and play with Cettia. Let’s start with an empty listener:
    package io.cettia.starter;
    
    import javax.servlet.ServletContextEvent;
    import javax.servlet.ServletContextListener;
    import javax.servlet.annotation.WebListener;
    
    @WebListener
    public class CettiaConfigListener implements ServletContextListener {
      @Override
      public void contextInitialized(ServletContextEvent event) {}
    
      @Override
      public void contextDestroyed(ServletContextEvent event) {}
    }
    As we proceed through the tutorial, this class will be fleshed out. Keep in mind that you should restart the server every time you modify the class, especially in Windows.
  3. src/main/webapp/index.html: an HTML page to play with the Cettia client. We will handle JavaScript only since other parts such as HTML and CSS are not important. The following HTML loads the Cettia client, cettia, through the script tag from the unpkg CDN:
     <!DOCTYPE html>
     <title>index</title>
     <script src="https://unpkg.com/cettia-client@1.0.1/cettia-browser.min.js"></script>
    We will use the console only on this page, accessed through http://127.0.0.1:8080, to play with the cettia object interactively, rather than editing and refreshing the page. Otherwise, you can use bundlers such as Webpack or other runtimes like Node.js.

I/O Framework Agnostic Layer

To enable greater freedom of choice on a technical stack, Cettia is designed to run on any I/O framework seamlessly on the Java Virtual Machine (JVM) without degrading the underlying framework’s performance; this is achieved by creating an Asity project as a lightweight abstraction layer for Java I/O frameworks. Asity supports Atmosphere, Grizzly, Java Servlet, Java WebSocket API, Netty, and Vert.x.

Let’s write an HTTP handler and a WebSocket handler mapped to /cettia on Servlet and Java WebSocket API with Asity. These frameworks literally take responsibility for managing HTTP resources and WebSocket connections, respectively. Add the following imports to the CettiaConfigListener class:

import io.cettia.asity.action.Action;
import io.cettia.asity.http.ServerHttpExchange;
import io.cettia.asity.websocket.ServerWebSocket;
import io.cettia.asity.bridge.jwa1.AsityServerEndpoint;
import io.cettia.asity.bridge.servlet3.AsityServlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;
import javax.websocket.DeploymentException;
import javax.websocket.server.ServerContainer;
import javax.websocket.server.ServerEndpointConfig;

And place the following contents in the contextInitialized method:

// Asity part
Action<ServerHttpExchange> httpTransportServer = http -> System.out.println(http);
Action<ServerWebSocket> wsTransportServer = ws -> System.out.println(ws);

// Servlet part
ServletContext context = event.getServletContext();
// When it receives Servlet's HTTP request-response exchange, converts it to ServerHttpExchange and feeds httpTransportServer with it
AsityServlet asityServlet = new AsityServlet().onhttp(httpTransportServer);
// Registers asityServlet and maps it to "/cettia"
ServletRegistration.Dynamic reg = context.addServlet(AsityServlet.class.getName(), asityServlet);
reg.setAsyncSupported(true);
reg.addMapping("/cettia");

// Java WebSocket API part
ServerContainer container = (ServerContainer) context.getAttribute(ServerContainer.class.getName());
ServerEndpointConfig.Configurator configurator = new ServerEndpointConfig.Configurator() {
  @Override
  public <T> T getEndpointInstance(Class<T> endpointClass) {
    // When it receives Java WebSocket API's WebSocket connection, converts it to ServerWebSocket and feeds wsTransportServer with it
    AsityServerEndpoint asityServerEndpoint = new AsityServerEndpoint().onwebsocket(wsTransportServer);
    return endpointClass.cast(asityServerEndpoint);
  }
};
// Registers asityServerEndpoint and maps it to "/cettia"
try {
  container.addEndpoint(ServerEndpointConfig.Builder.create(AsityServerEndpoint.class, "/cettia").configurator(configurator).build());
} catch (DeploymentException e) {
  throw new RuntimeException(e);
}

As you would intuitively expect, httpTransportServer and wsTransportServer are Asity applications, and they can run on any framework as long as it’s possible to feed them with ServerHttpExchange and ServerWebSocket. The Cettia server is also basically an Asity application.

In this step, you can play with Asity resources directly by submitting an HTTP request and WebSocket request to /cettia; but we won’t delve into Asity in this tutorial. Consult the Asity website if you are interested. Unless you need to write an Asity application from scratch, you can safely ignore Asity; just note that even if your favorite framework is not supported, with about 200 lines of code, you can write an Asity bridge to your framework and run Cettia via that bridge.

Installing Cettia

Before diving into the code, let’s establish three primary concepts of Cettia at the highest conceptual level:

  • Server - An interface used to interact with sockets. It offers an event to initialize newly accepted sockets and provides finder methods to find sockets matching the given criteria and execute the given socket action.
  • Socket - A feature-rich interface built on the top of the transport. It provides the event system that allows you to define your own events, regardless of the type of event data, and exchange them between the Cettia client and the Cettia server in real-time.
  • Transport - An interface to represent a full duplex message channel. It carries a binary as well as a text payload based on message framing, exchanges messages bidirectionally, and ensures no message loss and no idle connection. Unlike Server and Socket, you don’t need to be aware of the Transport unless you want to tweak the default transport behavior or introduce a brand new transport.

Let’s set up the Cettia server on top of the above I/O framework agnostic layer and open a socket as a smoke test. Add the following imports:

import io.cettia.DefaultServer;
import io.cettia.Server;
import io.cettia.ServerSocket;
import io.cettia.transport.http.HttpTransportServer;
import io.cettia.transport.websocket.WebSocketTransportServer;

Now, replace the above Asity part with the following Cettia part:

// Cettia part
Server server = new DefaultServer();
HttpTransportServer httpTransportServer = new HttpTransportServer().ontransport(server);
WebSocketTransportServer wsTransportServer = new WebSocketTransportServer().ontransport(server);

// The socket handler
server.onsocket((ServerSocket socket) -> System.out.println(socket));

As an implementation of Action<ServerHttpExchange> and TransportServer, HttpTransportServer consumes HTTP request-response exchanges and produces streaming transport and long-polling transport, and as an implementation of Action<ServerWebSocket> and TransportServer, WebSocketTransportServerconsumes the WebSocket resource and produces a WebSocket transport. These produced transports are passed into the Server and used to create and maintain ServerSockets.

It is true that WebSocket transport is enough these days, but if proxy, firewall, anti-virus software or arbitrary Platform as a Service (PaaS) are involved, it’s difficult to be absolutely sure that WebSocket alone will work. That’s why we recommend you install HttpTransportServer along with WebSocketTransportServer for broader coverage of full duplex message channels in a variety of environments.

ServerSocket created by Server is passed to socket handlers registered through server.onsocket(socket -> {}), and this handler is where you should initialize the socket. Because it is costly to accept transport and socket, you should authenticate requests in advance, if needed, outside of Cettia and filter out unqualified requests before passing them to Cettia. For example, it would look like this, assuming that Apache Shiro is used:

server.onsocket(socket -> {
  Subject currentUser = SecurityUtils.getSubject();
  if (currentUser.hasRole("admin")) {
    // ...
  }
});

On the client side, you can open a socket pointing to the URI of the Cettia server with cettia.open(uri). Run the following snippet in the console on the index page:

var socket = cettia.open("http://127.0.0.1:8080/cettia");

If everything is set up correctly, you should be able to see a socket log in the server-side and what has been happening through the network panel of the developer tools in the client-side. If WebSocket transport is not available in either the client or the server for some reason, the Cettia client falls back to HTTP-based transports automatically. Comment out the Java WebSocket API part and open a socket again, or open the index page in Internet Explorer 9. In any case, you’ll see that a socket is opened.

Socket Lifecycle

A socket always is in a specific state, such as opened or closed, and its state keeps changing based on the state of the underlying transport. Cettia defines the state transition diagram for the client socket and the server socket and provides various built-in events, which allows fine-grained handling of a socket when a state transition occurs. If you make good use of these diagrams and built-in events, you can easily handle stateful sockets in an event-driven way without having to manage their states by yourself.

Add the following code to the socket handler in the CettiaConfigListener. It logs the socket’s state when a state transition occurs.

Action<Void> logState = v -> System.out.println(socket + " " + socket.state());
socket.onopen(logState).onclose(logState).ondelete(logState);

Here’s the state transition diagram of a server socket:

server-state-diagram

  1. On the receipt of a transport, the server creates a socket with a NULL state and passes it to the socket handlers.
  2. If it fails to perform the handshake, it transitions to a CLOSED state and fires a close event.
  3. If it performs the handshake successfully, it transitions to an OPENED state and fires an open event. The communication is possible only in this state.
  4. If connection is disconnected for some reason, it transitions to a CLOSED state and fires a close event.
  5. If the connection is recovered by the client reconnection, it transitions to an OPENED state and fires open event.
  6. After one minute has elapsed since the CLOSED state, it transitions to the final state and fires a delete event. Sockets in this state shouldn’t be used.

As you can see, if a state transition of 4 happens, it is supposed to transition to either 5 or 6. You may want to resend events that the client couldn’t receive without a connection on the former, and take action to notify the user of missed events, like push notifications on the latter. We will discuss how to do that in detail later on.

On the client side, it’s very important to inform the user of what’s going on the wire in terms of the user experience. Open a socket and add an event handler to log the socket’s state when a state transition occurs:

var socket = cettia.open("http://127.0.0.1:8080/cettia");
var logState = arg => console.log(socket.state(), arg);
socket.on("connecting", logState).on("open", logState).on("close", logState).on("waiting", logState);

Here’s the state transition diagram of a client socket:

client-state-diagram

  1. If a socket is created by cettia.open and starts a connection, it transitions to a connecting state and fires a connecting event.
  2. If all transports fail to connect in time, it transitions to a closed state and fires a close event.
  3. If one of the transports succeeds in establishing a connection, it transitions to an opened state and fires an open event. The communication is possible only in this state.
  4. If the connection is disconnected for some reason, it transitions to a closed state and fires a close event.
  5. If reconnection is scheduled, it transitions to a waiting state and fires a waiting event with a reconnection delay and total reconnection attempts.
  6. If the socket starts a connection after the reconnection delay has elapsed, it transitions to a connecting state and fires a connecting event.
  7. If the socket is closed by the socket.close method, it transitions to the final state. Sockets in this state shouldn’t be used.

If there’s no problem with the connection, the socket will have a state transition cycle of 3-4-5-6. If not, it will have a state transition cycle of 2-5-6. Restart or shutdown the server for a state transition of 4-5-6 or 2-5-6.

Sending and Receiving Events

The most common pattern with which to exchange various types of data through a single channel is the Command Pattern; a command object is serialized and sent over the wire, and then deserialized and executed on the other side. At first, JSON and a switch statement should suffice for the purpose of implementing the pattern, but it becomes a burden to maintain and accrues technical debt if you have to handle binary types of data; implement a heartbeat and make sure you get an acknowledgment of the data. Cettia provides an event system that is flexible enough to accommodate these requirements.

A unit of exchange between the Cettia client and the Cettia server in real-time is the event which consists of a required name property and an optional data property. You can define and use your own events as long as the name isn’t duplicated with built-in events. Here’s the echo event handler where any received echo event is sent back. Add it to the socket handler:

socket.on("echo", data -> socket.send("echo", data));

In the code above, we didn’t manipulate or validate the given data, but it’s not as realistic to use a typeless input as it is in the server. The allowed types for the event data are determined by Jackson, a JSON processor that Cettia uses internally. If an event data is supposed to be one of the primitive types, you can cast and use it with the corresponding wrapper class, and if it’s supposed to be an object like List or Map and you prefer POJOs, you can convert and use it with JSON library like Jackson. It might look like this:

socket.on("event", data -> {
  Model model = objectMapper.convertValue(data, Model.class);
  Set<ConstraintViolation<Model>> violations = validator.validate(model);
  // ...
});

On the client side, event data is simply JSON with some exceptions. The following is the client code to test the server’s echo event handler. This simple client sends an echoevent with arbitrary data to the server on an open event and logs the data of an echo event to be received in return to the console.

var socket = cettia.open("http://127.0.0.1:8080/cettia");
socket.once("open", () => socket.send("echo", "Hello world"));
socket.on("echo", data => console.log(data));

As we decided to use the console, you can type and run code snippets, e.g.: socket.send("echo", {text: "I'm a text", binary: new TextEncoder().encode("I'm

a binary")}).send("echo", "It's also chainable") and watch results on the fly. Try it on your console.

As the example suggests, event data can be basically anything as long as it is serializable, regardless of whether data is binary or text. If at least one of the properties of the event data is byte[] or ByteBuffer in the server, Buffer in Node or ArrayBuffer in the browser, the event data is treated as binary and MessagePack format is used instead of JSON format. In short, you can exchange event data, including binary data, with no issue.

That's all for today! Tune in tomorrow when we'll cover broadcasting events, working with specific sockets, disconnection handling, and scaling your application. 

Deploy code to production now. Release to users when ready. Learn how to separate code deployment from user-facing feature releases with LaunchDarkly.

Topics:
javascript ,websocket ,asynchronous ,web dev ,web application development

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}