WebSocket#
書く#
public class ChatHandler implements WsHandler {
@Override
public boolean onUpgrade (WsSession session) {
// 認証はここで通す。Cookie が読めるのはこの時点だけ。断ると 403
return "secret".equals(session.cookie("token"));
}
@Override
public void onMessage (WsContext context, String message) {
context.session().send("echo: " + message);
}
}
登録する#
ws("/ws/posts", PostFeedHandler::new);
HTTP のルートと同じ場所に書きます。起動ログにも WS /ws/posts と出ます。
渡すのはインスタンスではなくコンストラクタ参照です。 接続ごとにハンドラが作られます。
呼ばれる順番#
| メソッド | いつ |
|---|---|
onUpgrade(WsSession) |
HTTP から昇格する前。false を返すと 403 |
onOpen(WsContext) |
繋がったあと |
onMessage(WsContext, String) |
テキストが来たとき |
onBinary(WsContext, byte[]) |
バイナリが来たとき |
onClose(WsContext, int, String) |
閉じたとき |
onError(WsContext, Throwable) |
落ちたとき |
すべて default 実装があるので、要るものだけ書けば済みます。
認証は onUpgrade で#
Cookie が読めるのは onUpgrade の時点だけです。
昇格したあとは、もう HTTP のヘッダはありません。
@Override
public boolean onUpgrade (WsSession session) {
return "secret".equals(session.cookie("token"));
}
false を返すと 403 で断ります。
メッセージごとに Context#
1メッセージにつき Context が1つ作られます。
リクエストやバッチの1実行と同じ扱いです。
つまり onMessage の中では、DB もセッションも普通に使えます。
メッセージの処理が終われば、接続が続いていても DB 接続は返されます。
public void onMessage (WsContext context, String message) {
Data request = Data.fromJsonString(message);
switch (request.getStringOptional("command")) {
case "latest" -> context.session().send(new Data()
.putData("type", "latest")
.putData("posts", titles()));
case "subscribe" -> {
SUBSCRIBERS.add(context.session());
context.session().send(new Data().putData("type", "subscribed"));
}
/*
* 何が使えるのかを返す。
* 黙って無視すると、クライアント側は
* 「届いていないのか、コマンドが違うのか」が分からない。
*/
default -> context.session().send(new Data()
.putData("type", "error")
.putData("message", "知らないコマンドです")
.putData("commands", List.of("latest", "subscribe")));
}
}
知らないコマンドを黙って捨てないでください。 クライアント側からは「届いていないのか、コマンドが違うのか」が分かりません。
他の接続へ送る#
繋がっているセッションを自分で持っておきます。
private static final Set<WsSession> SUBSCRIBERS = ConcurrentHashMap.newKeySet();
public static void notifyNewPost (String title) {
Data event = new Data().putData("type", "new_post").putData("title", title);
// send() が false なら、その接続はもう無い。そのまま外す
SUBSCRIBERS.removeIf(session -> !session.send(event));
}
サーバーが複数台あるなら、この配り方では他の台に繋がっている人へ届きません。 Redis の pub/sub などを間に入れてください。jimble はそこまでは持っていません。
SSE との使い分け#
クライアントから送るものが無いなら SSE のほうが簡単です。 プロキシの設定も要らず、繋ぎ直しもブラウザがやってくれます。