Using WebSockets

本指南解释了如何让你的 Quarkus 应用程序利用 WebSockets 创建交互式 Web 应用程序。因为它是一个 canonical WebSocket 应用程序,所以我们要创建一个简单的聊天应用程序。

Prerequisites

Unresolved directive in websockets.adoc - include::{includes}/prerequisites.adoc[]

Architecture

在本指南中,我们创建一个简单的聊天应用程序,使用 Web 套接字接收和向其他已连接用户发送消息。

websocket guide architecture

Solution

我们建议你按照后续章节中的说明,逐步创建应用程序。但是,你可以直接跳到已完成的示例。

克隆 Git 存储库: git clone {quickstarts-clone-url},或下载 {quickstarts-archive-url}[存档]。

解决方案位于 websockets-quickstart directory

Creating the Maven project

首先,我们需要一个新项目。使用以下命令创建一个新项目:

Unresolved directive in websockets.adoc - include::{includes}/devtools/create-app.adoc[]

此命令生成项目(没有任何类),并导入 websockets 扩展。

如果你已配置你的 Quarkus 项目,则可以通过在项目基本目录中运行以下命令向你的项目添加 websockets 扩展:

Unresolved directive in websockets.adoc - include::{includes}/devtools/extension-add.adoc[]

这会将以下内容添加到构建文件中:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-websockets</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-websockets")

如果你只想使用 WebSocket 客户端,你应该用 quarkus-websockets-client 代替。

Handling web sockets

我们的应用程序含有一个处理 WebSocket 的类。在 src/main/java 目录中创建 org.acme.websockets.ChatSocket 类。复制以下内容到创建的文件中:

package org.acme.websockets;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.websocket.OnClose;
import jakarta.websocket.OnError;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import jakarta.websocket.Session;

@ServerEndpoint("/chat/{username}")         (1)
@ApplicationScoped
public class ChatSocket {

    Map<String, Session> sessions = new ConcurrentHashMap<>(); (2)

    @OnOpen
    public void onOpen(Session session, @PathParam("username") String username) {
        broadcast("User " + username + " joined");
        sessions.put(username, session);
    }

    @OnClose
    public void onClose(Session session, @PathParam("username") String username) {
        sessions.remove(username);
        broadcast("User " + username + " left");
    }

    @OnError
    public void onError(Session session, @PathParam("username") String username, Throwable throwable) {
        sessions.remove(username);
        broadcast("User " + username + " left on error: " + throwable);
    }

    @OnMessage
    public void onMessage(String message, @PathParam("username") String username) {
        broadcast(">> " + username + ": " + message);
    }

    private void broadcast(String message) {
        sessions.values().forEach(s -> {
            s.getAsyncRemote().sendObject(message, result ->  {
                if (result.getException() != null) {
                    System.out.println("Unable to send message: " + result.getException());
                }
            });
        });
    }

}
<1>  配置 WebSocket URL
<1>  存储当前打开的 WebSocket

A slick web frontend

所有聊天应用程序都需要 nice UI,此示例可能不太美观,但能完成工作。Quarkus 自动为 META-INF/resources 目录中包含的静态资源提供服务。创建 src/main/resources/META-INF/resources 目录并将此 index.html 文件复制到其中。

Run the application

现在,让我们看看应用程序在实际应用中的情况。使用以下命令运行它:

Unresolved directive in websockets.adoc - include::{includes}/devtools/dev.adoc[]

然后在 2 个浏览器窗口中打开 [role="bare"][role="bare"]http://localhost:8080/:

  1. 在顶部文本区域中输入一个名称(使用 2 个不同的名称)。

  2. Click on connect

  3. Send and receive messages

websocket guide screenshot

和往常一样,可以使用以下命令打包应用程序:

Unresolved directive in websockets.adoc - include::{includes}/devtools/build.adoc[]

并使用 `java -jar target/quarkus-app/quarkus-run.jar`执行。

您还可以使用以下命令构建本机可执行文件:

Unresolved directive in websockets.adoc - include::{includes}/devtools/build-native.adoc[]

你还可以使用 here 中详述的方法测试 WebSocket 应用程序。

WebSocket Clients

Quarkus 还包含一个 WebSocket 客户端。你可以调用 ContainerProvider.getWebSocketContainer().connectToServer 创建 WebSocket 连接。默认情况下,quarkus-websockets 工件同时包括客户端和服务器支持。但是,如果你只想要客户端,你可以使用 quarkus-websockets-client 替代。

当连接到服务器时,你可以传入想要使用的注释客户端终端的类,或 jakarta.websocket.Endpoint 的实例。如果你正在使用注释终端,则可以使用与在服务器上完全相同的注释,除了必须使用 @ClientEndpoint 而不是 @ServerEndpoint 注释外。

以下示例展示了客户端被用来测试上述聊天终端。

package org.acme.websockets;

import java.net.URI;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;

import jakarta.websocket.ClientEndpoint;
import jakarta.websocket.ContainerProvider;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import io.quarkus.test.common.http.TestHTTPResource;
import io.quarkus.test.junit.QuarkusTest;

@QuarkusTest
public class ChatTest {

    private static final LinkedBlockingDeque<String> MESSAGES = new LinkedBlockingDeque<>();

    @TestHTTPResource("/chat/stu")
    URI uri;

    @Test
    public void testWebsocketChat() throws Exception {
        try (Session session = ContainerProvider.getWebSocketContainer().connectToServer(Client.class, uri)) {
            Assertions.assertEquals("CONNECT", MESSAGES.poll(10, TimeUnit.SECONDS));
            Assertions.assertEquals("User stu joined", MESSAGES.poll(10, TimeUnit.SECONDS));
            session.getAsyncRemote().sendText("hello world");
            Assertions.assertEquals(">> stu: hello world", MESSAGES.poll(10, TimeUnit.SECONDS));
        }
    }

    @ClientEndpoint
    public static class Client {

        @OnOpen
        public void open(Session session) {
            MESSAGES.add("CONNECT");
            // Send a message to indicate that we are ready,
            // as the message handler may not be registered immediately after this callback.
            session.getAsyncRemote().sendText("_ready_");
        }

        @OnMessage
        void message(String msg) {
            MESSAGES.add(msg);
        }

    }

}

More WebSocket Information

Quarkus WebSocket 实现是 Jakarta Websockets 的实现。