O autor selecionou a COVID-19 Relief Fund​​​​​ para receber uma doação como parte do programa Write for DOnations.

Introdução

Um buffer é um espaço de memória (tipicamente RAM) que armazena dados binários. No Node.js, podemos acessar esses espaços de memória com a classe Buffer integrada. Os buffers armazenam uma sequência de números inteiros, de maneira similar às matrizes em JavaScript. Diferentemente das matrizes, você não pode alterar o tamanho de um buffer após ele ser criado.

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

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

Neste tutorial, você usará o REPL do Node.js para percorrer vários exemplos relacionados a buffers, tais quais a criação de buffers, leitura a partir de buffers, escrever e copiar de buffers e usar buffers para converter dados entre 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á a você as duas maneiras básicas de criar um objeto buffer no Node.js.

Para decidir qual método usar, você precisa responder a esta pergunta: deseja criar um novo buffer ou extrair um buffer de dados existentes? Se você for armazenar dados em memória que você ainda não recebeu, você vai querer 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 vermos isso. Em seu terminal, digite o comando node:

  • node

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

A função alloc() recebe o tamanho do buffer como primeiro e único argumento necessário. O tamanho é um número inteiro representando quantos bytes de memória o objeto buffer usará. Por exemplo, se você quisesse criar um buffer que tivesse um tamanho de 1KB (kilobyte), ou seja, o equivalente a 1024 bytes, digitaria isto no console:

  • const firstBuf = Buffer.alloc(1024);

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

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

Em seu terminal, crie um novo buffer no prompt do REPL que esteja cheio de 1s:

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

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

Os dados binários podem estar em vários formatos diferentes. Por exemplo, vamos considerar uma sequência binária representando um byte de dados: 01110110. Se essa 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, a sequência binária poderia conter informações sobre a cor de um pixel.

O computador sabe processá-los de maneira diferente, pois os bytes são codificados de maneira diferente. A codificação do byte é o formato do byte. Um buffer no Node.js utiliza o esquema de codificação UTF-8 por padrão se for inicializado com dados em string. Um byte em UTF-8 representa um número, uma letra (em inglês e em outros idiomas), ou um símbolo. O UTF-8 é um superconjunto de ASCII, o Código em padrão americano para o intercâmbio de informações (do inglês, American Standard Code for Information Interchange). O ASCII pode codificar bytes com letras maiúsculas e minúsculas em inglês, os números 0-9, e alguns outros símbolos como ponto de exclamação (!) ou o E comercial (&).

Se estivéssemos escrevendo um programa que pudesse funcionar apenas 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:

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

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

Nota: 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 em funções da classe Buffer que aceitam um parâmetro encoding. Portanto, esses valores são todos válidos para o método alloc().

Até agora, estivemos criando novos buffers com a função alloc(). Mas, às vezes, pode ser que queiramos criar um buffer a partir de dados que já existem, como uma string ou matriz.

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

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

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

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

Agora, temos um objeto buffer criado a partir da string My name is Paul. Vamos criar um novo buffer a partir de outro buffer que fizemos anteriormente:

  • const asciiCopy = Buffer.from(asciiBuf);

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

Agora que experimentamos a criação de buffers, podemos nos aprofundar em exemplos de leitura dos dados deles.

Passo 2 — Lendo a partir 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 local do byte que queremos. Os buffers armazenam dados sequencialmente como matrizes. Eles também indexam seus dados como matrizes, começando em 0. Podemos usar a notação de uma matriz no objeto buffer para obter um byte individual.

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

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

Agora, vamos ler o primeiro byte do buffer:

  • hiBuf[0];

Assim que você pressionar ENTER, o REPL exibirá:

Output
72

O número 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, pode ter apenas um de dois valores: 0 ou 1. Se tivermos uma sequência de 8 bits e dois valores possíveis por bit, então teremos 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. Digite o seguinte no REPL:

  • hiBuf[1];

O REPL retorna 105, que representa a letra minúscula i.

Por fim, vamos obter o terceiro caractere:

  • hiBuf[2];

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

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

  • hiBuf[3];

O REPL retornará:

Output
undefined

Isso é igual a se tentamos 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 só vez. O objeto buffer vem com os métodos toString() e toJSON(), que retornam todo o conteúdo de um buffer em dois diferentes formatos.

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

No prompt, digite:

  • 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 em string.

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

  • const tenZeroes = Buffer.alloc(10);

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

  • tenZeroes.toString();

Veremos o seguinte resultado:

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

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

O toString() tem um parâmetro opcional, o enconding. Podemos usar esse parâmetro para alterar a codificação dos dados do buffer que são retornados.

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

  • hiBuf.toString('hex');

Essa instrução resultará em:

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 para outra, eles geralmente colocam a string em um buffer e chamam toString() com sua codificação desejada.

O método toJSON() comporta-se de maneira diferente. Independentemente de se ter sido feito a partir de uma string ou não, ele sempre retorna os dados com a representação de número inteiro do byte.

Vamos usar novamente os buffers hiBuf e tenZeroes para praticar o uso toJSON(). No prompt, digite:

  • hiBuf.toJSON();

O REPL retornará:

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

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

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

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

  • 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 você viu anteriormente. No entanto, os dados agora são uma matriz com dez zeros.

Agora que abordamos as principais formas de ler a partir de um buffer, vamos ver como modificar o conteúdo de um buffer.

