Cettia

Cettia is a full-featured real-time web application framework for Java that you can use to exchange events between server and client in real-time. It is meant for when you run into issues which are tricky to resolve with WebSocket, JSON, and switch statement per se: avoiding repetitive boilerplate code, supporting environments where WebSocket is not available, handling both text and binary data together, recovering missed events, providing multi-device user experience, scaling out an application, and so on. It offers a reliable full duplex message channel and elegant patterns to achieve better user experience in the real-time web, and is compatible with any web frameworks on the Java Virtual Machine.

If you are interested and would like to be more involved, feel free to join the community and share your feedback.

Getting Started

This guide is based on 1.1 which is in beta phase, io.cettia:cettia-server:1.1.0-Beta1. Features that available in 1.1 are labeled with 1.1.

This is a summary of a tutorial, "Building Real-Time Web Applications With Cettia", for quick start. If you want to get to know the reason behind key design decisions that the Cettia team have made in the Cettia, please read the tutorial.

The result of the tutorial, the starter kit, is available in the GitHub repository. If you have Java 8+ and Maven 3+ installed, you can run the example by cloning or downloading the repository and typing the following maven command.

git clone https://github.com/cettia/cettia-starter-kit
cd cettia-starter-kit
mvn jetty:run

Then, open a browser and connect to http://localhost:8080.

Setting Up the Project

Server

Add a io.cettia:cettia-server:1.0.0 (Javadoc) as a dependency of your application.

<dependency>
  <groupId>io.cettia</groupId>
  <artifactId>cettia-server</artifactId>
  <version>1.0.0</version>
</dependency>

Then, you can accept and handle sockets that connect to the server through server.onsocket(socket -> {}).

Server server = new DefaultServer();
HttpTransportServer httpAction = new HttpTransportServer().ontransport(server);
WebSocketTransportServer wsAction = new WebSocketTransportServer().ontransport(server);

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

// javax.servlet.Servlet asityServlet = new AsityServlet().onhttp(httpAction);
// javax.websocket.Endpoint asityEndpoint = new AsityServerEndpoint().onwebsocket(wsAction);

Cettia is a web fragment of Asity and compatible with any web framework on the Java Virtual Machine. As you can see in the commend out code, the above application is able to run on any framework as long as you feed httpAction and wsAction with the framework's HTTP request-response exchange and WebSocket connection through bridges per framework Asity provides like asityServlet and asityEndpoint. For the usage of bridge, see Asity's "Run Anywhere" section. Asity supports almost all popular web frameworks in Java: Servlet and Java API for WebSocket, Spring WebFlux, Spring MVC, Grizzly, Vert.x, Netty, Atmosphere, and so on.

The tutorial uses Servlet and Java API for WebSocket as a web framework and passes requests whose URI is /cettia to the Cettia server. In other means, the Cettia client can connect to this server through http://127.0.0.1:8080/cettia.

Client

Load the cettia object the way you want.

CDN
<script src="https://unpkg.com/cettia-client@1.0.1/cettia-browser.min.js"></script>
Webpack
npm install cettia-client --save
var cettia = require("cettia-client/cettia-bundler");
Node
npm install cettia-client --save
var cettia = require("cettia-client");

Then, you can open a socket pointing to the URI of the Cettia server with cettia.open(uri).

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

You can use the relative form, /cettia, if it represents the same URI. If everything is set up correctly, you should be able to see a socket log in the server-side.

Socket Lifecycle

A socket always is in a specific state, such as opened or closed. Its state keeps changing based on the state of the underlying connection and fires a built-in event. Just know that the communication is possible only in the opened state.

Server

The state transition diagram of a server socket.

server-state-diagram

Tracking the state transition of the server socket.

server.onsocket(socket -> { // By 1
  Action<Void> log = v -> System.out.println(socket.state());
  socket.onopen(log); // By 3 and 5
  socket.onclose(log); // By 2 and 4
  socket.ondelete(log); // By 6
});

Client

The state transition diagram of a client socket.

client-state-diagram

Tracking the state transition of the client socket.

var log = arg => console.log(socket.state(), arg);
socket.on("connecting", log); // By 1 and 6
socket.on("open", log); // By 3
socket.on("close", log); // By 2, 4, and 7
socket.on("waiting", log); // By 5

Sending and Receiving Events

A unit of exchange between the Cettia client and the Cettia server in real-time is the event. You can define and use your own events as long as the event name isn't duplicated with built-in events. Here's the echo event handler where any received echo event is sent back.

