2015年8月21日金曜日

How to parse Eddystone packets (Android)

It's easy if you use nv-bluetooth library. onLeScan method would be implemented like the following.


public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord)
{
    // Parse the payload part of an advertisement packet.
    List<ADStructure> structures =
        ADPayloadParser.getInstance().parse(scanRecord);
 
    // For each AD structure contained in the packet.
    for (ADStructure structure : structures)
    {
        if (structure instanceof EddystoneUID)
        {
            // Eddystone UID
            EddystoneUID es = (EddystoneUID)structure;

            // Beacon ID (= Namespace ID + Instance ID)
            // byte[] beaconId = es.getBeaconId();
            String beaconId = es.getBeaconIdAsString();

            // Namespace ID
            // byte[] namespaceId = es.getNamespaceId();
            String namespaceId = es.getNamespaceIdAsString();

            // Instance ID
            // byte[] instanceId = es.getInstanceId();
            String instanceId = es.getInstanceIdAsString();

            // Tx Power
            int power = es.getTxPower();
        }
        else if (structure instanceof EddystoneURL)
        {
            // Eddystone URL
            EddystoneURL es = (EddystoneURL)structure;

            // URL
            URL url = es.getURL();

            // Tx Power
            int power = es.getTxPower();
        }
        else if (structure instanceof EddystoneTLM)
        {
            // Eddystone TLM
            EddystoneTLM es = (EddystoneTLM)structure;

            // TLM Version
            int version = es.getTLMVersion();

            // Battery Voltage
            int voltage = es.getBatteryVoltage();

            // Beacon Temperature
            float temperature = es.getBeaconTemperature();

            // The number of advertisement packets since power-on or reboot.
            long count = es.getAdvertisementCount();

            // The elapsed time since power-on or reboot.
            long time = es.getElapsedTime();
        }
        else if (structure instanceof IBeacon)
        {
            // In passing, iBeacon
            IBeacon iBeacon = (IBeacon)structure;

            // Major Number
            int major = iBeacon.getMajor();

            // Minor Number
            int minor = iBeacon.getMinor();

            // Proximity UUID
            UUID uuid = iBeacon.getUUID();

            // Tx Power
            int power = iBeacon.getPower();
        }
    }
}


All you need to do to use nv-bluetooth is to add one line shown below into dependencies block in build.gradle.

compile 'com.neovisionaries:nv-bluetooth:1.6'


Links


Eddystone パケットをパースする (Android)

nv-bluetooth ライブラリを使えば簡単。onLeScan メソッドの実装例は次のようになる。


public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord)
{
    // アドバータイジングパケットのペイロードをパースする。
    List<ADStructure> structures =
        ADPayloadParser.getInstance().parse(scanRecord);
 
    // アドバータイジングパケットに含まれる各 AD structure ごとに
    for (ADStructure structure : structures)
    {
        if (structure instanceof EddystoneUID)
        {
            // Eddystone UID
            EddystoneUID es = (EddystoneUID)structure;

            // ビーコン ID (= 名前空間 ID + インスタンス ID)
            // byte[] beaconId = es.getBeaconId();
            String beaconId = es.getBeaconIdAsString();

            // 名前空間 ID
            // byte[] namespaceId = es.getNamespaceId();
            String namespaceId = es.getNamespaceIdAsString();

            // インスタンス ID
            // byte[] instanceId = es.getInstanceId();
            String instanceId = es.getInstanceIdAsString();

            // Tx Power
            int power = es.getTxPower();
        }
        else if (structure instanceof EddystoneURL)
        {
            // Eddystone URL
            EddystoneURL es = (EddystoneURL)structure;

            // URL
            URL url = es.getURL();

            // Tx Power
            int power = es.getTxPower();
        }
        else if (structure instanceof EddystoneTLM)
        {
            // Eddystone TLM
            EddystoneTLM es = (EddystoneTLM)structure;

            // TLM バージョン
            int version = es.getTLMVersion();

            // 電圧
            int voltage = es.getBatteryVoltage();

            // 温度
            float temperature = es.getBeaconTemperature();

            // 電源 ON もしくはリブートからのアドバタイズパケット総数
            long count = es.getAdvertisementCount();

            // 電源 ON もしくはリブートからの経過時間
            long time = es.getElapsedTime();
        }
        else if (structure instanceof IBeacon)
        {
            // ついでに iBeacon も
            IBeacon iBeacon = (IBeacon)structure;

            // メジャー番号
            int major = iBeacon.getMajor();

            // マイナー番号
            int minor = iBeacon.getMinor();

            // Proximity UUID
            UUID uuid = iBeacon.getUUID();

            // Tx Power
            int power = iBeacon.getPower();
        }
    }
}


nv-bluetooth を使うには、build.gradledependencies ブロックに次の行を追加すればよい。

compile 'com.neovisionaries:nv-bluetooth:1.7'


参考リンク


2015年6月20日土曜日

ZooKeeper でリーダー選出を実装する

ZooKeeper による分散システム管理』 を買って読みました。

書籍では、ZooKeeper を使う典型的な例として 「複数のマシンの中から一台をマスターとして選ぶ」 というユースケース、いわゆる 「リーダー選出」を挙げ、3 章以降でコーディング例を示しています。しかし、コールバックやら Watcher やらの組み合わせ方がややこしいため、コードのフローは追いにくいものになっています。

8 章では 「ZooKeeper の高レベル API」 として Curator フレームワークを紹介し、Curator を使った場合にリーダー選出のコーディングがどうなるかを示しているものの、それでも分かりにくいです。というか、分かりやすいかどうかよりも、リーダー選出のために、素の ZooKeeper API とはだいぶ異なる Curator API を学習しなければならないという点で既に、「なんか違う。そうじゃない」 感があります (あくまで個人的に)。

「リーダー選出、もっとすっきり書けるはず」、という信念のもと、もんもんと設計を考え、最終的に LeaderElection という一つのクラスにリーダー選出アルゴリズムをまとめるに至りました。このクラスを使うと、リーダー選出のコーディングは次のように直感的で簡潔になります。

// ZooKeeper インスタンスを用意します。
ZooKeeper zooKeeper = ...;

// リスナーの実装を用意します。
LeaderElection.Listener listener = new LeaderElection.Leader() {
    @Override
    public void onWin(LeaderElection election) {
        System.out.println("私がリーダーです。");
    }

    @Override
    public void onLose(LeaderElection election) {
        System.out.println("他の誰かがリーダーです。");
    }

    @Override
    public void onVacant(LeaderElection election) {
        System.out.println("リーダーが辞めました。選出を再実行します。");
    }

    @Override
    public void onFinish(LeaderElection election) {
        System.out.println("コールバックチェーン終了。もう選出には参加しません。");
    }

    @Override
    public void onStateChanged(LeaderElection election, State oldState, State newState) {
        System.out.format("状態が %s から %s に変わりました。\n", oldState, newState);
    }
};

// リーダー選出を実行します。直感的で簡潔でしょ?
new LeaderElection()
    .setZooKeeper(zooKeeper)
    .setListener(listener)
    .start();

