El autor seleccionó el COVID-19 Relief Fund para que reciba una donación como parte del programa Write for DOnations.

Introducción

Un búfer es un espacio en la memoria (en general, RAM) que almacena datos binarios. En Node.js, podemos acceder a estos espacios de memoria con la clase Buffer incorporada. Los búferes almacenan una secuencia de enteros, de forma similar a una matriz en JavaScript. A diferencia de las matrices, una vez que se crea un búfer, no se puede cambiar su tamaño.

Es posible que haya utilizado búferes de manera implícita si ya escribió código de Node.js. Por ejemplo, cuando lee de un archivo con fs.readFile(), los datos que se devuelven a la devolución de llamada o promesa 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 procesarlo todo a la vez.

Los búferes son útiles cuando se interactúa con datos binarios, habitualmente, en niveles de red inferiores. También le proporcionan la capacidad de manipular datos específicos en Node.js.

En este tutorial, utilizará el REPL de Node.js para analizar varios ejemplos con búferes, como la creación, la lectura, la escritura y la copia de búferes, así como su utilización para convertir datos binarios y codificados. Al final del tutorial, habrá aprendido a usar la clase Buffer para trabajar con datos binarios.

Requisitos previos

Paso 1: Crear un búfer

En este primer paso, se mostrarán las dos formas principales de crear un objeto de búfer en Node.js.

Para decidir qué método usar, hágase esta pregunta: ¿quiere crear un búfer nuevo o extraer un búfer de datos existentes? Si va a almacenar datos que aún no recibió en la memoria, es conveniente crear un búfer nuevo. En Node.js, usamos la función alloc() de la clase Buffer para hacerlo.

Vamos a abrir el REPL de Node.js para verlo por nosotros mismos. En su terminal, ingrese el comando node:

  • node

Verá que la línea de comandos comienza con >.

La función alloc() toma el tamaño del búfer como su primer y único argumento necesario. El tamaño es un entero que representa la cantidad de bytes de memoria que utilizará el objeto de búfer. Por ejemplo, si quisiéramos crear un búfer de 1 KB (kilobyte), lo que equivale a 1024 bytes, ingresaríamos esto en la consola:

  • const firstBuf = Buffer.alloc(1024);

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

Por defecto, cuando inicializa un búfer con alloc(), este se completa con ceros binarios como un marcador de posición para datos posteriores. Sin embargo, puede cambiar el valor predeterminado si lo desea. Si quisiéramos crear un búfer nuevo con números 1 en lugar de 0, estableceríamos el segundo parámetro de la función alloc(): fill.

En su terminal, cree un búfer nuevo en la línea de comandos de REPL con números 1:

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

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

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

La computadora sabe que debe procesarlos de forma distinta, dado que los bytes se codifican de un modo diferente. La codificación de los bytes es su formato. En Node.js, los búferes utilizan el esquema de codificación UTF-8 por defecto si se inicializan con datos de cadena. En UTF-8, un byte 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 Estadounidense Estándar para el Intercambio de Información (American Standard Code for Information Interchange). ASCII puede codificar bytes con mayúsculas y minúsculas en letras en inglés, los números 0 a 9 y algunos símbolos, como el de exclamación (!) o el de “y” comercial (&).

Si escribiéramos un programa que solo pudiera funcionar con caracteres ASCII, podríamos cambiar la codificación que utiliza nuestro búfer con el tercer argumento de la función alloc(): encoding.

Vamos a crear un búfer nuevo de cinco bytes de longitud que almacene únicamente caracteres ASCII:

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

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

Nota: De manera 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 en hex
  • ISO/IEC 8859-1, representado como latin1 o binary

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

Hasta ahora, creamos búferes nuevos con la función alloc(). Pero, a veces, podemos querer crear un búfer a partir de datos existentes, como una cadena o una matriz.

