DEV Community

Cover image for Introducción a Buffer en JavaScript
jorgeabad
jorgeabad

Posted on

Introducción a Buffer en JavaScript

Datos binarios.

Para poder tratar los flujos TCP, leer y escribir en el sistema de
archivos, es necesario tratar con flujos de datos que son puramente
binarios. Los datos, como regla general, se presentan internamente como
octetos, (8 bits). Un octeto o byte es una pequeña unidad de datos con
un valor numérico entre 0 y 255 (256 valores). Los valores numéricos se
pueden representar en la notación normal (decimal), pero también se
pueden usar otras presentaciones, como la octal (base 8) o la
hexadecimal (base 16).

Búfer. ArrayBuffer en JavaScript.

Un búfer es una región de almacenamiento ubicada en la memoria física
que es utilizada para almacenar temporalmente una cierta cantidad de
datos, octetos, mientras se mueven de un lugar a otro. En Node.js esta
memoria intermedia corresponde a memoria bruta asignada fuera del montón
(“heap”) de V8.

En JavaScript para representar un búfer genérico se usa el objeto
“ArrayBuffer”, que en definitiva es un área de memoria reservada donde
se almacenará un conjunto datos binarios. Esta área es de tamaño fijo y
no puede incrementarse ni reducirse dinámicamente. No se puede trabajar
directamente con esta área reservada, ya que no existen métodos o
funciones que sean capaces de acceder directamente al “ArrayBuffer”,
para poder trabajar y acceder a esos datos ubicados en memoria, desde
JavaScript puro, es necesario utilizar el objeto “TypedArray” o el
objeto “DataView” que funcionan como una vista de los datos contenidos
en un “ArrayBuffer”.

Estas matrices tipadas, son similares a un “array” de enteros que no
puede redimensionarse. El “tipo” se refiere a una “vista” concreta de
ese búfer como pueden ser: números enteros sin signo de 8 bits, números
enteros de 32 bits, coma flotante de 64 bits, etc.

  • “Int8Array”: entero de 8-bit con signo
  • “Uint8Array”: entero de 8-bit sin signo
  • “Uint8ClampedArray”: entero de 8-bit sin signo restringido a valores entre 0 y 255
  • “Int16Array”: 2 enteros de 16-bit con signo
  • “Uint16Array”: 2 enteros de 16-bit sin signo
  • “Int32Array”: 4 enteros de 32-bit con signo
  • “Uint32Array”: 4 enteros de 32-bit sin signo
  • “Float32Array”: 4 coma flotante de 32-bit
  • “Float64Array”: 8 coma flotante de 64-bit

Sobre un mismo “ArrayBuffer” podemos establecer una o más vistas de
matrices tipadas. Cada una de las matrices trabajará sobre el mismo
búfer, y cada una de ellas tratará los datos según el tipo de datos que
tienen definidos. Hay que tener en cuenta, cuando se almacena más de un
byte, si el entorno es BE (Big-endian) o LE (Little-endian). LE
considera el byte menos significativo en primera posición, en BE el byte
más significativo se almacena en primera posición.

En el siguiente ejemplo creamos un “ArrayBuffer” de 2 bytes y creamos
dos matrices con tipo sobre el mismo búfer de datos, una de enteros de 8
bits sin signo y otra de enteros de 16 bits sin signo.

Codigo disponible en el archivo buffer1.js GitHub

Usamos la vista de un entero 8 bits, para asignar los valores al primer
y segundo byte. En el primer octeto guardamos el valor 255 en binario
‘11111111’ en el segundo byte guardamos el valor 128 en binario
‘10000000’.

Cuando se visualiza el valor contenido en el “ArrayBuffer” desde la
vista de 16 bits, en un entorno LE, considera que el primer byte es el
menos significativo y el segundo byte el más significativo por lo el
valor obtenido es el siguiente 1000000011111111 que es 33023 en decimal.
En un entorno BE el valor decimal sería 65408 en binario
1111111110000000.

const memoria = new ArrayBuffer(2);
const vista8 = new Uint8Array(memoria);
const vista16 = new Uint16Array(memoria);
vista8[0] = 255; //11111111
vista8[1] = 128; //10000000
console.log(vista8); //Uint8Array [ 255, 128 ]
//LE byte menos significativo 1º
console.log(vista16, vista16[0].toString(2)); //Uint16Array [ 33023 ] '1000000011111111'
Enter fullscreen mode Exit fullscreen mode

