javascriptnode.jswebsocketbinaryblob

nodejs binary websocket mimetype handling


I'm not 100% sure but from what I read when I send a blob (binary data) over websocket, the blob does not contain any file information. (Also the official specification states that wesockets only send the raw binary)

  1. the filesize
  2. the mimetype
  3. user info (explain later)

I'm using https://github.com/websockets/ws

Testing:

Sending directly the blob from an input file.

ws.send(this.files[0]) //this should already contain the info

Creating a new blob with the native JavaScript API from file setting the proper mimetype.

ws.send(new Blob([this.files[0]],{type:this.files[0].type})); //also this

on both sides you can get only the effective blob without any other information.

Is it possible to append let's say a 4kb predefined JSON data converted also to binary that contains important information like the mimetype and the filesize, and then just split off the 4kb when needed?

{"mime":"txt/plain","size":345}____________4KB_REST_OF_THE_BINARY

OR

ws.send({"mime":"txt\/plain","size":345})
ws.send(this.files[0])

Even if the first one is the worst solution ever it would allow me to send everything at one time.

The second one has a big problem:

it's a chat that allows to send also files like documents, images, music videos.

I could write some sort of handshaking system when sending the file/user info before I send the binary data.

BUT

if another person sends also a file, as it's async, the handshaking system has no chance to determine which file is the right one for the correct user and mimetype.

So how do you properly send a binary file in a multiuser async environment?

I know I can convert to base64 but that's 30% bigger.

btw. Totally disappointed with Apple... while Chrome shows every binary data properly, my iOS devices are not able to handle blob's, only images will show in blob or base64 format, not even a simple txt file. Basically only a <img> tag can read dynamic files.

How everything works (now):

  1. user sends a file
  2. nodejs gets the binary data, also user info... but not mimetype, filename, size.
  3. nodejs broadcasts the raw binary file to all the users.(can't specify user & file info)
  4. clients create a bloburl (who send that? XD).

EDIT

what I have now:

client 1 (sends a file)CHROME

fileInput.addEventListener('change',function(e){
 var file=this.files[0];
 ws.send(new Blob([file],{
  type:file.type //<- SET MIMETYPE
 }));
 //file.size
},false);

note: file is already a blob ... but this is how you would normally create a new blob specifying the mimetype.

server (broadcasts the binary data to the other clients)NODEJS

aaaaaand the mimetype is gone...

ws.addListener('message',function(binary){
 var b=0,c=wss.clients.length;
 while(b<c){
  wss.clients[b++].send(binary)
 }
});

client 2 (receives the binary)CHROME

ws.addEventListener('message',function(msg){
 var blob=new Blob([msg.data],{
      type:'application/octet-stream' //<- LOST
     });
 var file=window.URL.createObjectURL(blob);
},false);

note: m.data is already a blob ... but this is how you would normally create a new blob specifying the mimetype which is lost.

In client 2 I need the mimetype and naturally I also need the info about the user, which can be retrieved from client 1 or the server (not a good choice)...


Solution

  • You're a bit out of luck with this because Node doesn't support the Blob interface and so any data you send or receive in Binary with Node is just Binary. You would have to have something that knew how to interpret a Blob object.

    Here's an idea, and let me know if this works. Reading through the documentation for websockets\ws it says it supports sending and receiving ArrayBuffers. Which means you can use TypedArrays.

    Here's where it gets nasty. You set a certain fixed n number of bytes at the beginning of every TypedArray to signal the mime type encoded in utf8 or what have you, and the rest of your TypedArray contains your file's bytes.

    I would recommend using UInt8Array because utf8 characters are 8 bits long and your text will probably be readable when encoded that way. As for the file bits you'll probably just end up writing those down somewhere and appending an ending to it.

    Also note, this method of interpretation works both ways whether from Node or in the Browser.

    This solution is really just a form of type casting and you might get some unexpected results. The fixed length of your mime type field is crucial.

    Here it is illustrated. Copy, paste, set the image file to whatever you want and then run that. You'll see the mime type I set pop out.

    var fs = require('fs');
    
    
    //https://stackoverflow.com/questions/8609289/convert-a-binary-nodejs-buffer-to-javascript-arraybuffer
    function toUint8Array(buffer) {
      var ab = new ArrayBuffer(buffer.length);
      var array = new Uint8Array(ab);
    
      for (var i = 0; i < buffer.length; ++i) {
        array[i] = buffer[i];
      }
    
      return array;
    }
    
    //data is a raw Buffer object
    fs.readFile('./ducklings.png', function (err, data) {
      var mime = new Buffer('image/png');
      var allBuffed = Buffer.concat([mime, data]);
      var array = toUint8Array(allBuffed);
      var mimeBytes = array.subarray(0,9); //number of characters in mime Buffer
      console.log(String.fromCharCode.apply(null, mimeBytes));
    });
    

    Here's how you do it on the client side:

    SOLUTION A: GET A PACKAGE

    Get buffer, an implementation of Node's Buffer API for browsers. The solution to concatenate Byte buffers will work exactly as before. You can append fields like To: and what not as well. The way you format your headers in order to best serve your clients will be an evolving process I'm sure.

    SOLUTION B: OLD SCHOOL

    STEP 1: Convert your Blob to an ArrayBuffer

    Notes: How to convert a String to an ArrayBuffer

    var fr = new FileReader();
    fr.addEventListener('loadend', function () {
    //Asynchronous action in part 2.
      var message = concatenateBuffers(headerStringAsBuffer, fr.result);
      ws.send(message);
    });
    fr.readAsArrayBuffer(blob);
    

    STEP 2: Concatenate ArrayBuffers

    function concatenateBuffers(buffA, buffB) {
      var byteLength = buffA.byteLength + buffB.byteLength;
      var resultBuffer = new ArrayBuffer(byteLength);
      //wrap ArrayBuffer in a typedArray/view
      var resultView = new Uint8Array(resultBuffer);
      var viewA = new Uint8Array(resultBuffer);
      var viewB = new Uint8Array(resultBuffer);
      //Copy 8 bit integers AKA Bytes
      resultView.set(viewA);
      resultView.set(viewB, viewA.byteLength);
      return resultView.buffer
    }
    

    STEP 3: Receive and Reblob

    I'm not going to repeat how to convert the concatenated String bytes back into a string because I've done it in the server example, but for turning the file bytes into a blob of your mime type is fairly simple.

    new Blob(buffer.slice(offset, buffer.byteLength), {type: mimetype});
    

    This Gist by robnyman goes into further details on how you would use an image transmitted via XHR, put it into localstorage, and use it in an image tag on your page.