back to top

Node.js Buffer: cosa sono e come usarli

In questa lezione su Node.js parleremo degli oggetti di tipo Buffer. Si tratta di un’introduzione all’argomento che necessiterebbe più tempo per essere trattato in maniera esaustiva. Vi consiglio quindi di leggere la documentazione ufficiale che è abbastanza chiara e presenta numerosi esempi.

Node.js Buffer: cos’è?

Quando Node.js è stato creato, ha introdotto alcune funzionalità che non erano disponibili in Javascript. Fra queste la possibilità di lavorare con file e socket. Da qui è nata la necessità di lavorare con file e stream binari. Per esempio, quando si legge un file con il metodo fs.readFile(nomeFile, callback), se non si specifica la codifica da usare, verrà passato alla funzione callback un oggetto di tipo Buffer. Prima dell’avvento di ES2015, con cui sono stati introdotti i Typed Array, Javascript non aveva gli strumenti necessari per questo tipo di operazioni. Node.js ha dunque introdotto un nuovo tipo di dato denominato Buffer, con lo scopo di lavorare con sequenze di byte. La ‘classe’ Buffer è disponibile globalmente e non è quindi necessario importare nulla con la funzione require(). Gli oggetti di tipo Buffer vengono allocati al di fuori della Heap di V8 e per questo un volta creati non è possibile modificarne la dimensione. Gli oggetti di tipo Buffer sono simili agli array di interi (dispongono anche di alcune proprietà e metodi analoghi), ma possono essere visti come un tipo di dato che contiene una rappresentazione in byte di un certo valore.

Come creare e usare un oggetto di tipo Buffer

È possibile creare un oggetto di tipo Buffer in diversi modi. I metodi che useremo sono essenzialmente tre:

  • Buffer.alloc()
  • Buffer.allocUnsafe()
  • Buffer.from()

Metodo 1: Buffer.alloc()

Una prima possibilità è usare il metodo Buffer.alloc(dimensioneBuffer[, valoreInizializzazione, encoding]) che permette di creare un nuovo oggetto di tipo Buffer della dimensione che specifichiamo col primo argomento. Possiamo anche indicare con quale valore inizializzare il buffer (valore di default 0) e che tipo di codifica dei caratteri usare. (valore di default ‘utf8’). Vediamo alcuni esempi.

// crea un buffer di dimensione pari a 3 e inizializzato a 0
const buffer0 = Buffer.alloc(3);

// restituisce una rappresentazione del buffer in formato JSON
buffer0.toJSON() // { type: 'Buffer', data: [ 0, 0, 0 ] }

// crea un buffer di dimensione pari a 5 e inizializzato a 1
const buffer1 = Buffer.alloc(5, 1) 

buffer1.toJSON() // { type: 'Buffer', data: [ 1, 1, 1, 1, 1 ] }

Metodo 2: Buffer.allocUnsafe()

Un’altra possibilità è quella di creare un nuovo Buffer con il metodo Buffer.allocUnsafe(dimensioneBuffer) che crea un nuovo oggetto di tipo Buffer della dimensione specificata. Rispetto a Buffer.alloc(), è più veloce ma il nuovo buffer creato, non essendo stato inizializzato con un preciso valore, potrebbe contenere vecchie informazioni già in memoria che possono essere sovrascritte usando il metodo buffer.fill(value[, offset[, end]][, encoding]) o buffer.write(string[, offset[, length]][, encoding]).

let buffer2 = Buffer.allocUnsafe(15);
// <Buffer 08 00 00 00 07 00 00 00 b0 6c 80 04 01 00 00>

buffer2.fill(0)
// <Buffer 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00>

Metodo 3: Buffer.from()

Un ultimo metodo che vediamo e forse anche il più interessante per certi versi, è Buffer.from() che costruisce un nuovo oggetto buffer a partire da un array, un arrayBuffer, un buffer già esistente, una stringa o un oggetto. Vediamo ora alcuni esempi.

let buffer3 = Buffer.from([1,2,3])
// <Buffer 01 02 03>

let buffer4 = Buffer.from(buffer3);
// <Buffer 01 02 03>

buffer3[0] = 2
// <Buffer 02 02 03>

buffer4 // <Buffer 01 02 03>

Abbiamo creato un primo oggetto di tipo buffer a partire dal quale ne creiamo un secondo con il metodo Buffer.from(). Dopo aver modificato buffer3, buffer4 resta invariato.

Vediamo un altro uso del metodo Buffer.from().

const buffer5 = Buffer.from('ciao');
// Crea un Buffer contenente i byte UTF-8  [0x63, 0x69, 0x61, 0x6f]

buffer5.toString() // ciao

buffer5.toJSON() // { type: 'Buffer', data: [ 99, 105, 97, 111 ] }

Come possiamo vedere dall’esempio, abbiamo creato un oggetto di tipo Buffer a partire dalla stringa ‘ciao’. Se ricordate, avevamo detto che un Buffer è un tipo di dato che contiene una rappresentazione in byte di un certo valore. In questo caso buffer5 conterrà la rappresentazione in byte di ogni lettera della parola ‘ciao’. Come sistema di codifica per convertire ogni carattere in un numero univoco viene usato il sistema Unicode e in particolare la codifica dei caratteri UTF-8. Nell’esempio in alto potete vedere la rappresentazione sia in formato esadecimale che decimale.

illustazione codifica dei caratteri buffer Node.js

