WebSockets in Helidon

原文はこちら。
The article was written by Santiago Pericas-Geertsen (Engineer, Oracle).
https://medium.com/helidon/websockets-in-helidon-se-1f5d7c220ccd
https://medium.com/helidon/websockets-in-helidon-mp-48259cf808f1

HelidonはTyrusを統合しており、Jakarta WebSocket APIをサポートします。WebSocket APIを使うとJavaアプリケーションがWebSocketを使った対話にサーバーもしくはクライアントとして参加できます。

Eclipse Tyrus
https://projects.eclipse.org/projects/ee4j.tyrus
Jakarta WebSocket
https://projects.eclipse.org/projects/ee4j.websocket

以下では、Helidon SE、Helidon MPについて説明しますが、いずれもRESTリソースを使ってメッセージを共有Queueにプッシュし、WebSocketエンドポイントを使ってメッセージを1個ずつQueueからダウンロードする、というサンプルアプリケーションを例にして、REST接続とWebSocket接続がどのようにして Helidon SE/Helidon MPアプリケーションにシームレスに組み合わされるかを紹介しています。

Helidon SE

Helidon SEでのサポートは、JerseySupportに似たTyrusSupportクラスに基づいており、これを使ってHelidonアプリケーションは注釈付きWebSocketエンドポイントとプログラムで記述するWebSocketエンドポイントの両方を定義できるようにしています。

Project Dependencies

WebSocket拡張をHelidon SEアプリケーションで利用するには、以下の依存関係をpom.xmlに追加する必要があります。

<dependency>
<groupId>io.helidon.webserver</groupId>
<artifactId>helidon-webserver-tyrus</artifactId>
</dependency>
view raw pom.xml hosted with ❤ by GitHub

Example

このサンプルアプリケーションのソースコードは以下の場所にあります。

WebSocket Example
https://github.com/oracle/helidon/tree/master/examples/webserver/websocket

まずMessageQueueServiceを見てみましょう。

public class MessageQueueService implements Service {
private final MessageQueue messageQueue = MessageQueue.instance();
@Override
public void update(Routing.Rules routingRules) {
routingRules.post("/board", this::handlePost);
}
private void handlePost(ServerRequest request, ServerResponse response) {
request.content().as(String.class).thenAccept(messageQueue::push);
response.status(204).send();
}
}

このクラスはメッセージのポスト先であるRESTリソースを公開します。メッセージを受け取ると、共有Queueにプッシュし、HTTP 204 (No content) を返します。QueueはシングルトンクラスのMessageQueueによって実装されていることに着目してください。

QueueにプッシュされたメッセージをMessageBoardEndpointが提供するWebSocket接続を開くことで取得できます。

public class MessageBoardEndpoint extends Endpoint {
private final MessageQueue messageQueue = MessageQueue.instance();
@Override
public void onOpen(Session session, EndpointConfig endpointConfig) {
session.addMessageHandler(new MessageHandler.Whole<String>() {
@Override
public void onMessage(String message) {
try {
if (message.equals("SEND")) {
while (!messageQueue.isEmpty()) {
session.getBasicRemote().sendObject(messageQueue.pop());
}
}
} catch (Exception e) {
// ...
}
}
});
}
}

このプログラム的なエンドポイントはEndPointクラスを拡張しており、onOpenメソッドを新規接続の都度呼び出します。この例では、アプリケーションは文字列用のメッセージハンドラを登録しています。特別なSENDメッセージを受け取ると、WebSocket 接続を介してメッセージを送信し、共有Queueを空にします。

Helidon SEでは、RESTクラスとWebSocketクラスは手作業でWebサーバーに登録する必要がありますが、以下のようにRoutingインスタンス作成時に実施します。

List<Class<? extends Encoder>> encoders =
Collections.singletonList(UppercaseEncoder.class);
Routing.builder()
.register("/rest", new MessageQueueService())
.register("/websocket",
TyrusSupport.builder().register(
ServerEndpointConfig.Builder.create(
MessageBoardEndpoint.class, "/").encoders(
encoders).build()).build())
.build();
view raw Main.java hosted with ❤ by GitHub

このコードスニペットではRouting、TyrusSupportとServerEndpointConfigのために複数のビルダーを使っています。特に、/websocket にMessageBoardEndpoint.classを登録し、メッセージエンコーダを関連付けています。メッセージエンコーダとデコーダの詳細については、以下を参照してください。この例では、UppercaseEncoder.classはサーバから送信されるすべてのメッセージを単純に大文字にします。

Jakarta WebSocket
https://projects.eclipse.org/projects/ee4j.websocket

Helidon SEのエンドポイントのメソッドをNettyのワーカースレッドプールで実行されます。このプール内のスレッドは非ブロッキングであることを意図しているため、エンドポイントメソッドを起点とするブロッキングや長時間の操作は、別のスレッドプールを使用して実行することをお勧めします。Helidonには、このような場合のための設定可能なスレッドプールのサプライヤが含まれています。詳細は以下をご覧ください。

ThreadPoolSupplier (io.helidon.common.configurable.ThreadPoolSupplier)
https://helidon.io/docs/latest/apidocs/io.helidon.common.configurable/io/helidon/common/configurable/ThreadPoolSupplier.html

Helidon MP

