Neste post, vamos criar um serviço Cloud Run com um servidor Node.js que registra os chunks de dados recebidos. Também vamos criar um cliente Node.js que envia chunks de dados ao servidor.
Vamos explorar como fazer streaming de dados do cliente para o servidor com HTTP/1 usando chunked transfer encoding.
Se estiver com pressa, dá para conferir todos os arquivos no meu repositório aqui.
Por quê?
A primeira coisa que deve estar passando pela sua cabeça é o PORQUÊ.
O HTTP/2 costuma ser a melhor opção nesses casos. Ele oferece ganhos significativos de performance, menor latência e melhor uso dos recursos. Mas, embora o Cloud Run ofereça HTTP/2 End-to-End para melhorar a performance, sua aplicação também precisa estar preparada para receber chamadas HTTP/2. Infelizmente, esse recurso não está disponível em todos os cenários, o que pode ser um obstáculo em algumas implantações:
- Há a necessidade de dar suporte a clientes mais antigos que não suportam HTTP/2 ou que apresentam problemas conhecidos com ele
- Se sua aplicação depende de bibliotecas ou frameworks que exigem chunked encoding e não são totalmente compatíveis com HTTP/2, usar HTTP/1 pode ser a única saída.
Vamos usar o header Transfer-Encoding: chunked, um recurso do HTTP/1.1 que permite ao cliente enviar dados em chunks ao servidor sem saber o tamanho total dos dados de antemão. Ele divide os dados em chunks e os envia separadamente. O servidor então remonta os dados a partir desses chunks.
Isso é especialmente útil quando o remetente não sabe o tamanho do conteúdo no momento em que começa a transmissão, o que costuma acontecer com conteúdo dinâmico ou em streaming.
Vale lembrar que HTTP/2 e HTTP/3 tratam a transmissão de dados de forma diferente, usando binary framing e multiplexing para permitir que várias requisições e respostas trafeguem ao mesmo tempo, o que pode trazer melhorias de performance. Ainda assim, talvez você não consiga adaptar sua aplicação para aceitar HTTP/2.
Tenha em mente que o conceito de chunked transfer encoding não se aplica diretamente em HTTP/2 e HTTP/3 como acontece no HTTP/1.1.
Direções do streaming de dados
Fazer streaming de dados do servidor para o cliente e do cliente para o servidor são dois conceitos distintos, cada um com seus próprios casos de uso e técnicas.
1. Servidor para Cliente (Server-Sent Events): normalmente usado quando o servidor tem novas informações para enviar ao cliente.
Por exemplo, em uma aplicação em tempo real, como um app de chat ou um app de placar esportivo ao vivo, o servidor pode precisar enviar novas mensagens ou atualizações ao cliente assim que elas estiverem disponíveis.
Isso é feito com uma técnica chamada Server-Sent Events (SSE), na qual o cliente abre uma conexão com o servidor e este mantém a conexão aberta, enviando atualizações por ela sempre que houver novidades.

2. Cliente para Servidor (HTTP Streaming ou Chunked Transfer Encoding): normalmente usado quando o cliente tem um grande volume de dados para enviar ao servidor e quer iniciar a transmissão antes que tudo esteja pronto.
Por exemplo, em um cenário de upload de arquivos, o cliente pode querer começar a enviar um arquivo grande antes que ele seja totalmente carregado na memória.
Isso é feito por meio de um recurso do protocolo HTTP chamado chunked transfer encoding, em que o cliente envia os dados em chunks e o servidor processa cada chunk conforme ele chega.
Em ambos os casos, o objetivo é permitir que os dados sejam enviados e processados de forma incremental, em vez de exigir que tudo esteja pronto no início da requisição. Isso pode levar a uma performance melhor e a um menor uso de memória, principalmente ao lidar com grandes volumes de dados.
Lembre-se: a escolha entre streaming servidor-cliente e cliente-servidor depende dos requisitos específicos da sua aplicação. Neste post, vamos focar apenas no streaming Cliente para Servidor. Se quiser ver streaming servidor para cliente com gRPC, dá uma olhada neste post do Google Blog.
Configuração do servidor
Primeiro, vamos criar o servidor. Vamos usar o Express, um framework popular do Node.js. Nosso servidor vai escutar requisições POST no endpoint /upload e registrar todos os chunks de dados recebidos.
Crie o arquivo package.json
{
"name": "stream-test",
"version": "1.0.0",
"description": "",
"main": "client.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.18.2"
}
}
E aqui está o server.js do nosso servidor:
// server.js
const express = require('express');
const app = express();
app.use(express.raw({ type: '*/*', limit: '5mb' }));
app.post('/upload', (req, res) => {
req.on('data', chunk => {
console.log(`Received chunk: ${chunk}`);
});
req.on('end', () => {
res.send('Upload complete');
});
});
app.listen(3000, () => console.log('Server listening on port 3000'));
Dá para colocar esse servidor em um container com o Docker. Aqui está um Dockerfile simples:
# Dockerfile.server
FROM node:14
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
CMD [ "node", "server.js" ]
Servidor no Cloud Run
Beleza, agora vamos criar as imagens dos containers, fazer o push para o Artifact Registry no GCP e o deploy no nosso serviço Cloud Run. Talvez você precise criar o repositório no AR primeiro:
REPOSITORY=us-central1-docker.pkg.dev/<GCP_PROJECT>/<REPO>
docker build -t $REPOSITORY/stream-server:1.0 -f Dockerfile.server .
Faça o deploy do servidor
gcloud run deploy stream-server --image $REPOSITORY/stream-server:1.0
Você deve ver logs parecidos depois que o serviço for implantado

