Cettia

Cettia is a full-featured real-time web framework for Java that you can use to exchange events between servers and web clients in real-time. Cettia works with any web frameworks on the Java Virtual Machine, provides reliable full duplex message channels working with every browser including IE 9, enables elegant patterns which make it fast and enjoyable to build real-time web applications, and scales horizontally using messaging passing.

Latest posts

Use whatever web framework you prefer

Cettia is designed not just to run on any web framework seamlessly on the JVM but also not to degrade the underlying framework's performance. Almost all popular web frameworks in Java are supported: Java EE (Servlet and Java API for WebSocket), Spring WebFlux, Spring Web MVC, Vert.x, Grizzly, Netty, Atmosphere, and so on.

Plug transport servers into your favorite web framework.

io.cettia.Server server = new DefaultServer();
io.cettia.transport.http.HttpTransportServer hts = new HttpTransportServer().ontransport(server);
io.cettia.transport.websocket.WebSocketTransportServer wts = new WebSocketTransportServer().ontransport(server);
@WebListener
public class CettiaInitializer implements ServletContextListener {
  @Override
  public void contextInitialized(ServletContextEvent event) {
    ServletContext context = event.getServletContext();
    Servlet asityServlet = new AsityServlet().onhttp(hts);
    ServletRegistration.Dynamic reg = context.addServlet(AsityServlet.class.getName(), asityServlet);
    reg.setAsyncSupported(true);
    reg.addMapping("/cettia");

    ServerContainer container = (ServerContainer) context.getAttribute(ServerContainer.class.getName());
    ServerEndpointConfig.Configurator configurator = new ServerEndpointConfig.Configurator() {
      public <T> T getEndpointInstance(Class<T> endpointClass) {
        AsityServerEndpoint asityServerEndpoint = new AsityServerEndpoint().onwebsocket(wts);
        return endpointClass.cast(asityServerEndpoint);
      }
    };
    try {
      container.addEndpoint(ServerEndpointConfig.Builder.create(AsityServerEndpoint.class, "/cettia").configurator(configurator).build());
    } catch (DeploymentException e) {
      throw new RuntimeException(e);
    }
  }

  @Override
  public void contextDestroyed(ServletContextEvent sce) {}
}
@SpringBootApplication
@EnableWebFlux
public class Application {
  @Bean
  public RouterFunction<ServerResponse> httpMapping() {
    AsityHandlerFunction asityHandlerFunction = new AsityHandlerFunction().onhttp(hts);

    return RouterFunctions.route(
      path("/cettia")
        // Excludes WebSocket handshake requests
        .and(headers(headers -> !"websocket".equalsIgnoreCase(headers.asHttpHeaders().getUpgrade()))), asityHandlerFunction);
  }

  @Bean
  public HandlerMapping wsMapping() {
    AsityWebSocketHandler asityWebSocketHandler = new AsityWebSocketHandler().onwebsocket(wts);
    Map<String, WebSocketHandler> map = new LinkedHashMap<>();
    map.put("/cettia", asityWebSocketHandler);

    SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
    mapping.setUrlMap(map);

    return mapping;
  }

  @Bean
  public WebSocketHandlerAdapter webSocketHandlerAdapter() {
    return new WebSocketHandlerAdapter();
  }

  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }
}
@SpringBootApplication
@EnableWebMvc
@EnableWebSocket
public class Application implements WebSocketConfigurer {
  @Bean
  public HandlerMapping httpMapping() {
    AsityController asityController = new AsityController().onhttp(hts);
    AbstractHandlerMapping mapping = new AbstractHandlerMapping() {
      @Override
      protected Object getHandlerInternal(HttpServletRequest request) {
        // Check whether a path equals '/cettia'
        return "/cettia".equals(request.getRequestURI()) &&
          // Delegates WebSocket handshake requests to a webSocketHandler bean
          !"websocket".equalsIgnoreCase(request.getHeader("upgrade")) ? asityController : null;
      }
    };
    mapping.setOrder(Ordered.HIGHEST_PRECEDENCE);
    return mapping;
  }