Si modificamos el valor desde la vista de 16 evidentemente también
cambia los valores para la vista de 8. Si asignamos el valor 65408 en
binario 1111111110000000 se puede observar cómo ha cambiado la vista de
8, al ser un entorno LE el byte menos significativo “10000000” (128
decimal) se coloca en primera posición el más significativo en segunda
posición “11111111” (255 en decimal). Si asignamos cualquier otro valor
desde la vista de 16, por ejemplo 1000 pasaría lo mismo.

vista16[0] = 65408; //1111111110000000
console.log(vista16); //Uint16Array [ 65408 ]
console.log(vista8, vista8[0].toString(2),vista8[1].toString(2)); 
//Uint8Array [ 128, 255 ] '10000000' '11111111'

vista16[0] = 1000; //0000001111101000
console.log(vista16); //Uint16Array [ 1000 ]
console.log(vista8, vista8[0].toString(2),vista8[1].toString(2)); 
//Uint8Array [ 232, 3 ] '11101000' '11'
Enter fullscreen mode Exit fullscreen mode

Codificación y juego de caracteres.

Como se ha explicado anteriormente cada octeto de información contiene
un valor numérico. Para poder representar los caracteres, hay que
establecer una correspondencia entre el valor numérico del octeto y un
carácter en función de alguna tabla de mapeo (codificación). Un
repertorio de caracteres especifica una colección de caracteres,
como "a", "!" Y "ä". Los códigos de carácter son códigos numéricos
definidos para los caracteres de un repertorio. Una codificación de
caracteres
define cómo las secuencias de códigos numéricos son
asignadas a las secuencias de octetos.

Codificación de caracteres. Estándares.

ASCII 7 bits

Una de las primeras codificaciones en aparecer fue ASCII (American
Standard Code for Information Interchange). La codificación, ASCII usa 7
bits por lo que dispone de 128 valores posibles que van desde el 0000000
a 1111111, cada número de código se presenta como un octeto con el mismo
valor y representa un carácter. ASCII tiene espacio suficiente para
todas las minúsculas y mayúsculas de las letras latinas y para cada
dígito numérico, signos de puntuación comunes, espacios y otros
caracteres de control. Empieza en el código 32 (asignado al espacio en
blanco) y termina en el 126 (asignado a el carácter tilde ~). Las
posiciones del 0 a 31 y 127 están reservadas para códigos de control. La
mayoría de los códigos de caracteres actualmente en uso contienen ASCII
como su subconjunto en algún sentido, los octetos que contienen valores
del 128 al 255 no se usan en ASCII.

Codificaciones de 8 bits

Mediante 8 bits se puede representar hasta el número 255 (256 valores),
por lo que se puede tener un conjunto de caracteres más amplios, ASCII
solo asigna hasta 127, los otros valores del 128 a 255 son de repuesto,
aprovechando estos valores de repuesto aparecieron diferentes códigos de
caracteres que fueron creados para adaptarse a los diferentes idiomas.
Estos código de caracteres son una extensión del ASCII, los más
importantes son los pertenecientes a la familia ISO 8859 y el juego de
caracteres de Windows. Los códigos ISO 8859 amplían el repertorio ASCII
de diferentes maneras con diferentes caracteres especiales utilizados en
diferentes idiomas y culturas. Las posiciones del código 0 - 127
contienen el mismo carácter que en ASCII, las posiciones 128 - 159 no se
usan (reservadas para los caracteres de control), y las posiciones 160 -
255 son la parte variable, utilizada de manera diferente en diferentes
miembros de la familia ISO 8859. ISO 8859-1 alias “Latín 1” contiene
varios caracteres acentuados y otras letras necesarias para escribir
idiomas de Europa occidental y algunos caracteres especiales. El juego
de caracteres de Windows, es similar a la familia ISO 8859, la principal
diferencia es que algunas de las posiciones del rango 128 a 159 se
asignan a caracteres imprimibles, como las comillas dobles, comillas
simples etc., en el rango de 160-255 se mantienen las mismas
asignaciones de caracteres que en ISO 8859 aunque no siempre, existiendo
así diferentes páginas de código (CP), que difieren del estándar ISO
8859 correspondiente.

ISO 10646, Unicode y UTF-8.

Las anteriores codificaciones de caracteres eran limitadas y no podían
contener suficientes caracteres para abarcar todos los idiomas del
mundo, además entraban en conflicto, pues dos codificaciones podrían
usar el mismo número o código para dos caracteres diferentes, o usar
números diferentes para el mismo carácter. Si se quiere codificar todos
los caracteres del mundo asignando a cada uno un código único no es
suficiente con los 256 valores que se pueden almacenar en un byte, para
su codificación es necesario un código multibyte.

