Cettia

A real-time web application framework

Cettia is a real-time web application framework that allows you to focus on event handling itself. It provides reliable full duplex connection which frees you from the pitfalls of traditional real-time programming and elegant patterns which eliminates the boilerplate code involved with full duplex connection. Besides, it's designed to work well with any transport technology, any I/O framework, and any scaling methods, which is critical for modern enterprise applications.

Explore the Cettia subprojects:


flowersinthesand flowersinthesand wrote Cettia JavaScript Client 1.0.0-Beta2 released on January 7, 2017.


I/O framework agnostic

Cettia runs on almost any platform including Java Servlet and Java Websocket API on the JVM (Java Virtual Machine) seamlessly. Here's an example of Grizzly.

public class Bootstrap {
  public static void main(String[] args) throws Exception {
    // Creates a Cettia app
    Server server = new DefaultServer();
    HttpTransportServer httpTransportServer = new HttpTransportServer().ontransport(server);
    WebSocketTransportServer wsTransportServer = new WebSocketTransportServer().ontransport(server);

    // Sets up the app on Grizzly
    HttpServer httpServer = HttpServer.createSimpleServer();
    httpServer.getServerConfiguration().addHttpHandler(new AsityHttpHandler().onhttp(httpTransportServer), "/cettia");
    httpServer.getListener("grizzly").registerAddOn(new WebSocketAddOn());
    WebSocketEngine.getEngine().register("", "/cettia", new AsityWebSocketApplication().onwebsocket(wsTransportServer));
    httpServer.start();

    System.in.read();
  }
}

Of course, besides Grizzly, Atmosphere, Java WebSocket API, Netty, Servlet and Vert.x are now supported. Although your favorite platform is not supported, you can bridge your application to the platform with only a little bit of effort.

Event-based

From the semantic point of view, the unit of data to be sent and received via a socket is event associated with a customizable type. You don't need to write the boilerplate code to implement your own event system to organize business logic.

Note that when sending an event, you can attach any arbitrary object including binary as well as text to the event. Never mind what type of data is being sent. FYI, JSON and MessagePack are used internally for serialization.

Java Server