Helidon MPでのサポートは、CDIを使ったアノテーションやbean discoveryが中心です。開発者は注釈付きWebSocketエンドポイントとプログラムで記述するWebSocketエンドポイントのどちらかを選択、もしくは両者の組み合わせを利用することもできますが、Helidon MPでは、通常より簡潔で読みやすいコードになるため、注釈付きエンドポイントを推奨します。

Project Dependencies

WebSocket拡張をHelidon MPアプリケーションで利用するには、以下の依存関係をpom.xmlに追加する必要があります。

<dependency>
<groupId>io.helidon.microprofile.websocket</groupId>
<artifactId>helidon-microprofile-websocket</artifactId>
</dependency>
view raw pom.xml hosted with ❤ by GitHub

Example

このサンプルアプリケーションのソースコードは以下の場所にあります。

Helidon MP WebSocket Example
https://github.com/oracle/helidon/tree/master/examples/microprofile/websocket

ここで紹介するHelidon MPアプリケーションはCDIとクラススキャンをフル活用しており、必要な情報は全てコードへの注釈から得られるため、追加コードは必要ありません。

RESTエンドポイントはJAX-RSリソースとして実装されており、Queueが直接注入されています。

@Path("rest")
public class MessageQueueResource {
@Inject
private MessageQueue messageQueue;
@POST
@Consumes("text/plain")
public void push(String s) {
messageQueue.push(s);
}
}

アプリケーションスコープにはMessageQueueという1個のインスタンスがあり、これをRESTとWebSocketのエンドポイントで共有します。REST エンドポイントはメッセージをQueueにプッシュし、WebSocket エンドポイントはメッセージをQueueから取り出します。

それでは、注釈付き API を使用するWebSocketエンドポイントの実装を探ってみましょう。

@ServerEndpoint(
value = "/websocket",
encoders = { UppercaseEncoder.class })
public class MessageBoardEndpoint {
@Inject
private MessageQueue messageQueue;
@OnMessage
public void onMessage(Session session, String message) {
if (message.equals("SEND")) {
while (!messageQueue.isEmpty()) {
session.getBasicRemote()
.sendObject(messageQueue.pop());
}
}
}
}

@ServerEndopointの注釈がエンドポイントを /websocket パスに固定し、メッセージエンコーダーを定義しています(詳細はのちほど説明します)。MessageBoardEndpointは単なるPOJOなので、イベントハンドラーに @OnMessage のような追加の注釈を使っています。このアプローチの利点の1つは、JAX-RS APIのように、メソッドシグネチャが固定されていない点です。上のコードスニペットでは、パラメータ(任意の順序で指定できます)には、WebSocket セッションと、呼び出しのトリガーとなった受信メッセージが含まれています。

onMessageの実装は文字列メッセージSENDを待ち、受信したら、各エントリを別個のWebSocketメッセージとしてその接続を使って送信し、Queueを空にします。

WebSocketエンドポイントはゼロ個以上のメッセージエンコーダーとデコーダーを指定できます。これらのコーデックはメッセージがエンドポイント(デコーダー)にルーティングされる前、もしくはメッセージがエンドポイント(エンコーダー)から送信された後に呼び出されます。このサンプルでは、UppercaseEncoderがクライアントに送信される各メッセージを大文字に変更しています。

public class UppercaseEncoder implements Encoder.Text<String> {
@Override
public String encode(String s) {
return s.toUpperCase();
}
@Override
public void init(EndpointConfig config) {
}
@Override
public void destroy() {
}
}

では、このHelidon MPアプリを実行するためには、他に何が必要なのでしょうか?サポートするクラスMessageQueue以外には何もありません。Helidon MP は @Path と @ServerEndpoint の両方を CDI Bean 定義アノテーションとして宣言しているので、必要なのは CDI ディスカバリを有効にすることだけです。

デフォルトでは、JAX-RSリソースとWebSocketエンドポイントの両方がルートパス “/” の下で利用可能になります。このデフォルト値は、それぞれ jakarta.ws.rs.Application と jakarta.websocket.server.ServerApplicationConfig のサブクラスおよび実装をそれぞれ提供すればオーバーライドできます。JAX-RSはアプリケーションのサブクラスで @ApplicationPath を使ってこのルートパスを提供しますが、WebSocket API には同等のものがないため、Helidon MPでは jakarta.websocket.server.ServerApplicationConfig の実装である独自のアノテーション @RoutingPath を使用しています。

Class Application (jakarta.ws.rs-api 2.1.6 API)
https://jakarta.ee/specifications/restful-ws/2.1/apidocs/javax/ws/rs/core/Application.html
Interface ServerApplicationConfig (Jakarta WebSocket API documentation)
https://jakarta.ee/specifications/websocket/1.1/apidocs/javax/websocket/server/ServerApplicationConfig.html
Annotation Type RoutingPath (Helidon Project 2.0.1 API)
https://helidon.io/docs/latest/apidocs/io.helidon.microprofile.server/io/helidon/microprofile/server/RoutingPath.html

Helidon MP内のすべてのエンドポイントメソッドは、Nettyとは独立した別のスレッドプールで実行されます。したがって、これらは通信データを処理するNettyの能力に影響を与えないため、ブロッキングや長時間実行される操作のために追加のスレッドを作成する必要はありません。

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中