Con este objetivo nacieron paralelamente la norma ISO 10646 y el
estándar Unicode (consorcio Unicode), que definen el Conjunto de
Caracteres Universales, (UCS), que contiene los caracteres necesarios
para representar prácticamente todos los idiomas conocidos, también
cubre una gran cantidad de símbolos gráficos, tipográficos, matemáticos
y científicos. UCS es un superconjunto de todos los demás estándares de
conjunto de caracteres. UCS y Unicode al necesitar codificación
multibyte abarca dos cosas: un conjunto de caracteres y un conjunto de
codificaciones.

  • Un conjunto de caracteres:

Tablas de códigos que asignan números enteros a los caracteres. UCS le asigna a cada carácter un nombre o
“punto de código” que consiste en un número hexadecimal que representa
el valor UCS o Unicode y que suele estar precedido por la cadena "U +".
Los caracteres UCS que van desde “U+0000” a “U+007F” son idénticos a los
de US-ASCII y el rango de “U+0000” a “U+00FF” es idéntico al ISO 8859-1
(Latin-1). Además proporciona códigos para signos diacríticos y permite
que ciertas secuencias de caracteres también se pueden representar como
un solo carácter, llamado carácter pre compuesto (o compuesto, o
carácter descomponible). Por ejemplo, el carácter "ñ" puede codificarse
como el único punto de código U+00F1 "ñ" o como una secuencia compuesta
del carácter base U+006E "n" seguido del carácter no espaciador U+0303
“~”.

Los caracteres más comúnmente utilizados, incluidos todos los que se encuentran en los principales estándares de codificación anteriores, se han colocado en un primer plano (0x0000 a 0xFFFD), 64K (2^16^ ) puntos de código, que se llama plano multilingüe básico (BMP) o Plano 0.

  • Un conjunto de codificaciones:

Para poder codificar todos los
caracteres de todos los idiomas utilizando varios bytes, es decir,
asignar una secuencia de bytes, “valor de código”, a cada carácter o
punto de código, existen varias alternativas.

Las dos codificaciones más obvias almacenan los caracteres Unicode como
una secuencia de bytes de ancho fijo de 2 o 4 bytes. Los términos
oficiales para estas codificaciones son UCS-2 y UCS-4, respectivamente.
A menos que se especifique lo contrario, el byte más significativo es el
primero (Big endian). Un archivo ASCII se puede transformar en un
archivo UCS-2 simplemente insertando un byte “0x00” delante de cada byte
ASCII. Si queremos tener un archivo UCS-4, tenemos que insertar tres
bytes de “0x00” antes de cada byte ASCII.

Para poder usar un ancho variable se creó el formato de transformación
Unicode (UTF) o "formato de transformación UCS” que mediante un mapeo
algorítmico asigna a cada punto de código Unicode una secuencia de bytes
única. Debido a que el punto de código tiene 21 bits, y a que los
ordenadores transfieren datos en múltiplos de 8 bits hay tres posibles
modos de expresar Unicode:

  • Usando una unidad de código de 32 bits, cada carácter se representa con 4 bytes (UTF-32).
  • Usando una o dos unidades de código de 16 bits, cada carácter se representa con 2 bytes como mínimo o 4 como máximo (UTF-16).
  • Usando de una a cuatro unidades de código de 8 bits cada carácter se representa con 1 byte como mínimo pudiendo llegar a 4 bytes como máximo (UTF-8).

UCS-4, UTF-32, UCS-2, UTF-16 en cierta medida son ineficientes. Las
computadoras intercambian muchas cadenas, y una gran mayoría de esas
cadenas solo usan caracteres ASCII que se pueden almacenar con un solo
byte, ocho bits. Es extremadamente ineficiente usar 4 bytes para
almacenar un carácter ASCII. Además estos formatos de codificación al
usar más de un byte, han de hacer frente al problema de la “endianidad”
ya que pueden almacenarse en memoria con el byte más significativo (MSB)
primero (BE) o el último (LE), lo que puede provocar que cuando se
intercambian datos, los bytes que aparecen en el orden "correcto" en el
sistema de envío, pueden aparecer como desordenados en el sistema
receptor. En algunos casos hay que usar una firma que define el orden de
los bytes y el formulario de codificación, (marca de orden de byte BOM),
que consiste en el código de carácter U+FEFF al comienzo de una
secuencia de datos.

  • UTF-8

UTF-8 es una codificación de longitud variable en la que cada punto de
código UCS se codifica utilizando 1, 2, 3 o 4 bytes según sea necesario
por lo tanto es más eficiente para caracteres ASCII, por otro lado no
presenta el problema de “endianidad” al tener siempre el mismo orden de
bytes.

