Usando Buffers no Node.js

O autor selecionou o Fundo de Auxílio COVID-19 para receber uma doação como parte do programa Escreva para Doações.

Introdução

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.

Você pode ter usado buffers implicitamente se já escreveu código Node.js. Por exemplo, quando você lê de um arquivo com fs.readFile(), os dados retornados para o callback ou Promise são um objeto buffer . Além disso, quando são feitas solicitações HTTP em Node.js, elas retornam fluxos de dados que são temporariamente armazenados em um buffer interno quando o cliente não pode processar o fluxo de uma só vez.

Buffers são úteis quando você está interagindo com dados binários, geralmente em níveis de rede mais baixos. Eles também equipam você com a capacidade de fazer manipulação de dados refinada em Node.js.

Neste tutorial, você usará o Node.js REPL para percorrer diversos exemplos de buffers, como criar buffers, ler de buffers, escrever em buffers, copiar de buffers e usar buffers para converter entre dados binários e codificados. No final do tutorial, você terá aprendido como usar a classe Buffer para trabalhar com dados binários.

Pré-requisitos

Passo 1 — Criando um Buffer

Este primeiro passo mostrará as duas principais maneiras de criar um objeto de buffer no Node.js.

Para decidir qual método usar, você precisa responder a esta pergunta: Você quer criar um novo buffer ou extrair um buffer de dados existentes? Se você pretende armazenar dados na memória que ainda não recebeu, precisará criar um novo buffer. No Node.js, usamos a função alloc() da classe Buffer para fazer isso.

Vamos abrir o REPL do Node.js para ver por nós mesmos. No seu terminal, digite o comando node:

  1. node

Você verá o prompt começar com >.

A função alloc() recebe o tamanho do buffer como seu primeiro e único argumento obrigatório. O tamanho é um número inteiro que representa quantos bytes de memória o objeto buffer usará. Por exemplo, se quisermos criar um buffer que tenha 1KB (quilobyte) de tamanho, equivalente a 1024 bytes, entraríamos isso no console:

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

Para criar um novo buffer, usamos a classe globalmente disponível Buffer, que possui o método alloc(). Ao fornecer 1024 como argumento para alloc(), criamos um buffer com 1 KB de tamanho.

Por padrão, ao inicializar um buffer com alloc(), o buffer é preenchido com zeros binários como espaço reservado para dados posteriores. No entanto, podemos alterar o valor padrão se desejarmos. Se quisermos criar um novo buffer com 1s em vez de 0s, definiríamos o segundo parâmetro da função alloc()fill.

No seu terminal, crie um novo buffer no prompt do REPL preenchido com 1s:

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

Acabamos de criar um novo objeto de buffer que faz referência a um espaço na memória que armazena 1 KB de 1s. Embora tenhamos inserido um número inteiro, todos os dados armazenados em um buffer são dados binários.

Dados binários podem vir em muitos formatos diferentes. Por exemplo, vamos considerar uma sequência binária representando um byte de dados: 01110110. Se esta sequência binária representasse uma string em inglês usando o padrão de codificação ASCII, seria a letra v. No entanto, se nosso computador estivesse processando uma imagem, essa sequência binária poderia conter informações sobre a cor de um pixel.

O computador sabe processá-los de forma diferente porque os bytes são codificados de maneira diferente. A codificação de bytes é o formato do byte. Um buffer no Node.js usa o esquema de codificação UTF-8 por padrão se for inicializado com dados de string. Um byte em UTF-8 representa um número, uma letra (em inglês e em outros idiomas) ou um símbolo. UTF-8 é um superconjunto de ASCII, o Código Padrão Americano para Intercâmbio de Informações. ASCII pode codificar bytes com letras maiúsculas e minúsculas do alfabeto inglês, os números 0-9 e alguns outros símbolos como o ponto de exclamação (!) ou o sinal de ampersand (&).

Se estivéssemos escrevendo um programa que só pudesse trabalhar com caracteres ASCII, poderíamos alterar a codificação usada pelo nosso buffer com o terceiro argumento da função alloc()encoding.

Vamos criar um novo buffer que tenha cinco bytes de comprimento e armazene apenas caracteres ASCII:

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

O buffer é inicializado com cinco bytes do caractere a, usando a representação ASCII.

Observação: Por padrão, o Node.js suporta as seguintes codificações de caracteres:

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

Todos esses valores podem ser usados nas funções da classe Buffer que aceitam um parâmetro de encoding. Portanto, esses valores são todos válidos para o método alloc().

Até agora, temos criado novos buffers com a função alloc(). Mas às vezes podemos querer criar um buffer a partir de dados que já existem, como uma string ou um array.

