HTTP Client APIの多様な利用方法(2)
WebSocketクライアント
WebSocketは現在、Webサービス上で双方向の通信を行うためには必須の技術です。特にチャットシステムや、リアルタイムでの情報提供を行うサービスの場合に広く使われています。そのため、利用したことがある方も多くいらっしゃるかと思います。
Ajaxを用いたポーリングによるリクエストよりも動作が軽いことから、WebSocketでの接続を推奨しているサービスも多々あります。
WebSocketはWeb APIとして標準化されていることから、異なる言語、もしくは異なるライブラリを使っても比較的同じ流れで利用できます。
HTTP Client APIの場合にはリスト3の流れでWebSocketを利用できます。
// (1) HttpClientのインスタンスを作成
URI uri = URI.create("ws://localhost/session/dummy");
HttpClient client = HttpClient.newBuilder().build();
// (2) 接続が閉じられた時のFutureを準備
CompletableFuture<Integer> closeFuture = new CompletableFuture<Integer>();
// (3) WebSocketの接続ビルダーを作成
WebSocket.Builder builder = client.newWebSocketBuilder();
// (4) WebSocketとして接続する
CompletableFuture<WebSocket> f = builder.buildAsync(uri, new WebSocket.Listener() {
private List<CharSequence> textBuffer;
@Override
public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
if(textBuffer == null){
textBuffer = new ArrayList<>();
}
try {
// (5) 受信したテキストデータを処理する
textBuffer.add(data);
if (last) {
String text = textBuffer.stream().collect(Collectors.joining());
log.info("onText : {}", text.trim());
textBuffer.clear();
}
}
catch(Exception ex){
log.error(ex.getMessage(),ex);
}
webSocket.request(1); // (6) 次のデータを要求する
return null;
}
@Override
public CompletionStage<?> onClose(WebSocket webSocket, int statusCode, String reason) {
closeFuture.complete(statusCode); // 切断した時のコードをFutureに設定し、終了を伝える
return null;
}
});
// WebSocketの接続を待ち、インスタンスを取得する
WebSocket socket = f.get();
// (7) テキストデータを送信する
socket.sendText("{",false);
socket.sendText("\"data\": 3 ",false);
socket.sendText("}",true);
// (8) 切断する
socket.sendClose(WebSocket.NORMAL_CLOSURE,"ByeBye");
// 切断されるまで待つ
int status = closeFuture.get();
log.info("close : {}",status);
最初に(1)HttpClientのインスタンスを作成するのはこれまでと同様です。(2)では接続が終了する時のFutureを、CompletableFutureクラスを用いて作っていますが、これは接続が切れるまで処理を待つために利用しています。
この処理は必ずしも必要ではありませんが、通常、切断されたタイミングで再接続など何らかの処理を行うので、こういった切断のタイミングを他のスレッド等が知るための仕組みが必要になることは多いはずです。
続いて、WebSocketの接続では、(3)のように作成したHttpClientのインスタンスのnewWebSocketBuilderメソッドで接続用のインスタンスを別途用意します。HTTP接続の中で、WebSocketというトンネル接続を作成するイメージになります。WebSocketへの接続の際に必要な設定がある場合にはこのWebSocket.Builderインスタンスを通じて設定を行います。
接続する際の設定が済んだら、(4)のようにbuildAsyncメソッドを使って非同期で接続します。その際に接続URIとイベント処理をするためのリスナーを登録します。
イベントリスナーには表2に示すメソッドがあり、それぞれのイベントに応じて実装を行います。
| メソッド | 概要 |
|---|---|
| onBinary(WebSocket socket, ByteBuffer data, boolean last) | バイナリデータを受信した時に呼ばれるメソッド |
| onText(WebSocket socket, CharSequence data, boolean last) | テキストデータを受信した時に呼ばれるメソッド |
| onOpen(WebSocket webSocket) | 接続に成功した時に呼ばれるメソッド |
| onClose(WebSocket webSocket, int statusCode, String reason) | 切断した時に呼ばれるメソッド |
| onError(WebSocket webSocket, Throwable error) | 何らかのエラーが発生した時に呼ばれるメソッド |
| onPing(WebSocket webSocket, ByteBuffer message) | Pingリクエストを受信した時に呼ばれるメソッド。自動的にPongが返るので自分で返す必要はない |
| onPong(WebSocket webSocket, ByteBuffer message) | Pongレスポンスを受信した時に呼ばれるメソッド |
onPing/onPongというメソッドについて、多少イメージしにくいと思いますが、Pingは接続が行われているか確認するためのリクエストで、そのリクエストを受けると、Pongを返して接続されていることを相手に伝えます。
これらの処理はWebSocketライブラリ内で隠されていて開発者が扱えない場合もあるのですが、このAPIではそれらのイベントも取得が可能です。
また、データを受信するonBinary/onTextメソッドではデータ以外にもlastというbooleanの引数があります。このフラグはひとくくりで扱ってほしいデータの終了を示すためのフラグです。
そのため、テキストデータを受信しても、(5)のように終了を示すフラグがあるまでデータをバッファにためてから処理する必要があります。
そして、処理が終了したら、(6)のようにrequestメソッドを使って次のデータを要求します。この処理はJava 9で導入されたリアクティブストリームと同様です。
このrequest処理を忘れると、データが受信できなかったり、他のイベントもコールされなくなったりするので、必ず忘れないよう注意してください。
また、データを送信する際にはsendBinaryもしくはsendTextメソッドを利用します。(7)ではテキストデータを送信しています。送信の際にもlastフラグが設定でき、サンプルではJSON形式のテキストを3回にわけて送信しています。
従って、このデータを受信した場合にはonTextが3回呼ばれ、3回目のonTextが呼ばれる際に最後のデータである旨が示されます。この流れは(5)の処理と比べてわかりやすいはずです。
そして、切断する際にも(8)のようにsendCloseメソッドを呼びます。WebSocketでは切断する際にも切断リクエストを送信しますので、この流れになっていると思われます。
HTTP Client APIでのWebSocketは、JavaScriptなどで扱うWebSocketのプログラムよりも多少、細かいコントロールが可能です。
その反面、WebSocketのプロトコルについても理解していないとどう利用してよいかわからない部分もあるので、注意が必要です。
最後に
HTTP Client APIは、Java 9で導入されたリアクティブストリーム機能に依存している部分が大きく、応用した使い方を知りたい方はぜひリアクティブストリームについても調べてみてください。
また、WebSocketを実際に使ってみると通信部分で課題が発生する場合があります。その際に必要となるプロトコルでの動作を、ある程度コントロールできるレベルでAPIが実装されている点に、筆者は好感をもちました。
実際に課題に遭遇した際には、まだ利用されているコードが少なく、WebSocketのプロトコルとAPIリファレンスを参照して実装する必要がでてくるかもしれません。
ほとんどのケースにおいて、HTTP Client APIを使えばHTTPクライアント機能は十分だと思われます。具体的なノウハウも今後は蓄積されていき、ますます多くの方にとって使いやすい環境が整っていくと思われるので、今回の記事を参考にしていただければうれしく思います。
