使用Cettia构建实时Web应用程序第1部分
在本教程中,我们将看看使用Cettia创建实时导向Web应用程序所需的功能,并构建Cettia入门工具包。
2011年,我开始了Cettia的前任(一个用于HTTP流的jQuery插件,我曾用它演示Servlet 3.0的异步Servlet和IE 6)。从那时起,WebSocket和Asynchronous IO已经被广泛使用,并且开发变得更容易并在客户端和服务器环境中维护实时Web应用程序。但与此同时,功能性和非功能性要求已经变得更加复杂和难以满足,并且越来越难以估计和控制伴随的技术债务。
Cettia是最初为解决这些挑战而开始的项目的结果,并且是一个创建实时Web应用程序而不妥协的框架:
-
它旨在无缝地在Java虚拟机(JVM)上的任何I / O框架上运行。
-
即使提供代理,防火墙,防病毒软件或任意平台即服务(PaaS),它也提供简单的全双工连接。
-
它旨在不在服务器之间共享数据,并且可以轻松进行水平缩放。
-
它提供了一个事件系统来对发生在服务器端和客户端的事件进行分类,并且可以实时交换它们。
-
它简化了一套套接字处理,有助于极大地改善多设备用户体验。
-
它以事件驱动的方式处理暂时断开和永久断开。
在本教程中,我们将看看使用Cettia创建实时导向Web应用程序所需的功能,并构建Cettia入门工具包。入门工具包的源代码可在https://github.com/cettia/cettia-starter-kit获得。
设置项目
开始之前,请确保您已安装Java 8+和Maven 3+。根据Maven Central的统计数据,Servlet 3和Java WebSocket API 1是编写Cettia应用程序时使用最多的I / O框架,因此我们将使用它们来构建Cettia入门工具包。当然,您可以使用其他框架,如Grizzly和Netty,稍后您会看到。
首先,创建一个名为的目录starter-kit。我们将只编写和管理目录中的以下三个文件:
1.pom.xml:Maven项目描述符。通过这个POM配置,我们可以启动服务器,而无需预先安装“servlet容器”,这是一个实现Servlet规范的应用服务器。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.cettia.starter</groupId>
<artifactId>cettia-starter-kit</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<failOnMissingWebXml>false</failOnMissingWebXml>
</properties>
<dependencies>
<dependency>
<groupId>io.cettia</groupId>
<artifactId>cettia-server</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>io.cettia.asity</groupId>
<artifactId>asity-bridge-servlet3</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>io.cettia.asity</groupId>
<artifactId>asity-bridge-jwa1</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>9.4.8.v20171121</version>
</plugin>
</plugins>
</build>
</project>
要在端口8080上启动服务器,请运行mvn jetty:run。这个Maven命令就是我们在本教程中对Maven所做的一切。如果您可以使用其他构建工具(如Gradle)来实现它,那么这样做绝对没问题。
2.src/main/java/io/cettia/starter/CettiaConfigListener.java:与Cettia服务器一起玩的Java类。ServletContext是一个上下文对象,用于在servlet容器中表示Web应用程序,当Web应用程序初始化过程通过实现ServletContextListener#contextInitialized方法启动时,我们可以访问它。在该方法中,我们将设置并与Cettia一起玩。让我们从一个空的监听器开始:
package io.cettia.starter;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
@WebListener
public class CettiaConfigListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent event) {}
@Override
public void contextDestroyed(ServletContextEvent event) {}
}
在我们继续完成本教程的过程中,本课将充实。请记住,每次修改课程时都应该重新启动服务器,特别是在Windows中。
3.src/main/webapp/index.html:与Cettia客户端一起使用的HTML页面。我们只会处理JavaScript,因为其他部分如HTML和CSS并不重要。以下HTML cettia通过unpkg CDN的脚本标记加载Cettia客户端:
<!DOCTYPE html>
<title>index</title>
<script src="https://unpkg.com/[email protected]/cettia-browser.min.js"></script>
我们将只使用此页面上的控制台(通过http://127.0.0.1:8080访问)以cettia交互方式播放对象,而不是编辑和刷新页面。否则,您可以使用捆绑程序,如Webpack或其他运行时,如Node.js.
I / O框架不可知层
为了在技术堆栈上实现更大的选择自由度,Cettia被设计为可以在Java虚拟机(JVM)上无缝运行任何I / O框架,而不会降低底层框架的性能; 这是通过将Asity项目创建为Java I / O框架的轻量级抽象层来实现的。Asity支持Atmosphere,Grizzly,Java Servlet,Java WebSocket API,Netty和Vert.x.
让我们编写一个HTTP处理程序和一个WebSocket处理程序,它们映射到/cettiaServlet和带有Asity的Java WebSocket API。这些框架分别负责管理HTTP资源和WebSocket连接。将以下导入添加到CettiaConfigListener该类中:
import io.cettia.asity.action.Action;
import io.cettia.asity.http.ServerHttpExchange;
import io.cettia.asity.websocket.ServerWebSocket;
import io.cettia.asity.bridge.jwa1.AsityServerEndpoint;
import io.cettia.asity.bridge.servlet3.AsityServlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;
import javax.websocket.DeploymentException;
import javax.websocket.server.ServerContainer;
import javax.websocket.server.ServerEndpointConfig;
并在该contextInitialized方法中放置以下内容:
// Asity part
Action<ServerHttpExchange> httpTransportServer = http -> System.out.println(http);
Action<ServerWebSocket> wsTransportServer = ws -> System.out.println(ws);
// Servlet part
ServletContext context = event.getServletContext();
// When it receives Servlet's HTTP request-response exchange, converts it to ServerHttpExchange and feeds httpTransportServer with it
AsityServlet asityServlet = new AsityServlet().onhttp(httpTransportServer);
// Registers asityServlet and maps it to "/cettia"
ServletRegistration.Dynamic reg = context.addServlet(AsityServlet.class.getName(), asityServlet);
reg.setAsyncSupported(true);
reg.addMapping("/cettia");
// Java WebSocket API part
ServerContainer container = (ServerContainer) context.getAttribute(ServerContainer.class.getName());
ServerEndpointConfig.Configurator configurator = new ServerEndpointConfig.Configurator() {
@Override
public <T> T getEndpointInstance(Class<T> endpointClass) {
// When it receives Java WebSocket API's WebSocket connection, converts it to ServerWebSocket and feeds wsTransportServer with it
AsityServerEndpoint asityServerEndpoint = new AsityServerEndpoint().onwebsocket(wsTransportServer);
return endpointClass.cast(asityServerEndpoint);
}
};
// Registers asityServerEndpoint and maps it to "/cettia"
try {
container.addEndpoint(ServerEndpointConfig.Builder.create(AsityServerEndpoint.class, "/cettia").configurator(configurator).build());
} catch (DeploymentException e) {
throw new RuntimeException(e);
}
正如你所期望直观,httpTransportServer而且wsTransportServer是裸眉鸫科的应用程序,并且有可能与喂养他们,他们可以在任何框架,只要运行ServerHttpExchange和ServerWebSocket。Cettia服务器也基本上是一个Asity应用程序。
在这一步中,您可以直接通过提交HTTP请求和WebSocket请求来使用Asity资源/cettia; 但我们不会在本教程中深入研究Asity。如果您有兴趣,请咨询Asity网站。除非您需要从头开始编写Asity应用程序,否则您可以安全地忽略Asity; 只要注意,即使您的最喜欢的框架不被支持,大约200行代码,您可以编写一个Asity桥到您的框架并通过该桥运行Cettia。
安装Cettia
在深入研究代码之前,让我们先从最高的概念层面上确定Cettia的三个主要概念:
-
服务器 - 用于与套接字交互的接口。它提供了一个事件来初始化新接受的套接字,并提供finder方法找到插座符合指定条件并执行给定的套接字行动。
-
套接字 - 构建在传输层顶部的功能丰富的接口。它提供的事件系统允许您定义自己的事件,而不考虑事件数据的类型,并实时在Cettia客户端和Cettia服务器之间交换它们。
-
传输 - 表示全双工消息信道的接口。它带有基于消息成帧的二进制和文本有效载荷,双向交换消息,并确保没有消息丢失和没有空闲连接。与服务器和套接字不同,除非要调整默认传输行为或引入全新传输,否则不需要知道传输。
让我们在上面的I / O框架不可知层上设置Cettia服务器,并打开一个套接字作为烟雾测试。添加以下导入:
import io.cettia.DefaultServer;
import io.cettia.Server;
import io.cettia.ServerSocket;
import io.cettia.transport.http.HttpTransportServer;
import io.cettia.transport.websocket.WebSocketTransportServer;
现在,用以下Cettia部分替换上面的Asity部分:
// Cettia part
Server server = new DefaultServer();
HttpTransportServer httpTransportServer = new HttpTransportServer().ontransport(server);
WebSocketTransportServer wsTransportServer = new WebSocketTransportServer().ontransport(server);
// The socket handler
server.onsocket((ServerSocket socket) -> System.out.println(socket));
如的实施方案Action<ServerHttpExchange>和TransportServer,HttpTransportServer消耗HTTP请求-响应交换,并产生流式传输和长轮询传输,并且如的实施方案Action<ServerWebSocket>和TransportServer,WebSocketTransportServer消耗的WebSocket资源,并产生一个网页套接字运输。这些产生的传输被传入Server并用于创建和维护ServerSockets。
当然,WebSocket传输已经足够了,但如果涉及代理,防火墙,防病毒软件或任意平台即服务(PaaS),很难确定单独使用WebSocket是否有效。这就是为什么我们建议您在各种环境中HttpTransportServer一起安装,WebSocketTransportServer以更广泛地覆盖全双工信息通道。
ServerSocket由创建者Server传递给注册的套接字处理程序server.onsocket(socket -> {}),并且此处理程序是您应该初始化套接字的位置。由于接受传输和套接字的成本很高,因此您应该在Cettia之外提前验证请求(如果需要),并在将它们传递给Cettia之前过滤掉不合格的请求。例如,假设使用Apache Shiro,它看起来像这样:
server.onsocket(socket -> {
Subject currentUser = SecurityUtils.getSubject();
if (currentUser.hasRole("admin")) {
// ...
}
});
在客户端,您可以打开指向Cettia服务器的URI的套接字cettia.open(uri)。在索引页面的控制台中运行以下片段:
var socket = cettia.open("http://127.0.0.1:8080/cettia");
如果所有设置都正确,您应该能够在服务器端看到套接字日志,并且可以在客户端通过开发人员工具的网络面板看到发生的情况。如果由于某种原因WebSocket传输在客户端或服务器中都不可用,则Cettia客户端会自动回退到基于HTTP的传输。注释掉Java WebSocket API部分并再次打开一个套接字,或者在Internet Explorer 9中打开索引页。在任何情况下,您都会看到打开一个套接字
套接字生命周期
套接字始终处于特定状态,如打开或关闭,并且其状态会根据底层传输的状态而不断变化。Cettia定义了客户端套接字和服务器套接字的状态转换图,并提供了各种内置事件,这些事件允许在发生状态转换时对套接字进行细粒度处理。如果您充分利用这些图表和内置事件,则可以轻松处理事件驱动的有状态套接字,而无需自行管理其状态。
将以下代码添加到中的套接字处理程序中CettiaConfigListener。它在发生状态转换时记录套接字的状态。
Action<Void> logState = v -> System.out.println(socket + " " + socket.state());
socket.onopen(logState).onclose(logState).ondelete(logState);
以下是服务器套接字的状态转换图:
-
在收到传输时,服务器将创建一个具有 NULL状态的套接字并将其传递给套接字处理程序。
-
如果它没有执行握手,它将转换到一个CLOSED状态并触发一个close事件。
-
如果它成功执行握手,它将转换为OPENED状态并触发open事件。只有在这种状态下通信才是可能的。
-
如果由于某种原因连接断开,它将转换为CLOSED状态并触发close事件。
-
如果连接通过客户端重新连接恢复,它将转换为OPENED状态并触发open事件。
-
自从CLOSED状态过去一分钟后,它转换到最终状态并触发delete事件。这种状态下的套接字不应该被使用。
正如你所看到的,如果发生4的状态转换,它应该转换为5或6。你可能想重新发送客户端在没有前者连接的情况下无法接收的事件,并采取行动通知错过事件的用户,例如后者的推送通知。我们将在稍后详细讨论如何做到这一点。
在客户端,从用户体验角度告知用户线路上发生了什么非常重要。打开套接字并添加事件处理程序以在发生状态转换时记录套接字的状态:
var socket = cettia.open("http://127.0.0.1:8080/cettia");
var logState = arg => console.log(socket.state(), arg);
socket.on("connecting", logState).on("open", logState).on("close", logState).on("waiting", logState);
以下是客户端套接字的状态转换图:
-
如果一个套接字由cettia.open一个连接创建并启动一个连接,它将转换为一个connecting状态并触发一个connecting事件。
-
如果所有传输都无法及时连接,它将转换为closed状态并触发close事件。
-
如果其中一个传输成功建立连接,则它将转换为opened状态并触发open事件。只有在这种状态下通信才是可能的。
-
如果连接由于某种原因断开连接,它将转换为closed状态并触发close事件。
-
如果计划重新连接,它将转换到一个waiting状态,并waiting以重新连接延迟和总重新连接尝试触发一个事件。
-
如果套接字在重新连接延迟结束后开始连接,它将转换到connecting状态并触发connecting事件。
-
如果套接字被socket.close方法关闭,它将转换到最终状态。这种状态下的套接字不应该被使用。
如果连接没有问题,套接字将有一个3-4-5-6的状态转换周期。如果没有,它将有一个2-5-6的状态转换周期。重新启动或关闭服务器以进行4-5-6或2-5-6的状态转换。
发送和接收事件
通过单个通道交换各种类型数据的最常见模式是命令模式; 一个命令对象被序列化并通过线路发送,然后反序列化并在另一端执行。起初,JSON和switch语句应该足以实现这种模式,但如果您必须处理二进制类型的数据,那么它将成为维护和累积技术债务的负担; 实施心跳检查并确保您获得数据的确认。Cettia提供了一个足够灵活的事件系统来满足这些要求。
Cettia客户端和Cettia服务器之间的实时交换单位是由必需的名称属性和可选的数据属性组成的事件。只要名称不与内置事件重复,就可以定义和使用自己的事件。这是echo事件处理程序,其中收到的任何echo事件都会被发回。将它添加到套接字处理程序中:
socket.on("echo", data -> socket.send("echo", data));
在上面的代码中,我们没有操作或验证给定的数据,但使用无类型输入与在服务器中一样不现实。事件数据允许的类型由Cettia在内部使用的JSON处理器Jackson确定。如果事件数据应该是原始类型之一,则可以将其与相应的包装类一起进行强制转换和使用,如果它应该是List或Map之类的对象,并且您更喜欢POJO,则可以将其转换并与其一起使用像杰克逊这样的JSON库。它可能看起来像这样:
socket.on("event", data -> {
Model model = objectMapper.convertValue(data, Model.class);
Set<ConstraintViolation<Model>> violations = validator.validate(model);
// ...
});
在客户端,事件数据只是JSON,但有一些例外。以下是测试服务器echo事件处理程序的客户端代码。这个简单的客户端向echo事件发送一个包含任意数据的open事件,并记录echo要接收的事件的数据以返回到控制台。
var socket = cettia.open("http://127.0.0.1:8080/cettia");
socket.once("open", () => socket.send("echo", "Hello world"));
socket.on("echo", data => console.log(data));
当我们决定使用控制台时,您可以键入并运行代码片段,例如: socket.send("echo", {text: "I'm a text", binary: new TextEncoder().encode("I'm a binary")}).send("echo", "It's also chainable")并在飞行中观看结果。在你的控制台上试试它。
如示例所示,事件数据基本上可以是任何数据,只要它是可序列化的,无论数据是二进制还是文本。如果事件数据的属性中的至少一个是byte[]或ByteBuffer在服务器中,Buffer在节点或ArrayBuffer在浏览器中,该事件数据被视为二值和MessagePack格式是用来代替JSON格式。总之,您可以交换包括二进制数据在内的事件数据,而不会造成任何问题。
今天就是这样!明天我们将介绍广播事件,使用特定套接字,断开连接处理以及扩展应用程序。