UTF-8 tiene las siguientes propiedades:

  • Los caracteres UCS U+0000 a U+007F (ASCII) están codificados simplemente como 1 byte 0x00 a 0x7F (compatibilidad ASCII).
  • Todos los caracteres de UCS > U+007F (127 decimal) están codificados como una secuencia de varios bytes, cada uno de los cuales tiene el bit más significativo establecido. Por lo tanto, ningún byte ASCII (0x00-0x7F) puede aparecer como parte de ningún otro carácter.

A continuación se muestra las reglas del algoritmo para la codificación
utf-8, partiendo de su punto de código se emplearán 1, 2, 3 o 4 bytes.
Las posiciones de los bits “xxx…” se llenan con los bits del número del
código de carácter en representación binaria, teniendo en cuenta que el
extremo derecho de los bits x será el bit menos significativo.

U+0000 - U+007F (0-127): 0xxxxxxx (1byte)
U+0080 - U+07FF (128-2047): 110xxxxx 10xxxxxx (2bytes)
U+0800 - U+FFFF (2048-65535): 1110xxxx 10xxxxxx 10xxxxxx (3bytes)
U+10000 - U+1FFFFF (65536-2097151): 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx (4bytes)

Si el primer bit de un byte es un "0", los 7 bits restantes del byte
contienen uno de los 128 caracteres originales ASCII de 7 bits.

El primer byte de una secuencia multibyte que representa un carácter no
ASCII siempre está en el rango de 0xC0 (192) a 0xFD (253) e indica de
cuántos bytes está compuesta la secuencia en función de los bits con
valor “1”.

Todos los bytes adicionales en una secuencia multibyte están en el rango de 0x80 (128) a 0xBF (191).

Por ejemplo el carácter “ñ” tendría el código número 241 (decimal) que es F1 en hexadecimal y en binario 11110001, su código Unicode por lo tanto sería U00F1, para su codificación se usaran 2 bytes, “110xxxxx 10xxxxxx”, sustituyendo las x por el valor de su código, 241, en binario el resultado sería 110000*11* 10*110001* equivalente a c3b1 en hexadecimal, y a [195, 177] en decimal.

Como hemos mencionado antes, también podríamos representar la letra “ñ” como una secuencia compuesta del carácter base U+006E "n" seguido del carácter no espaciador U+0303“~” que pertenece al bloque marcas diacríticas que van desde la U+0300 a U+036F. Para representarlo necesitaríamos 3 bytes:

  • 1 byte para la letra “n” que se corresponde con el condigo número 110 en decimal que es 6E hexadecimal y en binario 01101110
  • 2 bytes para la marca diacrítica “~” que se corresponde con el código número 771 en decimal que se corresponde con 0x303 en hexadecimal y en binario 1100000011 si lo pasamos a la codificación UTF8 110xxxxx 10xxxxxx tendríamos 2 bytes con los valores 1100*1100* 10*000011* que equivalen a cc83 en hexadecimal y a [204, 131] en decimal.

A continuación, usaremos JavaScript para ejemplificar lo explicado y relacionarlo con los ArrayBuffer, usaremos el objeto incorporado TextDecoder que nos permite leer el texto de un conjunto de datos binarios y convertirlo en un dato de tipo string de JavaScript, dados un búfer y la codificación, si no se especifica la codificación por defecto será utf-8. También usaremos el método TextEncoder que toma un String como una entrada y devuelve una secuencia de bytes con codificación UTF-8 siempre devuelve un tipo Uint8Array. A modo de curiosidad también usaremos el método codePointAt(pos)de String, que nos permite obtener el punto de código del valor Unicode del carácter que está situado en una posición (pos) determina de la cadena. También conviene señalar que en JavaScript, los literales de cadena se pueden expresar mediante su respectivo punto de código Unicode en codificación UTF-16 para ello se ha de utilizar la secuencia de escape Unicode, la sintaxis general es\uXXXX, donde X denota cuatro dígitos hexadecimales.

Codigo disponible en el archivo EncodingArrayBuffer.js GitHub

const encoder = new TextEncoder()
const decoder = new TextDecoder()

console.log('ñ'.codePointAt(0));//241
//dec=241 hex=F1 bin=11110001 => utf-8 (11000011 10110001 -> c3b1 -> 2bytes [195, 177]
const str1u8 = new Uint8Array([195,177]);//Generamos un búfer con los datos oportunos.
console.log(str1u8)//[195,177]
let str1 = decoder.decode(str1u8)//decodificamos el bufer
console.log(str1)//ñ
console.log(str1.length);//1
console.log(str1.codePointAt());//241

let string1 = '\u00F1';
console.log(string1);//ñ
console.log(string1===str1);//true

