Um cliente nos escreveu com uma observação simples que se mostrou exatamente correta: servir arquivos a partir do servidor HTTP usava muita memória. Se o TsgcWebSocketHTTPServer servia um arquivo grande a partir do DocumentRoot, o arquivo inteiro era lido na RAM para cada conexão. Cem clientes baixando o mesmo arquivo de 1 GB significavam aproximadamente 100 GB de RAM. Esta versão corrige isso. Os arquivos agora fazem streaming a partir do disco e a memória do servidor permanece estável, independentemente do tamanho do arquivo e de quantos clientes estão conectados. Ela está disponível no sgcWebSockets 2026.6.0.
O problema: uma cópia completa por conexão
Ambos os caminhos de arquivos estáticos, DoResponseHTTP para HTTP/1.x e DoHTTP2DocumentRoot para HTTP/2, construíam a resposta da mesma maneira. Eles criavam um TMemoryStream, chamavam LoadFromFile e entregavam esse stream à resposta. LoadFromFile lê o arquivo inteiro na memória de uma só vez, então o custo por requisição era o tamanho completo do arquivo, e cada download simultâneo mantinha sua própria cópia.
// Old behaviour, simplified
oStream := TMemoryStream.Create;
oStream.LoadFromFile(vDocument); // entire file into RAM
AResponseInfo.ContentStream := oStream;
Isso faz com que o pico de memória seja aproximadamente tamanho do arquivo × conexões simultâneas. Um punhado de clientes baixando um vídeo, um instalador, uma imagem de disco ou um backup de banco de dados é suficiente para levar um servidor ao swapping ou a uma falha por falta de memória.
O multiplicador de leitura lenta
O mesmo cliente descreveu um segundo cenário, e ele é um ataque real: muitas conexões que leem a resposta cada uma muito lentamente, na ordem de um byte por segundo. Isso é um ataque de leitura lenta, uma variante do Slowloris. Um cliente que goteja suas leituras mantém a conexão aberta por muito tempo, e com o código antigo cada uma dessas conexões também mantinha presa uma cópia completa do arquivo em memória. Alguns milhares de leitores lentos de um arquivo modesto poderiam manter gigabytes como reféns enquanto transferiam quase nada.
Como medimos isso
Construímos um pequeno harness: um servidor com um DocumentRoot e um arquivo de teste grande, além de um cliente com dois modos. Um modo "rápido" abre N downloads normais simultâneos, e um modo "lento" abre M conexões brutas que enviam uma requisição válida e então leem o corpo a um byte por segundo. Amostramos o working set do servidor com um arquivo de 100 MB, antes e depois da correção.
| Cenário (arquivo de 100 MB) | Antes da correção | Depois da correção |
|---|---|---|
| 5 downloads simultâneos | ~511 MB | ~11 MB |
| 10 downloads simultâneos | ~1.012 MB | ~11 MB |
| 20 downloads simultâneos | ~1.7 GB | ~12 MB |
| 100 conexões de leitura lenta (1 byte/sec) | ~1.67 GB mantidos estáveis | ~19 MB |
| Integridade do arquivo | n/a | SHA-256 byte a byte exato, Content-Length correto |
Antes da correção, a memória acompanhava quase exatamente o tamanho do arquivo vezes a quantidade de conexões. Depois da correção, ela permanece na casa das dezenas de megabytes, não importa o quão grande seja o arquivo ou quantos clientes se conectem.
A correção: streaming a partir do disco
O arquivo agora é servido com um TFileStream somente leitura e compartilhado em vez de um TMemoryStream. Nada mais no pipeline precisou mudar, porque o lado da escrita já fazia streaming: o servidor HTTP/1.x envia o stream de conteúdo em blocos de 32 KB, e o servidor HTTP/2 o emite em frames DATA de aproximadamente 64 KB. Então o servidor lê um pequeno bloco do disco, o envia e segue adiante, o que reduz o custo de memória por conexão do arquivo inteiro para algumas dezenas de kilobytes.
// New behaviour, simplified
oStream := TFileStream.Create(vDocument, fmOpenRead or fmShareDenyWrite);
oStream.Position := 0;
AResponseInfo.ContentStream := oStream;
O Content-Length continua correto, porque ele vem de TFileStream.Size, e o stream é liberado após a resposta exatamente como antes. Verificamos que os downloads são idênticos byte a byte ao arquivo de origem com uma comparação SHA-256, e que o cabeçalho informa o tamanho correto. Não há mudança de API e nada a configurar: definir DocumentRoot funciona como sempre funcionou, ele simplesmente não faz mais o buffer do arquivo.
oServer := TsgcWebSocketHTTPServer.Create(nil);
oServer.Port := 80;
oServer.DocumentRoot := 'C:\inetpub\files'; // large files now stream from disk
oServer.Active := True;
A edição .NET herda a correção automaticamente, porque o serviço de arquivos do seu DocumentRoot roda no mesmo core compilado que os componentes Delphi usam.
Desacelerando os leitores lentos
O streaming a partir do disco remove a amplificação de memória, então um leitor lento não consegue mais manter um arquivo inteiro preso na RAM. O que resta é a própria conexão, e para isso há um timeout de envio. Options.WriteTimeOut define um prazo para as escritas no socket, então um cliente que para de aceitar dados é descartado em vez de bloquear uma thread do servidor para sempre. Nesta versão esse timeout também funciona no Linux e em outras plataformas POSIX, não apenas no Windows. A API de socket POSIX espera um timeval em vez de um inteiro em milissegundos, e isso agora é tratado corretamente.
oServer := TsgcWebSocketHTTPServer.Create(nil);
oServer.DocumentRoot := 'C:\inetpub\files';
oServer.Options.WriteTimeOut := 15000; // drop a client that stalls for 15s, Windows and POSIX
oServer.Active := True;
Para um servidor público, recomendamos combinar três mecanismos: defina Options.WriteTimeOut para que escritas travadas sejam descartadas, use o RateLimit.MaxConnectionsPerIP do firewall para limitar quantas conexões um único endereço pode abrir, e considere um escalonador com thread-pool ou os handlers IOCP e EPOLL para que uma enxurrada de conexões lentas não consiga esgotar uma thread para cada uma.
Atualizando
A correção de streaming é um drop-in. Assim que você atualizar, o DocumentRoot serve arquivos a partir do disco com memória estável, sem mudança de código. A alteração foi verificada do Delphi 7 ao 13, nos caminhos HTTP/1.x e HTTP/2, e o build .NET a recebe através do core compartilhado. Se você serve arquivos estáticos grandes, esta é uma redução significativa de memória e um passo útil contra o abuso de leitura lenta.
Atualize a partir da página de download do sgcWebSockets, ou obtenha-o através do GetIt ou da sua conta registrada.
Dúvidas, feedback ou ajuda com a migração? Entre em contato, você receberá uma resposta das pessoas que escreveram o código.
