Getting Started

This is a summary of a tutorial, Building Real-Time Web Applications With Cettia, for quick start. The tutorial covers how to create real-time web applications with Cettia in more depth. We recommend to read it if you want better understanding of Cettia.

The result of the tutorial, the Cettia 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 running Jetty server with 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 an io.cettia:cettia-server:1.2.0 (Javadoc) as a dependency of your application.

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

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

Server server = new DefaultServer();
HttpTransportServer hts = new HttpTransportServer().ontransport(server);
WebSocketTransportServer wts = new WebSocketTransportServer().ontransport(server);

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

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

Cettia is based on 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 hts and wts with the framework’s HTTP request-response exchange and WebSocket connection through bridges per framework provided by Asity like the above 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("/cettia");

You may have to use an absolute URI, http://127.0.0.1:8080/cettia, if you use runtimes other than browser like Node.js. If everything is set up correctly, you should be able to see a socket log similar to the following in the server-side.

ServerSocket@9e14198f-fc59-47b1-9910-6de1174a13b5[state=null,tags=[],attributes={}]

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, firing one of built-in events. 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

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

An attributes and its sugar methods on ServerSocket are as follows.

Map<String, Object> attributes()
Returns an attributes of the socket.
Object get(key)
Returns the value mapped to the given name.
ServerSocket set(key, value)
Associates the value with the given name in the socket.
ServerSocket remove(key)
Removes the mapping associated with the given name.

Tags

A tags and its sugar methods on ServerSocket are as follows.

Set<String> tags()
Returns a tags of the socket.
ServerSocket tag(tags...)
Attaches given tags to the socket.
ServerSocket untag(tags...)
Detaches given tags from the socket.

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 used by Cettia 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.

Disconnection Handling

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.

List<Object[]> cache = new CopyOnWriteArrayList<>();
socket.oncache((Object[] args) -> cache.add(args));
socket.onopen(v -> cache.forEach(args -> {
  cache.remove(args);
  socket.send((String) args[0], args[1], (Action<?>) args[2], (Action<?>) args[3]);
}));
socket.ondelete(v -> cache.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. If there has been no reconnection within one minute since disconnection, the delete event is fired and the lifecycle of socket ended. With the delete event, you can send an email or push notifications about events which the socket finally missed.

Working with Sockets

The most common use case in a 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” pattern without a separate concept like Topic and Broadcaster.

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

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. Here’s an example to send a chat event to every socket in the server.

server.find(socket -> true).execute(socket -> socket.send("chat", "Hi, there"));

Along with server.find and sentence.execute, Cettia offers the following pre-defined predicates and socket actions through ServerSocketPredicates and Sentence, respectively, to make the code even more expressive and readable.

ServerSocketPredicates

The following are static methods to create socket predicates defined in ServerSocketPredicates.

all()
A predicate that always matches.
attr(String key, Object value)
A predicate that tests the socket attributes against the given key-value pair.
id(ServerSocket socket)
A predicate that tests the socket id against the given socket's id.
id(String id)
A predicate that tests the socket id against the given socket id.
tag(String... tags)
A predicate that tests the socket tags against the given tags.

Here’s an example to find sockets whose username is the same except the socket. Assume the attr and id are statically imported from the ServerSocketPredicates class.

ServerSocketPredicate p = attr("username", username).and(id(socket).negate());

Sentence

Each method on Sentence is mapped to a pre-implemented common socket action, so if the method is executed, its mapped action is executed with sockets matching the sentence’s predicate. Here is a list of methods on the sentence.

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

Here’s an example to send a klose event to given sockets and close their connections.

server.find(p).send("klose").close();

Scaling a Cettia 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. This 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.