Para crear un búfer a partir de datos preexistentes, usaremos el método from(). Podemos usar esa función para crear búferes a partir de lo siguiente:

  • Una matriz de enteros: los valores enteros pueden ser entre 0 y 255.
  • Un ArrayBuffer: un objeto de JavaScript que almacena una longitud fija de bytes.
  • Una cadena.
  • Otro búfer.
  • Otros objetos de JavaScript que tengan una propiedad Symbol.toPrimitive. Esa propiedad le indica a JavaScript cómo convertir el objeto en un tipo de datos primitivo: boolean, null, undefined, number, string o symbol. Puede obtener más información sobre los símbolos en la documentación de JavaScript de Mozilla.

Veamos cómo podemos crear un búfer a partir de una cadena. En la línea de comandos de Node.js, ingrese lo siguiente:

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

Ahora, tenemos un objeto de búfer creado a partir de la cadena My name is Paul. Vamos a crear un búfer nuevo a partir de otro búfer que creamos anteriormente:

  • const asciiCopy = Buffer.from(asciiBuf);

Creamos un búfer nuevo asciiCopy que contiene los mismos datos que asciiBuf.

Ahora que practicamos la creación de búferes, podemos incursionar en la lectura de sus datos.

Paso 2: Leer de un búfer

Hay muchas formas de acceder a los datos de un búfer. Podemos acceder a un byte individual de un búfer o podemos extraer todo su 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 de forma secuencial como las matrices. También indexan sus datos al igual que las matrices, a partir de 0. Podemos usar la notación de las matrices en el objeto de búfer para acceder a un byte individual.

Veamos un ejemplo al crear un búfer a partir de una cadena en el REPL:

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

Ahora, leamos el primer byte del búfer:

  • hiBuf[0];

Al pulsar INTRO, el REPL mostrará lo siguiente:

Output
72

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

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

Vamos a hacer lo mismo con el segundo byte. Ingrese lo siguiente en el REPL:

  • hiBuf[1];

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

Por último, vamos a obtener el tercer carácter:

  • hiBuf[2];

Verá el número 33 en el REPL, que corresponde a !.

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

  • hiBuf[3];

El REPL devolverá lo siguiente:

Output
undefined

Sucede lo mismo que si intentáramos acceder a un elemento de una matriz con un índice incorrecto.

Ahora que vimos cómo se leen los bytes individuales de un búfer, veremos distintas opciones para obtener todos los datos almacenados en un búfer de una vez. El objeto de búfer viene con los métodos toString() y toJSON(), que devuelven todo el contenido de un búfer en dos formatos diferentes.

Como lo indica en su nombre, el método toString() convierte los bytes de un búfer en una cadena y la devuelve al usuario. Si usamos este método en hiBuf, obtendremos la cadena Hi!. Vamos a probarlo.

En la línea de comandos, ingrese lo que se indica a continuación:

  • hiBuf.toString();

El REPL devolverá lo siguiente:

Output
'Hi!'

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

Vamos a crear un búfer nuevo, vacío, de 10 bytes:

  • const tenZeroes = Buffer.alloc(10);

Ahora, usaremos el método toString():

  • tenZeroes.toString();

Veremos el siguiente resultado:

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

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

El método 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 devuelven.

Por ejemplo, si quisiéramos obtener la codificación hexadecimal de hiBuf, ingresaríamos lo siguiente en la línea de comandos:

  • hiBuf.toString('hex');

Esa instrucción da el siguiente resultado:

Output
'486921'

486921 es la representación hexadecimal de los bytes que representan la cadena Hi! En Node.js, cuando los usuarios quieren convertir la codificación de datos de un formato a otro, suelen poner la cadena en un búfer e invocar toString() con su codificación deseada.

El método toJSON() se comporta de un modo diferente. Independientemente de que el búfer se haya creado a partir de una cadena o no, siempre devuelve los datos como la representación en números enteros del byte.

Volvamos a utilizar los búferes hiBuf y tenZeroes para practicar el uso de toJSON(). En la línea de comandos, ingrese lo siguiente:

  • hiBuf.toJSON();

El REPL mostrará lo siguiente:

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

Este objeto JSON tiene una propiedad type que siempre será Buffer para que los programas puedan distinguirlo de otros objetos JSON.