// 上記と同じですが、各変数を明示的に設定すると次のようになります。
new LeaderElection()
    .setZooKeeper(zooKeeper)
    .setListener(listener)
    .setPath("/leader")
    .setId(
        String.valueOf(Math.abs(new Random().nextLong()))
    )
    .setAclList(ZooDefs.Ids.OPEN_ACL_UNSAFE)
    .start();


LeaderElection の実装は、次のいずれかを検出するまでは、リーダー選出に参加し続けます ─すなわち、コールバック (および必要に応じて Watcher) をスケジュールし続けます。

  1. 与えられた ZooKeeper インスタンスの状態が AUTH_FAILED もしくは CLOSED である。
  2. LeaderElection.finish() メソッドにより 「終了すべき」 とマークされている。


LeaderElection.Adapter クラスは LeaderElection.Listener インターフェースの空実装です。 コールバックメソッドの幾つかにしか興味がない場合は、このクラスが便利かもしれません。 例えば、onStateChanged() にしか興味がない場合は、次のようにコードを短くできます。

// リーダー選出を実行します。
new LeaderElection()
    .setZooKeeper(zooKeeper)
    .setListener(new Adapter() {
        @Override
        public void onStateChanged(LeaderElection election, State oldState, State newState) {
            System.out.format("状態が %s から %s に変わりました。\n", oldState, newState);
        }
    })
    .start();

LeaderElection クラスは nv-zookeeper という Maven artifact に入れてあるので、pom.xml に次のように書けば、すぐに使えます。


nv-zookeeper は作成したばかりなのでテストも不十分ですが、よかったら試しに使ってみてください。不具合指摘、改善要望、プルリクエストは歓迎します。JavaDoc はこちらです。

2015年6月16日火曜日

ZooKeeper の起動に苦労した点

ZooKeeper クライアントを起動して、「Session 0x0 for server null, unexpected error, closing socket connection and attempting reconnect」というエラーが出たら、これは単純に ZooKeeper サーバーが起動していない可能性が高い。私の場合、ZooKeeper のソースコードを追っていって、ClientCnxnSocketNIO.java の 361 行目 (version 3.4.6) の sc.finishConnect() でエラーが起こっていることが分かったが、この sc というのは java.nio.channels.SocketChannel のインスタンスなので、もはや ZooKeeper の範疇ではない。そういうわけで、ZooKeeper の設定云々の話ではなくて、単なる通信エラー。

「zkServer.sh でサーバー起動しているはずなんだけど・・・」と思いながら、zookeeper.out というファイルの中身を見たら、「line 109: nohup: command not found」というエラーメッセージを発見。Windows の Git Bash で作業していたから、「nohup アップというコマンドがない」というエラーが起こってたということか。起動に失敗したのなら、エラー表示くらいしてくれよ > zkServer.sh

Windows では zkServer.sh ではなくて zkServer.cmd を使うべき、ということで、「zkServer.cmd conf\zoo.cfg」という形で起動したら、またもやエラー。

2015-06-16 19:43:30,086 [myid:] - WARN  [main:QuorumPeerMain@113]
  - Either no config or no quorum defined in config, run
ning  in standalone mode
2015-06-16 19:43:30,158 [myid:] - ERROR [main:ZooKeeperServerMain@54]
  - Invalid arguments, exiting abnormally
java.lang.NumberFormatException: For input string: "(略)\zookeeper-3.4.6\bin\..\conf\zoo.cfg"
  at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
  at java.lang.Integer.parseInt(Integer.java:580)
  at java.lang.Integer.parseInt(Integer.java:615)
  at org.apache.zookeeper.server.ServerConfig.parse(ServerConfig.java:60)
  at org.apache.zookeeper.server.ZooKeeperServerMain.initializeAndRun(ZooKeeperServerMain.java:83)
  at org.apache.zookeeper.server.ZooKeeperServerMain.main(ZooKeeperServerMain.java:52)
  at org.apache.zookeeper.server.quorum.QuorumPeerMain.initializeAndRun(QuorumPeerMain.java:116)
  at org.apache.zookeeper.server.quorum.QuorumPeerMain.main(QuorumPeerMain.java:78)
2015-06-16 19:43:30,163 [myid:] - INFO  [main:ZooKeeperServerMain@55]
   - Usage: ZooKeeperServerMain configfile | port datadir [ticktime] [maxcnxns]
Usage: ZooKeeperServerMain configfile | port datadir [ticktime] [maxcnxns]

このエラーメッセージがまた意味が分かりにくいため、結局 QuorumPeerMain.java, ZooKeeperServerMain.java, ServerConfig.java やらのソースコードを読むことになって、「汚いコードだな」とげんなり。

parseInt でエラーが発生している箇所 (ServerConfig.java 60 行目) は、コマンドライン引数が 1 つだけのときには通らない場所なんだけどなぁ、、、あっ、zkServer.cmd で引数追加してるんだな、これは。(くそっ、先に zkServer.cmd みておくべきだった)

zkServer.cmd の中身はこんな感じ (途中に改行追加)

setlocal
call "%~dp0zkEnv.cmd"

set ZOOMAIN=org.apache.zookeeper.server.quorum.QuorumPeerMain
echo on
java "-Dzookeeper.log.dir=%ZOO_LOG_DIR%" "-Dzookeeper.root.logger=%ZOO_LOG4J_PROP%" ^
  -cp "%CLASSPATH%" %ZOOMAIN% "%ZOOCFG%" %*

endlocal

%ZOOMAIN% のあとに、%ZOOCFG% が挿入されてるぞ・・・。

しかし、こういう書き方をされると、設定ファイル名をコマンドライン引数として簡単に渡せなくなる。別の設定ファイルを使おうとすると、ZOOCFG 変数をいじってから zkServer.cmd を起動しないといけない。

「引数の数の違いによって、同じ位置にある引数なのに解釈の仕方を変えてしまう」、というのは、下手な設計。ZooKeeper サーバーのコマンドライン引数処理はひどい。ついでにいうと、設定ファイルのパース処理 (QuorumPeerConfig.parseProperties) もひどすぎる。

昔読んだ HBase のソースコードの汚さに比べたら全然マシだけど、なんか Hadoop まわりは力技感が半端ない。コード品質の悪さを人数でカバーか・・・。

結局、引数無しで zkServer.cmd を実行。手元の実験ならこれでいい。ダウンロードしてきた zookeeper のソースツリー (zookeeper-3.4.6) 外で作業しようとしたばっかりに、無駄な苦労をしてしまった。


2015年5月12日火曜日

