« JettyでWebSocketを使うときのコールバックイベントの実行順 | メイン | Firefox14に追加された、ディスプレイをスリープさせないようにするAPI「mozPower」のメモ »

JettyのWebSocketでPing/Pongフレームを送る

皆さん! Pingフレーム送ってますか?
WebSocketは、無通信状態が続くと勝手に切断されちゃうことがあるので、Ping/Pongフレームは結構大切ですよ!
Pingフレームは双方向のハートビートに、Pongフレームは一方向のハートビートに使えます。
相手に鼓動が届くなんて、ロマンティックですね。

そして、Jettyさんの話。
WebSocketプロトコルにはドラフト版からRFC版までいくつかのバージョンがあります。
多くのWebSocketのライブラリは、バージョンが固定だったり、起動時にバージョンを指定するようになっています。
接続してくるクライアントのWebSocketのバージョンに合わせて、処理を切り替えることのできるサーバは少ないのですが、そのうちのひとつがJettyなんです。
Jettyさんは、バージョンの差異をラップし、sendMessageメソッドやcloseメソッドなんかを用意しています。
Jettyさん、懐が深いですね。

でも、その包容力のせいか、ちょっと困ったチャンなところがあって、Ping/Pongフレームを送るのがけっこうめんどうなんですね。
テキストやバイナリのメッセージを送るメソッドなんかはあるんですが、Ping/Pongフレームを送る専用のメソッドがないんです。
なんでかって?
実は、古いバージョンのWebSocketプロトコルにはPing/Pongフレームそのものがないんです。
sendPingメソッドやsendPongメソッドを準備しても、古いバージョンでは遅れないので、何もしないメソッドになってしまいます。
「送るよ!」って言いながら実は送ってないなんてウソは、Jettyさんは許せないようです。
うん。
ウソは良くない!

そうは言っても、Ping/Pongフレームを送りたいことがあるのも事実。
でも、Controlフレームを直接送ることでPing/Pongをおくることができます。
う〜ん。
Jettyさん、さすが!!!

下にJettyでPing/Pongフレームを3秒間隔で交互に送る方法と、Ping/Pongフレームを受け取ったことを検知する方法のサンプルを載せておきますね。
WebSocketPingServlet.javaとWebSocketPing.javaを見てね!

あと、sendPingとsendPongをもった専用のクラスを作って、Jettyさんにウソつきになってもらう方法も載せちゃいます!
WebSocketPingSenderServlet.javaとWebSocketPingSender.javaとWebSocketPingPongSender.javaですよ!
WebSocketPingServlet.java
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;

import org.eclipse.jetty.websocket.WebSocket;
import org.eclipse.jetty.websocket.WebSocketServlet;

@SuppressWarnings("serial")
@WebServlet("/*")
public class WebSocketPingServlet extends WebSocketServlet {

    public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) {
        return new WebSocketPing();
    }

}
WebSocketPing.java
import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;

import org.eclipse.jetty.websocket.WebSocket;

public class WebSocketPing implements WebSocket.OnFrame, WebSocket.OnControl {

    FrameConnection frameConnection;

    Timer timer = new Timer();

    TimerTask timerTask = new TimerTask() {
        public void run() {
            try {
                Thread.sleep(3000);
                System.out.println("send ping");
                if (frameConnection.isPing((byte)0x02)) {
                    frameConnection.sendControl((byte)0x02, null, 0, 0);
                } else if (frameConnection.isPing((byte)0x09)) {
                    frameConnection.sendControl((byte)0x09, null, 0, 0);
                } 
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                Thread.sleep(3000);
                System.out.println("send pong");
                if (frameConnection.isPong((byte)0x03)) {
                    frameConnection.sendControl((byte)0x03, null, 0, 0);
                } else if (frameConnection.isPong((byte)0x0a)) {
                    frameConnection.sendControl((byte)0x0a, null, 0, 0);
                } 
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };

    public void onOpen(Connection connection) {
        timer.schedule(timerTask, 0, 3000);
    }

    public void onClose(int closeCode, String message) {
        timer.cancel();
    }

    public boolean onFrame(byte flags, byte opcode, byte[] data, int offset, int length) {
        return false;
    }

    public void onHandshake(FrameConnection connection) {
        frameConnection = connection;
    }
    public void onMessage(byte[] data, int offset, int length) {
    }

    public boolean onControl(byte controlCode, byte[] data, int offset, int length) {
        if (frameConnection.isPing(controlCode)) {
            onPing(data, offset, length);
        }
        if (frameConnection.isPong(controlCode)) {
            onPong(data, offset, length);
        }
        return false;
    }

    void onPing(byte[] data, int offset, int length) {
        System.out.println("receive ping");
    }

    void onPong(byte[] data, int offset, int length) {
        System.out.println("receive pong");
    }

}
WebSocketPingSenderServlet.java
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;

import org.eclipse.jetty.websocket.WebSocket;
import org.eclipse.jetty.websocket.WebSocketServlet;

@SuppressWarnings("serial")
@WebServlet("/*")
public class WebSocketPingSenderServlet extends WebSocketServlet {

    public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) {
        return new WebSocketPingSender();
    }

}
WebSocketPingSender.java
import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;

import org.eclipse.jetty.websocket.WebSocket;

import com.kanasansoft.AttachWebSocketPingPongSender.WebSocketPingPongSender;
import com.kanasansoft.AttachWebSocketPingPongSender.WebSocketPingPongSender.PingPongSender;

public class WebSocketPingSender implements WebSocket.OnFrame, WebSocket.OnControl {

