Skip to content
mobius1qwe edited this page Nov 4, 2026 · 5 revisions

Internos: Utilitários 🚧

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).

Storages e o TBufDataset

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.

Texto em bytes (RALTypes)

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).

Tamanhos de buffer e de bloco (RALConsts)

  • 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: um TMemoryStream enchido 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 rede própria (RALNetPackage, src/net) 🚧

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 é sempre poll (WSAPoll no Windows): o socket é não bloqueante por baixo, e Connect/WaitFor esperam com prazo.
  • Um relógio só (RALSocketNow, milissegundos que só andam para frente): os prazos dos sockets e os do protocolo de cima (o RALMQTTNow devolve 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 o STARTTLS funciona.
  • A rede não conhece o protocolo de cima. Ela levanta ERALSocketError, ERALTLSError e ERALWebSocketError (com o CloseCode que 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, em RALSocketLoop): uma thread aceita nos sockets que o dono escuta, N threads de poll ficam cada uma com uma fatia das conexões (a nova vai para a que tem menos), e um relógio chama o dono a cada TimerInterval. Só a thread dona lê e escreve o canal de uma conexão; as outras enfileiram e chamam Wake, que manda um datagrama para a própria thread e encerra a espera. O dono desce de TRALSocketLoopConnection e dá o protocolo em Serve (uma passada: ler, responder, prazos), HandleError, FlushOutput, HasOutput e Stopping (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 mais RALPollOut com saída pendente; um dono devolve menos para parar de ler (contrapressão) ou só RALPollOut enquanto um connect termina. Returned diz o que o poll trouxe naquela passada, para Serve não ler à toa. Attach entrega uma conexão à mesma thread de outra: duas que conversam (os dois lados de um proxy) nunca precisam de trava. Thread.Context guarda 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 o SO_ERROR diz 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 de RALTLSSessionKey: 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). StrictServerName recusa com o alerta unrecognized_name.
  • Ler o ClientHello sem abrir o TLS (RALTLSPeekClientHello, sem OpenSSL; RALTLSPeekServerName com MSG_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 como SSL_ERROR_SYSCALL (lido como fechamento); a 3.x, como erro. O contexto liga SSL_OP_IGNORE_UNEXPECTED_EOF só na 3.x (o valor 0x80 era outra coisa antes), e os protocolos de cima delimitam as próprias mensagens.
  • O SCRAM é cripto pura (RALSCRAM, em src/crypto, no pacote-base): nenhum transporte; o MQTT (e depois SMTP, IMAP) leva as mensagens do seu jeito e traduz ERALSCRAMError no erro dele. Um usuário desconhecido percorre os mesmos passos com valores inventados: a resposta não conta quem existe.

O balanceador dedicado (RALBalancer) 🚧

  • 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 por Reload e 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 TRALBalancerPool em Thread.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 chunked passa 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.1 dá certo (no Linux, não): o teste de porta ocupada usa uma escuta em todos os endereços.

Clone this wiki locally