Server

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

Client

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

In the server side, the allowed types for the event data are not just Object, but 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);
  // ...
});

An 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 internally treated as binary, and that binary property is given as a ByteBuffer in the server, a Buffer in Node, and an ArrayBuffer in the browser.

Attributes and Tags

In order to store information regarding socket like username in a socket and find sockets based on the stored information, Cettia provides attributes and tags per socket. They are analogous to data-* attributes and class attribute defined in HTML, respectively.

Attributes 1.1

Map<String, Object> attributes = socket.attributes();

The shortcuts.

socket.get(key)
Returns the value mapped to the given name.
socket.set(key, value)
Associates the value with the given name in the socket.
socket.remove(key)
Removes the mapping associated with the given name.

Tags

Set<String> tags = socket.tags();

The shortcuts.

socket.tag(tag)
Attaches given tags to the socket.
socket.untag(tag)
Detaches given tags from the socket.

Find Sockets and Do Something

The most common use case in real-time web application is to push messages to certain clients, of course. Cettia supports this intuitively by enabling "find sockets and do something with them" without a separate concept like Topic and Broadcaster.

server.find(socket -> /* find sockets */).execute(socket -> /* do something with them */);

As you would intuitively expect, server.find(predicate) finds a certain set of sockets that matches the given predicate and returns an instance of fluent interface called Sentence, and sentence.execute(action) allows to deal with the sockets through the passed socket action.

Along with server.find and sentence.execute, Cettia offers the following pre-defined predicates and socket actions to make the code even more expressive and readable. (Now only the following shortcut methods are available, and server.find and sentence.execute are supposed to be added in 1.1)

Selector

server.all()
All sockets.
server.byTag(tag...)
Sockets tagged with a tag

Behavior

sentence.send(event, data)
Sends a given event with data through the socket.
sentence.close()
Closes the socket.
sentence.tag(tag)
Attaches given tags to the socket.
sentence.untag(tag)
Detaches given tags from the socket.

Recovering Missed Events

Cettia defines the temporary disconnection as one that is followed by reconnection within 60 seconds, and designs a socket's lifecycle to be unaffected by temporary disconnections, to support environments where temporary disconnections happen frequently just like the mobile environment. Here's an example to send events failed due to disconnection on the next connection.

Queue<Object[]> queue = new ConcurrentLinkedQueue<>();
socket.oncache(args -> queue.offer(args));
socket.onopen(v -> {
  while (socket.state() == ServerSocket.State.OPENED && !queue.isEmpty()) {
    Object[] args = queue.poll();
    socket.send((String) args[0], args[1], (Action<?>) args[2], (Action<?>) args[3]);
  }
});
socket.ondelete(v -> queue.forEach(args -> System.out.println(socket + " missed event - name: " + args[0] + ", data: " + args[1])));

The cache event above is fired with an argument array used to call the send method, if the socket has no active connection when the send method is called. With the cache and delete event, you could send missed events on the next reconnection or store them in a database and show them on the next visit.

Scaling a Cettia Application

Last but not least is scaling an application. Any publish-subscribe messaging system can be used to scale a Cettia application horizontally, and it doesn't require any modification in the existing application. Here's an example of Hazelcast. Replace Server server = new DefaultServer(); with ClusteredServer server = new ClusteredServer();, and add the following dependencies to your application:

<dependencies>
  <dependency>
    <groupId>com.hazelcast</groupId>
    <artifactId>hazelcast</artifactId>
    <version>3.9.3</version>
  </dependency>
  <dependency>
    <groupId>com.hazelcast</groupId>
    <artifactId>hazelcast-client</artifactId>
    <version>3.9.3</version>
  </dependency>
</dependencies>

Then place the following Hazelcast configuration after ClusteredServer server = new ClusteredServer();.

HazelcastInstance hazelcast = HazelcastInstanceFactory.newHazelcastInstance(new Config());
ITopic<Map<String, Object>> topic = hazelcast.getTopic("cettia");
server.onpublish(message -> topic.publish(message));
topic.addMessageListener(message -> server.messageAction().on(message.getMessageObject()));

If you start up the server with different port such as 8090, you should see servers listening to 8080 and 8090 form a a cluster of Hazelcast nodes. It means that a chat event sent from a client connected to the server on 8080 propagates to clients connected to the server on 8090 as well as 8080.

Examples

Here are community-driven examples: