サラリーマン技術者の調査レポート

日々の業務で気付いた当たり障りのない技術的なあれこれを綴ります。

WebSocketで大きなデータを送信

Glassfish4(Tyrus)のJava API for WebSocket(JSR-356)で、サーバからクライアントへサイズの大きなファイルをバイナリ転送するサンプルプログラムを作ってみました。
JSR-356でバイナリデータを転送する場合、byte[]やByteBufferなど固定長のデータを使う方法と、InputStream/OutputStreamといったStreamを使う方法の2種類があります。これらの組み合わせによっては、動きそうで実は正しく動かないことがあります。

ダメだった実装

先にダメだったほうの実装から書きます。

クライアント

java.io.InputStreamでバイナリメッセージを受けて、適当にファイルに保存するようにしました。

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;

import javax.websocket.ClientEndpoint;
import javax.websocket.OnMessage;

@ClientEndpoint
public class Client {
    ...
    @OnMessage
    public void onMessage(InputStream is) throws IOException {
        Files.copy(is, Paths.get("C:\\xxxxxxxx"));
    }
    ...
}

サーバ

javax.websocket.RemoteEndpoint.Basic.getSendStream()で取得したjava.io.OutputStreamを使ってバイナリメッセージを送信するようにしました。

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

import javax.websocket.Session;
...
    private void sendFile(Session session) throws IOException {
        Files.copy(Paths.get("C:\\xxxxxxxx"), session.getBasicRemote.getSendStream());
    }
...

何がダメか

とにかくスピードが遅いです。100KB/sec程度しか出ません。クライアントとサーバを同じマシンで動かしているにも関わらず1MB10秒以上かかりました。
StreamにBuffered~を適用したり、いろいろパラメータを変えて試しましたが、どれも効果がありませんでした。
さらに大きな問題として、送信前と送信後のファイルを比較すると、なぜかところどころデータが化けています。
データがどこで化けるのかはわかりませんでしたが、再現性はあるようです。

上手くいった実装

クライアント

byte[]でメッセージを受けるようにしました。ByteBufferでも正しく動きます。
ひとつのバイナリファイルで複数回onMessage()が呼び出されます。lastがtrueのときが最後の呼び出しになります。

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;

import javax.websocket.ClientEndpoint;
import javax.websocket.OnMessage;

@ClientEndpoint
public class Client {
    private OutputStream os = null;
    ...
    @OnMessage
    public void onMessage(byte[] bytes, boolean last) throws IOException {
        if (os == null) {
            os = new FileOutputStream("C:\\xxxxxxxx");
        }
        os.write(bytes);
        if (last) {
            os.close();
            os = null;
        }
    }
    ...
}

サーバ

力技でグルグルとループしてます。エラー発生時の処理は間違ってる可能性がありますが、正常な処理は概ねこのような感じになります。

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;

import javax.websocket.RemoteEndpoint.Basic;
import javax.websocket.Session;
...
    private static final int BUF_SIZE = 4 * 1024;
    private static final int EOF = -1;
    private void sendFile(Session session) throws IOException {
        byte[] prevBuffer = new byte[BUF_SIZE], nextBuffer = new byte[prevBuffer.length];
        Basic remote = session.getBasicRemote();
        InputStream is = new FileInputStream("C:\\xxxxxxxx");
        try {
            for (int nextLen = EOF, prevLen = is.read(prevBuffer); EOF != (nextLen = is.read(nextBuffer));) {
                remote.sendBinary(ByteBuffer.wrap(Arrays.copyOf(prevBuffer, prevLen)), false);
                prevLen = nextLen;
                System.arraycopy(nextBuffer, 0, prevBuffer, 0, prevBuffer.length);
                Arrays.fill(nextBuffer, (byte) 0);
            }
            remote.sendBinary(ByteBuffer.wrap(Arrays.copyOf(prevBuffer, prevLen)), true);
        } finally {
            is.close();
        }
    }
...

まとめ

  • 結局、正しく動くのはクライアント・サーバともにbyte[]/ByteBufferを使ったときだけでした。
  • 正しく動作する実装では10数MBのファイルの転送が一瞬で完了しました(正確には計測してません)。また、送信前後のファイル内容も完全に一致しました。
  • Streamを使った実装がうまく動かない理由はよくわかりませんが、Tyrusのバグじゃないかと思ってます。他のJSR-356の実装でも試してみようと思います。