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, 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, Play Framework, 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);
- Servlet 3 & Java API for WebSocket 1
- Spring WebFlux 5
- Spring MVC 4
- Play Framework 2
- Vert.x 3
- Netty 4
- Grizzly 2
- Vert.x 2
- Atmosphere 2
@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 CettiaController extends Controller {
private final ActorSystem actorSystem;
private final Materializer materializer;
@Inject
public CettiaController(ActorSystem actorSystem, Materializer materializer) {
this.actorSystem = actorSystem;
this.materializer = materializer;
}
public WebSocket websocket() {
WebSocket asityWebSocket = new AsityWebSocket(actorSystem, materializer).onwebsocket(wts);
return asityWebSocket;
}
}
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.
Resources
Here are a list of resources to help you get started with Cettia:
- Getting Started With Cettia
A quick start guide for those who are new to Cettia.
- Cettia Starter Kit
A starter kit to help you get started.
- Building Real-Time Web Applications With Cettia
A reference documentation in the form of a tutorial.
Browse community-driven resources as well:
- Various demo applications using Cettia and Spring 5 by Ralph Schaer
- Real-time messaging with Cettia and Spring Boot by Ralph Schaer
Community
As an open source project licensed under Apache License 2.0, Cettia has started since 2011 with the name of jQuery Stream 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.