undefined method `path2class'

直接の原因

psych.so ファイルが適切な場所に置かれていない。


問題回避方法

私のケースでは:

cp /usr/local/share/ruby/gems/2.0/gems/psych-2.0.13/lib/psych.so \
   /usr/share/ruby/vendor_ruby/2.0/

詳細

path2class メソッドは psych_to_ruby.c 内で定義され、rb_define_private_method() により Psych::ClassLoader クラスのプライベートメソッドとして登録される。下記はそのコード。Init_psych_to_ruby() 関数の最後の行に注目。


static VALUE path2class(VALUE self, VALUE path)
{
#ifdef HAVE_RUBY_ENCODING_H
    return rb_path_to_class(path);
#else
    return rb_path2class(StringValuePtr(path));
#endif
}

void Init_psych_to_ruby(void)
{
    VALUE psych     = rb_define_module("Psych");
    VALUE class_loader  = rb_define_class_under(psych, "ClassLoader", rb_cObject);

    VALUE visitors  = rb_define_module_under(psych, "Visitors");
    VALUE visitor   = rb_define_class_under(visitors, "Visitor", rb_cObject);
    cPsychVisitorsToRuby = rb_define_class_under(visitors, "ToRuby", visitor);

    rb_define_private_method(cPsychVisitorsToRuby, "build_exception", build_exception, 2);
    rb_define_private_method(class_loader, "path2class", path2class, 1);
}

Init_psych_to_ruby() 関数は、psych.c で定義されている Init_psych() 関数から呼ばれている。

void Init_psych(void)
{
    mPsych = rb_define_module("Psych");

    rb_define_singleton_method(mPsych, "libyaml_version", libyaml_version, 0);

    Init_psych_parser();
    Init_psych_emitter();
    Init_psych_to_ruby();
    Init_psych_yaml_tree();
}

Ruby は、ライブラリの共有ライブラリ (.so ファイル) をロードしたあと、「Init_ライブラリ名()」関数を呼ぶ。そのため、Psych の場合、psych.so が見つかりロードされれば、Init_psych() 関数が呼ばれ、それから Init_psych_to_ruby() が呼ばれ、結果 path2class メソッドが登録されることになる。しかし、もしも psych.so が存在しなければ、path2class メソッドは登録されず、"undefined method `path2class'" というエラーメッセージを見ることになる。

おそらく、Ruby もしくは Psych のパッケージングプロセスに何らかの問題があると思われる。


参照



2015年5月2日土曜日

WebSocket client library (Java SE 1.5+, Android)

This page explains nv-websocket-client, a WebSocket client library written in Java. Its JavaDoc is here.

Last Updated: May. 11, 2016


Overview

High-quality WebSocket client implementation in Java which

  1. complies with RFC 6455 (The WebSocket Protocol)
  2. works on Java SE 1.5+ and Android,
  3. supports all the frame types (continuation, binary, text, close, ping and pong),
  4. provides a method to send a fragmented frame in addition to methods for unfragmented frames,
  5. provides a method to get the underlying raw socket of a web socket to configure it,
  6. provides a method for Basic Authentication,
  7. provides a factory class which utilizes javax.net.SocketFactory interface,
  8. provides a rich listener interface to hook web socket events,
  9. has fine-grained error codes for fine-grained controllability on errors,
  10. allows to disable validity checks on RSV1/RSV2/RSV3 bits and opcode of frames.
  11. supports HTTP proxy, especially "Secure WebSocket" (wss) through "Secure Proxy" (https),
  12. and supports RFC 7692 (Compression Extensions for WebSocket), also known as permessage-deflate extension (not enabled by default).

Description

Create WebSocketFactory

WebSocketFactory is a factory class that creates WebSocket instances. The
first step is to create a WebSocketFactory instance.

// Create a WebSocketFactory instance.
WebSocketFactory factory = new WebSocketFactory();

By default, WebSocketFactory uses SocketFactory.getDefault() for non-secure WebSocket connections (ws:) and SSLSocketFactory.getDefault() for secure WebSocket connections (wss:). You can change this default behavior by using WebSocketFactory.setSocketFactory method, WebSocketFactory.setSSLSocketFactory method and WebSocketFactory.setSSLContext method. Note that you don't have to call a setSSL* method at all if you use the default SSL configuration. Also note that calling setSSLSocketFactory method has not meaning if you have called setSSLContext method. See the description of WebSocketFactory.createSocket(URI) method for details.