Server server = new DefaultServer();
server.onsocket((ServerSocket socket) -> {
  socket.onopen((Void v) -> {
    // Text data
    socket.send("discard", "test");
    // Binary data
    socket.send("discard", "test".getBytes());
    socket.send("discard", ByteBuffer.wrap("test".getBytes());
    // Composite data
    socket.send("discard", new LinkedHashMap<String, Object>() {{
      put("text", "test");
      put("binary", "test".getBytes());
    }});
    // POJO
    socket.send("discard", new MyPojo("test", "test".getBytes());
  });
  // Prints all data the client sent
  socket.on("discard", (Object data) -> System.out.println(data));
});

JavaScript Client

var socket = cettia.open(uri);
socket.on("open", () => {
  // Text data
  socket.send("discard", "test");
  // Binary data
  socket.send("discard", new TextEncoder().encode("test"));
  // Composite data
  socket.send("discard", {
    text: "test", 
    binary: encoder.encode("test")
  });
});
// Prints all data the server sent
socket.on("discard", data => console.log(data));

In addition, various built-in events are offered along the socket life cycle, which allows for fine-grained control over socket.

Entity handling

A single socket is not appropriate to represent a specific entity in the real world. To give you a way to handle the entity, Cettia provides server with Tag, an identifier of a group of sockets. You can handle a user (entity) logged in using multiple devices (sockets) by tagging the socket with the username of the user that the socket represents.

Server server = new DefaultServer();
server.onsocket((ServerSocket socket) -> {
  // Tags the given socket with the username
  String username = new MyUsernameResolver().resolve(socket.uri());
  socket.tag(username);

  // Let's say that dm event is called when the user sends a direct message to others
  // This message will be sent to all devices that the receiver logged in via sockets tagged with the receiver's username
  socket.on("dm", dm -> server.byTag(dm.receiver().username()).send("dm", dm.createMessage()));
});

Furthermore, you can handle a room where users chat by tagging the sockets tagged with the username with the room name again.

server.onsocket((ServerSocket socket) -> {
  // Tags the user with the room name when the users enters the room
  socket.on("entrance", room -> server.byTag(username).tag(room.name()));
  // With the room name, you can send a message to all the users who have entered the room
  socket.on("chat", chat -> server.byTag(chat.room().name()).send("chat", chat.createMessage()));
});

Authentication hook

As in the web application, you should be able to verify the identity of a given socket. As for the web application, there has been many ways e.g. token, cookie and header and dedicated frameworks which provide their own API. Cettia provides a hook to allow you to apply any method to do that.

Server server = new DefaultServer();
server.onsocket((ServerSocket socket) -> {
  // Token-based approach
  String token = new MyUri(socket.uri()).param("token");
  Map<String, String> authentication = new MyTokenVerifier().verify(token);
});

If you prefer to access authentication information the underlying platform provides, you can do that by unwrapping socket and transport. Here's an example of Servlet.

Server server = new DefaultServer();
server.onsocket((ServerSocket socket) -> {
  // Servlet approach
  HttpSession httpSession;
  ServerTransport transport = socket.unwrap(ServerTransport.class);
  ServerHttpExchange http = transport.unwrap(ServerHttpExchange.class);
  ServerWebSocket ws = transport.unwrap(ServerWebSocket.class);

  if (http != null) {
    // In case of HTTP-based transports
    httpSession = http.unwrap(HttpServletRequest.class).getSession(false);
  } else if (ws != null) {
    // In case of WebSocket transport
    // To make this work, you should put a HttpSession into a Map returned by ServerEndpointConfig#getUserProperties in advance.
    httpSession = (HttpSession) ws.unwrap(Session.class).getUserProperties().get(HttpSession.class.getName());
  } else {
    // Should not happen
    throw new IllegalStateException();
  }
});

Offline application

It is important to make applications relying on full-duplex connection functional while offline. This feature enables you to handle socket as stateless object by providing proper events to deal with temporary disconnection, which means that you don't have to worry about connection state before sending some event.

Java Server

Server server = new DefaultServer();
server.onsocket((ServerSocket socket) -> {
  Queue<Object[]> cache = new ConcurrentLinkedQueue<>();
  // Caches an event which couldn't be sent due to no connection
  socket.oncache((Object[] args) -> cache.offer(args));
  // Now that communication is possible, flushes the cache
  socket.onopen((Void v) -> {
    while (socket.state() == ServerSocket.State.OPENED
      && !cache.isEmpty()) {
      Object[] args = cache.poll();
      socket.send((String) args[0], args[1],
        (Action<?>) args[2], (Action<?>) args[3]);
    }
  });
});

JavaScript Client

var cache = [];
var socket = cettia.open(uri);
// Resets the cache on the beginning of the new lifecycle
// and the end of the old lifecycle
socket.on("new", () => cache.length = 0);
// Caches an event which couldn't be sent due to no connection
socket.on("cache", args => cache.push(args));
// Now that communication is possible, flushes the cache
socket.on("open", () => {
  while(socket.state() === "opened" && cache.length) {
    var args = cache.shift();
    socket.send(socket, args[0], args[1], args[2], args[3]);
  }
});

Dependency injection friendly

Sometimes it's necessary to push something from server to client in the business logic layer or even in the data access layer. With dependency injection, you can register a Server as a singleton component and inject it wherever you want to send some events. Here's an example of Spring.

@Configuration
@ComponentScan(basePackages = {"io.cettia.example.di.spring4"})
public class SpringConfig {
  // Registers the server as a component
  @Bean
  public Server server() {
    return new DefaultServer();
  }
}
@WebListener
public class Bootstrap implements ServletContextListener {
  @Override
  @SuppressWarnings("resource")
  public void contextInitialized(ServletContextEvent event) {
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
    Server server = applicationContext.getBean(Server.class);
    // Sets up the app on Servlet container
    // Skipped...
  }

  @Override
  public void contextDestroyed(ServletContextEvent sce) {}
}
@Component
public class PersistenceEventListener {
  // Injects the server
  @Autowired
  private Server server;
  
  @EventListener(condition = "#persistenceEvent.dirty")
  public void onDirty(PersistenceEvent<Dirty> event) {
    // Notifies of any changes in the database in real-time
    server.all().send("dirty", event.getDirty());
  }
}

Scalable

Because the location of the socket is transparent and servers don't share any data, you can scale application horizontally with ease without modifying existing event handlers. Here's an example of Hazelcast.

@WebListener
public class Bootstrap implements ServletContextListener {
  @Override
  public void contextInitialized(ServletContextEvent event) {
    // Creates a Cettia app
    ClusteredServer server = new ClusteredServer();

    // Configures a Hazelcast
    HazelcastInstance hazelcast = HazelcastInstanceFactory.newHazelcastInstance(new Config());
    ITopic<Map<String, Object>> topic = hazelcast.getTopic("cettia");

    // If some server in the cluster published a message, passes it to this local server
    topic.addMessageListener((Message<Map<String, Object>> message) -> server.messageAction().on(message.getMessageObject()));
    // If this server created a message, publishes it to every server in the cluster
    server.onpublish((Map<String, Object> message) -> topic.publish(message));
  }

  @Override
  public void contextDestroyed(ServletContextEvent sce) {}
}

Any publish-subscribe messaging system, e.g. JMS (Java Message Service) or AMQP (Advanced Message Queueing Protocol), can be used to scale the application.