hagino3000's blog

平成アーカイブス (更新停止)

WebSocketでバイナリデータを送受信してみる

この記事はHTML5 Advent Calendarの8日目です。

いつの間にか手元の環境(Chrome17 dev + Node 0.6.3)においてWebSocketでバイナリデータが扱える様になっていたので何か作ってみようかと。

まず、NodeでWebSocketを使おうとしたらそれ用のライブラリを使う事になるのだが現時点でバイナリデータが扱えるのはWebSocket-Nodeのみだった。
当初はサーバーから画像のRAWデータをガンガンクライアントに送りまくるというのを作ろうとしたのだがnode-pngがNodeの0.6系に対応していなかったので断念。CanvasのgetImageDataで得られるデータをそのままサーバーに投げて、サーバー側で加工した物を受け取るサンプルを作った。


バイナリデータの送信

クライアントのコード(client.js)

var socket = null;

function bootstrap() {

  // 適当な図形を描画
  var c = document.getElementById('mycanvas');
  var ctx = c.getContext('2d');
  // (中略)

  // Socketの初期化
  socket = new WebSocket('ws://localhost:8082');
  socket.binaryType = 'arraybuffer';
  socket.onopen = function() {
    send(ctx);
  }
  socket.onmessage = handleReceive;
};

function send(ctx) {
  // RAWデータをそのまま送信
  var data = ctx.getImageData(0, 0, 200, 200).data;
  var byteArray = new Uint8Array(data);
  socket.send(byteArray.buffer);
} 

CanvasのgetImageDataで得られるデータは8bit4チャンネルのRGBAの数値配列。1ピクセルあたり要素は4つ、それぞれが0-255の値なので、これをそのままUint8Arrayのコンストラクタに渡した。見なれないのは次の行ですね。

socket.binaryType = 'arraybuffer';

サーバーでRAWデータを加工

サーバーのコード(server.js)

#!/usr/bin/env node
var WebSocketServer = require('websocket').server,
    http = require('http'),
    fs = require('fs');

// (中略)

wsServer = new WebSocketServer({
  httpServer: server,
  // デフォルトでは65535byte以上受けつけないので
  // 値を増やしてみる
  maxReceivedFrameSize: 0x1000000,
  autoAcceptConnections: false
});

wsServer.on('request', function(request) {

  var connection = request.accept(null, request.origin);

  connection.on('message', function(message) {
    if (message.type === 'utf8') {
      // テキストデータを受信した場合
    }
    else if (message.type === 'binary') {
      // バイナリデータを受信した場合
      var data = message.binaryData;
      var len = data.length;

      // 受信したRAW画像をグレースケールにする
      var buf = new Buffer(len);
      for (var i = 0; i < len; i+=4 ) {
        var r = data.readUInt8(i);
        var g = data.readUInt8(i+1);
        var b = data.readUInt8(i+2);
        var y = Math.floor((77*r + 28*g + 151*b)/255);

        // Canvasにそのまま投入するために
        // 4チャンネル8ビットのRGBAにする
        var v = y + (y << 8) + (y << 16) + (0xFF << 24);
        buf.writeInt32LE(v, i);
      }
      // グレースケールにした物をクライアントに送信する
      connection.sendBytes(buf);
    }
  });

  connection.on('close', function(reasonCode, description) {
    // (略)
  });
});

受信したデータを8bitずつ読んで、それぞれRGBAとして処理する。加工したデータはNodeのBufferオブジェクトに値を書き込んで connection.sendBytesメソッドに渡す。writeInt32LEを使ってバッファに書き込んでいるのはビット演算子が使いたかっただけ。writeUInt8を4回やってもいい。

バイナリデータの受信

再びクライアントのコード(client.js)

WebSocketのonmessageイベントハンドラにセットした関数。

function handleReceive(message) {
  // 受信したRAWデータをcanvasに
  var c = resultCanvas = document.getElementById('result');
  var ctx = c.getContext('2d');
  var imageData = ctx.createImageData(200, 200);
  var pixels = imageData.data;

  var buffer = new Uint8Array(message.data);
  for (var i=0; i < pixels.length; i++) {
    pixels[i] = buffer[i];
  }
  ctx.putImageData(imageData, 0, 0);
}

サーバーでやったのと同様に8bitずつ値を取得したかったので、Uint8Arrayを使った。

まとめ

結果は上のスクリーンショットの通り。全てのコードはgistにアップしました。

とりあえずNodeのBufferオブジェクトとTyped Arrayの使い方を覚えればなんとかなりそう。これからは送信する前にbase64する手間が省けて嬉しいですね。