The following is an example to set a custom SSL context to a WebSocketFactory instance. (Again, you don't have to call a setSSL* method if you use the default SSL configuration.)

// Create a custom SSL context.
SSLContext context = NaiveSSLContext.getInstance("TLS");

// Set the custom SSL context.
factory.setSSLContext(context);

NativeSSLContext used in the above example is a factory class to create an SSLContext which naively accepts all certificates without verification. It's enough for testing purposes. When you see an error message "unable to find valid certificate path to requested target" while testing, try NaiveSSLContext.


HTTP Proxy

If a WebSocket endpoint needs to be accessed via an HTTP proxy, information about the proxy server has to be set to a WebSocketFactory instance before creating a WebSocket instance. Proxy settings are represented by ProxySettings class. A WebSocketFactory instance has an associated ProxySettings instance and it can be obtained by calling WebSocketFactory.getProxySettings() method.

// Get the associated ProxySettings instance.
ProxySettings settings = factory.getProxySettings();

ProxySettings class has methods to set information about a proxy server such as setHost method and setPort method. The following is an example to set a secure (https) proxy server.

// Set a proxy server.
settings.setServer("https://proxy.example.com");

If credentials are required for authentication at a proxy server, setId method and setPassword method, or setCredentials method can be used to set the credentials. Note that, however, the current implementation supports only Basic Authentication.

// Set credentials for authentication at a proxy server.
settings.setCredentials(id, password);


Create WebSocket

WebSocket class represents a web socket. Its instances are created by calling one of createSocket methods of a WebSocketFactory instance. Below is the simplest example to create a WebSocket instance.

// Create a web socket. The scheme part can be one of
// the following: 'ws', 'wss', 'http' and 'https'
// (case-insensitive). The user info part, if any, is
// interpreted as expected. If a raw socket failed to
// be created, of if HTTP proxy handshake or SSL
// handshake failed, an IOException is thrown.
WebSocket ws = new WebSocketFactory()
    .createSocket("ws://localhost/endpoint");

There are two ways to set a timeout value for socket connection. The first way is to call setConnectionTimeout(int timeout) method of WebSocketFactory.

// Create a web socket factory and set 5000 milliseconds
// as a timeout value for socket connection.
WebSocketFactory factory
    = new WebSocketFactory().setConnectionTimeout(5000);

// Create a web socket. The timeout value set above is used.
WebSocket ws
    = factory.createSocket("ws://localhost/endpoint");

The other way is to give a timeout value to createSocket method.

// Create a web socket factory. The timeout value remains 0.
WebSocketFactory factory = new WebSocketFactory();

// Create a web socket with a socket connection timeout value.
WebSocket ws
    = factory.createSocket("ws://localhost/endpoint", 5000);

The timeout value is passed to connect(SocketAddress, int) method of java.net.Socket.


Register Listener

After creating a WebSocket instance, you should call addListener method to register a WebSocketListener that receives web socket events. WebSocketAdapter is an empty implementation of WebSocketListener interface.

// Register a listener to receive web socket events.
ws.addListener(new WebSocketAdapter() {
    @Override
    public void onTextMessage(WebSocket websocket, String message)
        throws Exception
    {
        // Received a text message.
        ......
    }
});


Configure WebSocket

Before starting a WebSocket opening handshake with the server, you can configure the web socket instance by using the following methods.

METHOD DESCRIPTION
addProtocol Adds an element to Sec-WebSocket-Procotol.
addExtension Adds an element to Sec-WebSocket-Extensions.
addHeader Adds an arbitrary HTTP header.
setUserInfo Adds Authorization header for Basic Authentication.
getSocket Gets the underlying Socket instance to configure it.
setExtended Disables validity checks on RSV1/RSV2/RSV3 and opcode.
setFrameQueueSize Set the size of the frame queue for congestion control.
setMaxPayloadSize Set the maximum payload size.

Note that permessage-defeflate extension (RFC 7692) has been supported since version 1.17. To enable the extension, call addExtension method with "permessage-deflate".


// Enabled "permessage-deflate" extension (RFC 7692).
ws.addExtension(WebSocketExtension.PERMESSAGE_DEFLATE);


The permessage-deflate support is new and needs testing. Feedback is welcome.


Connect To Server

By calling connect() method, connection to the server is established and a WebSocket opening handshake is performed synchronously. If an error occurred during the handshake, a WebSocketException would be thrown. Instead, if the handshake succeeds, the connect() implementation creates threads and starts them to read and write web socket frames asynchronously.

try
{
    // Connect to the server and perform an opening handshake.
    // This method blocks until the opening handshake is finished.
    ws.connect();
}
catch (OpeningHandshakeException e)
{
    // A violation against the WebSocket protocol was detected
    // during the opening handshake.
}
catch (WebSocketException e)
{
    // Failed to establish a WebSocket connection.
}

In some cases, connect() method throws OpeningHandshakeException which is a subclass of WebSocketException (since version 1.19). OpeningHandshakeException provides additional methods such as getStatusLine(), getHeaders() and getBody() to access the response from a server. The following snippet is an example to print information that the exception holds.

catch (OpeningHandshakeException e)
{
    // Status line.
    StatusLine sl = e.getStatusLine();
    System.out.println("=== Status Line ===");
    System.out.format("HTTP Version  = %s\n", sl.getHttpVersion());
    System.out.format("Status Code   = %d\n", sl.getStatusCode());
    System.out.format("Reason Phrase = %s\n", sl.getReasonPhrase());

    // HTTP headers.
    Map<String, List<String>> headers = e.getHeaders();
    System.out.println("=== HTTP Headers ===");
    for (Map.Entry<String, List<String>> entry : headers.entrySet())
    {
        // Header name.
        String name = entry.getKey();

        // Values of the header.
        List<String> values = entry.getValue();

        if (values == null || values.size() == 0)
        {
            // Print the name only.
            System.out.println(name);
            continue;
        }

        for (String value : values)
        {
            // Print the name and the value.
            System.out.format("%s: %s\n", name, value);
        }
    }
}


Connect To Server Asynchronously

The simplest way to call connect() method asynchronously is to use connectAsynchronously() method. The implementation of the method creates a thread and calls connect() method in the thread. When the connect() call failed, onConnectError() of WebSocketListener would be called. Note that onConnectError() is called only when connectAsynchronously() was used and the connect() call executed in the background thread failed. Neither direct synchronous connect() nor connect(ExecutorService) (described below) will trigger the callback method.

// Connect to the server asynchronously.
ws.connectAsynchronously();

Another way to call connect(ExecutorService) method asynchronously is to use connect(ExecutorService) method. The method performs a WebSocket opening handshake asynchronously using the given ExecutorService.

// Prepare an ExecutorService.
ExecutorService es = Executors.newSingleThreadExecutor();

// Connect to the server asynchronously.
Future<WebSocket> future = ws.connect(es);

try
{
    // Wait for the opening handshake to complete.
    future.get();
}
catch (ExecutionException e)
{
    if (e.getCause() instanceof WebSocketException)
    {
        ......
    }
}

The implementation of connect(ExecutorService) method creates a Callable<WebSocket> instance by calling connectable() method and passes the instance to submit(Callable) method of the given ExecutorService. What the implementation of call() method of the Callable instance does is just to call the synchronous connect().


Send Frames

Web socket frames can be sent by sendFrame method. Other sendXxx methods such as sendText are aliases of sendFrame method. All of the sendXxx methods work asynchronously. However, under some conditions, sendXxx methods may block. See Congestion Control for details.

Below are some examples of sendXxx methods. Note that in normal cases, you don't have to call sendClose method and sendPong method (or their variants) explicitly because they are called automatically when appropriate.

// Send a text frame.
ws.sendText("Hello.");

// Send a binary frame.
byte[] binary = ......;
ws.sendBinary(binary);

// Send a ping frame.
ws.sendPing("Are you there?");

If you want to send fragmented frames, you have to know the details of the specification (5.4. Fragmentation). Below is an example to send a text message ("How are you?") which consists of 3 fragmented frames.

// The first frame must be either a text frame or
// a binary frame. And its FIN bit must be cleared.
WebSocketFrame firstFrame = WebSocketFrame
    .createTextFrame("How ")
    .setFin(false);

// Subsequent frames must be continuation frames. The FIN
// bit of all continuation frames except the last one must
// be cleared. Note that the FIN bit of frames returned
// from WebSocketFrame.createContinuationFrame() method
// is cleared, so the example below does not clear the
// FIN bit explicitly.
WebSocketFrame secondFrame = WebSocketFrame
    .createContinuationFrame("are ");

// The last frame must be a continuation frame with the
// FIN bit set. Note that the FIN bit of frames returned
// from WebSocketFrame.createContinuationFrame methods
// is cleared, so the FIN bit of the last frame must be
// set explicitly.
WebSocketFrame lastFrame = WebSocketFrame
    .createContinuationFrame("you?")
    .setFin(true);

// Send a text message which consists of 3 frames.
ws.sendFrame(firstFrame)
  .sendFrame(secondFrame)
  .sendFrame(lastFrame);

Alternatively, the same as above can be done like this.

ws.sendText("How ", false)
  .sendContinuation("are ")
  .sendContinuation("you?", true);


Send Ping/Pong Frames Periodically

You can send ping frames periodically by calling setPingInterval method with an interval in milliseconds between ping frames. This method can be called both before and after connect() method. Passing zero stops the periodical sending.

// Send a ping per 60 seconds.
ws.setPingInterval(60 * 1000);

// Stop the periodical sending.
ws.setPingInterval(0);

Likewise, you can send pong frames periodically by calling setPongInterval method. "A Pong frame MAY be sent unsolicited." (RFC 6455, 5.5.3. Pong)

You can customize payload of ping/pong frames that are sent automatically by using setPingPayloadGenerator() and setPongPayloadGenerator() methods. Both methods take an instance of PayloadGenerator interface. the following is an example to use the string representation of the current date as payload of ping frames.

ws.setPingPayloadGenerator(new PayloadGenerator() {
    @Override
    public byte[] generate() {
        // The string representation of the current date.
        return new Date().toString().getBytes();
    }
});

Note that the maximum payload length of control frames (e.g. ping frames) is 125. Therefore, the length of a byte array returned from generate() method must not exceed 125.


Auto Flush

By default, a frame is automatically flushed to the server immediately after sendFrame method is executed. This automatic flush can be disabled by calling setAutoFlush(false).

// Disable auto-flush.
ws.setAutoFlush(false);

To flush frames manually, call flush() method. Note that this method works asynchronously.

// Flush frames to the server manually.
ws.flush();


Congestion Control

sendXxx methods queue a WebSocketFrame instance to the internal queue. By default, no upper limit is imposed on the queue size, so sendXxx methods do not block. However, this behavior may cause a problem if your WebSocket client application sends too many WebSocket frames in a short time for the WebSocket server to process. In such a case, you may want sendXxx methods to block when many frames are queued.

You can set an upper limit on the internal queue by calling setFrameQueueSize(int) method. As a result, if the number of frames in the queue has reached the upper limit when a sendXxx method is called, the method blocks until the queue gets spaces. The code snippet below is an example to set 5 as the upper limit of the internal frame queue.

// Set 5 as the frame queue size.
ws.setFrameQueueSize(5);

Note that under some conditions, even if the queue is full, sendXxx methods do not block. For example, in the case where the thread to send frames (WritingThread) is going to stop or has already stopped. In addition, method calls to send a control frame (e.g. sendClose() and sendPing()) do not block.


Maximum Payload Size

You can set an upper limit on the payload size of WebSocket frames by calling setMaxPayloadSize(int) method with a positive value. Text, binary and continuation frames whose payload size is bigger than the maximum payload size you have set will be split into multiple frames.

// Set 1024 as the maximum payload size.
ws.setMaxPayloadSize(1024);

Control frames (close, ping and pong frames) are never split as per the specification.

If permessage-deflate extension is enabled and if the payload size of a WebSocket frame after compression does not exceed the maximum payload size, the WebSocket frame is not split even if the payload size before compression exceeds the maximum payload size.


Disconnect WebSocket

Before a web socket is closed, a closing handshake is performed. A closing handshake is started (1) when the server sends a close frame to the client or (2) when the client sends a close frame to the server. You can start a closing handshake by calling disconnect() method (or by sending a close frame manually).

// Close the web socket connection.
ws.disconnect();

disconnect() method has some variants. If you want to change the close code and the reason phrase of the close frame that this client will send to the server, use a variant method such as disconnect(int, String). disconnect() itself is an alias of disconnect(WebSocketCloseCode.NORMAL, null).


Reconnection

WebSocket.connect() method can be called at most only once regardless of whether the method succeeded or failed. If you want to re-connect to the WebSocket endpoint, you have to create a new WebSocket instance again by calling one of createSocket methods of a WebSocketFactory.

You may find recreate() method useful if you want to create a new WebSocket instance that has the same settings as the original instance. Note that, however, settings you made on the raw socket of the original WebSocket instance are not copied.

// Create a new WebSocket instance and connect to the same endpoint.
ws = ws.recreate().connect();

There is a variant of recreate() method that takes a timeout value for socket connection. If you want to use a timeout value that is different from the one used when the existing WebSocket instance was created, use recreate(int timeout) method.


Error Handling

WebSocketListener has some onXxxError() methods such as onFrameError() and onSendError(). Among such methods, onError() is a special one. It is always called before any other onXxxError() is called. For example, in the implementation of run() method of ReadingThread, Throwable is caught and onError() and onUnexpectedError() are called in this order. The following is the implementation.

@Override
public void run()
{
    try
    {
        main();
    }
    catch (Throwable t)
    {
        // An uncaught throwable was detected in the reading thread.
        WebSocketException cause = new WebSocketException(
            WebSocketError.UNEXPECTED_ERROR_IN_READING_THREAD,
            "An uncaught throwable was detected in the reading thread", t);

        // Notify the listeners.
        ListenerManager manager = mWebSocket.getListenerManager();
        manager.callOnError(cause);
        manager.callOnUnexpectedError(cause);
    }
}

So, you can handle all error cases in onError() method.

All onXxxError() methods receive a WebSocketException instance as the second argument (the first argument is a WebSocket instance). The exception class provides getError() method which returns a WebSocketError enum entry. Entries in WebSocketError enum are possible causes of errors that may occur in the implementation of this library. The error causes are so granular that they can make it easy for you to find the root cause when an error occurs.

Throwables thrown by implementations of onXxx() callback methods are passed to handleCallbackError() of WebSocketListener.

@Override
public void handleCallbackError(WebSocket websocket, Throwable cause)
    throws Exception {
    // Throwables thrown by onXxx() callback methods come here.
}


Sample Application

The following is a sample application that connects to the echo server on websocket.org (ws://echo.websocket.org) and repeats to (1) read a line from the standard input, (2) send the read line to the server and (3) prints the response from the server, until exit is entered. The source code can be downloaded from Gist.

2015年4月22日水曜日

WebSocket クライアントライブラリ (Java SE 1.5+, Android)

WebSocket クライアントライブラリ (Java) nv-websocket-client の説明です。JavaDoc はこちら

最終更新日: 2016 年 5 月 11 日


特長

  1. RFC 6455 (The WebSocket Protocol) に準拠している。
  2. Java SE 1.5+, Android で動く。
  3. 全てのフレームタイプ (continuation/binary/text/close/ping/pong) をサポートしている。
  4. 分割されたフレームを送信するメソッドを提供している。
  5. WebSocket が使用しているソケットを取得して設定できる。
  6. Basic 認証のためのメソッドを提供している。
  7. javax.net.SocketFactory インターフェースを利用するファクトリークラスを提供している。
  8. WebSocket イベントをフックするためのリッチなリスナーインターフェースを提供している。
  9. エラー発生時にきめ細かい制御ができるよう、きめ細かいエラーコードを定義している。
  10. フレームの RSV1/RSV2/RSV3 ビット及びオペコードの検証を無効化することができる。
  11. HTTP プロキシーをサポートしている。特に、セキュアプロキシー経由 (https) のセキュア WebSocket (wss) をサポート。
  12. RFC 7692 (Compression Extensions for WebSocket) をサポート (permessage-deflate として知られているもの)。デフォルトでは有効になっていません。


説明

WebSocketFactory 作成

WebSocketFactoryWebSocket インスタンスを作成するためのファクトリークラスです。最初のステップは WebSocketFactory インスタンスの作成です。

// WebSocketFactory のインスタンスを作成する。
WebSocketFactory factory = new WebSocketFactory();

デフォルトでは、WebSocketFactory はセキュアでない WebSocket 接続 (ws:) には SocketFactory.getDefault() を、セキュアな WebSocket 接続 (wss:) には SSLSocketFactory.getDefault() を使用します。このデフォルト動作は、WebSocketFactory.setSocketFactory メソッド、WebSocketFactory.setSSLSocketFactory メソッド、WebSocketFactory.setSSLContext メソッドで変更することができます。なお、デフォルトの SSL 設定を用いる場合は setSSL* メソッドを呼ぶ必要はまったくありません。また、setSSLContext メソッドを呼んだあとに setSSLSocketFactory メソッドを呼んでも意味はないので注意してください。詳細は WebSocketFactory.createSocket(URI) メソッドの説明を参照してください。

次の例は WebSocketFactory インスタンスにカスタムの SSL コンテキストを設定する例です。(もう一度言います。デフォルトの SSL 設定を用いる場合は setSSL* メソッドを呼ぶ必要はありません。)

// カスタムの SSL コンテキストを作成する。
SSLContext context = NaiveSSLContext.getInstance("TLS");

// カスタムの SSL コンテキストをセットする。
factory.setSSLContext(context);

上記の例で使用している NaiveSSLContext は、検証無しで全ての証明書を受け付ける SSLContext を作成するファクトリークラスです。テスト用としてはこれで十分です。 テスト中に "unable to find valid certificate path to requested target" というエラーメッセージをみたときは NaiveSSLContext を試してみてください。


HTTP プロキシー

WebSocket エンドポイントに HTTP プロキシー経由でアクセスする必要があるときは、WebSocket インスタンスを生成する前に、プロキシーサーバーに関する情報を WebSocketFactory インスタンスに設定しなければなりません。プロキシー設定は ProxySettings クラスによってあらわされます。WebSocketFactory インスタンスには、それに紐づいた ProxySettings インスタンスがあり、そのインスタンスは WebSocketFactory.getProxySettings() メソッドで取得できます。

// ProxySettings インスタンスを取得する。
ProxySettings settings = factory.getProxySettings();

ProxySettings クラスには setHost メソッドや setPort メソッドといった、 プロキシーサーバーの情報を設定するためのメソッドがあります。 次の例ではセキュア (https) プロキシーサーバーを設定しています。

// プロキシーサーバーを設定する。
settings.setServer("https://proxy.example.com");

プロキシーサーバーで認証が要求される場合は、setId メソッドと setPassword メソッド、もしくは setCredentials メソッドで認証情報を設定できます。ただし、現在の実装でサポートしているのは Basic 認証のみです。

// プロキシーサーバー用の認証情報を設定する。
settings.setCredentials(id, password);


WebSocket 作成

WebSocket は、WebSocket クラスで表現されます。WebSocket クラスのインスタンスは、WebSocketFactory インスタンスの createSocket メソッドのいずれかを使って作成します。WebSocket インスタンスを作成する最も簡単な例は次のようになります。

// WebSocket を作成する。スキーム部は ws, wss, http, https の
// いずれか (大文字小文字関係無し)。ユーザー情報部は、もしあれば、
// 期待した通りに解釈される。ソケットの生成に失敗した場合、または
// HTTP プロキシ―ハンドシェイクや SSL ハンドシェイクが失敗した場合、
// IOException が投げられる。
WebSocket ws = new WebSocketFactory()
    .createSocket("ws://localhost/endpoint");

ソケット接続のタイムアウト値を設定する方法が二つあります。一つは WebSocketFactorysetConnectionTimeout(int timeout) メソッドを呼ぶ方法です。

// WebSocketFactory を作成し、ソケット接続のタイムアウト値として
// 5000 ミリ秒を設定します。
WebSocketFactory factory
    = new WebSocketFactory().setConnectionTimeout(5000);

// WebSocket を作成します。上でセットしたタイムアウト値が使用されます。
WebSocket ws = factory.createSocket("ws://localhost/endpoint");

もう一つは、createSocket メソッドにタイムアウト値を渡す方法です。

// WebSocketFactory を作成します。タイムアウト値は 0 のままです。
WebSocketFactory factory = new WebSocketFactory();

// ソケット接続のタイムアウト値付きで WebSocket を作成します。
WebSocket ws = factory.createSocket("ws://localhost/endpoint", 5000);

タイムアウト値は、java.net.Socketconnect(SocketAddress, int) メソッドに渡されます。


リスナー登録

WebSocket インスタンス作成後、WebSocket イベントを受け取るため、WebSocketListener を登録します。WebSocketAdapterWebSocketListener インターフェースの空実装です。

// WebSocket イベントを受け取るため、リスナーを登録する。
ws.addListener(new WebSocketAdapter() {
    @Override
    public void onTextMessage(WebSocket websocket, String message)
        throws Exception {
        // テキスト・メッセージを受信
        ......
    }
});


WebSocket 設定

サーバーとのオープニング・ハンドシェイク (opening handshake) を実行する前に、次のメソッド群を用いて WebSocket インスタンスを設定することができます。


メソッド 説明
addProtocol Sec-WebSocket-Procotol に要素を追加する。
addExtension Sec-WebSocket-Extensions に要素を追加する。
addHeader 任意の HTTP ヘッダーを追加する。
setUserInfo Basic 認証用の Authorization ヘッダを追加する。
getSocket 設定するため、Socket インスタンスを取得する。
setExtended フレームの RSV1/RSV2/RSV3 ビット及びオペコードの検証をおこなわない。
setFrameQueueSize 輻輳制御のためにフレームキューのサイズを設定する。
setMaxPayloadSize ペイロードサイズの上限を設定する。


バージョン 1.17 で permessage-deflate 拡張 (RFC 7692) がサポートされました。この拡張を有効にするには、"permessage-deflate" を引数にして addExtension メソッドを呼びます。

// permessage-deflate 拡張 (RFC 7692) を有効にする。
ws.addExtension(WebSocketExtension.PERMESSAGE_DEFLATE);

permessage-deflate サポートは新しく、テストが必要です。フィードバックは歓迎します。


サーバー接続

connect() メソッドを呼ぶと、サーバーとの接続が確立され、オープニング・ハンドシェイクが同期で実行されます。ハンドシェイク中にエラーが発生したときは、WebSocketException が投げられます。一方、ハンドシェイクが成功したときは、WebSocket フレームの読み書きを非同期でおこなうためのスレッド群が生成されます。

try
{
    // サーバーに接続し、オープニング・ハンドシェイクを実行する。
    // このメソッドはハンドシェイクが終了するまでブロックする。
    ws.connect();
}
catch (OpeningHandshakeException e)
{
    // オープニング・ハンドシェイク中に WebSocket プロトコルに
    // 対する仕様違反が検出された。
}
catch (WebSocketException e)
{
    // WebSocket 接続の確立に失敗
}


connect() メソッドが WebSocketException のサブクラスである OpeningHandshakeException を投げる場合があります (バージョン 1.19 以降)。OpeningHandshakeException には、サーバーからのレスポンスにアクセスするための getStatusLine()getHeaders()getBody() というメソッド群が追加されています。 次のコードは、OpeningHandshakeException が保持する情報を表示する例です。

catch (OpeningHandshakeException e)
{
    // ステータス・ライン
    StatusLine sl = e.getStatusLine();
    System.out.println("=== ステータス・ライン ===");
    System.out.format("HTTP バージョン    = %s\n", sl.getHttpVersion());
    System.out.format("ステータス・コード = %d\n", sl.getStatusCode());
    System.out.format("理由               = %s\n", sl.getReasonPhrase());

    // HTTP headers.
    Map<String, List<String>> headers = e.getHeaders();
    System.out.println("=== HTTP ヘッダー ===");
    for (Map.Entry<String, List<String>> entry : headers.entrySet())
    {
        // ヘッダー名
        String name = entry.getKey();

        // ヘッダーの値のリスト
        List<String> values = entry.getValue();

        if (values == null || values.size() == 0)
        {
            // ヘッダー名のみ表示する。
            System.out.println(name);
            continue;
        }

        for (String value : values)
        {
            // ヘッダー名と値を表示する。
            System.out.format("%s: %s\n", name, value);
        }
    }
}


非同期サーバー接続

connect() メソッドを非同期で呼ぶ最も簡単な方法は connectAsynchronously() メソッドを使う方法です。このメソッドの実装は、スレッドを生成し、そのスレッドの中で connect() メソッドを呼びます。 connect() が失敗したときは WebSocketListeneronConnectError() メソッドが呼ばれます。 onConnectError() が呼ばれるのは、 connectAsynchronously() が使用され、バックグラウンド・スレッド内で実行された connect() が失敗したときのみです。同期版の connect() を直接呼んだときや、 connect(ExecutorService) (後述)を呼んだときは onConnectError() は呼ばれないので注意してください。

// サーバーに非同期で接続する。
ws.connectAsynchronously();

connect() メソッドを非同期で実行するもう一つの方法は connect(ExecutorService) メソッドを使う方法です。このメソッドは、渡された ExecutorService を使ってオープニング・ハンドシェイクを非同期で実行します。

// ExecutorService を用意する。
ExecutorService es = Executors.newSingleThreadExecutor();

// サーバーに非同期で接続する。
Future<WebSocket> future = ws.connect(es);

try
{
    // オープニング・ハンドシェイクが完了するのを待つ。
    future.get();
}
catch (ExecutionException e)
{
    if (e.getCause() instanceof WebSocketException)
    {
        ......
    }
}

connect(ExecutorService) メソッドの実装は、 connectable() メソッド呼んで Callable<WebSocket> インスタンスを作成し、そのインスタンスを ExecutorServicesubmit(Callable) メソッドに渡します。 Callable インスタンスの call() メソッドがやっているのは、同期バージョンの connect() を呼ぶことだけです。


フレーム送信

sendFrame メソッドで WebSocket フレームを送信することができます。sendText などの他の sendXxx メソッドは sendFrame メソッドのエイリアスです。全ての sendXxx メソッドは非同期で動作します。ただし、ある条件下では、sendXxx メソッドがブロックすることがあります。詳細は「輻輳制御」を参照してください。

sendXxx メソッドの例を幾つか示します。なお、普通は sendClose メソッドや sendPong メソッドを明示的に呼ぶ必要はありません。これらのメソッドは適切なときに自動的に呼ばれるからです。

// テキストフレームを送信する。
ws.sendText("Hello.");

// バイナリフレームを送信する。
byte[] binary = ......;
ws.sendBinary(binary);

// PING フレームを送信する。
ws.sendPing("Are you there?");

分割されたフレームを送信したい場合は、仕様 (5.4. Fragmentation) を詳細に理解しておかなければなりません。下記は、3 つのフレームで構成されるテキストメッセージ ("How are you?") を送信する例です。

// 一番目のフレームはテキストフレームかバイナリフレームのどちらか。
// FIN ビットはクリアされていなければならない。
WebSocketFrame firstFrame = WebSocketFrame
    .createTextFrame("How ")
    .setFin(false);

// 後に続くフレームは継続フレーム。最後のフレームを除き、全ての継続
// フレームの FIN ビットはクリアされていなければならない。
// WebSocketFrame.createContinuationFrame() メソッドが返すフレームの
// FIN ビットはクリアされているので、下記の例では FIN ビットをクリア
// する処理を省略している。
WebSocketFrame secondFrame = WebSocketFrame
    .createContinuationFrame("are ");

// 最後のフレームは FIN ビットがセットされた継続フレーム。
// WebSocketFrame.createContinuationFrame() メソッドが返すフレームの
// FIN ビットはクリアされているので、下記の例では FIN ビットを明示的に
// 設定している。
WebSocketFrame lastFrame = WebSocketFrame
    .createContinuationFrame("you?")
    .setFin(true);

// 3 つのフレームで構成されるテキストメッセージを送信する。
ws.sendFrame(firstFrame)
  .sendFrame(secondFrame)
  .sendFrame(lastFrame);

なお、上記と同じことを次のように別の方法で実行することもできます。

// 3 つのフレームで構成されるテキストメッセージを送信する。
ws.sendText("How ", false)
  .sendContinuation("are ")
  .sendContinuation("you?", true);


Ping/Pong フレーム定期送信

Ping フレーム間のインターバル(ミリ秒単位)を setPingInterval メソッドに渡すことで、Ping フレームの定期送信を実行することができます。このメソッドは connect() メソッドの前後どちらでも呼ぶことができます。ゼロを渡すと定期送信は停止します。

// 60 秒に一回 Ping を送信する。
ws.setPingInterval(60 * 1000);

// 定期送信をやめる。
ws.setPingInterval(0);

同様に、setPongInterval メソッドを呼べば Pong フレームを定期送信することができます。「要求されていないときでも Pong フレームを送信してかまいません。」 (RFC 6455, 5.5.3. Pong)

自動送信する Ping/Pong フレームのペイロードは、setPingPayloadGenerator() メソッドと setPongPayloadGenerator() メソッドでカスタマイズすることができます。どちらのメソッドも PayloadGenerator インターフェースのインスタンスを引数に取ります。次のコードは、現在時刻の文字列表現を Ping フレームのペイロードに設定する例です。

ws.setPingPayloadGenerator(new PayloadGenerator() {
    @Override
    public byte[] generate() {
        // 現在時刻の文字列表現
        return new Date().toString().getBytes();
    }
});

Ping フレーム等の制御フレームの最大ペイロード長は 125 なので注意してください。このため、generate() メソッドが返すバイト配列の長さは 125 を超えてはいけません。


自動フラッシュ

デフォルトでは、sendFrame メソッド実行後すぐ、フレームは自動的にサーバーへとフラッシュされます。 setAutoFlush(false) を呼べば、この自動フラッシュを無効にすることができます。

// 自動フラッシュを無効にする。
ws.setAutoFlush(false);

手動でフラッシュをおこなうときは、flush() メソッドを呼びます。このメソッドは非同期です。

// 手動でフレームをサーバーへとフラッシュする。
ws.flush();


輻輳制御

sendXxx メソッドは WebSocketFrame のインスタンスを内部キューに挿入します。デフォルトでは、キューのサイズに上限は課されていないため、sendXxx メソッド群はブロックしません。しかし、WebSocket サーバーが処理しきれないほどの数の WebSocket フレームを WebSocket クライアントアプリケーションが短時間に送ると、この動作は問題を引き起こすかもしれません。このような場合、キューイングされているフレームの数が多いときには sendXxx メソッドがブロックしてほしいと思うかもしれません。

setFrameQueueSize(int) メソッドを呼ぶことで、内部キューに上限を設定することができます。結果として、sendXxx メソッドが呼んだ時にキュー内のフレームの数が上限に達していた場合、キューに空きができるまで sendXxx メソッドがブロックするようになります。次のコードは、内部フレームキューの上限として 5 を設定する例です。

// フレームキューのサイズに 5 を設定する。
ws.setFrameQueueSize(5);

ある条件下では、キューがいっぱいであっても sendXxx メソッドはブロックしません。例えば、フレーム送信をおこなうスレッド (WritingThread) が停止しようとしている場合、または既に停止している場合です。また、コントロールフレームを送信するメソッド (sendClose()sendPing() など) はブロックしません。


ペイロードサイズ最大値

正数値を指定して setMaxPayloadSize(int) メソッドを呼ぶことにより、WebSocket フレームのペイロードサイズに上限を設定することができます。 設定したペイロードサイズ最大値を超えるテキストフレーム、バイナリフレーム、継続フレームは、複数のフレームに分割されます。

// ペイロードサイズの上限として 1024 を設定する。
ws.setMaxPayloadSize(1024);

仕様により、制御フレーム (Close フレーム、Ping フレーム、Pong フレーム) は、分割されることはありません。

permessage-deflate 拡張が有効になっていて、圧縮後のペイロードサイズがペイロード最大値を超えない場合、圧縮前のペイロードサイズがペイロード最大値を超えていたとしても、当該 WebSocket フレームは分割されません。


WebSocket 切断

WebSocket が閉じられる前には、クロージング・ハンドシェイク (closing handshake) が実行されます。クロージング・ハンドシェイクは、(1) サーバーがクライアントにクローズフレーム (close frame) を送るか、(2) クライアントがサーバーにクローズフレームを送るか、のどちらかにより開始されます。disconnect() メソッドを呼ぶことにより (または手作業でクローズフレームを送ることにより) クロージング・ハンドシェイクを開始することができます。

// WebSocket 接続を閉じる。
ws.disconnect();

disconnect() メソッドには幾つかバリアントがあります。クライアントがサーバーに送信するクローズフレームのクローズコードと切断理由を変更したい場合は、disconnect(int, String) などのバリアントを使用してください。 disconnect() メソッド自体は disconnect(WebSocketCloseCode.NORMAL, null) のエイリアスです。


再接続

WebSocket.connect() メソッドは、成功したか失敗したかにかかわらず、多くても一回しか呼ぶことができません。 WebSocket エンドポイントに再接続したいときは、WebSocketFactorycreateSocket メソッドで再度新しい WebSocket インスタンスを作成する必要があります。

元のインスタンスと同じ設定を持つ WebSocket インスタンスを作るのであれば、recreate() メソッドが便利でしょう。 ただし、元のインスタンスの低レベルソケットに加えた変更はコピーされないので注意してください。

// 新しい WebSocket インスタンスを作成して同じエンドポイントに接続する。
ws = ws.recreate().connect();

ソケット接続のタイムアウト値を引数に取る recreate(int timeout) というメソッドもあります。既存の WebSocket インスタンスを作成したときに使用したものとは異なるタイムアウト値を指定したい場合は、recreate(int timeout) を使ってください。


エラー処理

WebSocketListener には、onFrameError() メソッドや onSendError() メソッドなどの onXxxError() メソッドが幾つかあります。その中でも、onError() メソッドは特別です。このメソッドは、他の onXxxError() メソッドが呼ばれる前に常に呼ばれます。例えば、ReadingThreadrun() メソッドの実装では、Throwable がキャッチされ、onError() メソッドと onUnexpectedError() メソッドがこの順番で呼ばれます。下記はその実装です。

@Override
public void run()
{
    try
    {
        main();
    }
    catch (Throwable t)
    {
        // An uncaught throwable was detected in the reading thread.
        WebSocketException cause = new WebSocketException(
            WebSocketError.UNEXPECTED_ERROR_IN_READING_THREAD,
            "An uncaught throwable was detected in the reading thread", t);

        // Notify the listeners.
        ListenerManager manager = mWebSocket.getListenerManager();
        manager.callOnError(cause);
        manager.callOnUnexpectedError(cause);
    }
}

ですので、onError() メソッドの中で全てのエラーケースを扱うことができます。

全ての onXxxError() メソッドは第二引数に WebSocketException のインスタンスを受け取ります (第一引数は WebSocket のインスタンスです)。この例外クラスには、列挙型 WebSocketError のエントリーを返す getError() というメソッドがあります。列挙型 WebSocketError のエントリー群は、このライブラリの実装内で起こりうるエラーの原因をリストしたものです。エラー原因の粒度が細かいので、エラーが発生したとき、根本原因を見つけるのは容易です。

onXxx() コールバックメソッドが投げた ThrowableWebSocketListenerhandleCallbackError() メソッドに渡されます。

@Override
public void handleCallbackError(WebSocket websocket, Throwable cause)
    throws Exception {
    // onXxx() コールバックメソッドが投げた Throwable はここに来ます。
}


サンプルアプリケーション

このアプリケーションは、websocket.org 上の Echo サーバー (ws://echo.websocket.org) に接続し、exit と入力されるまで、(1) 標準入力から一行読み込み、(2) 読み込んだ行をサーバーに送り、(3) サーバーからの応答を出力する、という処理を繰り返します。ソースコードは Gist からダウンロードできます。




p.s.

実装の動機

オープンソースの WebSocket クライアントライブラリ (Java) は、検索すると幾つか見つかりますが、(1) Android で動かない、もしくは動かすためにはハックが必要、(2) 依存関係を含めるとサイズが大きい、(3) 品質が悪い、というような状況だったので、自作することにしました。

仕様について

WebSocket Java API (javax.websocket) では、無理ではありませんが、Basic 認証を実装しにくいです。

WebSocket JavaScript API は、オープニング・ハンドシェイク時に任意の HTTP ヘッダーを付ける方法を提供していないので、エンドポイントの URL に含まれる userInfo 部を実装 (ブラウザ) が期待通りに処理してくれない限り、Basic 認証はできません。heroku dev center の WebSocket Security という記事Authentication/authorization というセクションにも言及があります。

また、WebSocket JavaScript API では、明示的に接続を開始するためのメソッドがないので、以下推測ですが、「WebSocket インスタンス生成」が接続開始のトリガーにならざるを得ません。しかし、onopen, onmessage, onerror などのコールバックメソッドを登録できるのはインスタンスを生成したあとです。そのため、これらのコールバックメソッドが機能するためには、実際の接続は、インスタンス生成時に同期でやるわけにはいかず、インスタンス生成時には「接続のスケジューリング」をするだけ、という実装になります。しかし、こうなってくると、接続の失敗をちゃんと捕捉できるかどうか、怪しくなってきます。