Menu ▾ ▴

corpo unico

Mobius One

Corpo da requisição e da resposta sem cópias

Home > Corpo da requisição e da resposta sem cópias

[!IMPORTANT]
Em desenvolvimento (2.0). Ainda não está numa versão lançada e pode mudar.

Nível: avançado

Em resumo, para quem só usa:

  • uploads e respostas grandes gastam muito menos memória, sem mudar o seu código;
  • não converta Body.Content para TMemoryStream: leia como TStream;
  • para corpos de centenas de MB, ligue SpoolAbove (vai para o disco).

O resto da página é para quem precisa do detalhe. Até a 1.2 o corpo era copiado de
2 a 5 vezes entre o motor e a rota: um upload de 100 MB passava por meio gigabyte.

Desde a 2.0 existe um corpo por requisição (no servidor) e um por resposta
(no cliente). Os parâmetros leem esse corpo onde ele está, sem copiar, e a
compressão e a criptografia só acontecem na hora de enviar, escrevendo direto
no que vai para a rede.

Na prática, com um corpo de 8 MB:

situação 1.2 2.0
servidor recebe e a rota lê 3x o corpo além dele mesmo nada além do corpo
servidor responde um texto que a rota já tem 3x nada
servidor responde com gzip 3x 5% (o gzip de saída)
cliente recebe e lê ResponseText 4x 1x (a própria string)
cliente envia 2x nada

Ler o corpo: Content, AsString e AsStream

forma o que é custo
Body.Content o próprio corpo, onde ele está nenhum
Body.AsString o corpo como texto uma cópia (nenhuma quando o motor entregou uma string: mORMot2, fpHTTP, CGI)
Body.AsStream uma cópia que você libera uma cópia

Use Content quando só vai ler:

procedure TForm1.Importa(ARequest: TRALRequest; AResponse: TRALResponse);
var
  vCorpo: TStream;
begin
  vCorpo := ARequest.Body.Content;   // não libere: é do pedido
  MeuLeitorCSV.LoadFromStream(vCorpo);
  AResponse.Answer(HTTP_OK, 'importado', rctTEXTPLAIN);
end;

O corpo recebido é somente leitura: escrever em Body.Content levanta
exceção. Quem precisa alterar o corpo pega uma cópia com AsStream.

A classe de Content depende do tamanho, e por isso não converta para
TMemoryStream
:

  • até 8 MB é memória contínua (uma vista ou um TMemoryStream);
  • acima disso é um TRALChunkedStream, em blocos de 1023 KB;
  • acima de SpoolAbove (desligado por padrão) é um arquivo temporário.

Todas são TStream e se leem igual. Um código que faz
TMemoryStream(Body.Content).Memory deixa de funcionar acima de 8 MB.

Num multipart, cada parte (ParamByName('arquivo').Content) é uma janela sobre
o corpo que chegou, com as mesmas regras.

Responder sem copiar

forma o que acontece
Answer(status, texto) a resposta guarda a sua string, sem copiar
Answer(status, stream, tipo) copia o stream; você continua dono dele e o libera
Answer(status, stream, tipo, True) a resposta fica com o stream e o libera; não mexa mais nele
AResponse.BodyStream um stream para escrever o corpo direto, que a resposta envia
// um arquivo gerado pela rota: a resposta fica com ele
vPlanilha := TMemoryStream.Create;
GeraPlanilha(vPlanilha);
AResponse.Answer(HTTP_OK, vPlanilha, 'application/vnd.ms-excel', True);

// escrever direto no corpo da resposta
AResponse.ContentType := rctAPPLICATIONJSON;
MinhaQuery.SaveToStream(AResponse.BodyStream);

BodyStream devolve sempre o mesmo stream enquanto ele for o corpo, então
várias escritas se acumulam. O tipo do corpo é o ContentType da resposta,
mesmo que você o defina depois de escrever.

O que muda para quem lê a resposta ou o pedido inteiro

  • RequestText/RequestStream no servidor e ResponseText/ResponseStream no
    cliente são o corpo que chegou, já descomprimido e decifrado. Um multipart
    volta com os bytes que chegaram, não remontado com outra fronteira.
  • ResponseText/ResponseStream no servidor são o que a rota respondeu,
    descomprimido. Até a 1.2 eram o corpo já codificado. Um OnResponse que lia
    ResponseText recebe o texto, não gzip.
  • Quem escreveu um motor próprio e enviava ResponseStream deve passar a usar
    TakeWireStream (veja "Para quem escreve um motor").

Tipos já comprimidos não são comprimidos de novo

Imagens (menos SVG), áudio, vídeo, zip, gzip, 7z, rar, bzip2, xz, zstd, brotli,
pdf e fontes woff/woff2 saem sem Content-Encoding, mesmo com gzip pedido.
Comprimir de novo gasta processador e um buffer do tamanho do corpo e não reduz
nada. Qualquer cliente HTTP lê as duas formas, então nada muda para quem recebe.

  • No servidor: TRALCompressPlugin.SkipCompressedTypes (padrão True) e
    SkipContentTypes, uma lista de tipos a mais (application/x-meu, ou um
    prefixo terminado em /, como model/).
  • No cliente: TRALClient.SkipCompressedTypes (padrão True) e
    SkipCompressTypes.

SkipCompressedTypes := False volta ao comportamento da 1.2.

Corpos grandes em disco: SpoolAbove

TRALLimitsPlugin.SpoolAbove (servidor) e TRALClient.SpoolAbove (cliente),
em bytes, desligados por padrão (0). Acima desse tamanho, o corpo que o RAL
monta vai para um arquivo temporário, apagado quando o pedido ou a resposta é
liberado. A memória deixa de crescer com o corpo: medido, 1 MB de memória para
um corpo de 256 MB.

  • A pasta é a temporária do sistema, ou a da variável global RALSpoolFolder
    (unit RALStream). Precisa de permissão de escrita.
  • Um processo que morre no meio deixa o arquivo para trás; ao ligar o
    SpoolAbove, o RAL apaga os que têm mais de um dia.
  • No servidor Indy o corpo vai para o disco desde o primeiro byte. Os outros
    motores (mORMot2, fpHTTP, Sagui, CGI, QUIC) entregam o corpo já inteiro na
    memória deles: ali o disco guarda o que o RAL monta a partir dele (o corpo
    descomprimido ou decifrado) e a resposta.
  • No cliente, Indy, fpHTTP e netHTTP recebem direto no disco.

Win32: um processo de 32 bits tem 2 GB de endereços, já fragmentados.
Corpos de algumas centenas de MB, em Win32, só com SpoolAbove.

Para quem escreve um motor

  • Entrada: SetWireBody(AStream, APosse), SetWireBody(ATexto) ou
    SetWireBody(APonteiro, ATamanho), com ContentEncoding e
    ContentEncription já preenchidos. A posse (TRALBodyOwnership) diz de quem
    é o buffer: boBorrowed (do motor, que o mantém vivo até o fim do pedido),
    boBorrowedWritable (idem, e o RAL pode decifrar ali mesmo), boOwned (o RAL
    fica com ele) ou boCopy.
  • Saída: TakeWireStream (um stream que o motor libera) ou TakeWireString.
    Leia ContentType e ContentEncoding depois da chamada: eles dizem o que
    foi feito de verdade.
  • O pedido tem de nascer e morrer dentro do callback que segura o buffer do
    motor: é isso que torna o empréstimo seguro.