WebSocketでテキストデータとバイナリデータを同時に送信する方法

作成: 2020年06月29日

更新: 2021年03月29日

やりたいこと

WebsocketではJSONなどのテキストデータ,またはバイナリデータをサーバーとクライアント間で送受信できる.しかしテキストデータとバイナリデータを同時に送ることができず,バイナリデータをクライアントからサーバーに送信し,クライアント側が動的に設定したファイル名でサーバー側にそのバイナリデータを保存することができなかった.

解決法

参考にした記事: ArrayBufferの分割/結合方法 - Qiita, String と ArrayBuffer の相互変換 JavaScript

クライアント側のバイナリデータをArrayBufferとし,そのArrayBufferの先頭にテキスト(ファイル名)をArrayBuffer化したものを結合し,サーバー側でそのArrayBufferをテキストとバイナリデータに分離した.

今回の実装ではクライアント側からmediaRecorder.ondataavailable で発生したBlob型のバイナリをArrayBufferに変換しArrayBuffer化したファイル名と結合してサーバーに送信した.

クライアント側:

function string_to_buffer(src) { // String => ArrayBuffer
  return (new Uint16Array([].map.call(src, function(c) {
    return c.charCodeAt(0)
  }))).buffer;
}
function concatenation(segments) { // ArrayBufferの結合
    var sumLength = 0;
    for(var i = 0; i < segments.length; ++i){
        sumLength += segments[i].byteLength;
    }
    var whole = new Uint8Array(sumLength);
    var pos = 0;
    for(var i = 0; i < segments.length; ++i){
        whole.set(new Uint8Array(segments[i]),pos);
        pos += segments[i].byteLength;
    }
    return whole.buffer;
}

const options = { mimeType: "audio/webm" };
const mediaRecorder = new MediaRecorder(mediaStream, options);
const ws = new WebSocket("ws://localhost:8081");
ws.binaryType = "arraybuffer";
mediaRecorder.ondataavailable = function(e) {
  if (e.data.size > 0) {
    e.data.arrayBuffer().then(audioBuffer => { // Blob => ArrayBuffer 非同期処理
      if (ws.readyState == WebSocket.OPEN) {
        const fileNameBuffer = string_to_buffer("filename");
        const concatArrayBuffer = concatenation([fileNameBuffer, audioBuffer]);
        ws.send(concatArrayBuffer); // サーバーに送信
      }
    });
  }
};
mediaRecorder.start(1000);

サーバー側(Node.js):

function buffer_to_string(buf) { // ArrayBuffer => String
  return String.fromCharCode.apply("", new Uint16Array(buf))
}

const websocketServer = require("ws").Server;
const serverInstance = new websocketServer({ port: 8081 });
const fileNameBufferLength = 16 // ファイル名のArrayBufferの長さは文字数*2
serverInstance.on("connection", function(ws) {
    ws.on("message", function(message) {
        const fileNameBuffer = message.slice(0, fileNameBufferLength);
        const fileName = buffer_to_string(fileNameBuffer).replace(/\0/g, ""); // \u0000 のヌル文字が入ることがあるので取り除く
        const binaryArrayBuffer = message.slice(fileNameBufferLength);
        fs.appendFile(fileName, binaryArrayBuffer, function(err) { // バイナリファイル保存
            if (err) throw err;
        });
    }); 
});

問題点としてはファイル名の長さは一定である必要がある.もしファイル名の長さが一定にできないのであればファイル名ではなく一定の長さのハッシュを今回の方法で送り,別にハッシュとファイル名をセットにしたJSONをクライアントからサーバーに送る必要がある.

また通常の配列とは異なるArrayBufferに対して無理やりスライスと結合を行っているので動作保証ができているかは不安が残る.