Agora que o servidor já está rodando no Cloud Run, precisamos pegar a URL do Cloud Run:
export SERVER_URL=$(gcloud run services describe stream-server --region us-central1 --format 'value(status.url)')
Configuração do cliente
Agora vamos criar o cliente. Ele vai enviar chunks de dados ao servidor logo na inicialização. Lembre-se de substituir o <SERVER_URL> no campo hostname pela URL real do servidor.
Aqui está o client.js do nosso cliente:
// client.js
const https = require('https');
const options = {
hostname: '<SERVER_URL>',
port: 443,
path: '/upload',
method: 'POST',
headers: {
'Transfer-Encoding': 'chunked'
}
};
const req = https.request(options, (res) => {
res.on('data', (chunk) => {
console.log(`Response: ${chunk}`);
});
});
// write chunks of data to the request
req.write('chunk1');
req.write('chunk2');
req.write('chunk3');
req.end();
Também dá para colocar o cliente em um container com o Docker. Aqui está um Dockerfile simples:
# Dockerfile.client
FROM node:14
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
CMD [ "node", "client.js" ]
Agora é só fazer o build da imagem do cliente e rodar o código
docker build -t stream-client:1.0 -f Dockerfile.client .
docker run --rm stream-client:1.0
Vamos confirmar se está funcionando olhando os logs do Cloud Run no lado do servidor:

Calma aí, parece que os chunks não estão sendo registrados separadamente. O problema é que o protocolo HTTP não garante que cada chamada a req.write() vai gerar um evento 'data' separado no servidor.
Os chunks que você está enviando são pequenos e disparados em sequência rápida, então provavelmente estão chegando ao servidor em um único evento ‘data’.
Para confirmar que o streaming está funcionando, você pode enviar uma quantidade maior de dados, que não caiba em um único pacote TCP. Isso vai forçar a divisão dos dados em vários chunks, o que deve disparar vários eventos ‘data’ no servidor.
Vamos ajustar o client.js adicionando um "for loop" para escrever os dados, fazer o build do container de novo e testar mais uma vez.
// client.js
const https = require('https');
const options = {
hostname: 'stream-server-kdfodunfwq-uc.a.run.app',
port: 443,
path: '/upload',
method: 'POST',
headers: {
'Transfer-Encoding': 'chunked'
}
};
const req = https.request(options, (res) => {
res.on('data', (chunk) => {
console.log(`Response: ${chunk}`);
});
});
// write a large amount of data to the request
for (let i = 0; i < 1e6; i++) {
req.write(`This is chunk number ${i}\n`);
}
req.end();
docker build -t stream-client:2.0 -f Dockerfile.client .
docker run --rm stream-client:2.0
Agora sim, está funcionando!

Neste post, vimos como fazer streaming de dados entre duas instâncias do Cloud Run com Node.js. Essa configuração permite enviar dados em chunks do cliente para o servidor, com o servidor registrando cada chunk conforme ele chega.
Lembre-se de que o protocolo HTTP não garante que cada chamada a req.write() vai gerar um evento 'data' separado no servidor. Os chunks que estávamos enviando no início eram bem pequenos e disparados em sequência rápida, então acabavam chegando ao servidor em um único evento ‘data’.
Conseguimos confirmar que o streaming estava funcionando enviando uma quantidade maior de dados, que não cabia em um único pacote TCP. Isso forçou a divisão dos dados em vários chunks, o que disparou vários eventos ‘data’ no servidor.
Reforçando: o HTTP/2 costuma ser a melhor escolha. Use sempre que possível.
Espero que este post tenha sido útil! Se ficou com alguma dúvida, deixe um comentário aí embaixo.