Usando búferes en Node.js

El autor seleccionó el Fondo de Ayuda COVID-19 para recibir una donación como parte del programa Escribir para Donaciones.

Introducción

A buffer is a space in memory (typically RAM) that stores binary data. In Node.js, we can access these spaces of memory with the built-in Buffer class. Buffers store a sequence of integers, similar to an array in JavaScript. Unlike arrays, you cannot change the size of a buffer once it is created.

Es posible que hayas utilizado búferes implícitamente si ya has escrito código Node.js. Por ejemplo, cuando lees desde un archivo con fs.readFile(), los datos devueltos al callback o Promise son un objeto de búfer . Además, cuando se realizan solicitudes HTTP en Node.js, devuelven flujos de datos que se almacenan temporalmente en un búfer interno cuando el cliente no puede procesar el flujo de datos de una sola vez.

Los búferes son útiles cuando interactúas con datos binarios, generalmente en niveles de red más bajos. También te equipan con la capacidad de hacer manipulación de datos finamente detallada en Node.js.

En este tutorial, utilizarás el Node.js REPL para ejecutar varios ejemplos de búferes, como la creación de búferes, la lectura desde búferes, la escritura en búferes, la copia desde búferes y el uso de búferes para convertir entre datos binarios y codificados. Al final del tutorial, habrás aprendido cómo usar la clase Buffer para trabajar con datos binarios.

Prerrequisitos

Paso 1 — Crear un Buffer

Este primer paso te mostrará las dos formas principales de crear un objeto buffer en Node.js.

Para decidir qué método usar, necesitas responder a esta pregunta: ¿Quieres crear un nuevo buffer o extraer un buffer de datos existentes? Si vas a almacenar datos en la memoria que aún no has recibido, querrás crear un nuevo buffer. En Node.js usamos la función alloc() de la clase Buffer para hacer esto.

Veamos por nosotros mismos abriendo el REPL de Node.js. En tu terminal, ingresa el comando node:

  1. node

Verás que el prompt comienza con >.

La función alloc() toma el tamaño del buffer como su primer y único argumento requerido. El tamaño es un número entero que representa cuántos bytes de memoria usará el objeto buffer. Por ejemplo, si quisiéramos crear un buffer que fuera de 1KB (kilobyte), equivalente a 1024 bytes, lo ingresaríamos en la consola:

  1. const firstBuf = Buffer.alloc(1024);

Para crear un nuevo búfer, utilizamos la clase globalmente disponible Buffer, que tiene el método alloc(). Al proporcionar 1024 como argumento para alloc(), creamos un búfer que tiene un tamaño de 1 KB.

De forma predeterminada, al inicializar un búfer con alloc(), el búfer se llena con ceros binarios como marcador de posición para datos posteriores. Sin embargo, podemos cambiar el valor predeterminado si así lo deseamos. Si quisiéramos crear un nuevo búfer con 1s en lugar de 0s, estableceríamos el segundo parámetro de la función alloc(), que es fill.

En tu terminal, crea un nuevo búfer en el prompt del REPL que esté lleno de 1s:

  1. const filledBuf = Buffer.alloc(1024, 1);

Acabamos de crear un nuevo objeto de búfer que hace referencia a un espacio en memoria que almacena 1 KB de 1s. Aunque ingresamos un número entero, todos los datos almacenados en un búfer son datos binarios.

Los datos binarios pueden tener muchos formatos diferentes. Por ejemplo, consideremos una secuencia binaria que representa un byte de datos: 01110110. Si esta secuencia binaria representara una cadena en inglés utilizando el estándar de codificación ASCII, sería la letra v. Sin embargo, si nuestra computadora estuviera procesando una imagen, esa secuencia binaria podría contener información sobre el color de un píxel.

La computadora sabe procesarlos de manera diferente porque los bytes están codificados de manera diferente. La codificación de bytes es el formato del byte. Un búfer en Node.js utiliza el esquema de codificación UTF-8 de forma predeterminada si se inicializa con datos de cadena. Un byte en UTF-8 representa un número, una letra (en inglés y en otros idiomas) o un símbolo. UTF-8 es un superconjunto de ASCII, el Código Estándar Americano para el Intercambio de Información. ASCII puede codificar bytes con letras mayúsculas y minúsculas del inglés, los números del 0 al 9 y algunos otros símbolos como el signo de exclamación (!) o el signo de ampersand (&).