Passo 3 — Modificando um buffer

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

Vamos começar analisando como podemos alterar os bytes individualmente de um buffer. Lembre-se de nossa variável buffer hiBuf, que contém a string Hi! Vamos alterar cada byte para que, ao invés disso, ela contenha Hey.

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

  • hiBuf[1] = 'e';

Agora, vamos ver este buffer como uma string para confirmar se ele está armazenando os dados corretos. Em seguida, chame o método toString():

  • hiBuf.toString();

Ele será avaliado como:

Output
'H\u0000!'

Recebemos esse resultado estranho porque o buffer pode aceitar apenas um valor de número inteiro. Não podemos atribuir-lhe a letra e; ao invés disso, precisamos atribuir-lhe o número cujo equivalente binário represente e:

  • hiBuf[1] = 101;

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

  • hiBuf.toString();

Obtemos este resultado no REPL:

Output
'He!'

Para alterar o último caractere no buffer, precisamos definir o terceiro elemento como o número inteiro que corresponda ao byte para y:

  • hiBuf[2] = 121;

Vamos confirmar usando o método toString() novamente:

  • hiBuf.toString();

Seu REPL exibirá:

Output
'Hey'

Se tentarmos escrever um byte que esteja fora do alcance do buffer, ele será ignorado e o conteúdo do buffer não mudará. Por exemplo, vamos tentar definir o quarto elemento não existente do buffer como o:

  • hiBuf[3] = 111;

Podemos confirmar que o buffer permanece inalterado com o método toString():

  • hiBuf.toString();

O resultado ainda é:

Output
'Hey'

Se quiséssemos alterar o conteúdo de todo o buffer, poderíamos 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 do hiBuf de volta para Hi! Em seu shell do Node.js, digite o seguinte comando no prompt:

  • hiBuf.write('Hi!');

O método write() retornou 3 no REPL. Isso aconteceu porque ele escreveu três bytes de dados. Cada letra tem o tamanho de um byte, uma vez que este buffer utiliza a codificação UTF-8, a qual utiliza um byte para cada caractere. Se o buffer tivesse usado a 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():

  • hiBuf.toString();

O REPL produzirá:

Output
'Hi!'

Isso é mais rápido do que ter que alterar cada elemento byte por byte.

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

  • const petBuf = Buffer.alloc(3);

Agora, vamos tentar escrever Cats nele:

  • petBuf.write('Cats');

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

  • petBuf.toString();

O REPL retorna:

Output
'Cat'

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

De maneira contrária, vamos criar um Buffer que armazene quatro bytes:

  • const petBuf2 = Buffer.alloc(4);

Escreva o mesmo conteúdo nele:

  • petBuf2.write('Cats');

Então, adicione um novo conteúdo que ocupe menos espaço do que o conteúdo original:

  • petBuf2.write('Hi');

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

  • petBuf2.toString();

Receberíamos isto como resultado:

Output
'Hits'

Os dois primeiros caracteres foram substituídos, mas o resto do buffer permaneceu intacto.

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

Vamos criar dois novos buffers:

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

Os buffers wordsBuf e catchphraseBuf contêm ambos dados em string. Queremos modificar o catchphraseBuf de modo que ele armazene Nananana Turtle! ao invés de Not sure Turtle!. Usaremos o copy() para transferir o Nananana de wordsBuf para wordsBuf.

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

  • wordsBuf.copy(catchphraseBuf);

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

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

  • catchphraseBuf.toString();

O REPL retorna:

Output
'Banana Nananana!'

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

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

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

  • target - Este é o único parâmetro necessário de copy(). Como vimos em nosso uso anterior, ele é o buffer para o qual queremos copiar.
  • targetStart - este é o índice dos bytes no buffer de destino para onde devemos começar a copiar. Por padrão é 0, ou seja, ele copia dados começando no início de um 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, ele é o comprimento do buffer.

Assim, para copiar o Nananana de wordsBuf para catchphraseBuf, nosso target deve ser catchphraseBuf assim como anteriormente. O targetStart deve ser 0, já que queremos que o Nananana apareça no início de catchphraseBuf. O sourceStart deve ser 7, pois é o índice onde o Nananana começa em wordsBuf. O sourceEnd continuaria sendo o comprimento dos buffers.

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

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

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

Agora, vamos ver o conteúdo de catchphraseBuf:

  • catchphraseBuf.toString();

O REPL retorna:

Output
'Nananana Turtle!'

Success! Fomos capazes de modificar os dados de catchphraseBuf copiando o conteúdo de wordsBuf.

Você pode sair do REPL do Node.js caso queira fazer isso. Observe que todas as variáveis que foram criadas não estarão mais disponíveis quando você o fizer:

  • .exit

Conclusão

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

Os buffers te fazem compreender como os dados binários são manipulados pelo Node.js. Agora que você consegue interagir com buffers, observe 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 em string que não estejam codificados em UTF-8 ou ASCII e observar suas diferenças de tamanho. Você também pode usar um buffer com o UTF-8 e usar toString() para convertê-lo para outros esquemas de codificação.

Para aprender sobre buffers no Node.js, você pode ler parte da documentação do Node.js que diz respeito ao objeto Buffer. Se quiser continuar aprendendo sobre o Node.js, você pode retornar para a série Como programar em Node.js, ou pesquisar por projetos de programação e configurações em nossa página de tópicos do Node.

0 Comments

Creative Commons License