pascal-pipes-faa
Cross-platform inter-process communication for Delphi 12+ (Win64) and FPC/Lazarus (Linux x86_64/ARM64): the same API at three scopes — local IPC (Named Pipe/UDS), TCP and TLS with optional mTLS. Synchronous Request-Reply, topic pub/sub with wildcards and retain, AutoReconnect and thread-safe dispatch (pool/serialized/main thread).
- Active · last change 11-2-2026
- ★ 6 on GitHub
- 1 finds this interesting
README
pascal-pipes-faa
🇬🇧 This document is also available in English.
Antes
pascal-named-pipes-faa. O nome mudou porque o Named Pipe do Windows passou a ser apenas um dos transportes suportados — a API antiga segue funcionando (ver Compatibilidade).
Biblioteca multiplataforma de comunicação entre processos para Delphi 12+ (Win32, Win64 e Android) e FPC 3.2.2 / Lazarus (Windows x64 e Linux x86_64), com uma única base de código e uma API de alto nível que abstrai completamente as chamadas nativas do sistema operacional.
As suítes de teste rodam nessas cinco combinações: Delphi Win32/Win64 e FPC Windows x64 na
máquina de desenvolvimento, FPC Linux x86_64 em Docker (tools/test_fpc_docker.sh) e Delphi
Android em aparelho real (tests/Android). Linux ARM64 usa o mesmo código POSIX, mas não
é testado.
A mesma API atende três alcances, trocando uma property:
Transport |
Alcance | Por baixo |
|---|---|---|
ptLocal (padrão) |
mesma máquina | Named Pipe (Windows) / Unix Domain Socket (Linux) — não existe no Android |
ptTcp |
rede | socket TCP, com keepalive ligado por padrão |
ptTls |
rede não confiável | o mesmo TCP com TLS, e mTLS opcional por certificado |
// Servidor
Server := TPipeServer.Create('meu_app');
Server.OnMessage := MinhaClasse.HandleMessage; // procedure ... of object
Server.Listen;
// Cliente
Client := TPipeClient.Create('meu_app');
Client.Connect(5000);
Client.SendText('olá!');
Resposta := Client.RequestText('ping', 3000); // RPC síncrono com timeoutComo funciona por baixo
| Windows | Linux | |
|---|---|---|
| Transporte | Named Pipes reais (CreateNamedPipe/ConnectNamedPipe), modo byte, sempre overlapped |
Unix Domain Sockets (AF_UNIX/SOCK_STREAM) |
| Nome nativo | \\.\pipe\meu_app |
/tmp/meu_app.sock |
| Interrupção de I/O blocante | WaitForMultipleObjects + CancelIoEx |
fpPoll + self-pipe + fpShutdown |
No Linux, "named pipe" é implementado como Unix Domain Socket — a mesma abordagem do
.NET (NamedPipeServerStream/NamedPipeClientStream no Unix). UDS dá paridade semântica
total com o Named Pipe do Windows: conexões por cliente, bidirecional, detecção de queda.
FIFOs (mkfifo) ficaram fora da v1; a camada de transporte abstrata deixa a porta aberta.
Se Address já for um caminho nativo (\\.\pipe\... ou /caminho/abs.sock), ele é usado
como está — útil para controlar o diretório (e as permissões) do socket no Linux.
Transporte
A property Transport (TPipeTransport) escolhe por onde os frames trafegam. O padrão é
ptLocal, então quem só faz IPC local não precisa saber que isso existe:
TPipeServer.Create('meu_app'); // ptLocal (padrão)
TPipeServer.Create('meu_app', ptLocal); // idem, explícito
TPipeServer.Create('0.0.0.0:5000', ptTcp); // TCP
TPipeClient.Create('192.168.0.10:5000', ptTcp);
TPipeServer.Create('0.0.0.0:5000', ptTls); // TCP + TLS (credenciais em TlsOptions)O enum nomeia alcance, não mecanismo: ptLocal é "o melhor IPC local deste SO" —
Named Pipe no Windows, Unix Domain Socket no Linux. Um ptNamedPipe seria um nome errado
metade das vezes.
Address e Transport são validados juntos na ativação: Create('\\.\pipe\X', ptTcp)
falha com EPipeError explicando o conflito, em vez de estourar mais tarde num erro
obscuro de resolução de nome.
Endereços aceitos por ptTcp: host:porta, IPv6 entre colchetes ([::1]:5000) e *
como atalho de 0.0.0.0. A resolução é via getaddrinfo, então nome de host e IPv6
funcionam. TCP_NODELAY é ligado (o atraso do Nagle penalizaria muito Request/Reply).
Segurança: diferente de
ptLocal,ptTcpnão herda controle de acesso do SO (ACL do Windows, permissão de arquivo do UDS). Um listener em0.0.0.0aceita qualquer um que alcance a porta — autenticar é responsabilidade da aplicação. É esse buraco queptTlsfecha.
Android
Android é um terceiro eixo de plataforma (Delphi, Android64/Android32), ao lado de
Delphi/Win64 e FPC/POSIX. A API é a mesma; muda o que está disponível:
ptTcpeptTlsfuncionam, cliente e servidor.ptLocalnão existe — app Android é single-process e expor um Unix Domain Socket esbarra em sandboxing. Usá-lo levantaEPipeErrordizendo o que fazer, em vez de falhar obscuramente adiante.ptTlsé sempre OpenSSL (Schannel é Windows-only;TlsOptions.BackendemtbDefaultjá significa OpenSSL lá). Você precisa empacotarlibcrypto.soelibssl.sopor ABI no Deployment do app — versamples/EchoAndroid/LEIA-ME.md.- Permissão
INTERNETno manifesto é obrigatória, eptTcp(texto claro) ainda exigeusesCleartextTrafficdo Android 9 em diante. Mais um motivo para preferirptTls. - Recomendado num celular:
DispatchMode := pdmMainThread(eventos chegam pela thread principal, dá para mexer na UI direto) eConnectfora da thread principal, já que ele bloqueia até o prazo.
Verificação é em aparelho: não há par dual-compiler (o FPC não compila para Android neste
projeto). A suíte de device fica em tests/Android/. Racional completo em
docs/ARQUITETURA.md §13.
Endereço do cliente (TryClientAddress)
Em ptTcp/ptTls (com ou sem mTLS) o servidor sabe de onde cada cliente veio:
procedure TForm1.ClienteConectou(Sender: TObject; AConnId: TPipeConnectionId);
var
LEndereco: string;
begin
if Servidor.TryClientAddress(AConnId, LEndereco) then
FileLog('pipe-connection', 'Cliente conectado: ' + LEndereco)
else
FileLog('pipe-connection', 'Cliente conectado: ' + AConnId.ToString); // ptLocal
end;False em ptLocal: Named Pipe/UDS não tem endereço IP, só um handle/fd local — não é
"ainda não chegou", é sempre assim para esse transporte. O endereço vem no formato
'ip:porta' (IPv6 entre colchetes, mesma convenção do Address que você mesmo passa para
Connect) e sobrevive à saída do cliente, mesmo critério de TryClientIdentity logo
abaixo — dá para responder "de onde veio quem saiu?" dentro do próprio
OnClientDisconnected.
TLS (ptTls)
ptTls é o mesmo socket TCP com TLS por cima: mesmo formato de Address, mesmas
garantias de threading. O que muda é que o tráfego é cifrado e que o par pode ser
autenticado por certificado.
Srv := TPipeServer.Create('0.0.0.0:5000', ptTls);
Srv.TlsOptions.CertFile := 'srv.pfx'; // PFX no Windows/Schannel
Srv.TlsOptions.CertPassword := 'senha';
Srv.Listen;
Cli := TPipeClient.Create('servidor.empresa:5000', ptTls);
Cli.Connect(5000);As credenciais são lidas uma vez, no Listen/Connect: mudá-las com o componente
ativo levanta EPipeError em vez de aceitar em silêncio uma configuração sem efeito.
Erro de senha ou de arquivo aparece já no Listen, e não quando o primeiro cliente
conectar.
mTLS (autenticar o cliente)
Preencher CaFile no servidor liga mTLS: o cliente passa a ser obrigado a apresentar
um certificado que encadeie até aquela CA. Quem não apresentar — ou apresentar de outra CA
— é recusado, e OnClientConnected nunca dispara para ele.
Srv.TlsOptions.CaFile := 'ca.pem'; // liga mTLS
Cli.TlsOptions.CertFile := 'pdv-001.pfx';
Cli.TlsOptions.CertPassword := 'senha';Esse é o desenho pensado para o caso de PDVs de loja sobre VPN: o certificado, e não o IP de origem, é o que diz quem é quem.
Para gerar sua própria CA/certificados (não a PKI de teste de tests/pki), veja
tools/gerar-pki.sh e o guia passo a passo em tools/LEIA-ME.md.
O que muda entre os backends
Desde a 0.4.0 o backend é escolhido em tempo de execução, por TlsOptions.Backend:
tbDefault (Schannel no Windows, OpenSSL no resto), tbSChannel (só Windows; fora dele
levanta EPipeTls) ou tbOpenSsl. No Windows os dois estão sempre no executável e o
OpenSSL só é carregado no primeiro uso. Compilar com -dPIPES_OPENSSL continua valendo, mas
agora só muda o que tbDefault significa (OpenSSL também no Windows). Os dois backends vêm
da pascal-common-faa (PascalCommon.Transport.Tls).
Cli.TlsOptions.Backend := tbOpenSsl; // OpenSSL no Windows, sem outro buildDuas diferenças que importam na configuração:
| Schannel (Windows) | OpenSSL | |
|---|---|---|
| Formato do certificado | CertFile = PFX (cert + chave juntos), CertPassword |
CertFile = PEM, KeyFile = PEM da chave |
CaFile |
PEM nos dois: no servidor, a CA dos certificados de cliente (mTLS); no cliente, a CA que valida o servidor no lugar do trust store | idem |
Até a 0.3.0 o cliente Schannel ignorava CaFile e só validava contra o trust store do
Windows; uma PKI privada exigia instalar a CA na máquina ou usar o OpenSSL. Não exige mais.
Como o cliente vê uma recusa de mTLS — e por que difere por backend. O Schannel completa o handshake e só então entrega o certificado do cliente para a aplicação validar (é o que
VerifyClientChainfaz). Consequência: um cliente recusado vêOnConnecteddisparar normalmente e a conexão cair logo em seguida. No OpenSSL a validação acontece dentro do handshake, então a recusa chega como falha de conexão eOnConnectednunca dispara.Uma aplicação que precise distinguir "fui aceito" de "vou ser derrubado" no lado cliente não pode confiar só em
OnConnectedsob Schannel. O padrão prático é o do sampleChatSeguro: uma sessão que morre quase junto com oOnConnectedé recusa de credencial, não queda de rede — e aí não adianta reconectar, porque a credencial não vai passar a ser aceita sozinha.
Quem está do outro lado
Sob mTLS o servidor não só valida o certificado do cliente — ele guarda quem é:
procedure TForm1.ClienteConectou(Sender: TObject; AConnId: TPipeConnectionId);
var
LQuem: TPipePeerIdentity;
begin
if Servidor.TryClientIdentity(AConnId, LQuem) then
Memo.Lines.Add('conectou: ' + LQuem.CommonName); // 'pdv-loja-001'
end;O CommonName é confiável porque a cadeia foi validada antes: um certificado com
CN forjado não chega a disparar OnClientConnected, é recusado no handshake. Então dá
para usar esse nome para identificar — mostrar, logar, rotear. O que não se deve fazer é o
inverso: derivar autorização de um nome sem que a cadeia tenha sido verificada.
TryClientIdentity devolve False quando não há identidade verificada — sem TLS, ou com
TLS sem mTLS. False nunca significa "ainda não chegou": não há o que esperar.
A identidade sobrevive à saída do cliente, então OnClientDisconnected também pode
perguntar "quem saiu?" — sem isso um painel só conseguiria dizer "conexão 7 saiu". Ficam
retidas as últimas PIPES_RECENT_IDENTITIES (256) conexões autenticadas. O motivo de não
liberar junto com a conexão é que o evento e a limpeza vão para filas diferentes, sem
ordem garantida entre si: amarrar a vida da identidade à limpeza seria uma corrida.
Mudança de comportamento:
ClientCounteClientIdspassaram a contar apenas conexões estabelecidas — aquelas para as quaisOnClientConnectedjá disparou. Antes, uma conexão aceita mas ainda negociando TLS já aparecia ali, o que sob mTLS significava exibir como "cliente" um par que talvez fosse recusado em seguida.Broadcastsegue a mesma regra, e por um motivo mais forte que contagem: mandar payload para quem ainda não se autenticou seria vazar dado. ParaptLocaleptTcpnada muda na prática — sem handshake, a conexão nasce estabelecida.
MaxClientsé deliberadamente diferente: como é limite de recurso, conta também as conexões em negociação — senão um par que nunca conclui o handshake não ocuparia vaga.
Prazo do handshake
O handshake tem prazo próprio, PIPE_TLS_HANDSHAKE_TIMEOUT_DEFAULT (15 s), ajustável por
TlsOptions.HandshakeTimeoutMs. Sem ele, quem abrisse o TCP e nunca mandasse o
ClientHello prenderia uma thread do servidor para sempre — algumas dezenas de conexões
meia-abertas derrubariam o serviço sem enviar um byte útil. O prazo vale só durante a
negociação: depois dela a conexão volta a poder ficar ociosa à vontade, e quem cuida de
par morto ali é o keepalive.
HandshakeTimeoutMs = 0 significa o padrão, não "sem prazo" — desligar exige
PIPE_TLS_HANDSHAKE_NO_TIMEOUT explícito.
Keepalive (KeepAliveSeconds)
Uma conexão TCP pode morrer em silêncio — cabo, máquina desligada, ou o timeout de ociosidade de um túnel VPN/NAT. Nenhum dos dois lados é avisado, e o reader ficaria esperando para sempre. Em IPC local isso não existe: a morte do processo par sempre fecha o pipe.
Por isso ptTcp liga keepalive TCP por padrão, com 20 s de ociosidade
(PIPES_DEFAULT_KEEPALIVE_SECONDS). ptTls herda a mesma configuração — é o mesmo socket
por baixo, e o keepalive acontece na camada TCP, sem interferir na sessão TLS. ptLocal
ignora a property.
Server.KeepAliveSeconds := 20; // padrão
Server.KeepAliveSeconds := 0; // desligaO valor serve a dois propósitos, e o segundo costuma ser o mais importante:
- Detectar conexão morta — com os padrões, em ~35 s (20 s ociosos + 3 probes a cada
5 s). A detecção vira
EPipeClosed, que disparaOnClientDisconnectedno servidor eOnDisconnected+AutoReconnectno cliente. - Manter vivo o mapeamento de NAT/VPN de uma conexão ociosa, evitando que ela morra.
Por isso o valor precisa ser menor que o timeout de ociosidade do túnel, não maior
— se a sua VPN derruba sessão ociosa em 30 s,
KeepAliveSecondstem que ficar confortavelmente abaixo disso.
No servidor isso importa mais do que parece: sem keepalive ele acumula conexões zumbi
indefinidamente — Broadcast escrevendo para clientes que não existem mais e
ClientCount mentindo.
Diferença entre plataformas: no POSIX os três parâmetros (ocioso, intervalo, número
de probes) são ajustáveis por socket, então a detecção é exatamente a descrita. No Windows
usa-se SIO_KEEPALIVE_VALS (disponível desde o Windows 2000, ao contrário de
setsockopt(TCP_KEEPIDLE), que exige Win10 1709+ — relevante para hardware antigo), e ele
não expõe a contagem de probes: ela é fixa no SO (2 do Vista em diante). O tempo até
detectar difere um pouco; a manutenção do mapeamento NAT/VPN, que depende só do tempo
ocioso, é idêntica nos dois.
As mensagens trafegam num framing próprio (NPF1: header de 20 bytes little-endian com
magic, kind, correlation id e length), idêntico nos dois SOs — fronteiras de mensagem são
da biblioteca, nunca do transporte. Payloads são TBytes; os métodos *Text convertem
de/para UTF-8 de forma portátil.
Quem precisa falar com um TPipeServer/TPipeClient a partir de outra linguagem
(Python, C#, Go, …) tem a especificação completa do protocolo de fio — NPF1, envelopes de
tópico/comando, kind 7 (compressão), NPD1 (descoberta) — em
docs/INTEROP.md, com exemplos em hex e pseudocódigo de leitura.
Heartbeat de aplicação (HeartbeatIntervalMs)
Complementar ao Keepalive acima, não substituto. KeepAliveSeconds é um probe do SO:
barato, mas tipicamente leva minutos para acusar e enxerga só o socket TCP por baixo —
nunca o que atravessa o registro cifrado do ptTls. HeartbeatIntervalMs resolve o mesmo
problema em cima do framing, com um frame de aplicação (pfkPing) e controle fino do
tempo de detecção:
Server.HeartbeatIntervalMs := 5000; // desligado por padrao (0); so' ptTcp/ptTls
Client.HeartbeatIntervalMs := 5000; // mesmo valor nos dois lados, por simplicidadeSimétrico e sem correlação: qualquer frame recebido (o próprio Ping incluso) reseta o
relógio de leitura de quem o recebeu — não existe pfkPong nem "ping em aberto" a
rastrear. Conexão morta = nenhum frame recebido em 2x o intervalo configurado; quem
detecta fecha a própria conexão e segue o fluxo normal de queda
(OnClientDisconnected/OnDisconnected + AutoReconnect, sem evento dedicado). ptLocal
ignora a property, pela mesma razão do Keepalive: a morte do processo par já fecha o
pipe/UDS local na hora.
Métricas/observabilidade (Stats/ConnectionStats)
Snapshot sob demanda — mesmo molde de ClientCount/ClientIds/Subscriptions: a lib não
empurra nada periodicamente, o app pergunta quando quiser. Sempre ativos, sem opt-in (o
custo é um incremento atômico por frame). Válido em qualquer transporte, inclusive
ptLocal.
LConnStats: TPipeConnStats;
if Server.ConnectionStats(AConnId, LConnStats) then
WriteLn(LConnStats.BytesReceived, ' bytes recebidos desta conexao');
LSrvStats := Server.Stats; // agregado, cumulativo desde o Listen
LCliStats := Client.Stats; // da SESSAO atual, zera a cada reconexao
WriteLn('fila do pool: ', LSrvStats.PoolQueueDepth);
WriteLn('latencia media de request: ', LCliStats.AvgRequestLatencyMs, ' ms');Server.ConnectionStats(AConnId, out AStats): Boolean— bytes/mensagens enviados e recebidos por UMA conexão. Morre com ela (como as assinaturas de tópico), diferente deTryClientIdentity.Falsese a conexão não existe ou não está estabelecida.Server.Stats: TPipeServerStats— agregado cumulativo desde oListen, sobrevive a conexões que já caíram:TotalConnectionsAccepted(só estabelecidas),TotalBytesSent/ Received,TotalMessagesSent/Received,ClientCount, ePoolQueueDepth. Ressalva: empdmPool(padrão) o pool de despacho é oPcPoolda pascal-common-faa, GLOBAL do processo: compartilhado por todoTPipeServer/TPipeCliente pelas outras libs*-faaque o usam (amqp, redis) —PoolQueueDepthreflete o backlog de todo mundo, não só deste servidor nem só do pipes. Só é exclusivo dele empdmSerialized.Client.Stats: TPipeClientStats— bytes/mensagens da SESSÃO atual (zera a cadaConnect/reconexão, sem contador cumulativo entre sessões),ReconnectAttempts,PendingRequests, eAvgRequestLatencyMs/MaxRequestLatencyMs— só contam Requests que chegaram a ter reply (timeout e erro ficam de fora: são "o servidor não respondeu", uma pergunta diferente de "quanto tempo levou").BytesSent/BytesReceivedvsBytesSentWire/BytesReceivedWire(nos três:Server. ConnectionStats,Server.StatscomoTotalBytesSent/ReceivedWire, eClient.Stats) — os primeiros são o payload LÓGICO (visão do app, o queCompressionMinSizeNÃO muda); os*Wiresão o que de fato passou pelo fio, já refletindo a compressão quando ela comprimiu algum frame.BytesSent - BytesSentWireé a economia real de banda; semCompressionMinSizeligado os dois pares são sempre idênticos.
Ordem por grupo em pdmPool (AGroupKey de SendBytes/SendText)
pdmPool (o DispatchMode padrão) despacha cada mensagem recebida a um pool de workers —
rápido, mas sem ordem garantida de entrega entre mensagens distintas ao OnMessage (só a
ordem no fio, que já é sempre preservada). Para a maioria dos apps isso nunca importa; para
quem precisa que um SUBCONJUNTO das mensagens processe na ordem em que foi mandado (ex.: os
eventos de um caixa específico numa loja), sem abrir mão do paralelismo entre os demais:
Client.SendBytes(AData, 'caixa.3'); // toda mensagem de 'caixa.3' processa em ordem...
Client.SendBytes(AData2, 'caixa.4'); // ...e em PARALELO com 'caixa.4', nao atras dela- Sem
AGroupKey(padrão,''): comportamento de sempre, sem custo nenhum. - A garantia é sobre a ENTREGA ao
OnMessagedo lado que RECEBE, não sobre quem manda — tantoTPipeClient.SendBytesquantoTPipeServer.SendBytesaceitam o parâmetro, e é oDispatchModede quem RECEBE que decide se a chave faz diferença. - Só se aplica a
pdmPool: empdmSerialized/pdmMainThreada ordem já é total, a chave é ignorada (não muda nada, não é erro usar). - Chaves são efêmeras — não há limite de chaves distintas nem custo residual: a estrutura interna nasce com a primeira mensagem pendente daquela chave e morre quando não há mais nenhuma. Reaproveitar uma chave depois de esvaziar começa do zero.
- Sem mudança de wire format: a chave viaja no próprio
CorrIddo header NPF1 (hash de 64 bits), campo que já existia e que mensagens avulsas nunca usavam.
Connect assíncrono (ConnectAsync)
Connect(TimeoutMs) bloqueia a thread chamadora e levanta se o servidor não responder no
prazo. AutoReconnect não ajuda aí: ele só entra em ação depois que uma sessão JÁ existiu
e caiu. O caso que ficava sem resposta é o mais comum de todos numa loja: o app sobe antes
do servidor.
Client := TPipeClient.Create('retaguarda:9000', ptTcp);
Client.OnConnected := TrataConectado;
Client.OnError := TrataErro;
Client.AutoReconnect := True; // cuida das quedas DEPOIS da primeira conexao
Client.ReconnectDelayMs := 3000; // orcamento por tentativa E espacamento entre elas
Client.ConnectAsync; // volta na hora; segue tentando em segundo plano
// enquanto Client.Connecting, Client.Connected e' False e Send*/Request levantam
// EPipeClosed, como em qualquer janela sem sessao.- Não tem parâmetro de timeout de propósito. O orçamento é
MaxReconnectAttempts×ReconnectDelayMs, as properties que já existiam — comMaxReconnectAttempts = 0(padrão) tenta para sempre. É um teto APROXIMADO de tempo de parede; quem precisa de prazo exato chamaDisconnectde um timer. - Sucesso em
OnConnected, desistência emOnError('initial connection gave up after N attempts') — não há exceção a levantar, o chamador já voltou. Connectingdiz se ainda está tentando. É só sobreConnectAsync:Connectsíncrono e reconexão automática nunca o ligam.Disconnectcancela (e chamarConnectAsyncde novo também: cancela a tentativa anterior e recomeça). O cancelamento vale a partir da tentativa em curso — umPipeConnectjá em progresso ainda pode levar atéReconnectDelayMspara voltar, o mesmo trade-off que a reconexão automática já aceita.- Compõe com o resto:
AutoReconnectcuida das quedas depois da primeira conexão;FailoverAddressesé respeitado a cada tentativa, com a mesma regra da reconexão; assinaturas de pub/sub feitas antes de conectar são enviadas quando a sessão subir. - Enquanto tenta,
Address/Transport/TlsOptionsficam travados (EPipeError), como com o componente ativo — senão a tentativa seguinte miraria outro servidor sem ninguém ter pedido.
Diagnóstico de tentativas (OnConnectAttemptFailed)
A lib opera em silêncio: durante uma queda o cliente segue tentando, e se a reconexão der certo ninguém é interrompido. O preço disso é um log mudo justamente na janela que interessa quando alguém relata, horas depois, que "não estava funcionando".
OnConnectAttemptFailed dispara a cada tentativa que fracassa — de ConnectAsync (primeira
conexão) ou da reconexão automática, indistintamente:
procedure TApp.TentativaFalhou(Sender: TObject; const AAddress: string;
AAttempt: Integer; const AErro: string);
begin
Log.Debug(Format('tentativa %d contra %s falhou: %s', [AAttempt, AAddress, AErro]));
end;
Client.OnConnectAttemptFailed := TentativaFalhou;- Não é
OnError, de propósito.OnErroré o que o app precisa tratar; uma tentativa falha é o estado normal de quem espera o servidor — a próxima já está a caminho. O desfecho definitivo continua emOnConnectedou noOnErrorde esgotamento. AAddressé o endereço que FALHOU, passado por valor. Não leiaActiveAddressno handler: comFailoverAddresses, o índice já avançou quando o evento sai, e a property responderia pelo próximo endereço.AAttempté o mesmo contador queMaxReconnectAttemptslimita (começa em 1, zera quando uma sessão dura mais queReconnectDelayMs).AErroré a mensagem do transporte — é o que separa "servidor ausente" de "servidor recusou meu certificado".- Frequência: ~1 evento por
ReconnectDelayMsenquanto durar a queda. A lib não decima nem agrega;AAttempté o que permite ao seu logger espaçar os registros e só acumular o contador. Sem handler atribuído, custo zero.
Failover de endereço (FailoverAddresses)
Só em TPipeClient (TPipeServer escuta um único Address; não há o que "falhar para" do
lado de quem aceita). Endereços tentados em ordem DEPOIS de Address, o primário — vazio
por padrão, comportamento idêntico a antes desta property existir. Todos compartilham
Transport/TlsOptions/KeepAliveSeconds do cliente: são locais de rede alternativos do
MESMO serviço (ex.: loja principal e DR da mesma retaguarda), não um jeito de falar com um
servidor diferente.
Client := TPipeClient.Create('loja-principal:9000', ptTcp);
Client.FailoverAddresses := ['loja-dr:9000'];
Client.AutoReconnect := True;
Client.Connect(5000); // divide o prazo entre os enderecos da lista
WriteLn(Client.ActiveAddress); // qual deles a sessao atual usaConnect(ATimeoutMs) dá voltas pela lista com uma fatia igual do prazo por endereço, até um
conectar ou o total estourar. A reconexão automática (AutoReconnect) avança um endereço por
tentativa que falha, e volta a preferir o primário assim que uma sessão dura mais que
ReconnectDelayMs — uma sessão "de verdade" num alternativo não deixa o cliente grudado
nele: a PRÓXIMA falha tenta o primário de novo antes de espalhar pelos outros.
MaxReconnectAttempts/ReconnectDelayMs continuam contando/espaçando por TENTATIVA, sem
orçamento separado por endereço.
Compressão de payload (CompressionMinSize)
Deflate opcional para payloads grandes e compressíveis (JSON verboso, texto repetitivo) —
zero dependência nova: System.ZLib no Delphi e paszlib/zstream no FPC, os dois já vêm
na instalação padrão. CompressionMinSize (em TPipeServer/TPipeClient, igual
MaxMessageSize) é 0 por padrão — desligado, comportamento idêntico a antes desta
property existir. Ligar só afeta a PRODUÇÃO local; a decodificação de frames comprimidos
recebidos do peer é sempre ativa, então dá para ligar de um lado só, ou nos dois em momentos
diferentes do rollout, sem quebrar nada.
Client.CompressionMinSize := 512; // so' tenta comprimir payload >= 512 bytes
Client.SendText(JsonGrandeERepetitivo); // vai comprimido se compensarSó SendBytes/SendText/Request/Publish (e as versões em lote) são candidatos —
payload abaixo do mínimo, ou que não compensa (dado já compresso tipo imagem), sai cru sem
aviso nenhum: é uma otimização silenciosa, não uma garantia de formato no fio. BytesSent/
BytesReceived de Stats continuam contando o payload LÓGICO (visão do app); quem quer ver
a economia real de banda usa os campos irmãos BytesSentWire/BytesReceivedWire (ver seção
"Métricas/observabilidade" acima) — inclusive do lado de quem SÓ RECEBE, que de outro jeito
não teria como saber (a descompressão já devolve os bytes lógicos antes do app ver o frame).
MaxMessageSize é validado no payload ORIGINAL antes de comprimir, e a decodificação
tem proteção contra zip bomb (payload comprimido pequeno que "explode" ao descomprimir):
o teto é o mesmo MaxMessageSize, verificado durante a descompressão, não só no resultado
final. Racional completo (por que é um kind novo do NPF1 e não um bit de flag) em
docs/ARQUITETURA.md.
Descoberta de servidor na LAN (Pipes.Discovery)
"Onde está o servidor?" sem digitar IP: o servidor anuncia a si mesmo com um
TPipeDiscoveryResponder e o cliente pergunta por broadcast UDP com
PipeDiscoverServers. A resposta traz o endereço pronto para Address (o IP é o
endereço de origem da resposta — funciona certo até com servidor multi-NIC), o
transporte e um nome de exibição. É um complemento aos transportes, não um
transporte: nada do NPF1 passa por aqui, e a segurança continua onde sempre esteve —
descoberta encontra candidatos, ptTls autentica quem é de verdade.
// Lado servidor, junto do Listen:
Responder := TPipeDiscoveryResponder.Create(
9000, // porta do SERVICO (a do TPipeServer)
ptTls, // transporte que o cliente deve usar
'Retaguarda Loja 3' // nome de exibicao
); // porta de descoberta e token opcionais
Responder.Start;
// Lado cliente, antes do Connect:
Encontrados := PipeDiscoverServers(1000); // janela de 1s na sub-rede
if Length(Encontrados) > 0 then
begin
Client.Address := Encontrados[0].Address; // 'ip:porta' pronto
// sobrando mais de um, o resto vira failover:
// Client.FailoverAddresses := [Encontrados[1].Address, ...];
Client.Connect(5000);
end;Lista vazia não é erro — é "ninguém respondeu" (a janela expira em silêncio). Um Token
opcional separa instalações que dividem a mesma rede (é discriminador, não autenticação).
Alcance é a sub-rede local: broadcast não atravessa roteador nem VPN — PDV remoto
continua com IP configurado + FailoverAddresses. A forma dirigida
PipeDiscoverServers('192.168.1.10', ...) sonda um host específico ("o servidor está
vivo?"). Detalhes e racional em docs/ARQUITETURA.md §16. Vitrine executável no sample
EchoDiscovery (ver Samples).
Recursos
- Servidor multi-cliente — acceptor + uma reader thread por conexão;
MaxClientsopcional;SendBytes/SendTextpor conexão;Broadcast/BroadcastText;DisconnectClient; eventosOnClientConnected/OnClientDisconnected. - Request-Reply síncrono —
Request/RequestText(dados, timeout)no cliente bloqueia o chamador (nunca a thread de leitura) até o reply correlacionado; no servidor, o handlerOnRequestdevolve o reply e a lib o envia com o correlation id certo. Exceção no handler vira reply de erro (EPipeErrorno cliente, com a mensagem do servidor) — oOnErrordo servidor NÃO dispara nesse caso, porque o erro já tem destino. Chamadas concorrentes de várias threads no mesmo cliente são suportadas. - Pub/sub por tópico — quem envia nomeia um assunto, não um destinatário:
Server.Publish('caixa.3.status', dados)chega a todos os clientes cujo filtro alcança o tópico, e a mais ninguém. O cliente assina com curingas hierárquicos (Subscribe('caixa.*.status'),Subscribe('caixa.#')) e recebe emOnTopicMessage. As assinaturas são restauradas automaticamente em cada reconexão. Opcionalmente o servidor retém o último valor de um tópico (Publish(..., ARetain := True)), entregue na hora a quem assinar depois. Ver Pub/sub. - AutoReconnect — o cliente reconecta sozinho após queda do servidor
(
ReconnectDelayMs,MaxReconnectAttempts). Durante a janela de reconexão,Send*levantaEPipeClosedtransitório — re-tente (contrato igual ao republish de um client MQ). As tentativas são sempre espaçadas porReconnectDelayMs, inclusive contra um par que aceita a conexão e a derruba em seguida (o caso de um servidor mTLS recusando o certificado); eMaxReconnectAttemptsalcança esse caso também. O contador zera quando uma sessão dura mais queReconnectDelayMs— sessão curta demais conta como tentativa, para que um cliente rejeitado não fique reconectando indefinidamente, e um cliente de longa duração que reconecta legitimamente não acumule rumo ao teto. - ConnectAsync — a primeira conexão tentada em SEGUNDO PLANO, para quando o app sobe
ANTES do servidor.
Connectbloqueia e falha no prazo;AutoReconnectsó entra depois de uma sessão ter existido e caído —ConnectAsynccobre o buraco entre os dois. Ver Connect assíncrono. - Diagnóstico de tentativas (
OnConnectAttemptFailed) — um evento por tentativa de conexão que fracassa, com endereço, número da tentativa e a mensagem do transporte. Para log forense: preenche a janela silenciosa entre a queda e a volta. Ver Diagnóstico de tentativas. - Modos de despacho (
DispatchMode) — onde os SEUS handlers executam:pdmPool(padrão): pool de threads; paralelo entre conexões.pdmSerialized: worker único; ordem FIFO global garantida.pdmMainThread: direto na thread da UI viaTThread.Queue— para apps VCL/LCL, semSynchronizemanual e sem risco de evento pós-destroy (objeto-guarda interno).
- Proteção —
MaxMessageSize(padrão 16 MB) rejeita frames acima do limite nas duas pontas; magic/kind inválidos derrubam só a conexão ofensora (EPipeProtocolemOnError). - TLS e mTLS (
ptTls) — tráfego cifrado sobre TCP, com autenticação opcional do cliente por certificado: preencherCaFileno servidor faz quem não apresentar certificado daquela CA ser recusado antes deOnClientConnected. Backend nativo por plataforma (Schannel no Windows, OpenSSL no Linux) e prazo próprio de handshake, para que um par que abra a conexão e não fale não consuma uma thread indefinidamente. - Descoberta na LAN (
Pipes.Discovery) —TPipeDiscoveryResponderno servidor +PipeDiscoverServersno cliente acham o servidor na sub-rede por broadcast UDP, sem IP configurado; o resultado alimentaAddress/FailoverAddresses. Ver Descoberta de servidor na LAN.
Pub/sub (tópicos)
SendBytes precisa de um ConnId; Broadcast vai para todo mundo. Entre os dois falta o
caso mais comum de um sistema com várias pontas: mandar por assunto, sem que o remetente
saiba quem está interessado. É o que o pub/sub resolve.
// --- servidor ---
Server.Publish('caixa.3.status', dados); // só quem assinou o assunto recebe
Server.PublishText('loja.tabela.versao', 'v42', True); // True = retém o último valor
// --- cliente ---
Client.OnTopicMessage := Self.Recebeu; // (...; const ATopic; const AData; ARetained)
Client.Subscribe('caixa.*.status'); // um segmento no lugar do '*'
Client.Subscribe('loja.#'); // tudo abaixo de 'loja'
Client.PublishText('caixa.3.status', 'ocupado');Nomes e curingas. Tópico é hierárquico, separado por ponto, sensível a caixa e sem segmento vazio. Quem publica usa nome literal; quem assina pode usar curingas, sempre ocupando um segmento inteiro:
| Filtro | Alcança | Não alcança |
|---|---|---|
caixa.3.status |
caixa.3.status |
caixa.4.status |
caixa.*.status |
caixa.3.status |
caixa.3.a.status |
caixa.# |
caixa.3, caixa.3.a.b, caixa |
loja.3 |
# só pode ser o último segmento (a.#.b é recusado: as duas leituras possíveis dariam
resultados diferentes). caixa* também é recusado — curinga colado em texto prometeria um
casamento parcial que a lib não faz. Subscribe levanta EPipeError na hora para filtro
inválido; um Publish com nome inválido também.
Quem pode retransmitir. Uma publicação de cliente não vai para os outros clientes
por padrão: ela chega ao servidor em OnPublish, que decide. Ligar
RelayClientPublish := True faz a lib retransmitir sozinha (incluindo de volta ao próprio
autor, se ele assinar o tópico) — cômodo para um chat, e perigoso num sistema onde um
cliente não deveria poder injetar conteúdo no assunto de outro. O padrão desligado deixa o
servidor autoritativo, como nos samples de jogo:
procedure TRetaguarda.OnPublicacaoDeCliente(Sender: TObject; AConnId: TPipeConnectionId;
const ATopic: string; const AData: TBytes);
begin
if not PipeTopicMatches('caixa.*.status', ATopic) then Exit; // fora do lugar: ignora
Server.Publish(ATopic, AData, True); // republica retendo
end;Retenção (ARetain) é cache de último valor, não fila. O servidor guarda uma
mensagem por tópico e a entrega a quem assinar depois — é a resposta para "o cliente que
acabou de ligar precisa do estado atual" sem que ele tenha de pedir. Publicar com corpo
vazio e ARetain := True apaga o valor retido. O teto é MaxRetained (256 por padrão;
além dele o tópico retido mais antigo sai). Mensagem que precise sobreviver ao processo, ou
ser entregue com garantia, pede uma fila de verdade — para isso existe o
pascal-amqp-faa.
O último parâmetro de OnTopicMessage, ARetained, responde a pergunta que o
consumidor precisa fazer: isto acabou de acontecer, ou é o valor que já vigorava?
True só em catch-up de assinatura; uma publicação ao vivo chega sempre False,
mesmo quando o publicador pediu retenção (semântica do MQTT, e pela mesma razão: quem
recebe quer saber se a mensagem é notícia ou histórico, não o que o remetente pediu ao
servidor). Use para não tocar campainha nem contar venda duas vezes ao reconectar. Do lado
do servidor, o ARetained de OnPublish tem o outro sentido — o único possível ali: o
cliente pediu para reter.
Reconexão. As assinaturas são estado desejado do cliente, não da sessão: Subscribe
funciona desconectado, e tudo é reenviado ao servidor a cada nova sessão antes de
OnConnected disparar — o seu handler não precisa reassinar nada. O que não se recupera é
a janela entre a queda e a reassinatura: publicação que passou ali está perdida (é aí que o
retain ajuda).
Ordem e limites. A entrega de um mesmo publicador preserva a ordem; entre publicadores
diferentes, não há ordem global (use pdmSerialized se precisar de FIFO nos seus handlers).
OnPublish, OnSubscribe e OnUnsubscribe são notificações — o roteamento já
aconteceu quando eles rodam, e não há como vetar de dentro deles; para negar uma assinatura,
chame DisconnectClient. MaxSubscriptionsPerClient (64 por padrão) limita quantos filtros
um cliente pode registrar; a recusa aparece em OnError nos dois lados, e a conexão
continua de pé.
Confirmação de entrega. O log de Publish (uma chamada, do lado de quem publicou) não
diz se cada assinante recebeu — para isso existem OnDelivered/OnDeliveryFailed, no
servidor: disparam uma vez por conexão que casou o filtro de uma publicação do próprio
servidor (Publish/PublishBatch, ao vivo ou replay de retido — ARetained no mesmo
sentido de OnTopicMessage), depois do Write daquela conexão retornar. "Entregue" vai só
até o SO — o payload passou para o buffer do socket/pipe, não que o app do cliente
processou; o protocolo nunca teve ACK de aplicação. OnPublish é outra coisa: é sobre um
cliente publicando, não alcança o servidor sendo o publicador.
Compatibilidade de wire. Os tipos de frame do pub/sub são novos no protocolo NPF1. Um
peer compilado com uma versão anterior da lib que receba um deles cai com EPipeProtocol
("o peer provavelmente fala uma versão mais nova do protocolo") em vez de interpretar bytes
errados — atualize os dois lados.
Garantias de threading
- A thread de leitura nunca executa código do usuário — só decodifica e despacha.
Stop/Disconnect/destructors são síncronos, idempotentes e sem deadlock: sinalizam tudo, aguardam o join das threads e drenam os callbacks em voo antes de liberar qualquer objeto (verificado por teste:Stopsob flood de 4 clientes conclui em < 2 s).- Queda abrupta do par (processo morto, handle fechado) dispara
OnClientDisconnected/OnDisconnectedsem vazar handles/fds (verificado por teste com contagem de handles). - Callbacks são sempre
procedure ... of object. - Exceção que escapa de um handler vira
OnErrordo mesmo componente (desde a 0.2.0; até a 0.1.1 era engolida em silêncio). Vale para todo evento do usuário —OnMessage,OnTopicMessage,OnConnected/OnDisconnected,OnClientConnected/OnClientDisconnected,OnPublish,OnSubscribe/OnUnsubscribe,OnDelivered/OnDeliveryFailed,OnConnectAttemptFailed— e em todoDispatchMode. Detalhes:AConnIdé o da conexão do evento (0 no cliente e emOnConnectAttemptFailed);- a mensagem é
'<Evento> raised <ClasseDaExceção>: <mensagem>', por exemplo'OnMessage raised EConvertError: $.quantity: ...'; - o
OnErrorroda na mesma thread e logo depois do handler que falhou (worker do pool, ou main thread empdmMainThread), não num despacho novo; - a conexão continua aberta e as mensagens seguintes chegam normalmente — fechar ou
não é decisão do app (
DisconnectClient(AConnId)dentro doOnErrordo servidor); - exceção levantada dentro do próprio
OnErroré engolida (sem recursão); OnRequesté a exceção à regra: lá a exceção vira reply de erro para o cliente.
- Mensagens de runtime (exceções e
OnError) são em inglês desde a 0.3.0. Decida pela CLASSE da exceção (EPipeClosed,EPipeTimeout,EPipeTls,EPipeProtocol…), não pelo texto: a tabela do que mudou está no CHANGELOG. - Detalhe de semântica: um cliente que conecta e morre antes de o servidor aceitar a conexão é invisível (nenhum evento) — a instância/backlog é reciclada.
Instalação
Versão atual: 0.4.0. Enquanto for 0.x, a API pode mudar entre versões minor; toda mudança está no CHANGELOG.
Dependência de compilação: pascal-common-faa
1.27.0 ou mais nova. Atomics, ticks monotônicos, o monitor e o pool de threads (PcPool)
vêm dela, compartilhados com as outras libs *-faa (amqp, redis, db). A aplicação
fornece uma cópia só:
Delphi: adicione ao search path o src\ desta lib e o src\ da pascal-common-faa
(ou abra Pipes.groupproj, cujos projetos já apontam para o submódulo).
Lazarus: abra/compile pascal_common_faa.lpk uma vez (o Lazarus passa a achá-lo pelo
nome) e depois packages\pipes_faa.lpk, que o exige pelo nome; adicione pipes_faa aos
requisitos do seu projeto (ou use lazbuild --add-package-link nos dois .lpk).
Este repositório tem a pascal-common-faa como submódulo em external/pascal-common-faa,
só para os próprios testes e samples: não compile uma aplicação contra essa cópia se ela
também usa outra lib *-faa (seriam duas cópias da mesma unit). Uma pascal-common-faa velha
demais para o build com "pascal-named-pipes-faa precisa da pascal-common-faa 1.27.0 ou mais
nova (transporte TCP/TLS)".
Dependências de execução: nenhuma para ptLocal e ptTcp. Para ptTls depende do
backend:
- Schannel (padrão no Windows): nada a instalar — é SSPI, parte do SO.
- OpenSSL (
TlsOptions.Backend := tbOpenSsl, ou o padrão com-dPIPES_OPENSSL; único fora do Windows): precisa delibssl/libcryptona máquina que roda. Elas são carregadas dinamicamente na primeira conexão TLS, então a ausência não impede compilar nem iniciar o programa — ela aparece comoEPipeTls("OpenSSL não encontrado") na primeira conexão. Aceita as séries 3.x e 1.1. No Linux as distribuições já trazem; no Windows é preciso fornecer as DLLs (libcrypto-3-x64.dll+libssl-3-x64.dll, ou os equivalentes 1.1).
API (resumo)
TPipeBase (abstrata)
Address, Transport, KeepAliveSeconds, HeartbeatIntervalMs, Active, DispatchMode,
MaxMessageSize
CompressionMinSize // 0 (padrao) = producao desligada; decodificacao
// de frames recebidos e' sempre ativa
TlsOptions: TPipeTlsConfig // só usado em ptTls; lido no Listen/Connect
CertFile, CertPassword, KeyFile, CaFile, SkipServerVerification, HandshakeTimeoutMs
OnMessage: TPipeMessageEvent; OnError: TPipeErrorEvent
TPipeServer
Listen; Stop; // Listen não-blocante; Stop síncrono
SendBytes/SendText(ConnId, ..., AGroupKey = '') // EPipeError se ConnId não existe
// AGroupKey: ordem entre chamadas preservada no
// OnMessage do RECEPTOR mesmo em pdmPool (o
// padrão); chaves diferentes continuam paralelas
SendBytesBatch(ConnId, TArray<TBytes>) // N mensagens, um Write só; ordem preservada
Broadcast/BroadcastText(...) // snapshot; falha por conexão é engolida
Publish/PublishText(Topico, ..., Retain = False) // só quem assinou o tópico
PublishBatch(TArray<TPipePublishItem>) // N itens (Topic/Payload/Retain); um Write por conexão
SubscriberCount(Topico) // quantos receberiam uma publicação
ClientSubscriptions(ConnId) // filtros que aquele cliente assinou
ClearRetained // valores retidos não morrem no Stop
RelayClientPublish // False: cliente não injeta nos outros
MaxSubscriptionsPerClient; MaxRetained
OnPublish: TPipeTopicEvent // notificação: o fanout já ocorreu
// ARetained aqui = o cliente PEDIU para reter
OnSubscribe/OnUnsubscribe: TPipeSubscriptionEvent // idem; negue com DisconnectClient
OnDelivered: TPipeTopicEvent // 1x por assinante que RECEBEU uma publicação
// do PRÓPRIO servidor (Publish/PublishBatch);
// "entregue" = Write sem exceção, não ACK de app
OnDeliveryFailed: TPipeDeliveryFailedEvent // idem, do lado da falha; AError = a exceção
DisconnectClient(ConnId) // assíncrono e idempotente
ClientCount; ClientIds // só conexões ESTABELECIDAS
TryClientIdentity(ConnId, out Ident) // quem é, pelo certificado mTLS validado
TryClientAddress(ConnId, out Addr) // 'ip:porta'; False em ptLocal (sem endereco IP)
MaxClients // limite de recurso: conta as em handshake
OnClientConnected/OnClientDisconnected: TPipeConnectionEvent
OnRequest: TPipeRequestEvent // (const ARequest: TBytes; out AReply: TBytes)
Stats: TPipeServerStats // agregado, cumulativo desde o Listen
ConnectionStats(ConnId, out Stats): Boolean // por conexao; morre com ela
TPipeClient
Connect(TimeoutMs); Disconnect; // Connect re-tenta até o prazo
ConnectAsync; // nao bloqueia; insiste ate' o servidor aparecer
// (sucesso em OnConnected, desistencia em OnError)
SendBytes/SendText(..., AGroupKey = '') // fire-and-forget; AGroupKey ver TPipeServer acima
SendBytesBatch(TArray<TBytes>) // N mensagens, um Write só; ordem preservada
Request/RequestText(..., TimeoutMs) // RPC síncrono; EPipeTimeout no prazo
Subscribe/Unsubscribe(Filtro) // funciona desconectado; refeito na reconexão
Subscriptions // filtros assinados (estado desejado)
Publish/PublishText(Topico, ...) // EPipeClosed sem sessão
PublishBatch(TArray<TPipePublishItem>) // N itens (Topic/Payload/Retain), um Write só
OnTopicMessage: TPipeTopicEvent // (...; ATopic; AData; ARetained)
// ARetained: True só em catch-up de assinatura
Connected; AutoReconnect; ReconnectDelayMs; MaxReconnectAttempts
Connecting // ha' um ConnectAsync em voo (so' sobre ele)
FailoverAddresses: TArray<string> // tentados apos Address; vazio = so Address (padrao)
ActiveAddress // qual endereco a sessao atual usa (snapshot)
OnConnected/OnDisconnected: TPipeConnectionEvent
OnConnectAttemptFailed: TPipeAttemptFailedEvent // (...; AAddress; AAttempt; AError)
// diagnostico: UMA tentativa falhou
Stats: TPipeClientStats // da SESSAO atual; zera a cada reconexao
Pipes.Topics (unit pura, ??til tamb??m fora da lib)
PipeTopicMatches(Filtro, Topico); PipeIsValidTopic; PipeIsValidTopicFilter
TPipePublishItem = record Topic; Payload: TBytes; Retain: Boolean; end // ver PublishBatch
Pipes.Json (OPCIONAL ??? s?? inclui quem for usar; ver "JSON" abaixo)
TPipeJSONValue // = TJSONValue (Delphi) / TJSONData (FPC)
PipeBytesToJSON(Data): TPipeJSONValue // parse; EPipeJSONError se inválido/vazio
PipeJSONToBytes(Value): TBytes // serializa; não libera Value
PipeSendJSON(Client/Server, ..., Value) // wrapper de SendBytes
PipeRequestJSON(Client, Value, TimeoutMs): TPipeJSONValue // wrapper de Request
Pipes.Commands (OPCIONAL ??? s?? inclui quem for usar; ver "Comandos" abaixo)
TPipeCommandRouter.RegisterCommand(Comando, Handler, AMinSize = -1, AMaxSize = -1)
// EPipeCommandError: duplicado, handler nil,
// nome/limites invalidos (erro de programacao)
TPipeCommandRouter.HandleMessage // mesma assinatura de TPipeMessageEvent;
// atribua direto a Server/Client.OnMessage
OnUnknownCommand; OnInvalidPayload: TPipeCommandEvent // opcionais, silenciosos
TPipeCommandRouter.RegisterRequestCommand(Comando, Handler, AMinSize = -1, AMaxSize = -1)
// registro PROPRIO, independente do de mensagem
TPipeCommandRouter.HandleRequest // mesma assinatura de TPipeRequestEvent;
// atribua direto a Server.OnRequest — LEVANTA
// (EPipeProtocol) em comando desconhecido/payload
// invalido, ao contrario de HandleMessage
PipeEncodeCommandPayload/PipeDecodeCommandPayload(Comando, Corpo) // envelope manual
PipeSendCommand/PipeSendCommandText(Client|Server, ..., Comando, Corpo) // wrapper de SendBytes
PipeRequestCommand/PipeRequestCommandText(Client, Comando, Corpo, TimeoutMs) // wrapper de Request
Exce????es: EPipeError > EPipeClosed | EPipeTimeout | EPipeProtocol | EPipeTls |
EPipeJSONError | EPipeCommandErrorJSON (Pipes.Json.pas, opcional)
A API trafega TBytes; texto vira JSON como qualquer outro texto — SendText/
RequestText já bastam se o app monta e lê o JSON com a lib da sua preferência. O que
Pipes.Json.pas acrescenta é só a fronteira bytes↔JSON usando a lib nativa de cada
compilador (System.JSON no Delphi, fpjson/jsonparser no FPC), escondida atrás do
alias TPipeJSONValue, mais dois wrappers finos (PipeSendJSON/PipeRequestJSON) sobre
SendBytes/Request. Não faz parte do core — Pipes.Client/Pipes.Server não a
conhecem — e não precisa ser incluída por quem não for usar JSON.
Construir e ler o valor (AddPair vs Add, GetValue<T> vs Get) continua sendo a API
nativa de cada lib; unificar isso também não está no escopo da unit — quem tiver um caso
específico (outro formato, outra lib JSON) implementa por conta própria em cima de
TBytes/SendBytes, sem nenhuma penalidade.
uses Pipes.Client, Pipes.Json, System.JSON; // ou fpjson no FPC
var
Obj, Reply: TJSONObject;
begin
Obj := TJSONObject.Create;
try
Obj.AddPair('evento', 'abriu-caixa');
Obj.AddPair('caixa', TJSONNumber.Create(3));
PipeSendJSON(Client, Obj); // fire-and-forget
Reply := PipeRequestJSON(Client, Obj, 3000) as TJSONObject; // RPC síncrono
try
// ... Reply.GetValue<...>(...)
finally
Reply.Free;
end;
finally
Obj.Free;
end;
end;Comandos (Pipes.Commands.pas, opcional)
Quando o app trafega várias operações na mesma conexão (SALVAR_PEDIDO, CANCELAR,
PING, ...), a alternativa a uma cadeia de if/case dentro de um único OnMessage é
TPipeCommandRouter: um RegisterCommand por comando, cada um com seu próprio handler.
Não muda nada no fio — o nome do comando viaja dentro do payload, e HandleMessage tem a
mesma assinatura de OnMessage, então basta atribuir direto:
uses Pipes.Server, Pipes.Commands;
var
Router: TPipeCommandRouter;
begin
Router := TPipeCommandRouter.Create;
Router.RegisterCommand('PING', OnPing);
Router.RegisterCommand('SALVAR_PEDIDO', OnSalvarPedido, 1); // AMinSize = 1: corpo nao pode vir vazio
Server.OnMessage := Router.HandleMessage;
Server.Listen;
end;
procedure TMeuApp.OnSalvarPedido(Sender: TObject; AConnId: TPipeConnectionId;
const ACommand: string; const APayload: TBytes);
begin
// APayload ja' vem SEM o prefixo do envelope, so' o corpo
end;Quem envia pode montar o mesmo envelope com PipeEncodeCommandPayload('SALVAR_PEDIDO', Dados)
e mandar por SendBytes/Request diretamente, ou usar o wrapper fino que já faz isso:
PipeSendCommand(Client, 'SALVAR_PEDIDO', Dados) (fire-and-forget; há também o overload
PipeSendCommand(Server, AConnId, ...) para o servidor responder). As duas formas têm as
variantes ...Text (PipeSendCommandText) para corpo/resposta em texto UTF-8, mesmo padrão
de SendText/RequestText.
RegisterCommand aceita AMinSize/AMaxSize opcionais (PIPE_COMMAND_NO_LIMIT, o padrão,
desliga o respectivo teto) validados ANTES do handler rodar, e levanta EPipeCommandError
na hora do registro se o comando já existir, o nome for inválido, o handler não estiver
atribuído ou os limites forem inconsistentes — erro de programação, não de rede. Comando
sem handler cai em OnUnknownCommand; payload fora da faixa ou envelope malformado caem em
OnInvalidPayload — os dois opcionais e silenciosos quando ninguém assina, do mesmo jeito
que um OnMessage sem assinante. Nomes de comando são case-sensitive (mesmo raciocínio dos
tópicos: não há upcase portátil para UTF-8). Racional completo, inclusive por que é por
CIMA de OnMessage em vez de um kind novo do NPF1, em docs/ARQUITETURA.md §18.
O mesmo roteador também cobre o lado request-reply, com registro e método PRÓPRIOS
(RegisterRequestCommand/HandleRequest, independentes de RegisterCommand/
HandleMessage — o mesmo nome de comando pode existir nos dois sem conflito, porque
OnMessage/OnRequest já chegam por kinds diferentes no fio):
Router.RegisterRequestCommand('SOMAR', OnSomarRequest, 1); // AMinSize = 1
Server.OnRequest := Router.HandleRequest; // mesma assinatura de TPipeRequestEvent
procedure TMeuApp.OnSomarRequest(Sender: TObject; AConnId: TPipeConnectionId;
const ACommand: string; const APayload: TBytes; out AReply: TBytes);
begin
AReply := ...; // vira o reply; SEM envelope — quem chamou Request ja sabe o comando
end;O contrato de erro é o OPOSTO do lado mensagem, de propósito: HandleRequest LEVANTA
(EPipeProtocol) em comando desconhecido ou payload fora da faixa, em vez de chamar
OnUnknownCommand/OnInvalidPayload — não há esses eventos deste lado. A razão é que
TPipeServer.ExecuteRequest já transforma QUALQUER exceção vinda do OnRequest num reply
de erro para o cliente (o Request do lado cliente relança como EPipeError), o mesmo
caminho que o sample EchoJsonServer já usa de propósito com EPipeJSONError — e, ao
contrário de OnMessage, um Request sempre recebe ALGUMA resposta, então não existe um
"silêncio" equivalente para reaproveitar. Racional completo em docs/ARQUITETURA.md §18.8.
Do lado de quem CHAMA o request, PipeRequestCommand(Client, 'SOMAR', Dados, TimeoutMs) monta
o envelope e chama Request num só passo — o retorno é o AReply cru do handler (sem
envelope, mesmo contrato de HandleRequest). PipeRequestCommandText é a variante em texto
UTF-8, mesmo padrão de RequestText. Racional completo em docs/ARQUITETURA.md §18.9.
Compatibilidade com a API anterior
Os nomes antigos continuam válidos e compilam sem alteração — TNamedPipeBase,
TNamedPipeServer e TNamedPipeClient são aliases dos tipos acima, e a property
PipeName lê e escreve o mesmo campo de Address:
Server := TNamedPipeServer.Create('meu_app'); // igual a TPipeServer
Server.PipeName := 'outro'; // igual a Server.AddressO nome antigo amarrava a API ao Named Pipe do Windows, que passa a ser apenas um dos
transportes possíveis — no Linux o backend já é Unix Domain Socket. Os aliases serão
marcados deprecated só depois que samples e testes migrarem.
Sem alias, de propósito: os nomes de concorrência que foram para a pascal-common-faa
(decisão "sem compatibilidade retroativa" daquela lib). Quem usava algum deles direto no
próprio código troca o nome e acrescenta a unit ao uses:
Antes (Pipes.Threading) |
Agora | Unit |
|---|---|---|
PipeAtomicInc/Dec/Get/Set/CompareExchange |
PcAtomicInc/Dec/Get/Set/CompareExchange |
PascalCommon.Threading |
PipeAtomicRead64/Write64/CompareExchange64/Add64 |
PcAtomicRead64/Write64/CompareExchange64/Add64 |
PascalCommon.Threading |
PipeTickMs |
PcTickMs |
PascalCommon.Threading |
TPipeMonitor, TPipeWorkItem, TPipeThreadPool |
TPcMonitor, TPcWorkItem, TPcThreadPool |
PascalCommon.ThreadPool |
PipePool, PIPES_WAIT_INFINITE |
PcPool, PC_WAIT_INFINITE |
PascalCommon.ThreadPool |
Ficam em Pipes.Threading com o mesmo nome: TPipeKeyedDispatcher, PipeGroupDispatcher
e TPipeHeartbeatThread. Duas diferenças de comportamento: o PcPool é do processo inteiro
(o backlog de PoolQueueDepth inclui trabalho de outras libs *-faa), e
TPipeKeyedDispatcher.Destroy agora espera as drenagens em voo — a ordem "pool primeiro,
dispatcher depois" deixou de ser obrigatória (docs/ARQUITETURA.md
§15.4 e §23).
Transporte (0.4.0). TCP, TLS e o socket POSIX vêm da pascal-common-faa
(docs/ARQUITETURA.md §26). A API pública (TPipeServer/
TPipeClient, PipeConnect/PipeCreateListener, TPipeEndpoint/TPipeListener, as
exceções EPipe*) não muda. O que mudou embaixo:
TPipeTlsOptionseTPipePeerIdentitysão aliases deTPcTlsOptions/TPcPeerIdentity; o record ganhouBackend(eTlsOptions.Backend, a property).Pipes.Transport.Tcp/.Tlsficam comTcpPipeConnect/TcpPipeCreateListener/TlsPipeConnect/TlsPipeCreateListenercomo funções obsoletas por esta versão minor;Pipes.Transport.Schannel/.OpenSSLficam vazias, só para não quebrar umuses. As classes de backend (TPipeTcpWinEndpoint,TPipePosixEndpoint,TPipeTlsEndpoint,TPipeSchannelStream,TPipeOpenSslStream...) não existem mais.TPipeEndpointeTPipeListenerviraram fachadas sobre oTPcEndpoint/TPcListenerde baixo (.Inner); traduzemEPcTransport*paraEPipe*com a mesma mensagem.PipeParseHostPortcontinua aceitando IPv6 sem colchetes com porta ('::1:5000'), quePcParseHostPortrecusa por ser ambíguo. Forma obsoleta: use'[::1]:5000'.
Samples (samples/)
-
EchoServer / EchoClient — console, mesmo fonte nos dois compiladores. Rode o servidor, depois o cliente: texto simples usa
SendText(eco assíncrono viaOnMessage); linhas começando com?usamRequestText(RPC). Os dois aceitam um segundo parâmetro opcionaltcp(EchoServer.exe *:5300 tcp,EchoClient.exe 192.168.0.10:5300 tcp) para trocarptLocalporptTcp— é como o EchoAndroid fala com eles, já que celular não alcança Named Pipe nem Unix Domain Socket. Sem o parâmetro, o comportamento é o de sempre. -
EchoJson (
EchoJsonServer+EchoJsonClient) — o mesmo eco, mas com payload JSON viaPipes.Json.pasem vez de texto cru (ver seção "JSON" acima). Digiteitem quantidade(ex.:cafe 2) paraPipeSendJSONfire-and-forget — a confirmação assíncrona chega emOnMessage— ou?item quantidadeparaPipeRequestJSONsíncrono, com o total calculado pelo servidor no reply. Mostra também a única parte quePipes.Json.pasnão esconde: montar/ler o valor (AddPairvsAdd,GetValue<T>vsGet) é{$IFDEF FPC}local ao sample, atrás de duas funções pequenas (JStr/JInt). -
EchoCommand (
EchoCommandServer+EchoCommandClient) — vitrine deTPipeCommandRouterdos dois lados (ver seção "Comandos" acima): comandos fire-and-forget (PING/ECOno servidor,PONG/ECO_OKno cliente, viaOnMessage) e um comando request-reply (SOMAR, viaOnRequest/Request), cada um com seu próprio handler em vez de uma cadeia deif. Digiteping/eco <texto>para os assíncronos,?soma <a>para o RPC síncrono (soma dois inteiros) — e?pingpara ver o caminho de "comando desconhecido" do lado request-reply de propósito:PINGsó está registrado do lado mensagem, então pedi-lo comoRequestlevantaEPipeErrorno cliente. O envio dos dois lados usa os wrappers de conveniênciaPipeSendCommand/PipeSendCommandText/PipeRequestCommand/PipeRequestCommandTextem vez de montarPipeEncodeCommandPayload+SendBytes/Requestna mão. -
EchoFailover (só
EchoFailoverClient— reaproveita oEchoServer.exede sempre, rodado duas vezes) — vitrine deFailoverAddresses/ActiveAddress(ver seção "Failover de endereço" acima). SubaEchoServer.exe pipes_faa_primarioeEchoServer.exe pipes_faa_backup, depoisEchoFailoverClient.exe pipes_faa_primario pipes_faa_backup: troque mensagens (o log mostraendereço ativo: pipes_faa_primario), derrube a janela do primário e troque mensagens de novo — sem reiniciar o cliente, o log passa a mostrarendereço ativo: pipes_faa_backup. Roteiro completo no cabeçalho deEchoFailoverClient.dpr. -
EchoDiscovery (só
EchoDiscoveryClient— reaproveita oEchoServer.exede sempre, com um parâmetro a mais) — vitrine dePipes.Discovery(ver seção "Descoberta de servidor na LAN" acima). SubaEchoServer.exe *:5300 tcp discover(odiscoverno fim liga umTPipeDiscoveryResponderjunto doListen) e depoisEchoDiscoveryClient.exesem argumento nenhum: ele sonda a sub-rede por 1s, logaencontrado: "EchoServer" em <ip>:5300 (ptTcp)e conecta sozinho, nenhum IP digitado. ComEchoServer.exe *:5300 tls ..\..\tests\pki mtls discover+EchoDiscoveryClient.exe ..\..\tests\pki clidá pra ver a distinção do §16.4 na prática: a descoberta só acha o candidato (endereço, transporte, nome) — quem autentica de verdade é o handshakeptTls/mTLS que vem a seguir, não a sonda UDP. -
EchoSeguro (
EchoSeguroServer+EchoSeguroClient) — o mesmo eco, mas sobreptTlscom mTLS: servidor exige certificado de cliente (CaFile), cliente apresenta o dele, tráfego cifrado ponta a ponta. Usa a PKI de teste versionada emtests/pki; um cliente sem certificado (ou umTPipeClientcomum) é recusado antes deOnClientConnecteddisparar — prova de que o mTLS não é decorativo. -
EchoAndroid — cliente Android (FMX, só Delphi) do
EchoServer: conecta porptTcpouptTls, manda texto e mostra a resposta. Vitrine do que muda num celular —pdmMainThread,Connectfora da thread principal,HeartbeatIntervalMsligado (Wi-Fi que dorme e NAT de operadora derrubam conexão ociosa em silêncio). OLEIA-ME.mddo sample tem os passos de IDE que não dá para versionar no.dproj: permissãoINTERNET,usesCleartextTraffice o empacotamento do OpenSSL por ABI. -
ChatVcl — chat com UI (VCL no Delphi, LCL no Lazarus, mesmo fonte): uma instância é o servidor-hub (retransmite via
Broadcast), as outras são clientes. Vitrine dopdmMainThread(handlers mexem na UI direto) e doAutoReconnect. -
ChatSeguro — o mesmo chat sobre
ptTlscom mTLS, e a diferença não é só a cifra: quem está na sala vem do certificado, não de um apelido digitado. O hub rotula cada mensagem com oCommonNamequeTryClientIdentitydevolve, e a lista de participantes sai deClientIds— que só mostra conexões estabelecidas. O combo de identidade troca o certificado apresentado, incluindo os que devem ser recusados (rogue,selfsigned): é aí que se vê o mTLS trabalhando, e não no caminho feliz. Precisa da PKI detests/pki/, que o próprio form localiza. -
PontosECaixas — o jogo "pontos e caixas" (dots and boxes) para dois jogadores, uma janela cada (VCL no Delphi, LCL no Lazarus, mesmo fonte). Um lado clica Hospedar, o outro digita o endereço e clica Entrar; o combo escolhe
ptLocal(duas janelas na mesma máquina) ouptTcp(duas máquinas) — o código do jogo não muda, só o valor deTransport. O que ele mostra e os outros não: servidor autoritativo (o hospedeiro tem a únicaTJogoPartidaque vale; o convidado pede a jogada e espera o estado voltar, nunca aplica a própria — jogar fora da vez ou numa aresta já usada é recusado no mesmoTentarJogarque valida o clique local); reconexão que devolve a vaga (o convidado repete um token noOnConnected, que a lib dispara também a cada reconexão automática — feche a janela dele no meio da partida e reabra para ver o tabuleiro voltar inteiro); duas camadas de recusa com papéis diferentes (MaxClientsé teto de recurso, "a partida já tem dois jogadores" é regra de negócio noOI, e por isso consegue mandar o motivo antes de desligar); e o tratamento da conexão zumbi — quando o mesmo token chega numa conexão nova, a anterior costuma continuar viva no servidor (TCP morto em silêncio, típico de VPN/NAT) e é derrubada comDisconnectClientantes de a vaga ser reatribuída. Mostra também o preço de sincronizar por estado em vez de por evento: o tabuleiro completo não diz o que mudou, então quem recebe não saberia onde o adversário jogou — a aresta simplesmente apareceria. OESTADOcarrega por isso a última aresta jogada, que a UI realça por ~0,6 s (aresta que engrossa e clareia + anéis expandindo nos dois pontos). Duas marcações no momento de entrar ligam o jogador controlado por IA (Jogo.Ia.pas, heurística de cadeias com double-cross): "computador joga por mim", válida nos dois papéis — no convidado o bot vira um cliente autônomo, mandandoJOGADApela rede e passando pela mesma validação de qualquer humano — e "computador ocupa a vaga do convidado", que dá a partida solo contra a máquina (aí um humano que tentar entrar é recusado com o motivo). Marcando as duas, dá para assistir bot contra bot. -
PingPong — o pong clássico para dois jogadores, uma janela cada, mesmo esquema de
Hospedar/Entrare mesmo comboptLocal/ptTcpdo PontosECaixas. Para experimentar sozinho não precisa de segunda janela: marque "Computador ocupa a vaga do convidado" e clique Hospedar. O que ele mostra e o PontosECaixas não mostra é o mundo que anda sozinho — e as cinco consequências disso: o relógio do jogo não é oTTimer(um acumulador mede o tempo real e dá quantos passos de 16 ms couberem, senão a bola andaria mais rápido ou mais devagar conforme o jitter do timer, e as duas telas divergiriam); entrada vai por borda, estado vem por nível (o convidado só mandaENTRADAquando a direção da raquete muda, o hospedeiro manda a fotografia inteira ~31x por segundo — a primeira escolha só é segura porque o transporte é confiável e ordenado, e seria um bug em UDP); o convidado prevê (roda a mesma física localmente entre um snapshot e o próximo, comAEhAutoridade = False: prever movimento é uma coisa, decidir ponto é outra — o espelho que vê a bola sair do campo apenas para, e espera oESTADOdizer o que houve); até a previsão tem prazo (1,5 s sem snapshot e ela congela, com a tela dizendo isso, em vez de animar uma partida que talvez não exista mais); e o bot não tem timer — é chamado dentro do passo de simulação, junto com a física, e a direção que ele devolve entra pela mesma porta do teclado (no convidado, viraENTRADAna rede). Os números de ponto flutuante vão no fio como inteiro em centésimos de propósito:FloatToStrusa o separador decimal do locale, e um hospedeiro pt-BR mandando412,75para um convidado en-US é um bug de rede que ninguém procura. O nível do bot (Pong.Ia.pas) mexe só no horizonte de reação e na qualidade da previsão, nunca na velocidade da raquete — ver no cabeçalho da unit por que essa é a única alavanca que fecha ponto. Junto vai oPongCheck, um programa de console no mesmo diretório que verifica o núcleo do jogo sem abrir janela e sem esperar o relógio: ele roda ~48 minutos de jogo simulado em ~40 ms. Só é possível porque as três units de jogo não dependem nem da UI nem da biblioteca (uses SysUtilse mais nada), o que é o argumento prático para a separação que o sample prega. Ele confere o round-trip doESTADOcampo a campo, que o espelho não pontua sozinho depois de 15 s sem snapshot, o erro da previsão em quatro combinações de taxa, e que o bot fecha ponto nos três níveis — foi ele que pegou a primeira versão do bot, em que dois "médios" empatavam por dez minutos. Semente deRandomfixa, para uma falha ser reproduzível. Não cobreuPongMain.pasnem a biblioteca: é teste do núcleo puro, não de integração. -
PdvDualScreen (
Operador+Cliente) — PDV de tela dupla: o operador lança itens e pede a forma de pagamento; o cliente acompanha e responde. Mostra o padrão recomendado para uso em produção: a UI de cada lado não falaTBytes/TPipeConnectionIddiretamente, só os tipos de domínio (TPdvItem,TPdvFormaPagamento) através de uma fachada (Pdv.OperadorChannel/Pdv.ClienteChannel) que encapsulaTPipeServer/TPipeCliente o protocolo de mensagens (Pdv.Protocolo.pas). -
FilaImpressao (
FilaServidor+FilaCliente) — mostrapdmSerializedvspdmPoolna prática: um handler com estado compartilhado sem lock (de propósito) processa jobs vindos em sequência;FilaServidor pipe serialized(padrão) nunca acusa reentrância e conclui na ordem de chegada,FilaServidor pipe poolacusa concorrência real e conclusão fora de ordem com a mesma carga. -
DespachoTarefas (
DespachoServidor+DespachoWorker) — mostra endereçamento por conexão em vez deBroadcast: o operador digitajob <texto>e o servidor despacha para UM worker por vez (round-robin sobreClientIds); também exercitaMaxClients,DisconnectClient(comandokick) elist. -
ServicoInstavel (
ServicoInstavel+ClienteResiliente) — servidor que simula lentidão e falhas de negócio aleatórias emOnRequest; o cliente mostra um padrão de retry com backoff exponencial que trataEPipeTimeout/EPipeClosed(transitório, repete) eEPipeError(erro de negócio, não repete) de formas diferentes. -
RpcConcorrente (
RpcConcorrenteServidor+RpcConcorrenteCliente) — prova a garantia de que chamadasRequest/RequestTextde várias threads no MESMOTPipeClientsão suportadas: váriasTThreadcompartilham uma única instância de cliente e disparam RPCs em paralelo; cada uma confere que a resposta que voltou é exatamente a do pedido que ela fez (correlation id), expondo qualquer cruzamento de respostas entre chamadores como bug. Ao final imprimeClient.Stats: é a vitrine de latência de Request (média/máxima), a métrica que só faz sentido com tráfego concorrente como este. -
GatewaySeguro (
ServicoLocal+GatewaySeguro+ClienteRemoto) — o único sample em queTPipeServereTPipeClientestão vivos ao mesmo tempo no mesmo processo, com transportes diferentes em cada ponta:[ClienteRemoto] --ptTls+mTLS--> [GatewaySeguro] --ptLocal--> [ServicoLocal]. Os outros samples provam que o alcance é uma property; este prova que os alcances se compõem. O caso de uso é o que as pessoas realmente têm: um serviço que só fala IPC local e nunca vai aprender TLS, e a necessidade de expô-lo à rede com autenticação. O gateway autentica por mTLS, sabe quem é o par (TryClientIdentitydevolve oCommonNamejá validado contra a CA — CN forjado não chega ali) e precisa contar ao serviço local quem está chamando (frameIDENT|), porqueptLocalnão tem TLS e portanto não tem identidade nenhuma. Por que o serviço deveria acreditar? PorqueptLocalherda o controle de acesso do sistema operacional — a segurança do gateway não vem do gateway; vem do alcance do transporte de trás. Com oServicoLocalemptTcpouvindo em0.0.0.0, o esquema inteiro cai: qualquer um pula o gateway e se declara quem quiser. Mostra também as duas camadas de "conectado" (o remoto pode autenticar com sucesso e ainda assim receberRECUSADO|<motivo>porque oServicoLocalestá fora do ar — handshake TLS concluído não é o mesmo que sessão útil, e por isso a recusa carrega motivo em vez de ser um socket fechado calado) e o padrão que evita o pior deadlock deste desenho:TPipeClient.Disconnecté síncrono (join da reader thread +DrainInFlight), então nenhumFree/Disconnectde par acontece dentro de callback — o callback só marca e um ceifador (TThreadcom fila e evento) destrói. O racional completo, com as invariantes de lock e o que ficou fora desta versão (relay deRequest), está no cabeçalho deGateway.Nucleo.pas.ClienteRemoto <endereço> <identidade>escolhe o certificado:cliecaixaentram — rode os dois juntos e veja oServicoLocalcarimbar a identidade certa em cada resposta, porque cruzamento de identidade seria o pior bug possível num gateway;rogue,selfsignedenenhumsão recusados e oServicoLocalnão registra absolutamente nada, que é a prova de que não vazou nada para trás. Precisa da PKI detests/pki/. Roteiro de execução dos três processos (e checklist de regressão) emsamples/GatewaySeguro/LEIA-ME.md. -
PainelLoja — o sample de pub/sub: um executável, três papéis escolhidos pelo primeiro parâmetro, uma janela para cada.
PainelLoja retaguarda PainelLoja caixa 3 PainelLoja painelNenhum dos três chama
SendBytescomConnIdnenhum: a retaguarda publica emloja.tabela.versao, cada caixa publicacaixa.<n>.status, o painel assinacaixa.#e vê todos — incluindo caixas que ainda não existiam quando ele assinou. Adicionar um caixa não muda uma linha da retaguarda. O que ele mostra e os outros não: relay desligado (o padrão) com a retaguarda decidindo emOnPublishse republica, e usandoPipeTopicMatchespara conferir se o cliente publicou no lugar certo — o mesmo desenho autoritativo dos samples de jogo, agora sobre tópicos; e o retain trabalhando de verdade: abra o painel por último e ele desenha o estado atual da loja imediatamente, sem esperar o próximo tick de ninguém. Feche e reabra: ele reconstrói o estado, não a conversa — o retain guarda o último valor de cada tópico, não histórico. O caixa, comAutoReconnect, mostra os dois lados da moeda: publicar sem sessão levanta (ele registra e o próximo tick tenta de novo), enquanto as assinaturas voltam sozinhas, sem nada noOnConnected. -
MonitorTopicos — o pub/sub com UI (VCL no Delphi, LCL no Lazarus, mesmo fonte) e, na prática, uma ferramenta: serve para depurar o pub/sub de qualquer app feito com a lib. Uma janela
Hospedar, as outrasEntrar, mesmo comboptLocal/ptTcpdos samples de jogo. Três coisas do recurso só ficam visíveis aqui: assinatura manipulada ao vivo (assine e cancele com o app rodando; melhor, monte a lista desconectado —Subscribeé estado desejado — conecte depois e ela já vale); o efeito doRelayClientPublishnum clique (duas janelas cliente assinando o mesmo tópico: marque a caixa no hospedeiro e o que uma publica alcança a outra, desmarque e a entrega para na hora — é a decisão central do recurso, e a property não temEnsureInactivejustamente para permitir esse experimento); e o caminho da recusa, que nenhum outro sample exercita (baixeMaxSubscriptionse veja a assinatura ser recusada com a mensagem aparecendo nos dois lados, conexão de pé; digitecaixa*e vejaEPipeErrorna hora, antes de virar frame). A lista de recebidas carimbaretno que veio do cache de retidos, o que torna visível a diferença entre desenhar o estado que já existia e acompanhar o que acontece. Hospedando, o painel de assinaturas mostra quem assinou o quê (ClientSubscriptionspor conexão) e a contagem deSubscriberCountdo tópico do campo Publicar — a visão do roteador; como cliente, mostra as próprias, editáveis. Roteiro de 2 minutos no cabeçalho deMonitorTopicos.dpr. -
TransferenciaArquivos — envio de arquivo com UI (VCL no Delphi, LCL no Lazarus, mesmo fonte), vitrine de
CompressionMinSizee dos campos*WiredeStats. Uma instânciaSer servidor(salva o que chega emrecebidos/), a outraSer cliente(Selecionar...escolhe o arquivo,Enviar arquivomanda). O checkbox "Comprimir" só fica editável ANTES de conectar — trava depois, mesma regra deMaxMessageSize(EnsureInactive) — e mostra na prática por que é um kind novo no NPF1, não um bit deFlags: liga só de um lado sem quebrar nada no outro. Cada envio loga a economia real dos DOIS lados: o cliente pelo delta deClient.Stats(BytesSentvsBytesSentWire) antes/ depois doSendBytes, e o servidor porConnectionStats.BytesReceivedWire— a única forma de quem só RECEBE enxergar a economia, já que a descompressão devolve o payload lógico antes deOnMessagerodar (opaco por design). Protocolo do arquivo em si é só deste sample (não da lib): envelope[TamanhoDoNome][NomeUTF8][Bytes]cru sobreSendBytes, arquivo inteiro em memória — sem chunking, não é streaming de produção.
Testes
Toda suíte compila o submódulo da pascal-common-faa: rode git submodule update --init uma
vez depois de clonar. Sem --recursive: o submódulo da própria pascal-common-faa só serve
aos testes dela.
- Os testes são escritos uma vez, nos mestres DUnitX (
tests/Unit/*Tests.pas,tests/Integration/*Tests.pas); os espelhos FPCUnit detests/*/fpcsão GERADOS porpython tools/gen_fpc_mirror.pye não se editam à mão. O gerador e o adaptadortests/Unit/Pipes.DUnitXCompat.pasvêm do kit de testes da skilldual-compiler-delphi-lazarus(pascal-skills-faa); os dois scripts abaixo rodamgen_fpc_mirror.py --checkantes de compilar. - Delphi: abra
Pipes.groupproje rodePipes.UnitTestsePipes.IntegrationTests(DUnitX). - FPC/Lazarus (Windows):
sh tools/test_fpc.sh(lazbuild + as duas suítes; critério: 0 erros, 0 falhas, "0 unfreed memory blocks" do heaptrc e nenhuma linha "FINALIZATION CHECK FAILED"). À mão:lazbuildnos.lpidetests\*\fpce rodar os exes com--all --format=plain(sem parâmetros abre a GUI de testes). - Linux (Docker, FPC 3.2.2 do Debian Bookworm):
sh tools/test_fpc_docker.sh. A suíte de TLS roda sempre (desde a 0.4.0 optTlsexiste em todo build);CPUS=1eRUNS=Nservem para caçar corrida de concorrência (vários containers ao mesmo tempo, cada um limitado a 1 CPU). A imagem padrão éfpc322-bookworm(debian:bookworm+apt-get install fpc);FPC_IMAGEtroca. - OpenSSL 1.1 (o outro ramo suportado):
FPC_IMAGE=fpc-bullseye-ssl sh tools/test_fpc_docker.sh, uma imagemdebian:bullseye(FPC 3.2.0) comlibssl 1.1.1e sem a 3.x. À mão, como abaixo. Não é redundante com a anterior — é a única forma de exercitar as divergências 1.1/3.x, e duas já morderam de verdade:docker run --rm -v "$PWD:/work" debian:bullseye bash -c ' apt-get update -qq && apt-get install -y -qq fpc libssl1.1 >/dev/null cd /work/tests/Integration/fpc C=../../../external/pascal-common-faa/src fpc -MDelphi -Sh -B -Fu../../../src -Fi../../../src -Fu$C -Fi$C \ -FU/tmp -o/tmp/t PipesIntegrationTestsFpc.lpr /tmp/t --all --format=plain'- O fallback de símbolo do getter do certificado do par, que o 3.x renomeou
(
SSL_get_peer_certificate→SSL_get1_peer_certificate). Com as duas versões instaladas o loader escolheria a 3.x e o ramo antigo nunca rodaria. - A validação por endereço IP (
Tls_ValidaServidorPorIp_Aceita). Endereço IP mora em SAN do tipoiPAddresse exigeX509_VERIFY_PARAM_set1_ip_asc; na 3.x oSSL_set1_hostaceita um literal de IP e mascara a diferença. Esse teste passa na 3.x com ou sem a correção — só a imagem 1.1 o torna um guarda de verdade. Histórico emdocs/ARQUITETURA.md§13.9.
- O fallback de símbolo do getter do certificado do par, que o 3.x renomeou
(
A suíte de integração inclui stress de encerramento (Stop sob flood < 2 s), detector de vazamento de handle/fd em quedas abruptas repetidas e correlação RPC sob concorrência.
Android não entra nessa suíte: o FPC não compila para Android neste projeto e um APK não
tem runner de console, então o backend Android se verifica em aparelho, com a suíte FMX de
tests/Android (loopback: servidor e cliente no mesmo app). Ver tests/Android/LEIA-ME.md
para os limites numéricos de cada caso e a rodada de referência.
Testes de TLS
O fixture TPipeTlsTests roda com o backend padrão do build; no Windows (Schannel padrão)
TPipeTlsOpenSslTests repete os mesmos testes com tbOpenSsl na mesma rodada, se a libssl
carregar (sem ela, um aviso no console e o fixture não é registrado). Boa parte dos testes é
de recusa (cliente sem certificado,
de outra CA, auto-assinado, mudo no handshake): é a metade que prova que existe
autenticação, e não só que o caminho feliz funciona.
As credenciais vêm de tests/pki/ — uma PKI de teste versionada
no repositório de propósito, sem valor de segurança. Um scanner de segredos vai
apontá-la; o apontamento está certo quanto ao fato e errado quanto ao risco. A alternativa
de gerá-la no Setup com openssl foi descartada porque, onde não houvesse openssl, os
testes de TLS sumiriam — e teste de segurança que some em silêncio é pior que teste
ausente. A ausência da PKI falha, não pula.
Estrutura
src/ biblioteca (Pipes.Types, Pipes.Framing,
Pipes.Transport[.Windows|.Posix|.Android],
Pipes.Base, Pipes.Server, Pipes.Client, Pipes.Threading, pipes.inc)
pub/sub: Pipes.Topics (nomes, curingas e envelope; unit pura)
rede: Pipes.Transport.Tcp
TLS: Pipes.Transport.Tls (sobre PascalCommon.Transport.Tls;
.Schannel / .OpenSSL sao units obsoletas, vazias)
descoberta LAN: Pipes.Discovery (broadcast UDP; complemento, nao transporte)
Pipes.Compression (deflate opcional, CompressionMinSize; kind pfkCompressed)
Pipes.Json (bytes<->JSON, OPCIONAL ??? nao acoplada ao core)
Pipes.Commands (roteador de comandos por nome, OPCIONAL, por
cima de OnMessage ??? nao acoplada ao core)
packages/ pipes_faa.lpk (pacote Lazarus; exige pascal_common_faa pelo nome)
external/ submodulo pascal-common-faa (so' testes e samples; ver "Instalacao")
tools/ test_fpc.sh (Windows), test_fpc_docker.sh (Linux), gerar-pki.sh,
gen_fpc_mirror.py (gera os espelhos FPCUnit)
samples/ EchoServer, EchoClient, EchoJson (Pipes.Json.pas, opcional),
EchoCommand (Pipes.Commands.pas, opcional),
EchoFailover (FailoverAddresses, reaproveita o EchoServer.exe),
EchoDiscovery (Pipes.Discovery, idem, so' o EchoServer.exe ganha
"discover" na linha de comando),
EchoSeguro (TLS + mTLS), ChatVcl, ChatSeguro,
PontosECaixas (jogo de turno), PingPong (jogo em tempo real),
PainelLoja (pub/sub por topico, tres papeis num exe),
MonitorTopicos (explorador de pub/sub com UI VCL/LCL),
PdvDualScreen (Operador + Cliente),
FilaImpressao, DespachoTarefas, ServicoInstavel, RpcConcorrente,
GatewaySeguro (ptTls -> ptLocal, servidor + cliente no mesmo processo),
TransferenciaArquivos (CompressionMinSize e Stats.*Wire, UI VCL/LCL),
EchoAndroid (FMX/Android, so Delphi)
tests/ Unit + Integration (mestres DUnitX; espelhos FPCUnit gerados)
tests/Android/ suite de DEVICE do backend Android (loopback; sem par dual-compiler)
tests/pki/ PKI de TESTE versionada, sem valor de seguranca (ver LEIA-ME)
docs/ARQUITETURA.md arquitetura completa (wire format, ciclo de vida das threads, racional)
docs/INTEROP.md protocolo de fio para implementar a outra ponta em outra linguagem
Pipes.groupproj grupo de projetos Delphi Pipes.lpg grupo LazarusLicença
MIT — © 2026 Fabiano Arndt