Para criar um buffer a partir de dados pré-existentes, usamos o método from(). Podemos usar essa função para criar buffers a partir de:

  • Um array de inteiros: Os valores inteiros podem estar entre 0 e 255.
  • Um ArrayBuffer: Este é um objeto JavaScript que armazena um comprimento fixo de bytes.
  • A string.
  • Outro buffer.
  • Outros objetos JavaScript que possuem uma propriedade Symbol.toPrimitive. Essa propriedade informa ao JavaScript como converter o objeto em um tipo de dado primitivo: boolean, null, undefined, number, string ou symbol. Você pode ler mais sobre Símbolos na documentação do JavaScript da Mozilla.

Vamos ver como podemos criar um buffer a partir de uma string. No prompt do Node.js, insira o seguinte:

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

Agora temos um objeto buffer criado a partir da string Meu nome é Paul. Vamos criar um novo buffer a partir de outro buffer que fizemos anteriormente:

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

Agora criamos um novo buffer asciiCopy que contém os mesmos dados que asciiBuf.

Agora que tivemos experiência em criar buffers, podemos explorar exemplos de leitura de seus dados.

Passo 2 — Leitura de um Buffer

Há muitas maneiras de acessar dados em um Buffer. Podemos acessar um byte individual em um buffer ou podemos extrair todo o conteúdo.

Para acessar um byte de um buffer, passamos o índice ou a localização do byte desejado. Buffers armazenam dados sequencialmente como arrays. Eles também indexam seus dados como arrays, começando em 0. Podemos usar a notação de array no objeto buffer para obter um byte individual.

Vamos ver como isso fica criando um buffer a partir de uma string no REPL:

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

Agora vamos ler o primeiro byte do buffer:

  1. hiBuf[0];

Ao pressionar ENTER, o REPL exibirá:

Output
72

O inteiro 72 corresponde à representação UTF-8 da letra H.

Nota: Os valores para bytes podem ser números entre 0 e 255. Um byte é uma sequência de 8 bits. Um bit é binário, e portanto só pode ter um dos dois valores: 0 ou 1. Se temos uma sequência de 8 bits e dois possíveis valores por bit, então temos um máximo de 2⁸ valores possíveis para um byte. Isso resulta em um máximo de 256 valores. Como começamos a contar a partir de zero, isso significa que nosso número mais alto é 255.

Vamos fazer o mesmo para o segundo byte. Insira o seguinte no REPL:

  1. hiBuf[1];

O REPL retorna 105, que representa o i minúsculo.

Finalmente, vamos pegar o terceiro caractere:

  1. hiBuf[2];

Você verá 33 exibido no REPL, que corresponde a !.

Vamos tentar recuperar um byte de um índice inválido:

  1. hiBuf[3];

O REPL retornará:

Output
undefined

Isso é como se tentássemos acessar um elemento em uma matriz com um índice incorreto.

Agora que vimos como ler bytes individuais de um buffer, vamos ver nossas opções para recuperar todos os dados armazenados em um buffer de uma vez. O objeto buffer vem com os métodos toString() e toJSON(), que retornam o conteúdo inteiro de um buffer em dois formatos diferentes.

Como o nome sugere, o método toString() converte os bytes do buffer em uma string e a retorna para o usuário. Se usarmos este método em hiBuf, obteremos a string Hi!. Vamos tentar!

No prompt, digite:

  1. hiBuf.toString();

O REPL retornará:

Output
'Hi!'

Esse buffer foi criado a partir de uma string. Vamos ver o que acontece se usarmos o toString() em um buffer que não foi feito a partir de dados de string.

Vamos criar um novo buffer vazio que tenha 10 bytes de tamanho:

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

Agora, vamos usar o método toString():

  1. tenZeroes.toString();

Veremos o seguinte resultado:

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

A string \u0000 é o caractere Unicode para NULL. Corresponde ao número 0. Quando os dados do buffer não estão codificados como uma string, o método toString() retorna a codificação UTF-8 dos bytes.

O toString() possui um parâmetro opcional, encoding. Podemos usar este parâmetro para mudar a codificação dos dados do buffer que são retornados.

Por exemplo, se você quisesse a codificação hexadecimal para hiBuf, você digitaria o seguinte no prompt:

  1. hiBuf.toString('hex');

Essa instrução será avaliada como:

Output
'486921'

486921 é a representação hexadecimal para os bytes que representam a string Hi!. No Node.js, quando os usuários querem converter a codificação de dados de uma forma para outra, geralmente colocam a string em um buffer e chamam toString() com a codificação desejada.

O método toJSON() se comporta de forma diferente. Independentemente de o buffer ter sido feito a partir de uma string ou não, ele sempre retorna os dados como a representação inteira do byte.

