Repository navigation
internos utils
Home > Guia de contribuição > Internos: Utilitários
Para quem mexe no código: stream, compressão, criptografia, hash, JSON, storages e tokens. Aqui fica o porquê de cada parte ser como é, e o que não pode mudar sem quebrar algo. O código traz só a descrição curta de cada coisa.
Conteúdo da versão 2.0, ainda em desenvolvimento (branch 2.0).
O TBufDataset do FPC procura no próprio log de mudanças a cada Post, e um
registro carregado entra nesse log: carregar um storage num TBufDataset era
quadrático (40 mil registros, 2,5 s; 100 mil, 17 s). Todo storage chama
RALStoragePosted depois de cada Post - que esvazia o log a cada mil
registros - e o TRALStorageLink chama RALStorageLoaded no fim, porque o que
um storage traz é dado, não mudança a aplicar. No Delphi as duas não fazem
nada.
-
Um JSON só.
RALJsoné parser e gravador do RAL nos dois compiladores: o mesmo texto sai igual em toda plataforma, e nenhum pacote depende de fpJSON ou deSystem.JSON. Números que vão para o fio continuam saindo peloTRALJSONTextWritere peloRALJSONNumberText(forma curta com.), não peloToJSONdo valor. -
BSON canônico.
TRALBSON.Parsedevolve, por padrão, os tipos do BSON que o JSON não tem como JSON estendido ($numberLong, datas): é o que deixa uma ida e volta BSON → JSON → BSON sem perder o tipo. O storage BSON lê os dois formatos de data (o antigo, em segundos, e o de milissegundos). -
Código antes da mensagem.
ERALExceptionguarda oErrorCodee põe[RAL <código>]na frente da mensagem traduzida (RALErrorCodesliga código e texto nos três idiomas). Uma exceção que precisa descender da exceção de outra biblioteca fica fora disso:ERALfpConnectionClosedé umEHTTPClient, porque oSendUrldo fpHTTP a testa antes daEHTTPClientgenérica; ela leva o texto puro e quem a pega monta a mensagem com a URL. -
Carregar biblioteca.
TRALDynLibsé um carregamento por objeto, comTryBinddeixandonilo símbolo que falta: quem chama decide sem o que não vive. ORALTLStem por cima dele o carregador por lista de nomes (libssl-3-x64.dll,libssl.so.3, ...): o primeiro que carrega vale, e os ponteiros resolvidos voltam anilquando a biblioteca é liberada.
StringToBytesUTF8 é uma cópia direta, sem TEncoding: StringRAL já é UTF-8.
TEncoding.UTF8.GetBytes passa por UTF-16 e volta (duas alocações) e troca cada
sequência inválida por U+FFFD: os bytes A3 9A 4F C2 00 7E FF 10 voltariam
EF BF BD EF BF BD 4F EF BF BD 00 7E EF BF BD 10. Uma chave derivada é feita de bytes
assim (ver HMACAsDigest em RALHashBase).
-
DEFAULTBUFFERSTREAMSIZE(64 KB) é o teto do buffer de trabalho de toda transformação (zlib, zstd, brotli, AES, hashes, base64, hex, multipart): o buffer éMin(tamanho, 64 KB). Um buffer do tamanho do corpo seria uma cópia a mais em cada transformação, e o gzip roda na mesma velocidade com 16 KB e com 4 MB. Com ele, todo caminho em pedaços roda em corpos acima de 64 KB, então um defeito de pedaço (base64 com=no meio, hash sobrescrevendo um bloco parcial) aparece nos testes. O valor tem de ser múltiplo de 64 (o bloco dos hashes); o 3 e o 4 do base64 ele resolve sozinho. -
DEFAULTCHUNKABOVE(8 MB): um bloco contíguo até 8 MB, lista de blocos acima. Medido no Delphi 13 Win64 com FastMM: umTMemoryStreamenchido em pedaços de 64 KB reserva 2 MB além de um corpo de 4 MB, 7 MB além de 8 MB, 18 MB além de 16 MB (366 MB além de 512 MB), e enche 2 a 3 vezes mais devagar que a lista, que fica abaixo de 1% de sobra em todo tamanho. Em 8 MB o crescimento já custa o que juntar os blocos custa aos motores que precisam de memória contígua (mORMot2, QUIC). -
DEFAULTCHUNKSIZEé 1 MB menos 1 KB: o cabeçalho do gerenciador de memória empurra uma potência de dois exata para o próximo grânulo de 64 KB. Blocos de 1 MB reservam 6,4% a mais que o corpo, e de 256 KB, 25%; de 1023 KB e 255 KB, 0,4%. O tamanho do bloco não muda a velocidade do gzip entre 64 KB e 64 MB.
-
A classe de cada formato fica guardada no registro.
RegisterCompressrecebe a classe eCompressClassesguarda o ponteiro: a escolha do formato (GetBestCompress, várias vezes por pedido) não passa porGetClass, que trava o registro global de classes do RTL.CompressDefsexiste só para a lista por nome do design. -
ctDeflateé deflate cru nos dois compiladores, que é o que o FPC grava; o Delphi usawindowBits-15. Na leitura,StartsWithZlibHeaderolha os dois primeiros bytes: o "deflate" do HTTP é zlib (RFC 9110 8.4.1.2), e o RAL e alguns servidores mandam cru. A leitura de gzip no FPC pula o cabeçalho e confere o trailer (CRC e tamanho) à mão, porque a zlib do FPC não lê o enquadramento gzip; a janela passada a ela é uma fatia do stream de quem chama, que pode ser a vista somente leitura do buffer de um motor. -
Um corpo vazio sai comprimido mesmo assim: um gzip, um zlib ou um deflate de
nada ainda é um stream (20, 8 e 2 bytes), e um decodificador estrito recusa um corpo
de zero bytes debaixo de um
Content-Encoding. ODecompressaceita o vazio como vazio. -
Janela e
memLevelpelo tamanho do corpo (ZLibSizes), com os pedaços na pilha: uma resposta pequena comprime sem alocar os 64 KB que oTCompressionStreamalocaria, e blocos desse tamanho o gerenciador de memória serve debaixo de uma trava só. -
O brotli é conduzido direto, um codificador para o corpo inteiro: o
TBrotliCompressionStreamdo pascal_brotli perde 1 KB por stream noFlushe cria um codificador porWrite, que é a coisa mais cara que o brotli faz (e pedaços comprimidos à parte comprimem pior). A saída é um stream brotli padrão. -
O zstd e o brotli se desregistram no
finalization: oRALCompressguarda o ponteiro da classe, e o pacote pode ser descarregado pela IDE sem ele.
-
AES-CBC com IV aleatório por mensagem e HMAC-SHA256: IV (16), texto cifrado com
PKCS#7, MAC (32) sobre IV e texto cifrado. As quatro formas de passar o corpo pela
cifra trabalham com dois buffers de
DEFAULTBUFFERSTREAMSIZEe assinam com o HMAC incremental, sem guardar uma segunda cópia da mensagem; o decifrar escreve cada pedaço em cima do texto cifrado de onde ele saiu, porque a leitura anda sempre à frente da escrita. - Sem threads. O CBC não se paraleliza na cifragem (cada bloco precisa do anterior), e um corpo pequeno pagaria a criação das threads.
-
A chave derivada (
rkdPBKDF2) fica em cache por texto de chave: uma cifra é criada para cada corpo, e as 100 mil rodadas são caras de propósito. Ela é calculada fora da trava; duas threads que encontram a mesma chave nova ao mesmo tempo calculam as duas, o que custa tempo e mais nada. Sem derivação (rkdNone, o padrão), a chave curta é completada com zeros até o tamanho do AES. -
Os hashes desligam overflow e range check na unit inteira: a aritmética é
módulo 2^32 / 2^64 por definição, e um build Debug com
{$Q+}levantariaEIntOverflowem todo bloco. Um pedaço que não completa o bloco soma ao que o bloco já tem (HashUpdate): 16 + 16 bytes têm de dar o mesmo resumo que 32 de uma vez. -
JWT: só um objeto é cabeçalho (um array, um texto ou um número deixam o cabeçalho
como estava, como texto que não interpreta); um texto que não é token não deixa nada
do token anterior; uma claim gravada duas vezes é substituída, porque a lista é
ordenada pela linha
chave=valorinteira e uma chave repetida é JSON inválido; as datas (exp,iat,nbf) passam porRALDateTimeToGMTantes de virar tempo Unix. - Basic: uma credencial que não é base64 é uma credencial errada (401), não uma exceção do decodificador (500).
-
RandomBytesusa a fonte criptográfica do sistema (RtlGenRandomno Windows,/dev/urandomcom o handle guardado nos outros): nonce, id de token e IV do AES não podem ser adivinháveis. -
RALOpenSSLeRALCriptoOpenSSLficam em modo Delphi no FPC: as funções do OpenSSL são variáveis procedurais, e no ObjFPCEVP_sha256sem parênteses devolveria a variável em vez de chamar a função. Antes do OpenSSL 1.1.0 toda operação passa por uma trava (BeginOperation), porque a biblioteca não é thread-safe sozinha. -
O certificado do
RALX509segue a receita do certificado de desenvolvimento do ASP.NET Core: folha (cA FALSE),digitalSignature+keyEncipherment,serverAuth, nomes e endereços nosubjectAltNamee os identificadores de chave. Um autoassinado comcA TRUEé recusado pelo Firefox como certificado de servidor, e um semsubjectAltNamepor todo navegador atual. O PKCS#12 é o que o OpenSSL 3 grava (PBES2 com PBKDF2-HMAC-SHA256 e AES-256-CBC, MAC HMAC-SHA256), que o SChannel lê a partir do Windows 10 1709.
-
TRALChunkedStreamguarda o corpo em blocos deBlockSize: acrescentar nunca move o que já está lá, e comReleaseOnReadcada bloco lido volta na hora, então uma transformação segura entrada e saída ao custo de um bloco. OReadenche o pedido inteiro atravessando blocos, porque o RAL lê um tamanho com umReadsó. -
Toda leitura de tamanho fixo confere o que leu (
ReadExactdoTRALBinaryWritere doTRALStorageBIN): esses valores vêm da rede, e uma leitura curta deixaria o valor com o lixo da pilha. Um byte que vira enumerado é conferido antes do cast (ReadEnum,RALIsFieldTypeOrdinal), e uma contagem que o resto do stream não comporta é recusada antes de alocar. -
O memo viaja como texto UTF-16 little-endian no BIN, seja qual for a codificação
do campo (ANSI no
ftMemodo Delphi, UTF-8 no do FPC), porque o leitor o carrega numftWideMemo. No JSON e no CSV o memo é o texto. -
Os leitores de storage devolvem os campos somente leitura e reativam os controles
num
finally: um registro que falha no meio não pode deixar o dataset aberto para escrita. -
CSV: o stream é lido byte a byte (um
Chartem dois bytes no Delphi), o separador é o deFormatOptions, aspas dentro de um valor saem dobradas (RFC 4180) e o BOM é escrito como três bytes - um array dinâmico passado a um parâmetroconstsem tipo é o ponteiro dele. -
Multipart: o delimitador é
--mais o boundary e nada mais (RFC 2046), como oContent-Typeo anuncia; o parser do libmicrohttpd (Sagui) ancora o delimitador no início da linha e descarta o corpo que difere. O boundary é aleatório, não do relógio: um boundary previsível deixa um valor forjado encerrar o multipart. Um boundary entre aspas (boundary="...", o que oHttpClientdo .NET manda) perde as aspas. -
TRALMIMETypeprocura a extensão num hash (endereçamento aberto, potência de dois, no máximo meio cheio), porqueGetMIMETyperoda para cada arquivo servido, e a lista é ordenada porAnsiCompareText, que no Windows ignora hífen: uma busca binária byte a byte não acharia uma extensão com hífen. -
TRALStringListSafe.IsEmptylê sem a trava:Counté um campo, e a resposta já está velha no instante em que volta, com trava ou sem; quem chama a usa como dica. As listas de segurança, vazias na maioria dos servidores, são perguntadas em todo pedido. -
FixRoutedevolve o mesmo texto quando o caminho já está certo (os motores e a busca de rota perguntam os dois) e junta barras repetidas numa passada só: linear no tamanho, seja quantas barras a URI tiver.
Socket, TLS, WebSocket e proxy que o RAL usa sem motor (o MQTT hoje; e-mail, balanceador e clientes WebSocket depois). As restrições:
-
Um código só para todo sistema. Só as declarações de onde vem cada função (ws2_32
ou a libc) e um punhado de constantes mudam, todas num bloco no começo da
implementação de
RALSocket. Esperar é semprepoll(WSAPollno Windows): o socket é não bloqueante por baixo, eConnect/WaitForesperam com prazo. -
Um relógio só (
RALSocketNow, milissegundos que só andam para frente): os prazos dos sockets e os do protocolo de cima (oRALMQTTNowdevolve o mesmo) se comparam. -
A OpenSSL é carregada em execução, 1.1.1 ou 3.x, para que o pacote não dependa de
nada na compilação; o TLS é um canal de camada (
TRALLayerChannel) que entra por cima de um canal aberto, e é assim que oSTARTTLSfunciona. -
A rede não conhece o protocolo de cima. Ela levanta
ERALSocketError,ERALTLSErroreERALWebSocketError(com oCloseCodeque a falha pede); quem está em cima traduz (o broker MQTT manda o DISCONNECT com o motivo certo). O subprotocolo do WebSocket é uma propriedade (Protocol), não uma constante. - Valores de enumeração publicados mantêm o nome antigo (
mtv12,mpkHTTP): os formulários gravam o nome do valor. -
O laço de eventos é da rede, o protocolo é do dono (
TRALSocketLoop, emRALSocketLoop): uma thread aceita nos sockets que o dono escuta, N threads depollficam cada uma com uma fatia das conexões (a nova vai para a que tem menos), e um relógio chama o dono a cadaTimerInterval. Só a thread dona lê e escreve o canal de uma conexão; as outras enfileiram e chamamWake, que manda um datagrama para a própria thread e encerra a espera. O dono desce deTRALSocketLoopConnectione dá o protocolo emServe(uma passada: ler, responder, prazos),HandleError,FlushOutput,HasOutputeStopping(as últimas palavras quando o servidor para). O broker MQTT é o primeiro dono. -
O laço não decide o que esperar: a conexão diz (
PollEvents). O padrão é o que o canal quer maisRALPollOutcom saída pendente; um dono devolve menos para parar de ler (contrapressão) ou sóRALPollOutenquanto um connect termina.Returneddiz o que opolltrouxe naquela passada, paraServenão ler à toa.Attachentrega uma conexão à mesma thread de outra: duas que conversam (os dois lados de um proxy) nunca precisam de trava.Thread.Contextguarda um objeto do dono por thread. -
Connect sem esperar (
ConnectStart+PendingError) para quem está num laço: o socket fica gravável quando termina e oSO_ERRORdiz se deu. No Windows, para um endereço de loopback, o SYN não é repetido (SIO_TCP_INITIAL_RTO): sem isso uma porta fechada só falha depois de ~2 s, porque o Windows insiste antes de aceitar o RST. -
Retomada de sessão TLS do cliente. Os contextos são um por conexão, então as
sessões ficam fora deles, em
RALTLSSessions, pela chave deRALTLSSessionKey: o nome conferido, o host e a porta, e o certificado do cliente - uma sessão só volta para o mesmo servidor e a mesma identidade. Ela chega pelo retorno de chamada de sessão nova, porque no TLS 1.3 o ticket vem depois do handshake, com a primeira leitura. A regra do certificado roda de novo numa conexão retomada (a sessão guarda o certificado do servidor). -
Um certificado por nome no servidor (
AddCertificate): cada nome ganha um contexto filho, e o retorno de chamada de SNI troca o contexto da conexão (SSL_set_SSL_CTX). O filho responde o ALPN do pai e copia a confiança dos certificados de cliente (a verificação usa a loja do contexto em vigor).StrictServerNamerecusa com o alertaunrecognized_name. -
Ler o ClientHello sem abrir o TLS (
RALTLSPeekClientHello, sem OpenSSL;RALTLSPeekServerNamecomMSG_PEEK): um handshake pode vir partido em vários registros, e o que foi espiado continua no socket para a OpenSSL. -
Fim sem
close_notifyé fim. A OpenSSL 1.1.1 avisa isso comoSSL_ERROR_SYSCALL(lido como fechamento); a 3.x, como erro. O contexto ligaSSL_OP_IGNORE_UNEXPECTED_EOFsó na 3.x (o valor0x80era outra coisa antes), e os protocolos de cima delimitam as próprias mensagens. -
O SCRAM é cripto pura (
RALSCRAM, emsrc/crypto, no pacote-base): nenhum transporte; o MQTT (e depois SMTP, IMAP) leva as mensagens do seu jeito e traduzERALSCRAMErrorno erro dele. Um usuário desconhecido percorre os mesmos passos com valores inventados: a resposta não conta quem existe.
-
Os dois lados de uma conexão são duas conexões do laço, na mesma thread
(
Attach): um lê direto no buffer de saída do outro, sem trava e sem cópia intermediária. Quem não esvazia o próprio buffer faz o outro parar de ler. -
As rotas que as threads leem são uma cópia (
TRALBalancerRouting), trocada inteira porReloade guardada até o balanceador parar: um pedido em curso continua com a regra e o upstream que pegou. Pelo mesmo motivo os upstreams que saem de uso não são liberados pelo balanceador. -
As conexões mantidas com os servidores de trás são da thread (um
TRALBalancerPoolemThread.Context): o lado do cliente só pode usar uma conexão da sua thread. Uma ociosa que fica legível foi fechada pelo outro lado e sai. -
O enquadramento é lido, não refeito: o
chunkedpassa como veio e só é seguido até o fim, para saber onde a mensagem termina; bytes depois do fim de um pedido são o próximo pedido (pipelining) e voltam para a fila do cliente. Por isso as regras de contrabando são estritas: o balanceador e o servidor de trás têm de concordar sobre onde cada pedido acaba. - Uma resposta de erro fecha devagar (o lingering close do NGINX): o que o cliente ainda manda depois é lido e jogado fora por até 2 s. Fechar com bytes não lidos faz o sistema mandar RST, e o cliente perde a resposta (no Windows sempre).
- No Windows, escutar em todos os endereços numa porta que outro programa tem só em
127.0.0.1dá certo (no Linux, não): o teste de porta ocupada usa uma escuta em todos os endereços.
Começar
Servidor
- Rotas e parâmetros
- Rotas avançadas: tipos, grupos, versões 🚧
- Ler o pedido e responder
- Objetos no corpo 🚧
- Resposta em fluxo 🚧
- Plugins 🚧
- Autenticação
- Banco de dados (DBWare)
- Servir site e arquivos
- Swagger e Postman
- Servidor SOAP 🚧
- WS-Security 🚧
- HTTPS
- Criptografia ponto a ponto
- Segurança para produção
- Colocar no ar
- Desempenho
- Testes em memória 🚧
MQTT 🚧
- O que é e quando usar 🚧
- Cliente MQTT 🚧
- Broker MQTT 🚧
- MQTT-SN 🚧
- Ponte REST 🚧
- Segurança 🚧
- Rede ruim 🚧
Cliente
Motores
Avançado
Ajuda
- Problemas comuns
- Atualizar o Indy
- Várias versões do Delphi
- Vídeos e guias
- Módulos de terceiros
- Licenças
- Contribuir
- Roadmap e changelog
🚧 = em desenvolvimento, pode mudar