    PingPongSender sender;
    FrameConnection frameConnection;

    Timer timer = new Timer();

    TimerTask timerTask = new TimerTask() {
        public void run() {
            try {
                Thread.sleep(3000);
                System.out.println("send ping");
                sender.sendPing();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                Thread.sleep(3000);
                System.out.println("send pong");
                sender.sendPong();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };

    public void onOpen(Connection connection) {
        timer.schedule(timerTask, 0, 3000);
    }

    public void onClose(int closeCode, String message) {
        timer.cancel();
    }

    public boolean onFrame(byte flags, byte opcode, byte[] data, int offset, int length) {
        return false;
    }

    public void onHandshake(FrameConnection connection) {
        frameConnection = connection;
        sender = WebSocketPingPongSender.getSender(connection);
    }
    public void onMessage(byte[] data, int offset, int length) {
    }

    public boolean onControl(byte controlCode, byte[] data, int offset, int length) {
        if (frameConnection.isPing(controlCode)) {
            onPing(data, offset, length);
        }
        if (frameConnection.isPong(controlCode)) {
            onPong(data, offset, length);
        }
        return false;
    }

    void onPing(byte[] data, int offset, int length) {
        System.out.println("receive ping");
    }

    void onPong(byte[] data, int offset, int length) {
        System.out.println("receive pong");
    }

}
WebSocketPingPongSender.java
package com.kanasansoft.AttachWebSocketPingPongSender;

import java.io.IOException;

import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.websocket.WebSocket.FrameConnection;

public class WebSocketPingPongSender {

    private enum PingPong {

        D00_AND_NONE   (null      , null      ),
        D06            ((byte)0x02, (byte)0x03),
        D08_AND_RFC6455((byte)0x09, (byte)0x0a);

        private Byte ping;
        private Byte pong;

        PingPong(Byte ping, Byte pong) {
            this.ping = ping;
            this.pong = pong;
        }

        Byte getPing() {return ping;}
        Byte getPong() {return pong;}

        boolean isSame(FrameConnection connection) {
            return connection.isPing(getPing()) && connection.isPong(getPong());
        }

        static PingPong get(FrameConnection connection) {
            if (D06            .isSame(connection)) {return D06            ;}
            if (D08_AND_RFC6455.isSame(connection)) {return D08_AND_RFC6455;}
            return D00_AND_NONE;
        }

    }

    public static PingPongSender getSender(FrameConnection connection) {
        switch(PingPong.get(connection)) {
        case D06: return new PingPongSenderD06(connection);
        case D08_AND_RFC6455: return new PingPongSenderD08AndRFC6455(connection);
        }
        return new PingPongSenderD00AndNone(connection);
    }

    public static abstract class PingPongSender {

        FrameConnection frameConnection = null;

        public PingPongSender(FrameConnection frameConnection) {
            this.frameConnection = frameConnection;
        }

        public void sendPing() throws IOException {
            sendPing(null, 0, 0);
        }
        public void sendPing(String content) throws IOException {
            byte[] data = content.getBytes(StringUtil.__UTF8);
            sendPing(data, 0, data.length);
        }
        public abstract void sendPing(byte[] data, int offset, int length) throws IOException;

        public void sendPong() throws IOException {
            sendPong(null, 0, 0);
        }
        public void sendPong(String content) throws IOException {
            byte[] data = content.getBytes(StringUtil.__UTF8);
            sendPong(data, 0, data.length);
        }
        public abstract void sendPong(byte[] data, int offset, int length) throws IOException;

    }
    static class PingPongSenderD00AndNone extends PingPongSender {
        public PingPongSenderD00AndNone(FrameConnection frameConnection) {
            super(frameConnection);
        }
        @Override
        public void sendPing(byte[] data, int offset, int length) {}
        @Override
        public void sendPong(byte[] data, int offset, int length) {}
    }
    static class PingPongSenderD06 extends PingPongSender {
        public PingPongSenderD06(FrameConnection frameConnection) {
            super(frameConnection);
        }
        @Override
        public void sendPing(byte[] data, int offset, int length) throws IOException {
            frameConnection.sendControl(PingPong.D06.getPing(), data, offset, length);
        }
        @Override
        public void sendPong(byte[] data, int offset, int length) throws IOException {
            frameConnection.sendControl(PingPong.D06.getPong(), data, offset, length);
        }
    }
    static class PingPongSenderD08AndRFC6455 extends PingPongSender {
        public PingPongSenderD08AndRFC6455(FrameConnection frameConnection) {
            super(frameConnection);
        }
        @Override
        public void sendPing(byte[] data, int offset, int length) throws IOException {
            frameConnection.sendControl(PingPong.D08_AND_RFC6455.getPing(), data, offset, length);
        }
        @Override
        public void sendPong(byte[] data, int offset, int length) throws IOException {
            frameConnection.sendControl(PingPong.D08_AND_RFC6455.getPong(), data, offset, length);
        }
    }
}

コメントを投稿

(いままで、ここでコメントしたことがないときは、コメントを表示する前にこのブログのオーナーの承認が必要になることがあります。承認されるまではコメントは表示されません。そのときはしばらく待ってください。)

Google

タグ クラウド