Possiamo quindi costruire un array contenente la rappresentazione in formato decimale o esadecimale di alcuni caratteri, creare un oggetto di tipo Buffer e ricavare la stringa corrispondente usando il metodo buffer.toString()

const array = [0x65, 0x73, 0x65, 0x6D, 0x70, 0x69, 0x6F];
const array2 = [67, 105, 97, 111];
const array3 = [0xF0, 0x9F, 0x98, 0x81];

const buffer = Buffer.from(array);
const buffer2 = Buffer.from(array2);
const buffer3 = Buffer.from(array3);

buffer.toString(); // esempio
buffer2.toString(); // Ciao
buffer3.toString(); // 😁

UTF-8 usa fino a quattro byte per rappresentare un carattere Unicode. Consideriamo un altro esempio in cui usiamo una stringa composta dal solo carattere ‘è’ (e accentata). Sebbene la lunghezza della stringa è chiaramente pari a uno, se a partire da questo carattere costruiamo un oggetto di tipo buffer, quest’ultimo sarà composto da due byte.

const e = 'è';

e.length // 1

const buffer = Buffer.from(e) // <Buffer c3 a8>

buffer.length // 2

Buffer.byteLength(e) // 2

Nell’esempio appena visto, abbiamo usato il metodo Buffer.byteLength per visualizzare la lunghezza reale in byte della stringa contenente il carattere ‘è’.

Node.js Buffer: alcuni metodi utili

Vediamo ora degli esempi in cui useremo alcuni metodi che possono essere utili quando si lavora con i buffer.

Un metodo che abbiamo già usato è buffer.toString([encoding[, start[, end]]]) che permette di trasformare un buffer in stringa usando la codifica di caratteri specificata come primo argomento. (‘utf8’ è il valore predefinito). È anche possibile indicare un byte di inizio e fine.

const buffer = Buffer.from([67, 105, 97, 111]); // 'ciao'

buffer.toString('utf8', 1, 3); // ia

Con il metodo Buffer.toString() è anche possibile convertire il valore contenuto in un oggetto Buffer in una diversa codifica di caratteri. Nell’esempio sottostante leggiamo per prima cosa un file .jpg e lo convertiamo in formato base64.

// file index.js

const fs = require('fs');

const buffer = fs.readFileSync('./penguin.png');

const base64 = buffer.toString('base64');

console.log(base64); // iVBORw0KGgoAAAANSU...

const buffer2 = Buffer.from(base64, 'base64');

console.log(Buffer.isBuffer(buffer2)); // true

console.log(buffer.equals(buffer2)); // true

fs.writeFileSync('./penguin_copy.png', buffer2);

Abbiamo importato il modulo nativo fs (ne parleremo nella prossima lezione) e abbiamo letto il contenuto dell’immagine ‘penguin.png’ presente nella stessa cartella del file index.js. Con l’ausilio del metodo Buffer.toString() abbiamo ottenuto la rappresentazione in formato base64 dell’immagine. La stringa così ottenuta potrebbe essere usata, per esempio, come valore dell’attributo src di un elemento immagine all’interno di una pagina HTML.

<!-- Abbiamo omesso l'intero contenuto della stringa -->
<img 
src="data:image/png;base64, iVBORw0KGgoAAAANSU..." 
alt="Pinguino" />

Abbiamo quindi creato un nuovo oggetto buffer a partire dall’immagine codificata in base64. Con il metodo Buffer.isBuffer() abbiamo verificato che quello creato è realmente un oggetto di tipo Buffer. Questo nuovo oggetto contiene gli stessi byte dell’oggetto buffer (buffer.equals(buffer2)). Infine, usando il contenuto di buffer2 abbiamo creato una copia dell’immagine originale penguin.png

Un altro metodo molto utile è Buffer.compare(buffer1, buffer2) (esiste anche la versione buffer1.compare(buffer2)) che può essere usato per ordinare un array. Questo metodo restituisce un intero pari a:

  • 1 se buffer1 > buffer2
  • -1 se buffer1 < buffer2
  • 0 se buffer1 = buffer2
const buffer1 = Buffer.from('appartamento');
const buffer2 = Buffer.from('villa');
    
const array = [buffer2, buffer1];
    
const result = Buffer.compare(buffer1, buffer2);  // -1

array.sort(Buffer.compare)
/*
* [ 
*    <Buffer 61 70 70 61 72 74 61 6d 65 6e 74 6f>, 
*    <Buffer 76 69 6c 6c 61> 
*  ]
*/

Vediamo alla fine il metodo buffer.slice([start[, end]]) che restituisce un nuovo oggetto di tipo buffer il quale però punta alla stessa locazione di memoria dell’originale. Bisogna quindi prestare attenzione perché eventuali modifiche fatte su un oggetto, si riflettono anche sull’altro.

const buffer1 = Buffer.from('esempio');
const buffer2 = buffer1.slice(0, 2); // es

buffer1[0] = 69;
console.log(buffer1.toString()) // Esempio
console.log(buffer2.toString()) // Es

buffer2[1] = 's'.toUpperCase().charCodeAt(0)

console.log(buffer1.toString()) // ESempio
console.log(buffer2.toString()) // ES

Conclusioni

Abbiamo visto alcuni esempi in cui abbiamo usato la ‘classe’ Buffer la quale offre un numero elevato di metodi che è impossibile trattare nel poco spazio che ho a disposizione. Vi consiglio pertanto di leggere la documentazione ufficile. Nella prossima lezione inizieremo a lavorare con file e directory usando il modulo nativo fs.

Pubblicitร