ひまlab

ひまな時になんか書く。

WebSocketを死活監視に使ってみた

はじめに

Websocketを使って、キープアライブのようなこと*1をやってみました。

やってみたキッカケは、お仕事でTomcatのWebSocket Sevletを作った際にAPIを見ていたら
接続が切れた時にイベントが拾えるようで、キープアライブに使えるのでは?と思ったからです。
やってみたら、案外ちゃんと使えたので内容を公開します。

ちなみに、WebSocketの実装にはTomcatのWebSocket Servletを使用します。
WebSocket Servletについては、色々解説しているサイトがあるので、ここでは説明を省きます。

環境

ライブラリ

仕組みについて

ざっくりと流れはこんな感じ

  1. クライアントがサーバにWebSocketで接続する
  2. 接続後に、クライアントからサーバにメッセージを送る
    • この時に、クライアントを識別する情報を渡してあげると、クライアントが識別できます
  3. サーバは、クライアントからのメッセージを受けて、クライアントに応答を返す
  4. クライアントは、サーバからのメッセージを受けて、サーバに応答を返す
    • 正常時は、上のサーバとクライアントのやり取りが繰り返される
  5. (クライアントが切れた時)サーバ側のonCloseが呼ばれる
    • ここで、切れた時に行いたい処理を呼び出す

ソース

サーバ側

以下のライブラリにパスを通す

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;

import org.apache.catalina.websocket.MessageInbound;
import org.apache.catalina.websocket.StreamInbound;
import org.apache.catalina.websocket.WebSocketServlet;
import org.apache.catalina.websocket.WsOutbound;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

@WebServlet(name = "KeepAliveServlet", urlPatterns = { "/KeepAliveServlet" })
public class KeepAliveServlet extends WebSocketServlet {

    private static final long serialVersionUID = -1L;
    private static final Log log = LogFactory.getLog(KeepAliveServlet.class);
    private static Set<MessageInbound> messages = new HashSet<MessageInbound>();

    @Override
    protected StreamInbound createWebSocketInbound(String arg0, HttpServletRequest arg1) {
        log.info("called createWebSocketInbound.");
        return new KeepAliveInbound();
    }

    private class KeepAliveInbound extends MessageInbound {
        private WsOutbound outbound;

        private String clientId;

        @Override
        public void onOpen(WsOutbound outbound) {
            log.info("onOpen.");
            this.outbound = outbound;
            messages.add(this);
        }

        @Override
        public void onClose(int status) {
            log.info("onClose.");

            if (clientId != null) {
                log.info(String.format("クライアントID[%s]が切れた", clientId));
            }
            messages.remove(this);
        }

        @Override
        public void onTextMessage(CharBuffer cb) throws IOException {

            if (clientId == null) {
                clientId = cb.toString();
            }

            log.info("onTextMessage. " + cb);

            // すぐにメッセージを返すと通信量が多すぎるので、間隔を設ける
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            outbound.writeTextMessage(CharBuffer.wrap("echo from server."));
        }

        @Override
        public void onBinaryMessage(ByteBuffer bb) throws IOException {

        }

        @Override
        public int getReadTimeout() {
            // 初期状態だとクライアントと通信が切れても待ち続けるので、時間を指定しています。
            return 10000;
        }
    }
}

クライアント側

以下のライブラリにパスを通す

  • java_websocket.jar
import java.net.URI;
import java.net.URISyntaxException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.drafts.Draft_17;
import org.java_websocket.handshake.ServerHandshake;

public class KeepAliveClient extends WebSocketClient {

    private static final Log log = LogFactory.getLog(KeepAliveClient.class);
    private static final String CLIENT_ID = "123";

    private KeepAliveClient(URI serverURI) {
        super(serverURI, new Draft_17());
    }

    @Override
    public void onClose(int arg0, String arg1, boolean arg2) {
        // サーバ側と接続が切れた時に呼ばれる
        log.info("onClose.");
    }

    @Override
    public void onError(Exception arg0) {
        log.error("onError. " + arg0.toString());
    }

    @Override
    public void onMessage(String arg0) {
        log.info(arg0);
        this.send("echo from client.");
    }

    @Override
    public void onOpen(ServerHandshake arg0) {
        log.info("onOpen called. " + arg0.getHttpStatus());
        this.send(CLIENT_ID);
    }

    public static KeepAliveClient getInstance() {
        URI uri;
        try {
            // 環境に合わせて接続先を変更
            uri = new URI("ws://localhost:8080/TomcatProject/KeepAliveServlet");
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }
        KeepAliveClient client = new KeepAliveClient(uri);
        return client;
    }

}

実行用のメインクラス

public class Main {

    public static void main(String[] args) {
        KeepAliveClient client = KeepAliveClient.getInstance();
        client.connect();
    }

}

動かしてみる

まず、Mainクラスを適当に実行 クライアント側のログはこんな感じ

KeepAliveClient onOpen
情報: onOpen called.
KeepAliveClient onMessage
情報: echo from server.
KeepAliveClient onMessage
情報: echo from server.
KeepAliveClient onMessage
情報: echo from server.

サーバ側のログはこんな感じ

sample.KeepAliveServlet createWebSocketInbound
情報: called createWebSocketInbound.
KeepAliveServlet onOpen
情報: onOpen.
KeepAliveServlet onTextMessage
情報: onTextMessage. 123
KeepAliveServlet onTextMessage
情報: onTextMessage. echo from client.
KeepAliveServlet onTextMessage
情報: onTextMessage. echo from client.

ここで、クライアント側の接続を切る(LANを切るとか) そうすると、しばらくしてサーバ側にクライアントが切れたとログが出る

KeepAliveServlet onClose
情報: クライアント[123]が切れた

このような感じで、クライアント側の死活を監視することができる

終わりに

WebSocketのちょっと変わった使い方を紹介してみました。
あまりミッション・クリティカルな用途には向きませんが
比較的簡単に、キープアライブ的なことができるのでお試しあれ!*2

*1:ここでは、サーバに接続してきたクライアントの死活(ネットワークの切断など)
調べるという普通とは逆のパターンについて書いてます

*2:僕はAndroidクライアントとサーバの接続状態の確認にこのような仕組みを使ってみました