Vamos reutilizar os buffers hiBuf e tenZeroes para praticar o uso do toJSON(). Na linha de comando, insira:

  1. hiBuf.toJSON();

O REPL retornará:

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

O objeto JSON tem uma propriedade type que sempre será Buffer. Isso é para que os programas possam distinguir esses objetos JSON de outros objetos JSON.

A propriedade data contém uma matriz da representação inteira dos bytes. Você pode ter notado que 72, 105 e 33 correspondem aos valores que recebemos quando extraímos individualmente os bytes.

Vamos tentar o método toJSON() com tenZeroes:

  1. tenZeroes.toJSON();

No REPL, você verá o seguinte:

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

O type é o mesmo que mencionado anteriormente. No entanto, os dados agora são uma matriz com dez zeros.

Agora que cobrimos as principais maneiras de ler de um buffer, vamos ver como modificamos o conteúdo de um buffer.

Passo 3 — Modificando um Buffer

Há muitas maneiras de modificar um objeto de buffer existente. Semelhante à leitura, podemos modificar bytes de buffer individualmente usando a sintaxe de array. Também podemos escrever novos conteúdos em um buffer, substituindo os dados existentes.

Vamos começar olhando como podemos alterar bytes individuais de um buffer. Lembre-se da nossa variável de buffer hiBuf, que contém a string Hi!. Vamos alterar cada byte para que contenha Hey em vez disso.

No REPL, vamos primeiro tentar definir o segundo elemento de hiBuf para e:

  1. hiBuf[1] = 'e';

Agora, vamos ver este buffer como uma string para confirmar que está armazenando os dados corretos. Siga chamando o método toString():

  1. hiBuf.toString();

Será avaliado como:

Output
'H\u0000!'

Recebemos essa saída estranha porque o buffer só pode aceitar um valor inteiro. Não podemos atribuir a ele a letra e; ao invés disso, temos que atribuir o número cujo equivalente binário representa e:

  1. hiBuf[1] = 101;

Agora, quando chamamos o método toString():

  1. hiBuf.toString();

Obtemos esta saída no REPL:

Output
'He!'

Para mudar o último caractere no buffer, precisamos definir o terceiro elemento para o inteiro que corresponde ao byte para y:

  1. hiBuf[2] = 121;

Vamos confirmar usando o método toString() mais uma vez:

  1. hiBuf.toString();

Seu REPL exibirá:

Output
'Hey'

Se tentarmos escrever um byte que está fora do intervalo do buffer, ele será ignorado e o conteúdo do buffer não mudará. Por exemplo, vamos tentar definir o quarto elemento inexistente do buffer para o:

  1. hiBuf[3] = 111;

Podemos confirmar que o buffer não foi alterado com o método toString():

  1. hiBuf.toString();

A saída ainda é:

Output
'Hey'

Se quisermos alterar o conteúdo do buffer inteiro, podemos usar o método write(). O método write() aceita uma string que substituirá o conteúdo de um buffer.

Vamos usar o método write() para alterar o conteúdo de hiBuf de volta para Hi!. No seu shell Node.js, digite o seguinte comando no prompt:

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

O método write() retornou 3 no REPL. Isso ocorre porque ele escreveu três bytes de dados. Cada letra tem um byte de tamanho, já que esse buffer usa codificação UTF-8, que usa um byte para cada caractere. Se o buffer usasse codificação UTF-16, que tem um mínimo de dois bytes por caractere, então a função write() teria retornado 6.

Agora verifique o conteúdo do buffer usando toString():

  1. hiBuf.toString();

O REPL produzirá:

Output
'Hi!'

Isso é mais rápido do que ter que mudar cada elemento byte a byte.

Se você tentar escrever mais bytes do que o tamanho de um buffer, o objeto de buffer aceitará apenas os bytes que couberem. Para ilustrar, vamos criar um buffer que armazena três bytes:

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

Agora vamos tentar escrever Cats nele:

  1. petBuf.write('Cats');

Quando a chamada de write() é avaliada, o REPL retorna 3, indicando que apenas três bytes foram escritos no buffer. Agora confirme que o buffer contém os três primeiros bytes:

  1. petBuf.toString();

O REPL retorna:

Output
'Cat'

A função write() adiciona os bytes em ordem sequencial, então apenas os três primeiros bytes foram colocados no buffer.

Por outro lado, vamos criar um Buffer que armazena quatro bytes:

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

Escreva o mesmo conteúdo nele:

  1. petBuf2.write('Cats');

Em seguida, adicione algum conteúdo novo que ocupa menos espaço que o conteúdo original:

  1. petBuf2.write('Hi');

Como os buffers escrevem sequencialmente, começando do 0, se imprimirmos o conteúdo do buffer:

  1. petBuf2.toString();