Si estuviéramos escribiendo un programa que solo pudiera trabajar con caracteres ASCII, podríamos cambiar la codificación utilizada por nuestro búfer con el tercer argumento de la función alloc()encoding.

Creemos un nuevo búfer que tenga cinco bytes de longitud y almacene solo caracteres ASCII:

  1. const asciiBuf = Buffer.alloc(5, 'a', 'ascii');

El búfer se inicializa con cinco bytes del carácter a, usando la representación ASCII.

Nota: De forma predeterminada, Node.js admite las siguientes codificaciones de caracteres:

  • ASCII, representado como ascii
  • UTF-8, representado como utf-8 o utf8
  • UTF-16, representado como utf-16le o utf16le
  • UCS-2, representado como ucs-2 o ucs2
  • Base64, representado como base64
  • Hexadecimal, representado como hex
  • ISO/IEC 8859-1, representado como latin1 o binary

Todos estos valores se pueden utilizar en funciones de la clase Buffer que aceptan un parámetro encoding. Por lo tanto, todos estos valores son válidos para el método alloc().

Hasta ahora hemos estado creando nuevos buffers con la función alloc(). Pero a veces podemos querer crear un buffer a partir de datos que ya existen, como una cadena o un array.

Para crear un buffer a partir de datos preexistentes, usamos el método from(). Podemos usar esa función para crear buffers a partir de:

  • Un array de enteros: Los valores enteros pueden estar entre 0 y 255.
  • Un ArrayBuffer: Este es un objeto de JavaScript que almacena una longitud fija de bytes.
  • A string.
  • Otro buffer.
  • Otros objetos de JavaScript que tienen una propiedad Symbol.toPrimitive. Esa propiedad le dice a JavaScript cómo convertir el objeto a un tipo de dato primitivo: boolean, null, undefined, number, string o symbol. Puedes leer más sobre Símbolos en la documentación de JavaScript de Mozilla.

Vamos a ver cómo podemos crear un búfer a partir de una cadena. En el prompt de Node.js, ingresa esto:

  1. const stringBuf = Buffer.from('My name is Paul');

Ahora tenemos un objeto búfer creado a partir de la cadena My name is Paul. Creemos un nuevo búfer a partir de otro búfer que hicimos anteriormente:

  1. const asciiCopy = Buffer.from(asciiBuf);

Ahora hemos creado un nuevo búfer asciiCopy que contiene los mismos datos que asciiBuf.

Ahora que hemos experimentado en la creación de búferes, podemos sumergirnos en ejemplos de cómo leer sus datos.

Paso 2 — Lectura desde un Búfer

Hay muchas formas de acceder a los datos en un búfer. Podemos acceder a un byte individual en un búfer o podemos extraer todo el contenido.

Para acceder a un byte de un búfer, pasamos el índice o la ubicación del byte que queremos. Los búferes almacenan datos secuencialmente como los arrays. También indexan sus datos como los arrays, comenzando en 0. Podemos usar la notación de array en el objeto búfer para obtener un byte individual.

Veamos cómo se ve esto creando un búfer a partir de una cadena en el REPL:

  1. const hiBuf = Buffer.from('Hi!');

Ahora leamos el primer byte del búfer:

  1. hiBuf[0];

Al presionar ENTER, el REPL mostrará:

Output
72

El entero 72 corresponde a la representación UTF-8 de la letra H.

Nota: Los valores para los bytes pueden ser números entre 0 y 255. Un byte es una secuencia de 8 bits. Un bit es binario, y por lo tanto solo puede tener uno de dos valores: 0 o 1. Si tenemos una secuencia de 8 bits y dos posibles valores por bit, entonces tenemos un máximo de 2⁸ posibles valores para un byte. Eso significa un máximo de 256 valores. Como empezamos a contar desde cero, eso significa que nuestro número más alto es 255.

Hagamos lo mismo para el segundo byte. Ingresa lo siguiente en el REPL:

  1. hiBuf[1];

El REPL devuelve 105, que representa la minúscula i.