La propiedad data contiene una matriz de la representación en enteros de los bytes. Debe haber observado que 72, 105, y 33 corresponden a los valores que recibimos cuando obtuvimos los bytes individualmente.

Probemos el método toJSON() con tenZeroes:

  • tenZeroes.toJSON();

En el REPL, verá lo siguiente:

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

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

Ahora que cubrimos las principales formas de leer desde un búfer, vamos a ver cómo se puede modificar su contenido.

Paso 3: Modificar un búfer

Un objeto de búfer existente se puede modificar de diversas maneras. De forma similar a la lectura, podemos modificar los bytes de un búfer individualmente con la sintaxis de matriz. También podemos escribir contenido nuevo en un búfer, lo que sustituye los datos existentes.

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

En el REPL, primero, vamos fijar el segundo elemento de hiBuf en e:

  • hiBuf[1] = 'e';

Ahora, vamos a ver este búfer como una cadena para confirmar que esté almacenando los datos correctos. A continuación, invoque el método toString():

  • hiBuf.toString();

Se evaluará de la siguiente manera:

Output
'H\u0000!'

Obtuvimos ese resultado extraño porque el búfer solo puede aceptar valores enteros. No podemos asignarle la letra e; en su lugar, debemos asignarle el número cuyo equivalente binario representa e:

  • hiBuf[1] = 101;

Ahora, al invocar el método toString() de la siguiente manera:

  • hiBuf.toString();

Obtenemos este resultado en el REPL:

Output
'He!'

Para cambiar el último carácter del búfer, debemos fijar el tercer elemento en el entero que corresponde al byte de y:

  • hiBuf[2] = 121;

Vamos a confirmar utilizando el método toString() una vez más:

  • hiBuf.toString();

Su REPL mostrará lo siguiente:

Output
'Hey'

Si intentamos escribir un byte que está fuera del alcance del búfer, se ignorará y el contenido del búfer no cambiará. Por ejemplo, vamos a intentar fijar el cuarto elemento del búfer en o:

  • hiBuf[3] = 111;

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

  • hiBuf.toString();

El resultado sigue siendo el siguiente:

Output
'Hey'

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

Vamos a utilizar el método write() para volver a cambiar el contenido de hiBuf a Hi!. En su shell de Node.js, escriba lo siguiente en la línea de comandos:

  • 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, dado que este búfer utiliza la codificación UTF8, que usa un byte para cada carácter. Si el búfer utilizara la codificación UTF-16, que tiene un mínimo de dos bytes por carácter, la función write() hubiera devuelto 6.

Ahora, verifique el contenido del búfer utilizando toString():

  • hiBuf.toString();

El REPL producirá lo siguiente:

Output
'Hi!'

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

Si intenta escribir más bytes que los que admite el tamaño de un búfer, el objeto de búfer aceptará únicamente los que pueda admitir. Para ilustrar esto, vamos a crear un búfer que almacene tres bytes:

  • const petBuf = Buffer.alloc(3);

Ahora, vamos a intentar escribir Cats en él:

  • petBuf.write('Cats');

Cuando se evalúa la invocación de write(), el REPL devuelve 3, lo que indica que solo tres bytes se escribieron en el búfer. Ahora, confirme que el búfer contenga los primeros tres bytes:

  • petBuf.toString();

El REPL devuelve lo siguiente:

Output
'Cat'

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

Ahora, en cambio, vamos a crear un búfer que almacene cuatro bytes:

  • const petBuf2 = Buffer.alloc(4);

Escriba el mismo contenido en él:

  • petBuf2.write('Cats');

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

  • petBuf2.write('Hi');

Como los búferes escriben de forma secuencial, a partir de 0, si imprimimos el siguiente contenido del búfer:

  • petBuf2.toString();

Se nos saludará con esto:

Output
'Hits'

Los primeros dos caracteres se sobrescriben, pero el resto del búfer permanece intacto.

A veces, los datos que queremos en nuestro búfer preexistente no se encuentran en una cadena, sino en otro objeto de búfer. En esos casos, podemos utilizar la función copy() para modificar lo que almacena nuestro búfer.

Vamos a crear dos búferes nuevos:

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

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