  @Override
  public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    AsityWebSocketHandler asityWebSocketHandler = new AsityWebSocketHandler().onwebsocket(wts);
    registry.addHandler(asityWebSocketHandler, "/cettia");
  }

  public static void main(String[] args) {
    SpringApplication.run(Application.class);
  }
}
public class CettiaVerticle extends AbstractVerticle {
  @Override
  public void start() {
    HttpServer httpServer = vertx.createHttpServer();
    AsityRequestHandler asityRequestHandler = new AsityRequestHandler().onhttp(hts);
    httpServer.requestHandler(request -> {
      if (request.path().equals("/cettia")) {
        asityRequestHandler.handle(request);
      }
    });
    AsityWebSocketHandler asityWebsocketHandler = new AsityWebSocketHandler().onwebsocket(wts);
    httpServer.websocketHandler(socket -> {
      if (socket.path().equals("/cettia")) {
        asityWebsocketHandler.handle(socket);
      }
    });
    httpServer.listen(8080);
  }
}
public class CettiaServer {
  public static void main(String[] args) throws Exception {
    EventLoopGroup bossGroup = new NioEventLoopGroup();
    EventLoopGroup workerGroup = new NioEventLoopGroup();
    try {
      ServerBootstrap bootstrap = new ServerBootstrap();
      bootstrap.group(bossGroup, workerGroup)
        .channel(NioServerSocketChannel.class)
        .childHandler(new ChannelInitializer<SocketChannel>() {
          @Override
          public void initChannel(SocketChannel ch) {
            AsityServerCodec asityServerCodec = new AsityServerCodec() {
              @Override
              protected boolean accept(HttpRequest req) {
                return URI.create(req.uri()).getPath().equals("/cettia");
              }
            };
            asityServerCodec.onhttp(hts).onwebsocket(wts);

            ChannelPipeline pipeline = ch.pipeline();
            pipeline.addLast(new HttpServerCodec()).addLast(asityServerCodec);
          }
        });
      Channel channel = bootstrap.bind(8080).sync().channel();
      channel.closeFuture().sync();
    } finally {
      workerGroup.shutdownGracefully();
      bossGroup.shutdownGracefully();
    }
  }
}
public class CettiaServer {
  public static void main(String[] args) throws Exception {
    HttpServer httpServer = HttpServer.createSimpleServer();
    ServerConfiguration config = httpServer.getServerConfiguration();
    config.addHttpHandler(new AsityHttpHandler().onhttp(hts), "/cettia");
    NetworkListener listener = httpServer.getListener("grizzly");
    listener.registerAddOn(new WebSocketAddOn());
    WebSocketEngine.getEngine().register("", "/cettia", new AsityWebSocketApplication().onwebsocket(wts));
    httpServer.start();

    System.in.read();
  }
}
public class CettiaVerticle extends Verticle {
  @Override
  public void start() {
    HttpServer httpServer = vertx.createHttpServer();
    RouteMatcher httpMatcher = new RouteMatcher();
    httpMatcher.all("/cettia", new AsityRequestHandler().onhttp(hts));
    httpServer.requestHandler(httpMatcher);
    AsityWebSocketHandler websocketHandler = new AsityWebSocketHandler().onwebsocket(wts);
    httpServer.websocketHandler(socket -> {
      if (socket.path().equals("/cettia")) {
        websocketHandler.handle(socket);
      }
    });
    httpServer.listen(8080);
  }
}
@WebListener
public class CettiaInitializer implements ServletContextListener {
  @Override
  public void contextInitialized(ServletContextEvent event) {
    ServletContext context = event.getServletContext();
    Servlet asityServlet = new AsityAtmosphereServlet().onhttp(hts).onwebsocket(wts);
    ServletRegistration.Dynamic reg = context.addServlet(AsityAtmosphereServlet.class.getName(), asityServlet);
    reg.setAsyncSupported(true);
    reg.setInitParameter(ApplicationConfig.DISABLE_ATMOSPHEREINTERCEPTOR, Boolean.TRUE.toString());
    reg.addMapping("/cettia");
  }

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

WebSocket, JSON and switch statement are not enough

It might seem so at first glance but is likely to fall into the cracks at the enterprise level. Cettia's reliable full duplex message channels based on WebSocket and HTTP and elegant patterns including the robust event system free you from the pitfalls of traditional real-time web development.

In the client side,

import cettia from "cettia-client/cettia-bundler";

const socket = cettia.open("/cettia");

socket.on("open", () => socket.send("greeting", "Hello world")));
socket.on("greeting", data => console.log("Greeting from the server", data));
socket.on("klose", () => socket.close());

In the server side,

server.onsocket((io.cettia.ServerSocket socket) -> {
  socket.set("username", username);

  socket.onopen(v -> socket.send("greeting", "Hello world"));
  socket.on("greeting", data -> System.out.println("Greeting from the client " + data));
});

Find sockets, do something with them

Declare which sockets do you want to play with with a socket predicate and define what you want to do with them with a socket action. This simple functional programming model brings a new level of expressiveness and readability in socket handling.

For all sockets, send chat event with message data.

server.find(socket -> true, socket -> socket.send("chat", message));

Or, in short with ServerSocketPredicates#all and Sentence#send.

server.find(all()).send("chat", message);

Limiting only one socket per user in one line.

server.find(attr("username", username).and(id(socket).negate())).send("klose").close();

It finds sockets whose username is the same except the given socket, sends a klose event to prevent reconnection, and closes the sockets.

Recover missed events declaratively

The Cettia socket lifecycle is designed to be unaffected by temporary disconnections which happen commonly especially in the mobile environment. Declaratively decide and collect missed events to recover and send them on the next connection.

Collect events failed to send due to no connection in the cache event and resend them in the open event fired when the connection is recovered within 1 minute since the disconnection.

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]);
}));

After 1 minute has elapsed since the disconnection, delete event is fired. Here, you can send an email or push notifications about events which the socket finally missed.

Scale out horizontally

Cettia servers scale out horizontally using messaging passing without sharing any data between them. Any publish-subscribe messaging system can be used and it doesn't require any modification in the existing application.

With Hazelcast, in-memory data grid based on Java, you can scale a Cettia application out as follows.

((io.cettia.ClusteredServer) server).onpublish((message) -> {
  topic.publish(message);
});
((com.hazelcast.core.ITopic) topic).addMessageListener(message -> {
  server.messageAction().on(message.getMessageObject());
});

The message contains a socket predicate and a socket action used to call the server.find method. It is broadcast to every server in the cluster including one originally received the method call and applies to every server's sockets.

Get Involved

As an open source project licensed under Apache License 2.0, Cettia has started since 2011 with the name of jQuery Stream (a jQuery plugin for HTTP streaming to demonstrate Servlet 3.0's Async Servlet with IE 6) by Donghwan Kim and grown into a full-featured real-time web framework for Java with the continued love and support from the community. If you are interested and would like to be more involved, feel free to join the community and share your feedback.

Here are community-driven examples of Cettia.