const vista = encoder.encode(str1)
console.log(vista);//[195,177]

Enter fullscreen mode Exit fullscreen mode
const ene='n'
const diacritica='̃'
console.log(ene.codePointAt(0));//110
//dec=110 hex=6E bin=01101110 => utf-8 -> 01101110 -> 6E -> 1 byte [110]
console.log(diacritica.codePointAt(0));//771
//dec=771 hex=0x303 bin=1100000011 => utf-8 (11001100 10000011-> cc83 -> 2 bytes [204, 131].
const u8 = new Uint8Array([110,204,131]);//Generamos un búfer con los datos oportunos.
console.log(u8)//[110,204,131]
let str2 = decoder.decode(u8)//decodificamos el bufer
console.log(str2)//ñ
console.log(str2.length);//2 hemos usado dos caracteres
//Hallamos el punto de código de cada caracter.
console.log(str2.codePointAt(0));//110
console.log(str2.codePointAt(1));//771

let string2 = '\u006E\u0303';
console.log(string2);//ñ
console.log(string2===str2);//true

const vista2 = encoder.encode(str2)
console.log(vista2);//[110,204,131]
Enter fullscreen mode Exit fullscreen mode

Notas adicionales

Como hemos comprobado antes para el caso
del carácter “ñ” un punto de código, o una secuencia de puntos de código,
pueden representar el mismo carácter abstracto, esta información hay que
tenerle en cuenta a la hora de comparar cadenas y de operar con ellas, debido a
que los puntos de código son diferentes por lo que la comparación de cadenas no
los tratará como iguales, aunque visualmente lo parezcan, debido a que la
cantidad de puntos de código en cada versión es diferente por lo que las
cadenas tienen diferentes longitudes. Lo podemos comprobar en el siguiente
código.

//comparamos las dos cadenas string1='\u00F1' y string2='\u006E\u0303'
console.log(string1 === string2); //false
console.log(string1.length);      // 1
console.log(string2.length);      // 2
Enter fullscreen mode Exit fullscreen mode

Para resolver el anterior problema se puede usar el método normalize() de string, que permite convertir una cadena en una forma normalizada común para todas las secuencias de puntos de código que representan los mismos caracteres.

Se puede usar la normalización basada en la equivalencia canónica: dos secuencias de puntos de código tienen equivalencia canónica si representan los mismos caracteres abstractos y siempre
deben tener la misma apariencia visual y comportamiento. Para ello, podemos usar string.normalize([forma]) donde el argumento “forma” puede ser entre otros:

  • “NFC”: Normalization Form Canonical Composition. Forma canónica compuesta
  • “NFD”: Normalization Form Canonical Decomposition. Forma canónica descompuesta

Para producir una forma de la cadena que será la misma para todas las cadenas canónicamente equivalentes. Apliquemos las normalizaciones a las dos representaciones del carácter “ñ”

//Convertimos string1 '\u00F1' a su forma descompuesta
string1 = string1.normalize('NFD');//ñ
console.log(string1 === string2); // true
console.log(string1.length);      // 2
console.log(string2.length);      // 2
console.log(string1.codePointAt(0).toString(16)); //6E
console.log(string1.codePointAt(1).toString(16)); //303

//Convertimos string2 y string1 \u006E\u0303 a sus formas compuesta
string1 = string1.normalize('NFC');//ñ
string2 = string2.normalize('NFC');//ñ
console.log(string1 === string2);// true
console.log(string1.length);// 1
console.log(string2.length);//1
console.log(string2.codePointAt(0).toString(16)); //f1
console.log(string1.codePointAt(0).toString(16)); //f1

Enter fullscreen mode Exit fullscreen mode

Por ejemplo si quisiéramos obtener una cadena libre de marcas diacríticas, tildes, diéresis podríamos normalizarla a su forma descompuesta y a continuación eliminar las marcas diacríticas.

let cadenaDiacriticos ='áéíóúñüÁÉÍÓÚnaeiou'
console.log(cadenaDiacriticos.length)//18
//normalizamos forma descopmuesta y quitamos marcas diacríticas que van desde la `U+0300` a `U+036F`
cadenaDiacriticos=cadenaDiacriticos.normalize('NFD')
console.log(cadenaDiacriticos.length)//30
cadenaDiacriticos=cadenaDiacriticos.replace(/[\u0300-\u036f]/g,"");//aeiounuAEIOUnaeiou
console.log(cadenaDiacriticos)////aeiounuAEIOUnaeiou
console.log(cadenaDiacriticos.length)//18
Enter fullscreen mode Exit fullscreen mode

Top comments (0)