Finalmente, obtengamos el tercer carácter:

  1. hiBuf[2];

Verás 33 mostrado en el REPL, lo que corresponde a !.

Intentemos recuperar un byte de un índice no válido:

  1. hiBuf[3];

El REPL devolverá:

Output
undefined

Esto es como si intentáramos acceder a un elemento en una matriz con un índice incorrecto.

Ahora que hemos visto cómo leer bytes individuales de un búfer, veamos nuestras opciones para recuperar todos los datos almacenados en un búfer a la vez. El objeto de búfer viene con los métodos toString() y toJSON(), que devuelven el contenido completo de un búfer en dos formatos diferentes.

Como su nombre indica, el método toString() convierte los bytes del búfer en una cadena y la devuelve al usuario. Si usamos este método en hiBuf, obtendremos la cadena ¡Hola!. ¡Vamos a intentarlo!

En el indicador, ingrese:

  1. hiBuf.toString();

El REPL devolverá:

Output
'Hi!'

Ese búfer fue creado a partir de una cadena. Veamos qué sucede si usamos el toString() en un búfer que no se creó a partir de datos de cadena.

Creemos un nuevo búfer vacío que tenga 10 bytes de tamaño:

  1. const tenZeroes = Buffer.alloc(10);

Ahora, usemos el método toString():

  1. tenZeroes.toString();

Veremos el siguiente resultado:

'\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000'

La cadena \u0000 es el carácter Unicode para NULO. Corresponde al número 0. Cuando los datos del búfer no están codificados como una cadena, el método toString() devuelve la codificación UTF-8 de los bytes.

El toString() tiene un parámetro opcional, encoding. Podemos usar este parámetro para cambiar la codificación de los datos del búfer que se devuelve.

Por ejemplo, si quisieras la codificación hexadecimal para hiBuf, ingresarías lo siguiente en el indicador:

  1. hiBuf.toString('hex');

Esa declaración se evaluará como:

Output
'486921'

486921 es la representación hexadecimal de los bytes que representan la cadena ¡Hola!. En Node.js, cuando los usuarios desean convertir la codificación de datos de una forma a otra, generalmente colocan la cadena en un búfer y llaman a toString() con la codificación deseada.

El método toJSON() se comporta de manera diferente. Independientemente de si el búfer se creó a partir de una cadena o no, siempre devuelve los datos como la representación entera del byte.

Volvamos a utilizar los búferes hiBuf y tenZeroes para practicar el uso de toJSON(). En el indicador, ingresa:

  1. hiBuf.toJSON();

La REPL devolverá:

Output
{ type: 'Buffer', data: [ 72, 105, 33 ] }

El objeto JSON tiene una propiedad type que siempre será Buffer. Esto permite que los programas distingan estos objetos JSON de otros objetos JSON.

La propiedad data contiene una matriz de la representación entera de los bytes. Puede haber notado que 72, 105 y 33 corresponden a los valores que recibimos cuando extraímos individualmente los bytes.

Intentemos el método toJSON() con tenZeroes:

  1. tenZeroes.toJSON();

En la REPL verás lo siguiente:

Output
{ type: 'Buffer', data: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] }

El type es el mismo que se mencionó antes. Sin embargo, los datos ahora son una matriz con diez ceros.

Ahora que hemos cubierto las principales formas de leer desde un búfer, veamos cómo modificamos el contenido de un búfer.

Paso 3 — Modificando un Búfer

Hay muchas formas en que podemos modificar un objeto de búfer existente. Similar a la lectura, podemos modificar los bytes del búfer individualmente usando la sintaxis de matriz. También podemos escribir nuevos contenidos en un búfer, reemplazando los datos existentes.

Comencemos por ver cómo podemos cambiar bytes individuales de un búfer. Recordemos nuestra variable de búfer hiBuf, que contiene la cadena ¡Hola!. Vamos a cambiar cada byte para que contenga Hola en su lugar.

En el REPL, primero intentemos establecer el segundo elemento de hiBuf como e:

  1. hiBuf[1] = 'e';

Ahora, veamos este búfer como una cadena para confirmar que está almacenando los datos correctos. Continuemos llamando al método toString():

  1. hiBuf.toString();

Se evaluará como:

Output
'H\u0000!'

Recibimos esa extraña salida porque el búfer solo puede aceptar un valor entero. No podemos asignarle la letra e; más bien, debemos asignarle el número cuyo equivalente binario representa a e:

  1. hiBuf[1] = 101;

Ahora, cuando llamemos al método toString():

  1. hiBuf.toString();

Obtendremos esta salida en el REPL:

Output
'He!'

Para cambiar el último carácter en el búfer, necesitamos establecer el tercer elemento en el entero que corresponde al byte para y:

  1. hiBuf[2] = 121;

Confirmemos usando nuevamente el método toString():

  1. hiBuf.toString();

Tu REPL mostrará:

Output
'Hey'

Si intentamos escribir un byte que esté fuera del rango del búfer, será ignorado y el contenido del búfer no cambiará. Por ejemplo, intentemos establecer el cuarto elemento inexistente del búfer como o:

  1. hiBuf[3] = 111;

Podemos confirmar que el búfer no ha cambiado con el método toString():

  1. hiBuf.toString();

La salida sigue siendo:

Output
'Hey'

Si queremos cambiar el contenido de todo el búfer, podemos usar el método write(). El método write() acepta una cadena que reemplazará el contenido de un búfer.

Usaremos el método write() para cambiar el contenido de hiBuf de vuelta a ¡Hola!. En tu shell de Node.js, escribe el siguiente comando en el prompt:

  1. hiBuf.write('Hi!');

El método write() devolvió 3 en el REPL. Esto se debe a que escribió tres bytes de datos. Cada letra tiene un tamaño de un byte, ya que este búfer utiliza codificación UTF-8, que utiliza un byte para cada carácter. Si el búfer utilizara codificación UTF-16, que tiene un mínimo de dos bytes por carácter, entonces la función write() habría devuelto 6.

Ahora verifica el contenido del búfer usando toString():

  1. hiBuf.toString();

El REPL producirá:

Output
'Hi!'

Esto es más rápido que tener que cambiar cada elemento byte por byte.

Si intentas escribir más bytes de los que tiene un búfer, el objeto de búfer solo aceptará los bytes que quepan. Para ilustrar, creemos un búfer que almacene tres bytes:

  1. const petBuf = Buffer.alloc(3);

Ahora intentemos escribir Gatos en él:

  1. petBuf.write('Cats');

Cuando se evalúa la llamada a write(), el REPL devuelve 3, lo que indica que solo se escribieron tres bytes en el búfer. Ahora confirma que el búfer contiene los primeros tres bytes:

  1. petBuf.toString();

El REPL devuelve:

Output
'Cat'

La función write() agrega los bytes en orden secuencial, por lo que solo se colocaron los primeros tres bytes en el búfer.

Por el contrario, vamos a crear un Buffer que almacene cuatro bytes:

  1. const petBuf2 = Buffer.alloc(4);

Escriba el mismo contenido en él:

  1. petBuf2.write('Cats');

Luego agregue un nuevo contenido que ocupe menos espacio que el contenido original:

  1. petBuf2.write('Hi');

Dado que los búferes escriben de manera secuencial, comenzando desde 0, si imprimimos el contenido del búfer:

  1. petBuf2.toString();

Nos encontraríamos con:

Output
'Hits'

Los dos primeros caracteres son sobrescritos, pero el resto del búfer permanece intacto.

A veces, los datos que queremos en nuestro búfer preexistente no están en una cadena, sino que residen en otro objeto de búfer. En estos casos, podemos usar la función copy() para modificar lo que nuestro búfer está almacenando.

Creemos dos nuevos búferes:

  1. const wordsBuf = Buffer.from('Banana Nananana');
  2. const catchphraseBuf = Buffer.from('Not sure Turtle!');

Los búferes wordsBuf y catchphraseBuf contienen datos de cadena. Queremos modificar catchphraseBuf para que almacene Nananana Turtle! en lugar de Not sure Turtle!. Usaremos copy() para obtener Nananana de wordsBuf a catchphraseBuf.

Para copiar datos de un búfer a otro, usaremos el método copy() en el búfer que es la fuente de la información. Por lo tanto, como wordsBuf tiene los datos de cadena que queremos copiar, necesitamos copiar de esta manera:

  1. wordsBuf.copy(catchphraseBuf);