Seríamos recebidos com:

Output
'Hits'

Os dois primeiros caracteres são sobrescritos, mas o restante do buffer permanece intocado.

Às vezes, os dados que queremos em nosso buffer preexistente não estão em uma string, mas residem em outro objeto de buffer. Nestes casos, podemos usar a função copy() para modificar o que nosso buffer está armazenando.

Vamos criar dois novos buffers:

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

Os buffers wordsBuf e catchphraseBuf ambos contêm dados de string. Queremos modificar catchphraseBuf para que ele armazene Nananana Tartaruga! em vez de Não tenho certeza Tartaruga!. Usaremos copy() para obter Nananana de wordsBuf para catchphraseBuf.

Para copiar dados de um buffer para o outro, usaremos o método copy() no buffer que é a fonte das informações. Portanto, como wordsBuf possui os dados de string que queremos copiar, precisamos copiar assim:

  1. wordsBuf.copy(catchphraseBuf);

O parâmetro target neste caso é o buffer catchphraseBuf.

Quando inserimos isso no REPL, ele retorna 15, indicando que 15 bytes foram escritos. A string Nananana usa apenas 8 bytes de dados, então imediatamente sabemos que nossa cópia não ocorreu conforme o esperado. Use o método toString() para ver o conteúdo de catchphraseBuf:

  1. catchphraseBuf.toString();

O REPL retorna:

Output
'Banana Nananana!'

Por padrão, copy() pegou todo o conteúdo de wordsBuf e o colocou em catchphraseBuf. Precisamos ser mais seletivos para alcançar nosso objetivo e apenas copiar Nananana. Vamos reescrever o conteúdo original de catchphraseBuf antes de continuar:

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

A função copy() tem alguns parâmetros adicionais que nos permitem personalizar quais dados são copiados para o outro buffer. Aqui está uma lista de todos os parâmetros desta função:

  • target – Este é o único parâmetro necessário de copy(). Como vimos em nosso uso anterior, é o buffer para o qual queremos copiar.
  • targetStart – Este é o índice dos bytes no buffer de destino onde devemos começar a copiar. Por padrão, é 0, o que significa que copia dados a partir do início do buffer.
  • sourceStart – Este é o índice dos bytes no buffer de origem de onde devemos copiar.
  • sourceEnd – Este é o índice dos bytes no buffer de origem onde devemos parar de copiar. Por padrão, é o comprimento do buffer.

Então, para copiar Nananana de wordsBuf para catchphraseBuf, nosso alvo deve ser catchphraseBuf como antes. O targetStart seria 0 pois queremos que Nananana apareça no início de catchphraseBuf. O sourceStart deve ser 7 pois é o índice onde Nananana começa em wordsBuf. O sourceEnd continuaria sendo o comprimento dos buffers.

No prompt do REPL, copie o conteúdo de wordsBuf assim:

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

O REPL confirma que 8 bytes foram escritos. Observe como wordsBuf.length é usado como valor para o parâmetro sourceEnd. Assim como em arrays, a propriedade length nos dá o tamanho do buffer.

Agora vamos ver o conteúdo de catchphraseBuf:

  1. catchphraseBuf.toString();

O REPL retorna:

Output
'Nananana Turtle!'

Sucesso! Conseguimos modificar os dados de catchphraseBuf copiando o conteúdo de wordsBuf.

Você pode sair do Node.js REPL se desejar. Note que todas as variáveis que foram criadas não estarão mais disponíveis quando o fizer:

  1. .exit

Conclusão

Neste tutorial, você aprendeu que buffers são alocações de comprimento fixo na memória que armazenam dados binários. Primeiro, você criou buffers definindo seu tamanho na memória e inicializando-os com dados pré-existentes. Em seguida, você leu dados de um buffer examinando seus bytes individuais e usando os métodos toString() e toJSON(). Finalmente, você modificou os dados armazenados por um buffer alterando seus bytes individuais e usando os métodos write() e copy().

Buffers oferecem grande insight sobre como os dados binários são manipulados pelo Node.js. Agora que você pode interagir com buffers, pode observar as diferentes maneiras como a codificação de caracteres afeta como os dados são armazenados. Por exemplo, você pode criar buffers a partir de dados de string que não sejam de codificação UTF-8 ou ASCII e observar a diferença em tamanho. Você também pode pegar um buffer com UTF-8 e usar toString() para convertê-lo para outros esquemas de codificação.

Para aprender sobre buffers no Node.js, você pode ler a documentação do Node.js sobre o objeto Buffer. Se você gostaria de continuar aprendendo Node.js, você pode retornar para a série Como Codificar em Node.js, ou navegar por projetos de programação e configurações em nossa página de tópicos do Node.

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