Para copiar datos de un búfer a otro, utilizaremos el método copy() en el búfer que es la fuente de información. Por lo tanto, dado que wordsBuf contiene los datos de cadena que queremos copiar, debemos copiar de la siguiente manera:

  • wordsBuf.copy(catchphraseBuf);

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

Cuando lo ingresamos en el REPL, devuelve 15, lo que indica que se escribieron 15 bytes. La cadena Nananana solo utiliza 8 bytes de datos, por lo tanto, nos damos cuenta de inmediato que nuestra copia no funcionó de la manera prevista. Utilice el método toString() para ver el contenido de catchphraseBuf:

  • catchphraseBuf.toString();

El REPL devuelve lo siguiente:

Output
'Banana Nananana!'

De manera predeterminada, copy() tomó todo el contenido de wordsBuf y lo colocó en catchphraseBuf. Debemos ser más selectivos para nuestro objetivo y copiar únicamente Nananana. Para continuar, vamos a volver a escribir el contenido original de catchphraseBuf:

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

La función copy() contiene otros parámetros que nos permiten personalizar los datos que se copian al otro búfer. Esta es una lista de todos los parámetros de esta función:

  • target: este es el único parámetro requerido de copy(). Como vimos en nuestro ejemplo de uso anterior, es el búfer al que queremos copiar.
  • targetStart: este es el índice de los bytes del búfer de destino donde debemos comenzar a copiar. De manera predeterminada, es 0, lo que significa que copia datos desde el principio del búfer.
  • sourceStart: este es el índice de los bytes del búfer de origen de donde debemos copiar.
  • sourceEnd: este es el índice de los bytes del búfer de origen donde debemos dejar de copiar. De manera predeterminada, es la longitud del búfer.

Por lo tanto, para copiar Nananana de wordsBuf a catchphraseBuf, nuestro target debe ser catchphraseBuf como antes. El targetStart será 0, dado que queremos que Nananana aparezca al principio de catchphraseBuf. El sourceStart debe ser 7, ya que ese es el índice donde Nananana comienza en wordsBuf. El sourceEnd debe continuar siendo la longitud de los búferes.

En la línea de comandos de REPL, copie el contenido de wordsBuf de la siguiente manera:

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

El REPL confirma que se escribieron 8 bytes. Observe como wordsBuf.length se utiliza como valor del parámetro sourceEnd. Como sucede con las matrices, la propiedad length nos proporciona el tamaño del búfer.

Ahora, vamos a ver el contenido de catchphraseBuf:

  • catchphraseBuf.toString();

El REPL devuelve lo siguiente:

Output
'Nananana Turtle!'

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

Puede salir del REPL de Node.js si así lo desea. Tenga en cuenta que todas las variables que se crearon dejarán de estar disponibles cuando lo haga:

  • .exit

Conclusión

En este tutorial, aprendió que los búferes son asignaciones de longitud fija en la memoria que almacenan datos binarios. Primero, creó búferes definiendo su tamaño en la memoria e iniciándolos con datos preexistentes. Luego, leyó datos de un búfer examinando sus bytes individuales y utilizando los métodos toString() y toJSON(). Por último, modificó los datos almacenados en un búfer cambiando sus bytes individuales y utilizando los métodos write() y copy().

Los búferes le proporcionan excelente información sobre cómo manipula datos binarios Node.js. Ahora que puede interactuar con búferes, puede observar las distintas maneras en que la codificación de caracteres afecta cómo se almacenan los datos. Por ejemplo, puede crear búferes a partir de datos de cadena que no tengan codificación UTF-8 o ASCII y observar sus diferencias de tamaño. También puede tomar un búfer con UTF-8 y utilizar toString() para convertirlo a otros esquemas de codificación.

Para obtener más información sobre los búferes en Node.js, puede leer la documentación de Node.js sobre el objeto Buffer. Si desea continuar aprendiendo sobre Node.js, puede regresar a la serie Cómo programar en Node.js o consultar proyectos de programación y configuraciones en nuestra página temática de Node.

0 Comments

Creative Commons License