El parámetro target en este caso es el búfer catchphraseBuf.

Cuando ingresamos eso en el REPL, devuelve 15 indicando que se escribieron 15 bytes. La cadena Nananana solo usa 8 bytes de datos, por lo que inmediatamente sabemos que nuestra copia no se realizó como se pretendía. Utilice el método toString() para ver el contenido de catchphraseBuf:

  1. catchphraseBuf.toString();

El REPL devuelve:

Output
'Banana Nananana!'

De forma predeterminada, copy() tomó todo el contenido de wordsBuf y lo colocó en catchphraseBuf. Necesitamos ser más selectivos para nuestro objetivo y solo copiar Nananana. Reescribamos el contenido original de catchphraseBuf antes de continuar:

  1. catchphraseBuf.write('Not sure Turtle!');

La función copy() tiene algunos parámetros más que nos permiten personalizar qué datos se copian en el otro búfer. Aquí hay una lista de todos los parámetros de esta función:

  • target – Este es el único parámetro requerido de copy(). Como hemos visto en nuestro uso anterior, es el búfer al que queremos copiar.
  • targetStart – Este es el índice de los bytes en el búfer de destino donde deberíamos comenzar a copiar. De forma predeterminada, es 0, lo que significa que copia datos comenzando desde el principio del búfer.
  • sourceStart – Este es el índice de los bytes en el búfer de origen donde deberíamos copiar desde.
  • sourceEnd – Este es el índice de los bytes en el búfer de origen donde deberíamos dejar de copiar. De forma predeterminada, es la longitud del búfer.

Entonces, para copiar Nananana desde wordsBuf hacia catchphraseBuf, nuestro objetivo debería ser catchphraseBuf como antes. El targetStart sería 0 ya que queremos que Nananana aparezca al principio de catchphraseBuf. El sourceStart debería ser 7 ya que ese es el índice donde comienza Nananana en wordsBuf. El sourceEnd seguiría siendo la longitud de los búferes.

En el indicador de REPL, copie el contenido de wordsBuf de esta manera:

  1. wordsBuf.copy(catchphraseBuf, 0, 7, wordsBuf.length);

El REPL confirma que se han escrito 8 bytes. Note cómo se utiliza wordsBuf.length como el valor para el parámetro sourceEnd. Al igual que en los arreglos, la propiedad length nos da el tamaño del búfer.

Ahora veamos el contenido de catchphraseBuf:

  1. catchphraseBuf.toString();

El REPL devuelve:

Output
'Nananana Turtle!'

¡Éxito! Pudimos modificar los datos de catchphraseBuf copiando el contenido de wordsBuf.

Puedes salir del REPL de Node.js si así lo deseas. Ten en cuenta que todas las variables que se crearon ya no estarán disponibles cuando lo hagas:

  1. .exit

Conclusión

En este tutorial, aprendiste que los buffers son asignaciones de longitud fija en la memoria que almacenan datos binarios. Primero creaste buffers definiendo su tamaño en la memoria e inicializándolos con datos preexistentes. Luego, leíste datos de un buffer examinando sus bytes individuales y usando los métodos toString() y toJSON(). Finalmente, modificaste los datos almacenados por un buffer cambiando sus bytes individuales y usando los métodos write() y copy().

Los buffers te brindan una gran comprensión de cómo se manipulan los datos binarios en Node.js. Ahora que puedes interactuar con los buffers, puedes observar las diferentes formas en que la codificación de caracteres afecta cómo se almacenan los datos. Por ejemplo, puedes crear buffers a partir de datos de cadena que no estén codificados en UTF-8 o ASCII y observar la diferencia en tamaño. También puedes tomar un buffer con UTF-8 y usar toString() para convertirlo a otros esquemas de codificación.

Para aprender sobre buffers en Node.js, puedes leer la documentación de Node.js sobre el objeto Buffer. Si deseas continuar aprendiendo Node.js, puedes regresar a la serie Cómo Programar en Node.js, o explorar proyectos de programación y configuraciones en nuestra página de temas de Node.

Source:
https://www.digitalocean.com/community/tutorials/using-buffers-in-node-js