05.12 使用Cettia構建實時Web應用程序第1部分

在本教程中,我們將看看使用Cettia創建實時導向Web應用程序所需的功能,並構建Cettia入門工具包。

使用Cettia構建實時Web應用程序第1部分

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規範的應用服務器。

<project>

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>

<dependencies>

<dependency>

<groupid>io.cettia/<groupid>

<artifactid>cettia-server/<artifactid>

<version>1.0.0/<version>

<dependency>

<groupid>io.cettia.asity/<groupid>

<artifactid>asity-bridge-servlet3/<artifactid>

<version>1.0.0/<version>

<dependency>

<groupid>io.cettia.asity/<groupid>

<artifactid>asity-bridge-jwa1/<artifactid>

<version>1.0.0/<version>

<dependency>

<groupid>javax.servlet/<groupid>

<artifactid>javax.servlet-api/<artifactid>

<version>3.1.0/<version>

<scope>provided/<scope>

<dependency>

<groupid>javax.websocket/<groupid>

<artifactid>javax.websocket-api/<artifactid>

<version>1.0/<version>

<scope>provided/<scope>

<build>

<plugins>

<plugin>

<groupid>org.eclipse.jetty/<groupid>

<artifactid>jetty-maven-plugin/<artifactid>

<version>9.4.8.v20171121/<version>

/<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客戶端:

<title>index/<title>

我們將只使用此頁面上的控制檯(通過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);/<serverhttpexchange>

Action<serverwebsocket> wsTransportServer = ws -> System.out.println(ws);/<serverwebsocket>

// 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 getEndpointInstance(Class 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。/<serverwebsocket>/<serverhttpexchange>

當然,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());/<void>

socket.onopen(logState).onclose(logState).ondelete(logState);

以下是服務器套接字的狀態轉換圖:

使用Cettia構建實時Web應用程序第1部分

  1. 在收到傳輸時,服務器將創建一個具有 NULL狀態的套接字並將其傳遞給套接字處理程序。

  2. 如果它沒有執行握手,它將轉換到一個CLOSED狀態並觸發一個close事件。

  3. 如果它成功執行握手,它將轉換為OPENED狀態並觸發open事件。只有在這種狀態下通信才是可能的。

  4. 如果由於某種原因連接斷開,它將轉換為CLOSED狀態並觸發close事件。

  5. 如果連接通過客戶端重新連接恢復,它將轉換為OPENED狀態並觸發open事件。

  6. 自從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構建實時Web應用程序第1部分

  1. 如果一個套接字由cettia.open一個連接創建並啟動一個連接,它將轉換為一個connecting狀態並觸發一個connecting事件。

  2. 如果所有傳輸都無法及時連接,它將轉換為closed狀態並觸發close事件。

  3. 如果其中一個傳輸成功建立連接,則它將轉換為opened狀態並觸發open事件。只有在這種狀態下通信才是可能的。

  4. 如果連接由於某種原因斷開連接,它將轉換為closed狀態並觸發close事件。

  5. 如果計劃重新連接,它將轉換到一個waiting狀態,並waiting以重新連接延遲和總重新連接嘗試觸發一個事件。

  6. 如果套接字在重新連接延遲結束後開始連接,它將轉換到connecting狀態並觸發connecting事件。

  7. 如果套接字被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>> violations = validator.validate(model);/<constraintviolation>

// ...

});

在客戶端,事件數據只是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格式。總之,您可以交換包括二進制數據在內的事件數據,而不會造成任何問題。

今天就是這樣!明天我們將介紹廣播事件,使用特定套接字,斷開連接處理以及擴展應用程序。


分享到:


相關文章: