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.
Join the DZone community and get the full member experience.
Join For FreeI 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:
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.
To start up the server on port 8080, run<?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>
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.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 aServletContextListener#contextInitialized
method. Within the method, we will set up and play with Cettia. Let’s start with an empty listener:
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.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) {} }
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:
We will use the console only on this page, accessed through http://127.0.0.1:8080, to play with the<!DOCTYPE html> <title>index</title> <script src="https://unpkg.com/cettia-client@1.0.1/cettia-browser.min.js"></script>
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
, WebSocketTransportServer
consumes the WebSocket resource and produces a WebSocket transport. These produced transports are passed into the Server
and used to create and maintain ServerSocket
s.
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:
- On the receipt of a transport, the server creates a socket with a
NULL
state and passes it to the socket handlers. - If it fails to perform the handshake, it transitions to a
CLOSED
state and fires aclose
event. - If it performs the handshake successfully, it transitions to an
OPENED
state and fires anopen
event. The communication is possible only in this state. - If connection is disconnected for some reason, it transitions to a
CLOSED
state and fires aclose
event. - If the connection is recovered by the client reconnection, it transitions to an
OPENED
state and firesopen
event. - After one minute has elapsed since the
CLOSED
state, it transitions to the final state and fires adelete
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:
- If a socket is created by
cettia.open
and starts a connection, it transitions to aconnecting
state and fires aconnecting
event. - If all transports fail to connect in time, it transitions to a
closed
state and fires aclose
event. - If one of the transports succeeds in establishing a connection, it transitions to an
opened
state and fires anopen
event. The communication is possible only in this state. - If the connection is disconnected for some reason, it transitions to a
closed
state and fires aclose
event. - If reconnection is scheduled, it transitions to a
waiting
state and fires awaiting
event with a reconnection delay and total reconnection attempts. - If the socket starts a connection after the reconnection delay has elapsed, it transitions to a
connecting
state and fires aconnecting
event. - 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 echo
event 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.
Opinions expressed by DZone contributors are their own.
Comments