Rede, HTTP & URL
O Glide entrega uma stack de rede completa como stdlib em espaço de usuário: primitivas de socket, todos os transportes comuns (TCP, UDP, multicast, raw, TLS, WebSocket), três gerações de cliente HTTP (1.1, 2, 3), um servidor HTTP/1.1 roteado e o codec de URL que os cola. Tudo é escrito em Glide sobre uma única superfície fina de syscalls de runtime (gnet_* em netcore.c); não há código C de protocolo escondido acima da camada de socket.
A I/O segue o modelo coro-blocking. Uma chamada como accept, read ou connect bloqueia a coroutine chamadora, não a thread do SO. No Linux o file descriptor é registrado no reactor epoll e a coroutine fica parqueada até o kernel sinalizar prontidão, então a thread worker fica livre para rodar outras coroutines no meio-tempo — um único processo pode segurar dezenas de milhares de conexões vivas. No Windows e no macOS a mesma superfície cai para I/O síncrona bloqueante: seu código é idêntico e portável, mas essas plataformas prendem um worker por chamada em voo até os backends assíncronos (IOCP / kqueue) chegarem. Você escreve código em linha reta; o scheduler faz a multiplexação.
Erros são valores. Quase toda chamada falível retorna !T (um Result), inspecionado através de .ok mais .val no sucesso ou .err (uma string) na falha — sem exceções, sem panics no caminho feliz-ou-triste. Cheque explicitamente com if !r.ok { ... }, ou propague com o operador ? dentro de uma função que ela mesma retorna !T. Construtores que não podem falhar (IpAddr::v4, HttpResponse::ok) retornam o valor diretamente; tudo que toca a rede ou faz parsing de entrada não confiável retorna !T.
Import
// Endereçamento central + fundação de socket
import stdlib::net::ip::*; // IpAddr, SocketAddr
import stdlib::net::sys::*; // Socket, Stream — a fundação gnet_*
import stdlib::net::sockopt::*; // SO_REUSEADDR / TCP_NODELAY / timeouts
import stdlib::net::dns::*; // resolve()
// Transportes
import stdlib::net::tcp::*; // TcpStream (cliente)
import stdlib::net::listener::*; // Listener, Conn (servidor)
import stdlib::net::udp::*; // UdpSocket, UdpRecv
import stdlib::net::multicast::*; // join/leave de multicast IPv4
import stdlib::net::raw::*; // RawSocket (SOCK_RAW)
import stdlib::net::icmp::*; // ping, traceroute
import stdlib::net::tls::*; // TlsConfig, TlsStream (OpenSSL)
import stdlib::net::ws::*; // WebSocket, WsMessage
// Cliente HTTP
import stdlib::http::client::*; // HttpClient, HttpResponse, http_get
import stdlib::http::cookies::*; // CookieJar
import stdlib::http::form::*; // FormData (x-www-form-urlencoded)
import stdlib::http::multipart::*; // Multipart (multipart/form-data)
import stdlib::http::jwt::*; // JwtClaims, sign/verify
import stdlib::http::h2::*; // framing HTTP/2 + cliente GET
import stdlib::http::hpack::*; // compressão de header HPACK
import stdlib::http::h3::*; // HTTP/3 sobre QUIC
// Servidor HTTP
import stdlib::http::*; // HttpRequest, HttpResponse, http_listen
import stdlib::http::router::*; // Router, Route, Chain
import stdlib::http::route::*; // attrs @route / @get / @post, routes!()
import stdlib::http::handler::*; // proc macro @handler
import stdlib::http::extract::*; // FromRequest, Bearer, Path, Headers, Basic
import stdlib::http::typed::*; // Json<T>, JsonBind, into_response()
import stdlib::http::cors::*; // CorsConfig, middleware CORS
import stdlib::http::compress::*; // compressão de resposta gzip_mw
import stdlib::http::static::*; // serviço de arquivos estáticos
import stdlib::http::sse::*; // Server-Sent Events
import stdlib::http::proxy::*; // ReverseProxyConfig
// Codec de URL
import stdlib::url::*; // url_encode, url_decode
Mapa de módulos
| módulo | o que te dá |
|---|---|
stdlib::net::ip |
IpAddr (struct etiquetada IPv4/IPv6) e SocketAddr (ip+porta) — construir, fazer parsing, classificar (is_loopback/is_private/...) e renderizar para texto (RFC 5952). |
stdlib::net::sys |
A fundação de socket de primeira classe: o handle Socket e o trait Stream binary-safe sobre as syscalls gnet_* do runtime. Todo transporte mais alto se constrói sobre isto. |
stdlib::net::sockopt |
Helpers uniformes de opção de socket (SO_REUSEADDR, TCP_NODELAY, timeouts de recv/send) sobre qualquer .fd da fundação. |
stdlib::net::dns |
Resolução hostname → IP via o resolver do sistema (getaddrinfo); resolve() retorna !*Vector<*IpAddr>. |
stdlib::net::tcp |
TcpStream — connect TCP de saída (cliente) + read/write. |
stdlib::net::listener |
TCP do lado servidor: Listener (bind/accept) e Conn (socket aceito), ligados ao reactor assíncrono. |
stdlib::net::udp |
UdpSocket + UdpRecv — send_to / recv_from de datagramas sem conexão. |
stdlib::net::multicast |
Multicast IPv4: join/leave de um grupo e ajuste do comportamento de envio multicast num socket UDP. |
stdlib::net::raw |
RawSocket (SOCK_RAW) para payloads de camada IP. Precisa de root / CAP_NET_RAW / Administrador. |
stdlib::net::icmp |
ping e traceroute, construídos em Glide puro sobre um raw socket. |
stdlib::net::tls |
Cliente e servidor TLS 1.2 / 1.3 (TlsConfig, TlsStream) apoiados em OpenSSL; linka -lssl -lcrypto. |
stdlib::net::ws |
Cliente WebSocket (RFC 6455) sobre ws:// (TCP) ou wss:// (TLS): WebSocket, WsMessage. |
stdlib::http::client |
O cliente HTTP/1.1+2+3: HttpClient, HttpResponse e os helpers one-shot http_get / http_post_*. |
stdlib::http::cookies |
CookieJar para o cliente — faz parsing de Set-Cookie, reenvia Cookie:. |
stdlib::http::form |
FormData — builder + parser de corpo application/x-www-form-urlencoded. |
stdlib::http::multipart |
Multipart — builder multipart/form-data para upload de arquivos. |
stdlib::http::jwt |
JWT (JwtClaims) sign + verify sobre HMAC. |
stdlib::http::h2 |
Framing HTTP/2 (RFC 7540) + o transporte cliente só-GET (habilite com c.http2 = true). |
stdlib::http::hpack |
Compressão de header HPACK (RFC 7541) usada pelo transporte HTTP/2. |
stdlib::http::h3 |
Cliente e servidor HTTP/3 sobre QUIC (libngtcp2 / libnghttp3); habilite com c.http3 = true. |
stdlib::http |
O núcleo do servidor HTTP/1.1: HttpRequest, HttpResponse, o trait de streaming ChunkSource e http_listen / https_listen / http_listen_workers. |
stdlib::http::router |
Router ciente de método (Router, Route, Chain) com segmentos :param e *wildcard e cadeias de middleware. |
stdlib::http::route |
@route(METHOD, "/path") + attrs açucarados @get/@post/... e a macro de auto-registro routes!(r). |
stdlib::http::handler |
Proc macro @handler — params de handler tipados ao estilo Axum sobre a forma fn-pointer do router. |
stdlib::http::extract |
Extractors FromRequest: Bearer, Headers, Basic, Authorization<S>, Path<T>. |
stdlib::http::typed |
Ergonomia de JSON tipado: Json<T>, o trait JsonBind e value.into_response(). |
stdlib::http::cors |
CorsConfig + middleware CORS para o router. |
stdlib::http::compress |
gzip_mw — middleware de compressão gzip de resposta (linka -lz). |
stdlib::http::static |
Middleware de arquivos estáticos: monta um diretório num prefixo de URL com ETag / 304 / index.html. |
stdlib::http::sse |
Server-Sent Events (text/event-stream) em cima de ChunkSource. |
stdlib::http::proxy |
ReverseProxyConfig — encaminha requisições para uma origem upstream. |
stdlib::url |
Percent-encoding de URL: url_encode e url_decode (RFC 3986). |
IP & endereços de socket — IpAddr
Uma única struct etiquetada guarda um endereço IPv4 ou IPv6. Não há rotatividade de heap além do único malloc por endereço: v4 empacota num campo de 32 bits, v6 em duas metades de 64 bits. SocketAddr junta um IpAddr com uma porta. O parsing é falível e retorna !T (um Result); leia o resultado através de .ok / .val / .err.
IpAddr
pub struct IpAddr {
pub kind: i32, // 4 ou 6
pub v4: i32, // endereço de 32 bits em ordem-de-host, válido quando kind == 4
pub v6_hi: i64, // 64 bits superiores, ordem-de-host, válido quando kind == 6
pub v6_lo: i64, // 64 bits inferiores, ordem-de-host, válido quando kind == 6
}
kind é 4 ou 6; os outros campos só têm significado para o kind correspondente. Para v4, 127.0.0.1 empacota em 0x7F000001. Para v6, ::1 tem v6_hi = 0, v6_lo = 1.
Construtores
IpAddr::v4(a: i32, b: i32, c: i32, d: i32) -> *IpAddr— a partir de quatro octetos.IpAddr::v6(hi: i64, lo: i64) -> *IpAddr— a partir de duas metades de 64 bits (ordem-de-host).IpAddr::loopback_v4() -> *IpAddr—127.0.0.1.IpAddr::loopback_v6() -> *IpAddr—::1.IpAddr::unspec_v4() -> *IpAddr—0.0.0.0(bind em qualquer interface).IpAddr::unspec_v6() -> *IpAddr—::.
let ip: *IpAddr = IpAddr::v4(192, 168, 1, 100);
ip.to_string(); // => "192.168.1.100"
let one: i64 = 1;
IpAddr::v6(0, one).to_string(); // => "::1"
IpAddr::loopback_v4().to_string(); // => "127.0.0.1"
IpAddr::unspec_v4().is_unspecified(); // => true
Predicados
is_v4(self: *IpAddr) -> bool—truese for um endereço v4.is_v6(self: *IpAddr) -> bool—truese for um endereço v6.is_loopback(self: *IpAddr) -> bool—truepara127.0.0.0/8(v4) ou::1(v6).is_unspecified(self: *IpAddr) -> bool—truepara o endereço todo-zeros.is_private(self: *IpAddr) -> bool—truepara RFC 1918 (10/8,172.16/12,192.168/16) em v4 oufc00::/7unique-local em v6.
IpAddr::v4(8, 8, 8, 8).is_v4(); // => true
IpAddr::loopback_v6().is_v6(); // => true
IpAddr::v4(127, 0, 0, 1).is_loopback(); // => true
IpAddr::v4(8, 8, 8, 8).is_loopback(); // => false
IpAddr::v4(192, 168, 1, 5).is_private(); // => true
IpAddr::v4(8, 8, 8, 8).is_private(); // => false
Formatação & igualdade
to_string(self: *IpAddr) -> string— endereço textual; v6 segue a RFC 5952 (sequência de zeros mais longa colapsada em::).eq(self: *IpAddr, other: *IpAddr) -> bool— igualdade byte a byte. Um valor v4 e um v6 sempre comparam como diferentes mesmo quando se referem ao mesmo endereço.
IpAddr::v4(8, 8, 8, 8).to_string(); // => "8.8.8.8"
IpAddr::loopback_v6().to_string(); // => "::1"
let a: *IpAddr = IpAddr::v4(127, 0, 0, 1);
let b: *IpAddr = IpAddr::v4(127, 0, 0, 1);
a.eq(b); // => true
Parsing
IpAddr::parse(s: string) -> !*IpAddr— faz parse de"1.2.3.4"(v4) ou"::1"/"2001:db8::1"(v6). Retornaerrpara entrada malformada. A presença de um:seleciona o caminho v6.
let r: !*IpAddr = IpAddr::parse("192.168.0.1");
if !r.ok {
println!("parse falhou:", r.err);
} else {
r.val.is_private(); // => true
}
IpAddr::parse("not.an.ip").ok; // => false
SocketAddr
pub struct SocketAddr {
pub ip: *IpAddr,
pub port: i32,
}
SocketAddr::new(ip: *IpAddr, port: i32) -> *SocketAddr— junta um IP e uma porta.to_string(self: *SocketAddr) -> string— renderiza comohost:port; hosts v6 são envolvidos em colchetes conforme a RFC 3986.SocketAddr::parse(s: string) -> !*SocketAddr— faz parse de"1.2.3.4:80"ou"[::1]:80".free(self: *SocketAddr)— libera oIpAddrenvolvido e a própria structSocketAddr.
let sa: *SocketAddr = SocketAddr::new(IpAddr::loopback_v4(), 8080);
sa.to_string(); // => "127.0.0.1:8080"
SocketAddr::new(IpAddr::loopback_v6(), 80).to_string(); // => "[::1]:80"
Faça o round-trip de um endereço de socket textual, tratando o erro explicitamente:
let r: !*SocketAddr = SocketAddr::parse("127.0.0.1:8080");
if !r.ok {
println!("endereço inválido:", r.err);
return;
}
let sa: *SocketAddr = r.val;
defer sa.free();
println!(sa.port); // => 8080
SocketAddr::parse("[::1]:443").ok; // => true
Resolução DNS — resolve
stdlib::net::dns transforma um hostname em uma lista de endereços IP envolvendo o resolver do sistema (getaddrinfo). Não há um tipo de registro próprio — uma consulta produz um *Vector<*IpAddr> reaproveitando o tipo IpAddr da seção de IP, com cada entrada carregando sua própria família v4/v6.
import stdlib::net::dns::*;
A API é síncrona: uma chamada bloqueia o coro chamador até o SO responder. O escalonador do runtime mantém os outros coros do mesmo worker rodando, então uma consulta lenta não trava o app inteiro, mas cada lookup ainda é uma syscall bloqueante. Cargas dominadas por resolução devem se espalhar entre coros via spawn / WaitGroup.
Modelo de erro
Todo ponto de entrada retorna !*Vector<*IpAddr> (um Result). Inspecione .ok antes de ler .val; em caso de falha .err guarda a mensagem:
let r: !*Vector<*IpAddr> = resolve("does-not-exist.invalid");
if !r.ok {
println!(r.err); // => dns: resolve failed for does-not-exist.invalid
return;
}
O único erro produzido pelo wrapper do resolver é dns: resolve failed for <host>, levantado quando getaddrinfo reporta qualquer falha. Uma chamada bem-sucedida ainda pode retornar um vetor vazio se o host não tiver registros A ou AAAA.
resolve
pub fn resolve(host: string) -> !*Vector<*IpAddr>
Resolve host para todos os registros A e AAAA que o resolver do sistema retorna, na ordem em que o resolver os retornou. Endereços v4 e v6 são misturados em um único vetor; teste cada um com is_v4() / is_v6().
import stdlib::net::dns::*;
let r: !*Vector<*IpAddr> = resolve("example.com");
if r.ok {
for let i: i32 = 0; i < r.val.len(); i++ {
println!(r.val.get(i).to_string()); // => 93.184.216.34, etc.
}
}
Usando ? para propagar o erro em vez de ramificar:
fn first_addr(host: string) -> !string {
let ips: *Vector<*IpAddr> = resolve(host)?;
if ips.len() == 0 { return err("no addresses for ".concat(host)); }
return ok(ips.get(0).to_string());
}
resolve_v4
pub fn resolve_v4(host: string) -> !*Vector<*IpAddr>
Igual a resolve, mas mantém apenas registros v4 (A). O vetor resultante contém somente endereços onde is_v4() é verdadeiro.
let r: !*Vector<*IpAddr> = resolve_v4("example.com");
if r.ok && r.val.len() > 0 {
println!("first v4:", r.val.get(0).to_string()); // => first v4: 93.184.216.34
}
resolve_v6
pub fn resolve_v6(host: string) -> !*Vector<*IpAddr>
Igual a resolve, mas mantém apenas registros v6 (AAAA).
let r: !*Vector<*IpAddr> = resolve_v6("ipv6.google.com");
if r.ok && r.val.len() > 0 {
println!("first v6:", r.val.get(0).to_string()); // => first v6: 2607:f8b0:4005:80b::200e
}
Note que resolve_v4 e resolve_v6 ambos chamam resolve internamente e filtram o resultado, então uma falha do resolver aparece de forma idêntica através de .err, e um vetor vazio significa que o host não anunciou registros daquela família.
TCP — cliente e servidor
Sockets de fluxo de bytes brutos. O lado cliente é o TcpStream (conecta para um host remoto); o lado servidor é um Listener que aceita Conns em loop. Ambos passam pelo reactor assíncrono do runtime: no Linux um coro estacionado libera seu worker enquanto o kernel completa o handshake ou preenche o buffer de leitura; no Windows/macOS/BSD a mesma superfície roda como I/O síncrono bloqueante.
import stdlib::net::tcp::*;
import stdlib::net::listener::*;
import stdlib::net::ip::*;
Para um servidor HTTP, use http_listen (veja a seção do servidor HTTP) em vez de montar à mão um loop de accept com Listener — ele faz parsing de requisição, keep-alive e o caminho rápido de writev header+body por você. As primitivas abaixo são para protocolos TCP simples.
TcpStream — cliente de saída
Um socket de stream conectado. O único campo é o file descriptor:
pub struct TcpStream {
pub fd: i64,
}
TcpStream também implementa a trait Stream, então um valor pode ser mantido como *dyn Stream ao lado de um stream TLS e compartilhado por camadas superiores (ws, http).
TcpStream::connect(dst: *SocketAddr) -> !*TcpStream
Conecta a um endereço já resolvido. Retorna err se o socket não puder ser criado ou o handshake falhar.
let dst: *SocketAddr = SocketAddr::parse("93.184.216.34:80").val;
let r: !*TcpStream = TcpStream::connect(dst);
if !r.ok { println!(r.err); return 1; }
let s: *TcpStream = r.val;
defer s.close();
s.write_all("GET / HTTP/1.0\r\nHost: example.com\r\n\r\n");
let body: !string = s.read_to_end();
if body.ok { println!(body.val.len()); } // => contagem de bytes da resposta
TcpStream::connect_host(host: string, port: i32) -> !*TcpStream
Resolve host via DNS e conecta ao primeiro endereço alcançável na port, tentando cada endereço retornado em ordem. Retorna o último erro de conexão se todos os candidatos falharem.
let r: !*TcpStream = TcpStream::connect_host("example.com", 80);
if !r.ok { println!(r.err); return 1; }
let s: *TcpStream = r.val;
defer s.close();
s.write_all("GET / HTTP/1.0\r\n\r\n");
stream.write(data: string) -> !i32
Escreve data uma vez. Pode enviar menos bytes que data.len() sob back-pressure; a contagem retornada é o que realmente saiu. Use write_all quando precisar que cada byte seja entregue.
let w: !i32 = s.write("PING\r\n");
if w.ok { println!(w.val); } // => bytes escritos nesta chamada
stream.write_all(data: string) -> !i32
Escreve cada byte de data, fazendo loop no runtime até o payload ser drenado. Retorna o total escrito.
let r: !i32 = s.write_all("HELLO server\r\n");
if !r.ok { println!(r.err); }
stream.read(max: i32) -> !string
Lê até max bytes. ok com uma string não vazia é dado; ok com uma string de tamanho zero significa que o peer fechou a conexão de forma limpa; err é um erro de socket.
let r: !string = s.read(4096);
if !r.ok { println!(r.err); return 1; }
if r.val.len() == 0 {
println!("peer closed");
} else {
println!(r.val);
}
stream.read_to_end(self) -> !string
Lê repetidamente até EOF (o peer fecha), acumulando tudo. Útil para trocas no estilo HTTP/1.0 de "ler a resposta inteira".
s.write_all("GET / HTTP/1.0\r\n\r\n");
let r: !string = s.read_to_end();
if r.ok { println!(r.val); } // corpo completo da resposta
stream.set_timeout_ms(ms: i32)
Aplica um timeout combinado de leitura + escrita em milissegundos via SO_RCVTIMEO / SO_SNDTIMEO. Passe 0 para desabilitar. O fd do cliente permanece bloqueante para o timeout fazer efeito.
s.set_timeout_ms(5000); // deadline de leitura/escrita de 5s
let r: !string = s.read(4096); // err no timeout em vez de travar
Métodos da trait Stream
Como TcpStream implementa Stream, estes métodos de buffer binário também estão disponíveis:
stream.read_bytes(buf: *u8, max: i32) -> !i32— lê atémaxbytes para ostream.write_bytes(buf: *u8, n: i32) -> !i32— escrevenbytes debuf.stream.close(self)— fecha o socket e libera a struct. Apósclose()o
buffer do chamador; retorna a contagem (0 = peer fechou).
ponteiro fica pendurado.
let buf: *u8 = malloc(1024) as *u8;
let r: !i32 = s.read_bytes(buf, 1024);
if r.ok && r.val > 0 {
s.write_bytes(buf, r.val); // ecoa os bytes de volta
}
free(buf as *void);
Listener — loop de accept do servidor
Listener envolve um socket bound e em escuta; accept devolve um Conn por cliente. Ambos carregam apenas um fd:
pub struct Listener { pub fd: i32 }
pub struct Conn { pub fd: i32 }
Listener::bind(port: i32) -> *Listener
Faz bind e escuta na port em todas as interfaces. O Listener retornado tem fd < 0 em caso de falha (porta em uso, privilégios ausentes); sempre verifique ok() antes de aceitar.
let l: *Listener = Listener::bind(8080);
if !l.ok() { println!("bind failed"); return 1; }
defer l.close();
println!("listening on :8080");
Listener::bind_reuseport(port: i32) -> *Listener
Igual a bind, mas seta SO_REUSEPORT onde for suportado para que múltiplos processos possam compartilhar uma porta de accept e deixar o kernel balancear os accepts entre eles. No Windows isso degrada para o comportamento de bind comum (somente SO_REUSEADDR).
let l: *Listener = Listener::bind_reuseport(8080);
if !l.ok() { println!("bind failed"); return 1; }
defer l.close();
listener.ok(self) -> bool
true quando o listener está bound e pronto para aceitar.
listener.accept(self) -> *Conn
Bloqueia até um cliente conectar, retornando um Conn. O Conn tem fd < 0 em caso de erro — verifique ok() primeiro.
while true {
let c: *Conn = l.accept();
if !c.ok() { c.close(); continue; }
spawn handle(c); // um coro por conexão
}
listener.close(self)
Fecha o socket de escuta e libera a struct. Combine com defer.
Conn — uma conexão aceita
conn.ok(self) -> bool
true quando o handshake da conexão foi completado.
conn.read(buf: *void, max: i32) -> i32
Lê até max bytes para buf. Retorna a contagem de bytes, 0 quando o peer fechou de forma limpa, ou -1 em caso de erro.
let buf: *u8 = malloc(1024) as *u8;
let n: i32 = c.read(buf as *void, 1024);
if n <= 0 { free(buf as *void); c.close(); return; }
conn.write(buf: *void, n: i32) -> i32
Escreve n bytes de buf. Pode escrever menos que n — faça loop na contagem retornada para payloads maiores que o buffer de envio do kernel.
let mut sent: i32 = 0;
while sent < n {
let k: i32 = c.write(buf as *void, n - sent);
if k <= 0 { break; }
sent = sent + k;
}
conn.write_str(s: string) -> i32
Conveniência: escreve uma string inteira (nenhum NUL ao final é enviado). Retorna a contagem de bytes escritos.
c.write_str("HTTP/1.0 200 OK\r\n\r\nhi");
conn.write2(buf1: *void, n1: i32, buf2: *void, n2: i32) -> i32
Gather-write de dois buffers em uma única syscall writev / WSASend — envia um header e um corpo sem uma cópia extra ou uma segunda syscall. Retorna o total de bytes escritos.
conn.sendfile(path: string) -> i32
Transferência zero-copy de arquivo → socket: sendfile() no Linux, TransmitFile() no Windows, um loop de read+write no macOS/BSD. As páginas de disco vão direto do page cache do kernel para o buffer do socket. Retorna o total de bytes enviados (o tamanho do arquivo em caso de sucesso) ou -1 em caso de erro.
c.write_str("HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\n");
c.sendfile("public/index.html");
conn.close(self)
Fecha a conexão e libera a struct. O ponteiro fica pendurado depois; não o toque novamente. Combine com defer.
Servidor de echo completo
Um servidor de echo de linha completo: bind, accept em loop, e spawn de um coro por conexão que lê e escreve de volta até o peer fechar.
import stdlib::net::listener::*;
fn handle(c: *Conn) {
defer c.close();
let buf: *u8 = malloc(4096) as *u8;
defer free(buf as *void);
while true {
let n: i32 = c.read(buf as *void, 4096);
if n <= 0 { return; } // EOF ou erro
c.write(buf as *void, n); // ecoa os bytes direto de volta
}
}
fn main() -> i32 {
let l: *Listener = Listener::bind(9000);
if !l.ok() { println!("bind failed"); return 1; }
defer l.close();
println!("echo server on :9000");
while true {
let c: *Conn = l.accept();
if !c.ok() { c.close(); continue; }
spawn handle(c);
}
return 0;
}
UDP & multicast — UdpSocket
Sockets de datagrama sem conexão. Diferente do TCP não há fluxo nem handshake: todo send_to carrega seu destino e todo recv_from informa o remetente. A E/S de datagramas é bloqueante na v1. O socket assenta sobre a fundação net::sys, então um UdpSocket é apenas um descritor de arquivo encapsulado.
Quase toda chamada retorna um !T (Result); exponha-o via .ok / .err / .val, ou faça curto-circuito com ?.
UdpSocket
pub struct UdpSocket {
pub fd: i64,
}
O fd cru é exposto porque os helpers de multicast (e qualquer opção de socket de baixo nível que você precise) operam diretamente sobre o descritor.
UdpSocket::bind(addr: *SocketAddr) -> !*UdpSocket
Liga um socket UDP a um endereço local. Passe IpAddr::unspec_v4() (ou a forma v6) para escutar em todas as interfaces. A família do endereço é escolhida a partir de addr.ip automaticamente.
import stdlib::net::udp::*;
import stdlib::net::ip::*;
let any: *IpAddr = IpAddr::unspec_v4();
let addr: *SocketAddr = SocketAddr::new(any, 5353);
let r: !*UdpSocket = UdpSocket::bind(addr);
if !r.ok { println!(r.err); return 1; } // => texto da falha de bind
let sock: *UdpSocket = r.val;
defer sock.close();
UdpSocket::bind_port(port: i32) -> !*UdpSocket
Conveniência: liga em 0.0.0.0:port (todas as interfaces v4).
let sock: *UdpSocket = UdpSocket::bind_port(5000).val;
defer sock.close();
UdpSocket::send_to(self, data: string, dst: *SocketAddr) -> !i32
Envia data para dst. Retorna o número de bytes enviados. O UDP não fragmenta payloads de modo usuário, então mantenha cada chamada ≤ 1472 bytes para v4 seguro sobre Ethernet.
let dst: *SocketAddr = SocketAddr::new(IpAddr::v4(127, 0, 0, 1), 5000);
let s: !i32 = sock.send_to("ping", dst);
if !s.ok { println!(s.err); return 1; }
println!(s.val); // => 4
UdpSocket::recv_from(self, max: i32) -> !*UdpRecv
Bloqueia até um datagrama chegar. max limita o tamanho do payload — bytes além de max são descartados silenciosamente pelo kernel. O resultado emparelha o payload com o SocketAddr do remetente.
let r: !*UdpRecv = sock.recv_from(1500);
if !r.ok { println!(r.err); return 1; }
let pkt: *UdpRecv = r.val;
println!(pkt.data); // => os bytes recebidos
println!(pkt.from.port); // => porta de origem do remetente
UdpSocket::close(self)
Fecha o descritor e libera a struct. Combine com defer.
let sock: *UdpSocket = UdpSocket::bind_port(5000).val;
defer sock.close();
Um respondedor de eco completo, ligando em qualquer interface, lendo um datagrama e respondendo ao seu remetente:
import stdlib::net::udp::*;
import stdlib::net::ip::*;
let r: !*UdpSocket = UdpSocket::bind(SocketAddr::new(IpAddr::unspec_v4(), 9000));
if !r.ok { println!(r.err); return 1; }
let sock: *UdpSocket = r.val;
defer sock.close();
let got: !*UdpRecv = sock.recv_from(1500);
if !got.ok { println!(got.err); return 1; }
let pkt: *UdpRecv = got.val;
println!(pkt.data); // => "hello"
sock.send_to(pkt.data, pkt.from); // ecoa de volta ao endereço de origem
UdpRecv
Um datagrama recebido: payload mais endereço do remetente.
pub struct UdpRecv {
pub data: string, // o payload do datagrama
pub from: *SocketAddr, // quem o enviou
}
from é reconstruído a partir do endereço de origem do kernel — datagramas IPv4 decodificam para um IpAddr em octetos pontilhados, IPv6 para um de 128 bits — então você pode responder com send_to(.., pkt.from) sem rastrear pares você mesmo.
Multicast
Adesão a grupo multicast IPv4 e ajuste de envio, sobrepostos a um UdpSocket ligado. Estas são funções livres que recebem o fd do socket em vez de métodos — ligue o socket primeiro, depois entre no grupo. O empacotamento do endereço em ordem de bytes de rede é tratado no runtime, então você passa IpAddrs em ordem do host.
mc_join_v4(fd: i64, group: *IpAddr, iface: *IpAddr) -> !i32
Entra em um group multicast IPv4 na interface local iface. Use IpAddr::unspec_v4() para a interface padrão. O socket já deve estar ligado.
import stdlib::net::udp::*;
import stdlib::net::multicast::*;
import stdlib::net::ip::*;
let sock: *UdpSocket = UdpSocket::bind_port(5353).val;
defer sock.close();
let group: *IpAddr = IpAddr::v4(239, 1, 2, 3);
let j: !i32 = mc_join_v4(sock.fd, group, IpAddr::unspec_v4());
if !j.ok { println!(j.err); return 1; }
let r: !*UdpRecv = sock.recv_from(1500); // agora recebe o tráfego do grupo
if r.ok { println!(r.val.data); }
mc_leave_v4(fd: i64, group: *IpAddr, iface: *IpAddr) -> !i32
Sai de um grupo multicast IPv4 ao qual já se aderiu.
let l: !i32 = mc_leave_v4(sock.fd, group, IpAddr::unspec_v4());
if !l.ok { println!(l.err); }
mc_set_ttl(fd: i64, ttl: i32) -> !i32
Limite de saltos para datagramas multicast de saída. O padrão 1 mantém o tráfego na sub-rede local; aumente-o para rotear através de roteadores.
mc_set_ttl(sock.fd, 4); // permite até 4 saltos de roteador
mc_set_loop(fd: i64, on: bool) -> !i32
Se o multicast enviado deste socket retorna aos membros locais (padrão ligado). Desligue quando não quiser receber seus próprios envios.
mc_set_loop(sock.fd, false); // suprime o loopback local dos nossos próprios pacotes
Um remetente que entra em um grupo, desabilita o loopback para não ver seu próprio tráfego, e emite um datagrama com um TTL mais amplo:
import stdlib::net::udp::*;
import stdlib::net::multicast::*;
import stdlib::net::ip::*;
let sock: *UdpSocket = UdpSocket::bind_port(0).val;
defer sock.close();
let group: *IpAddr = IpAddr::v4(239, 1, 2, 3);
mc_set_loop(sock.fd, false);
mc_set_ttl(sock.fd, 2);
let dst: *SocketAddr = SocketAddr::new(group, 5353);
let s: !i32 = sock.send_to("announce", dst);
if !s.ok { println!(s.err); return 1; }
println!(s.val); // => 8
TLS — TlsStream
TLS 1.2 / 1.3, cliente e servidor, apoiado em OpenSSL. A linha de link precisa de libssl + libcrypto — o Glide adiciona -lssl -lcrypto incondicionalmente, então um OpenSSL ausente aparece como o erro de link cannot find -lssl de praxe.
- Windows:
mingw-w64-ucrt-x86_64-openssl(toolchain UCRT64). - Linux / macOS:
libssl-dev/openssl@3.
O SNI é definido automaticamente a partir do nome de host passado para connect, e o bundle de CA do sistema dirige a verificação. No Windows a cadeia de CA é ponteada a partir dos stores de sistema ROOT + CA via Crypt32, já que o build OpenSSL do mingw vem com um OPENSSLDIR vazio. Definir verify=false desabilita a validação de certificado — nunca faça isso em produção.
import stdlib::net::tls::*;
let cfg: *TlsConfig = TlsConfig::client();
let r: !*TlsStream = TlsStream::connect("example.com", 443, cfg);
if !r.ok { println!(r.err); return 1; }
let s: *TlsStream = r.val;
s.write_all("GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n");
let body: string = s.read_to_end().val;
println!(body.len(), "bytes"); // => 1256 bytes
s.close();
TlsConfig
Configuração para uma conexão TLS de saída.
pub struct TlsConfig {
pub verify: bool, // valida cert + hostname do peer (sempre true em prod)
pub min_proto: i32, // 0=TLS1.0 1=TLS1.1 2=TLS1.2 3=TLS1.3 mínimo (padrão 2)
pub alpn: string, // lista ALPN separada por vírgula, ex. "h2,http/1.1"; "" desabilita
}
TlsConfig::client() -> *TlsConfig— config de cliente padrão:verify=true, mínimo TLS 1.2, sem ALPN.TlsConfig::client_insecure() -> *TlsConfig— validação de cert desligada; só para dev local.cfg.free()— libera a struct de config.
alpn e min_proto são campos simples, então ajuste-os após a construção:
let cfg: *TlsConfig = TlsConfig::client();
defer cfg.free();
cfg.alpn = "h2,http/1.1"; // anuncia HTTP/2 e depois HTTP/1.1
cfg.min_proto = 3; // exige TLS 1.3
Config insegura contra um servidor de dev autoassinado:
let cfg: *TlsConfig = TlsConfig::client_insecure(); // NÃO use em prod
defer cfg.free();
let r: !*TlsStream = TlsStream::connect("localhost", 8443, cfg);
if r.ok { defer r.val.close(); /* fala com o servidor autoassinado */ }
TlsStream
Um stream TLS conectado. O único campo é o handle opaco do OpenSSL:
pub struct TlsStream {
pub handle: *void,
}
TlsStream também implementa a trait Stream, então read_bytes / write_bytes / close ficam disponíveis ao lado dos métodos inerentes.
Construtores
TlsStream::connect(host: string, port: i32, cfg: *TlsConfig) -> !*TlsStream— resolvehostvia DNS, abre o TCP, roda o handshake. O hostname alimenta o SNI e a verificação de cert.TlsStream::attach(tcp: *TcpStream, hostname: string, cfg: *TlsConfig) -> !*TlsStream— roda o handshake TLS sobre umTcpStreamjá conectado (upgrade estilo STARTTLS). Em caso de sucesso o stream retornado é dono do fd; não chametcp.close()depois — o wrapperTcpStreamé liberado para você.
I/O
stream.write(data: string) -> !i32— escrevedata; retorna os bytes escritos (pode ser parcial). Trunca em NUL.stream.write_all(data: string) -> !i32— escreve cada byte, fazendo loop em escritas parciais.stream.read(max: i32) -> !string— lê atémaxbytes;ok("")significa que o peer fechou de forma limpa.stream.read_to_end() -> !string— lê até o peer fechar a conexão.stream.read_bytes(buf: *u8, max: i32) -> !i32— leitura binária-segura para dentro debuf(trait Stream).stream.write_bytes(buf: *u8, n: i32) -> !i32— escrita binária-segura (trait Stream); ao contrário dewrite, não trunca em NUL.
Info de conexão
stream.alpn() -> string— protocolo ALPN negociado, ou""se nenhum.stream.version() -> i32—2para TLS 1.2,3para TLS 1.3,0caso contrário.stream.cipher() -> string— nome da suíte de cifra negociada, ex.TLS_AES_256_GCM_SHA384.stream.set_timeout_ms(ms: i32)— aplica timeouts de leitura + escrita no socket subjacente.
Encerramento
stream.close()— encerra o TLS, fecha o socket, libera o stream (trait Stream).
Uma requisição que inspeciona a conexão negociada:
let cfg: *TlsConfig = TlsConfig::client();
cfg.alpn = "h2,http/1.1";
let r: !*TlsStream = TlsStream::connect("cloudflare.com", 443, cfg);
cfg.free();
if !r.ok { eprintln(r.err); return 1; }
let s: *TlsStream = r.val;
defer s.close();
println!("alpn:", s.alpn()); // => alpn: h2
println!("version:", s.version()); // => version: 3
println!("cipher:", s.cipher()); // => cipher: TLS_AES_256_GCM_SHA384
Upgrade STARTTLS — conecta em texto puro, negocia, depois promove a mesma conexão:
import stdlib::net::tcp::*;
let tcp: *TcpStream = TcpStream::connect_host("smtp.example.com", 587).val;
tcp.write_all("EHLO me\r\nSTARTTLS\r\n");
// ... lê a resposta "220 Go ahead" no tcp ...
let cfg: *TlsConfig = TlsConfig::client();
let r: !*TlsStream = TlsStream::attach(tcp, "smtp.example.com", cfg);
cfg.free();
if !r.ok { eprintln(r.err); return 1; } // tcp é consumido; não o feche
let s: *TlsStream = r.val;
defer s.close();
s.write_all("EHLO me\r\n"); // agora sobre TLS
Enquadramento binário-seguro sobre os métodos Stream:
let s: *TlsStream = TlsStream::connect("example.com", 443, TlsConfig::client()).val;
defer s.close();
let frame: *u8 = malloc(9) as *u8; // payload de 9 bytes que pode conter NULs
// ... preenche o frame ...
let w: !i32 = s.write_bytes(frame, 9);
if !w.ok { eprintln(w.err); }
let buf: *u8 = malloc(4096) as *u8;
let rd: !i32 = s.read_bytes(buf, 4096);
if rd.ok { println!("read", rd.val, "bytes"); }
TlsListener
Listener do lado servidor: mantém o SSL_CTX (cert + chave) mais o socket TCP de escuta. accept bloqueia até o próximo cliente e retorna um TlsStream assim que o handshake tem sucesso.
Construtores
TlsListener::bind(port: i32, cert_path: string, key_path: string) -> !*TlsListener— faz bind em0.0.0.0:port, carrega o cert PEM (+ cadeia) e a chave privada PEM, começa a escutar.TlsListener::bind_alpn(port, cert_path, key_path, alpn_csv: string) -> !*TlsListener— o mesmo, mas anuncia protocolos ALPN (CSV, ordem de preferência do servidor — a primeira oferta correspondente do cliente vence).
Aceitando
listener.accept() -> !*TlsStream— bloqueia até o próximo cliente, roda o handshake, retorna um stream pronto. Clientes cujo handshake falha são reportados comoerr.listener.accept_raw() -> !i64— accept só do TCP; retorna o fd bruto, deixando o handshake a cargo do chamador (tipicamente numa thread worker). Evita que o loop de accept bloqueie em um handshake lento ou silencioso.listener.attach(fd: i64) -> !*TlsStream— completa o handshake em um fd vindo doaccept_raw, reutilizando o contexto de cert do listener. O fd é fechado automaticamente em falha de handshake.
Encerramento
listener.close()— libera o contexto de cert e o socket de escuta.
Um loop de servidor mínimo estilo HTTPS:
let r: !*TlsListener = TlsListener::bind(8443, "fullchain.pem", "privkey.pem");
if !r.ok { eprintln(r.err); return 1; }
let l: *TlsListener = r.val;
defer l.close();
while true {
let a: !*TlsStream = l.accept();
if !a.ok { continue; } // pula handshakes que falharam
let s: *TlsStream = a.val;
s.write_all("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi");
s.close();
}
Anuncie HTTP/2 via ALPN e ramifique conforme o protocolo negociado:
let r: !*TlsListener =
TlsListener::bind_alpn(443, "cert.pem", "key.pem", "h2,http/1.1");
if !r.ok { eprintln(r.err); return 1; }
let l: *TlsListener = r.val;
defer l.close();
while true {
let a: !*TlsStream = l.accept();
if !a.ok { continue; }
let s: *TlsStream = a.val;
if s.alpn().eq("h2") {
// fala HTTP/2
} else {
s.write_all("HTTP/1.1 200 OK\r\n\r\nhi");
}
s.close();
}
Descarregue o handshake para um worker para que um cliente travado não estacione o loop de accept:
let l: *TlsListener = TlsListener::bind(443, "cert.pem", "key.pem").val;
defer l.close();
loop {
let fd: !i64 = l.accept_raw();
if !fd.ok { continue; }
spawn_thread || {
let a: !*TlsStream = l.attach(fd.val); // handshake fora do loop de accept
if a.ok {
let s: *TlsStream = a.val;
s.write_all("HTTP/1.1 200 OK\r\n\r\nhi");
s.close();
}
};
}
tls_attach_server
Contraparte do lado servidor para TlsStream::attach: envolve uma conexão em texto puro já aceita em TLS para protocolos que promovem inline (SMTP STARTTLS, IMAP STARTTLS, POP3 STLS, FTPS AUTH TLS).
tls_attach_server(conn_fd: i32, cert_path: string, key_path: string) -> !*TlsStream
Em caso de sucesso o TlsStream retornado é dono do socket subjacente; não feche a conexão original depois.
// Num servidor SMTP, após o cliente enviar STARTTLS:
let r: !*TlsStream = tls_attach_server(conn_fd, "cert.pem", "key.pem");
if !r.ok { eprintln(r.err); return 1; }
let s: *TlsStream = r.val;
defer s.close();
s.write_all("220 ready\r\n"); // o resto da sessão é criptografado
Sockets de baixo nível — Socket
Sob as conveniências de TCP/UDP/TLS há uma fundação de socket fina e tipada: stdlib::net::sys declara a superfície de syscalls gnet_* do runtime e envolve um fd com posse em Socket, num trait Stream binário-seguro e num conjunto plano de constantes de família / opção / shutdown. stdlib::net::sockopt adiciona os toggles comuns de opção sobre qualquer .fd; stdlib::net::raw abre sockets SOCK_RAW; e stdlib::net::icmp constrói ping / traceroute em Glide puro sobre um socket raw. Toda chamada retorna !T e expõe erros via .ok / .err (sem panics), então um privilégio ausente ou uma opção sem suporte é apenas um valor de erro.
Constantes
De stdlib::net::sys (cada uma é pub const … : i32):
// tipo de socket
SOCK_STREAM = 1 SOCK_DGRAM = 2 SOCK_RAW = 3
// família de endereço
AF_V4 = 4 AF_V6 = 6
// shutdown(how)
SHUT_RD = 0 SHUT_WR = 1 SHUT_RDWR = 2
// tags de opção de gnet_const() (resolvidas para números por OS em netcore.c)
OPT_SOL_SOCKET = 0 OPT_SO_REUSEADDR = 1 OPT_IPPROTO_TCP = 2
OPT_TCP_NODELAY = 3 OPT_SO_BROADCAST = 4 OPT_SO_REUSEPORT = 5
OPT_SO_RCVBUF = 6 OPT_SO_SNDBUF = 7 OPT_IPPROTO_IP = 8
OPT_IP_TTL = 9 OPT_IP_MULTICAST_TTL = 10 OPT_IP_MULTICAST_LOOP = 11
As tags OPT_* são índices em gnet_const, que as mapeia para os números reais de opção da plataforma — passe-as para Socket::set_opt_int. De stdlib::net::raw, IPPROTO_ICMP = 1 é o número de protocolo IANA para um socket ICMP raw.
Socket
pub struct Socket {
pub fd: i64,
}
Um único fd com posse. Socket implementa Stream, então é fechado através do close do trait (não há um close inerente separado).
Statics e métodos:
Socket::open(family: i32, ty: i32, proto: i32) -> !*Socket— abre um socket.familyéAF_V4/AF_V6,tyéSOCK_STREAM/SOCK_DGRAM/SOCK_RAW,protoé0para o padrão.bind(self: *Socket, sa: *SocketAddr) -> !i32— faz bind a um endereço local.connect(self: *Socket, sa: *SocketAddr) -> !i32— conecta a um endereço remoto.listen(self: *Socket, backlog: i32) -> !i32accept(self: *Socket) -> !*Socket— aceita uma conexão, retornando umSocketnovo com posse para o peer (integrado ao reactor; o socket aceito fica não-bloqueante +TCP_NODELAY).local_addr(self: *Socket) -> !*SocketAddr— o endereço com bind local.peer_addr(self: *Socket) -> !*SocketAddr— o endereço do peer conectado.set_opt_int(self: *Socket, level: i32, opt: i32, val: i32) -> !i32— define uma opção com valor int;level/optvêm degnet_const.set_reuse_addr(self: *Socket, on: bool) -> !i32—SO_REUSEADDR.shutdown(self: *Socket, how: i32) -> !i32—howéSHUT_RD/SHUT_WR/SHUT_RDWR.send(self: *Socket, buf: *void, n: i32) -> !i32— envianbytes; retorna os bytes enviados.recv(self: *Socket, buf: *void, max: i32) -> !i32— recebe atémaxbytes; retorna os bytes lidos (0no EOF).
import stdlib::net::sys::*;
import stdlib::net::ip::*;
// Abrir, fazer bind e listen num socket de stream à mão.
let so = Socket::open(AF_V4, SOCK_STREAM, 0)?;
so.set_reuse_addr(true)?;
let addr = SocketAddr::new(IpAddr::v4(127, 0, 0, 1), 8080);
so.bind(addr)?;
so.listen(128)?;
let where = so.local_addr()?;
println!("listening on", where.to_string()); // => listening on 127.0.0.1:8080
Stream
O trait de transporte binário-seguro. Socket o implementa, e os streams de TLS/raw compartilham o mesmo formato, então código genérico pode receber um *dyn Stream.
pub trait Stream {
fn read_bytes(self: *Self, buf: *u8, max: i32) -> !i32; // bytes lidos (0 = EOF)
fn write_bytes(self: *Self, buf: *u8, n: i32) -> !i32; // bytes escritos
fn close(self: *Self);
}
Duas funções livres envolvem um *dyn Stream para I/O em formato de string:
stream_read(s: *dyn Stream, max: i32) -> !string— lê atémaxbytes como uma string.stream_write_all(s: *dyn Stream, data: string) -> !i32— escreve uma string inteira; retorna os bytes escritos.
import stdlib::net::sys::*;
import stdlib::net::ip::*;
let so = Socket::open(AF_V4, SOCK_STREAM, 0)?;
so.connect(SocketAddr::new(IpAddr::v4(93, 184, 216, 34), 80))?;
stream_write_all(so, "GET / HTTP/1.0\r\n\r\n")?;
let head = stream_read(so, 256)?;
println!(head); // => HTTP/1.0 200 OK ...
so.close(); // Stream::close libera o Socket
Opções de socket — sockopt
stdlib::net::sockopt dá uma superfície uniforme sobre qualquer fd da fundação (um Socket, TcpStream ou UdpSocket — todos expõem .fd). Cada uma recebe o fd e retorna !i32. Uma opção sem suporte na plataforma atual (ex.: SO_REUSEPORT fora do Linux) retorna um erro em vez de ter sucesso silenciosamente.
sock_set_reuse_addr(fd: i64, on: bool) -> !i32—SO_REUSEADDR, refaz bind numa porta recém-fechada imediatamente.sock_set_reuse_port(fd: i64, on: bool) -> !i32—SO_REUSEPORT, balanceia accepts entre workers (Linux).sock_set_nodelay(fd: i64, on: bool) -> !i32—TCP_NODELAY, desabilita o Nagle para que escritas pequenas sejam enviadas de uma vez.sock_set_broadcast(fd: i64, on: bool) -> !i32—SO_BROADCAST, permite enviar a um endereço de broadcast (UDP).sock_set_ttl(fd: i64, ttl: i32) -> !i32—IP_TTL, limite de hops unicast de saída.sock_set_recv_buf(fd: i64, bytes: i32) -> !i32— dica de tamanho deSO_RCVBUF.sock_set_send_buf(fd: i64, bytes: i32) -> !i32— dica de tamanho deSO_SNDBUF.sock_set_timeout_ms(fd: i64, ms: i32) -> !i32— timeout de leitura + escrita em ms (0desabilita); apoiado porSO_RCVTIMEO/SO_SNDTIMEO. Só atua num fd bloqueante.
import stdlib::net::sys::*;
import stdlib::net::sockopt::*;
import stdlib::net::ip::*;
let so = Socket::open(AF_V4, SOCK_STREAM, 0)?;
sock_set_reuse_addr(so.fd, true)?;
sock_set_nodelay(so.fd, true)?;
let r = sock_set_reuse_port(so.fd, true);
if !r.ok { println!("no SO_REUSEPORT here:", r.err); } // fora do Linux: imprime o erro
sock_set_timeout_ms(so.fd, 5000)?; // limita leituras bloqueantes a 5s
Sockets raw — RawSocket
stdlib::net::raw abre sockets SOCK_RAW para payloads de camada IP (ICMP, protocolos customizados, inspeção de pacotes). Sockets raw exigem privilégio — root / CAP_NET_RAW no Linux+macOS, Administrator no Windows — então open_* retorna um erro (tipicamente "1:Operation not permitted") quando o processo não tem permissão.
pub struct RawSocket {
pub fd: i64,
}
// um pacote recebido (inclui o cabeçalho IP num recv de SOCK_RAW)
pub struct RawRecv {
pub data: string,
pub n: i32,
pub from: *SocketAddr,
}
RawSocket::open_ip(proto: i32) -> !*RawSocket— abre um socket IPv4 raw paraproto(ex.:IPPROTO_ICMP).RawSocket::open_icmp() -> !*RawSocket— abre um socket ICMP raw (o caso comum).send_to(self: *RawSocket, buf: *void, n: i32, dst: *SocketAddr) -> !i32— envianbytes paradst; a camada IP adiciona o cabeçalho. Retorna os bytes enviados.recv_from(self: *RawSocket, buf: *void, max: i32) -> !*RawRecv— recebe um pacote num buffer com posse do chamador; retorna a contagem de bytes (.n) e o remetente (.from). Bloqueante.set_ttl(self: *RawSocket, ttl: i32) -> !i32— limite de hops de saída (usado pelo traceroute).set_timeout_ms(self: *RawSocket, ms: i32) -> !i32— timeout de recepção em ms (0desabilita).close(self: *RawSocket)
import stdlib::net::raw::*;
let r: !*RawSocket = RawSocket::open_icmp();
if !r.ok {
println!("need root:", r.err); // => need root: 1:Operation not permitted
return 1;
}
let sock: *RawSocket = r.val;
sock.set_timeout_ms(2000)?;
sock.close();
ICMP — ping & traceroute
stdlib::net::icmp monta mensagens de echo ICMP byte a byte (cabeçalho + checksum de complemento-de-um) sobre um socket raw — nenhum C além da fundação. Como os sockets raw, estes precisam de privilégio e retornam err(...) quando não permitidos.
icmp_checksum(buf: *u8, n: i32) -> i32— o checksum da Internet (RFC 1071). Sobre um pacote completo cujo campo de checksum está preenchido, retorna0(a checagem de validade).icmp_build_echo(buf: *u8, id: i32, seq: i32, payload_len: i32) -> i32— monta uma echo-request embuf(precisa de8 + payload_lenbytes); retorna o comprimento total.ping(host: string, timeout_ms: i32) -> !i64— pingahostuma vez; retorna o round-trip time em milissegundos.traceroute(host: string, max_hops: i32, timeout_ms: i32) -> !*Vector<string>— sonda TTL1..max_hops; uma linha por hop,"<ttl> <ip>"para um roteador que responde ou"<ttl> *"em timeout. Para no echo reply do destino. (Hops intermediários são só Linux/macOS; no Windows os replies de time-exceeded dos roteadores não chegam a um socket raw, então os hops aparecem como*até o reply final.)
import stdlib::net::icmp::*;
let r: !i64 = ping("example.com", 2000);
if r.ok { println!("rtt:", r.val, "ms"); } // => rtt: 14 ms
else { println!(r.err); } // ex.: icmp: timeout
import stdlib::net::icmp::*;
let tr = traceroute("example.com", 16, 1000)?;
for let i: i32 = 0; i < tr.len(); i++ {
println!(tr.get(i)); // => 1 192.168.0.1
} // 2 10.0.0.1
// 3 *
WebSocket — WebSocket
stdlib::net::ws é um cliente WebSocket RFC 6455 sobre TCP puro (ws://) ou TLS (wss://). A V1 cobre o caso comum: frames de texto + binários, ping/pong e fechamento limpo. Extensões (permessage-deflate, múltiplos subprotocolos) estão fora de escopo, e payloads de entrada têm um teto para manter os limites de buffer controlados. Ainda não há uma API de accept no lado do servidor — este módulo conecta, ele não escuta.
import stdlib::net::ws::*;
Toda chamada falível retorna !T, exposta através de .ok / .val / .err. Frames do cliente são sempre mascarados, conforme RFC 6455 §5.3 — a biblioteca cuida do mascaramento para você.
WsOp — opcodes de frame
WsOp é um namespace de constantes de opcode (RFC 6455 §11.8). Compare-as contra WsMessage.kind.
| Static | Valor | Significado |
|---|---|---|
WsOp::cont() |
0 |
Frame de continuação (mensagens fragmentadas) |
WsOp::text() |
1 |
Frame de texto |
WsOp::binary() |
2 |
Frame binário |
WsOp::close() |
8 |
Frame de fechamento (WsMessage.close_code carrega o motivo) |
WsOp::ping() |
9 |
Frame de ping |
WsOp::pong() |
10 |
Frame de pong |
let m: !*WsMessage = ws.recv();
if m.ok {
match m.val.kind {
k if k == WsOp::text() => println!("text:", m.val.text),
k if k == WsOp::ping() => { ws.pong(m.val.text); }, // keep-alive
k if k == WsOp::close() => println!("closed:", m.val.close_code),
_ => {},
}
}
WsMessage
Um frame de entrada, retornado por WebSocket::recv.
pub struct WsMessage {
pub kind: i32, // um de WsOp::text() / binary() / close() / ping() / pong()
pub text: string, // payload (frames de texto carregam a string; binários reusam este campo)
pub close_code: i32, // preenchido quando kind == WsOp::close(), senão 0
}
Ainda não há um tipo dedicado de bytes, então payloads binários também chegam em text.
WsParsedUrl
A forma parseada de uma URL ws:// / wss://.
pub struct WsParsedUrl {
pub is_wss: bool, // true para wss://, false para ws://
pub host: string, // host sem porta
pub port: i32, // porta explícita, senão 80 (ws) / 443 (wss)
pub path: string, // caminho da requisição, padrão "/"
}
host_header(self) -> string
Formata o host para o header HTTP Host, descartando a porta quando ela coincide com o padrão do esquema (80 para ws, 443 para wss).
// (valores de WsParsedUrl vêm de WebSocket::connect internamente; mostrado pela forma)
// host_header() => "example.com:8443" quando uma porta não-padrão está definida,
// => "example.com" quando a porta é o padrão do esquema
WebSocket
A conexão do cliente. Obtenha uma com WebSocket::connect; ela possui um *dyn Stream sobre um TcpStream (ws://) ou um TlsStream (wss://), então nenhuma ramificação de transporte vaza para o seu código.
WebSocket::connect(url) -> !*WebSocket
Abre uma conexão e realiza o handshake de HTTP Upgrade. url deve ser ws://host[:port]/path ou wss://host[:port]/path. A chamada verifica que o servidor retornou 101 Switching Protocols e que Sec-WebSocket-Accept corresponde a base64(sha1(key + magic)); qualquer divergência retorna um err.
let r: !*WebSocket = WebSocket::connect("wss://echo.example.com/sock");
if !r.ok { println!(r.err); return 1; }
let ws: *WebSocket = r.val;
ws.send_text("hello");
let m: !*WsMessage = ws.recv();
if m.ok { println!("got:", m.val.text); } // => got: hello
ws.close(1000, "bye");
send_text(self, msg) -> !i32
Envia um frame de texto UTF-8 (opcode 1). Sempre mascarado.
let w: !i32 = ws.send_text("{\"type\":\"hello\"}");
if !w.ok { println!("send failed:", w.err); }
send_binary(self, data) -> !i32
Envia um frame binário (opcode 2). Os bytes do payload ficam em uma string.
ws.send_binary("\x00\x01\x02\x03");
pong(self, payload) -> !i32
Envia um Pong (opcode 10), tipicamente ecoando um payload de Ping recebido para manter a conexão viva.
let m: !*WsMessage = ws.recv();
if m.ok && m.val.kind == WsOp::ping() {
ws.pong(m.val.text); // ecoa o payload do ping de volta
}
recv(self) -> !*WsMessage
Lê um frame de entrada. Pings são retornados literalmente — o chamador decide se faz auto-pong. Frames de fechamento aparecem com kind == WsOp::close() e close_code preenchido. Payloads mascarados são desmascarados para você.
let m: !*WsMessage = ws.recv();
if !m.ok { println!("recv error:", m.err); return 1; }
match m.val.kind {
k if k == WsOp::text() => println!(m.val.text),
k if k == WsOp::close() => println!("closed:", m.val.close_code),
_ => {},
}
close(self, code, reason) -> ()
Envia um frame de fechamento e desmonta o transporte. code 1000 é "fechamento normal"; veja RFC 6455 §7.4 para o registro. Isso libera a conexão — não use ws depois disso.
ws.close(1000, "bye"); // fechamento normal
ws.close(4000, "app reset"); // código definido pela app (4000–4999)
Cliente completo: connect, send, loop de recv, close
Um driver completo de cliente echo. Ele conecta, envia uma mensagem, depois faz loop em recv, fazendo auto-pong nos pings e quebrando em um frame de fechamento.
import stdlib::net::ws::*;
fn run() -> i32 {
let r: !*WebSocket = WebSocket::connect("wss://echo.example.com/sock");
if !r.ok { println!("connect:", r.err); return 1; }
let ws: *WebSocket = r.val;
let s: !i32 = ws.send_text("ping me");
if !s.ok { println!("send:", s.err); ws.close(1011, "send failed"); return 1; }
for let i: i32 = 0; i < 8; i++ {
let m: !*WsMessage = ws.recv();
if !m.ok { println!("recv:", m.err); break; }
let msg: *WsMessage = m.val;
match msg.kind {
k if k == WsOp::text() => println!("text:", msg.text),
k if k == WsOp::binary() => println!("binary bytes:", msg.text.len()),
k if k == WsOp::ping() => { ws.pong(msg.text); },
k if k == WsOp::close() => { println!("closed:", msg.close_code); break; },
_ => {},
}
}
ws.close(1000, "done");
return 0;
}
Cliente HTTP — HttpClient
Um cliente HTTP/1.1 e HTTP/2 bloqueante. HttpClient guarda a configuração reutilizável (user-agent, política de redirecionamento, cookie jar, timeout, verificação de TLS, opt-in de HTTP/2); HttpClientRequest é o envelope mutável da requisição que você monta e entrega a ele. Tanto http:// quanto https:// são suportados — HTTPS roda sobre stdlib::net::tls::TlsStream, e quando http2 está habilitado o cliente tenta h2 via ALPN e cai de volta para HTTP/1.1 de forma transparente.
Toda chamada retorna !*HttpResponse (um Result): teste .ok, leia o valor em .val, a mensagem em .err. O tipo da resposta é HttpResponse de stdlib::http — seus acessores (.status, .body, .header(name), .headers(name)) estão documentados na seção do servidor HTTP.
import stdlib::http::client::*;
let r: !*HttpResponse = http_get("http://example.com/");
if r.ok { println!(r.val.status, r.val.body.len(), "bytes"); }
HttpClientRequest
A requisição sendo montada. Um simples contêiner mutável — distinto do HttpRequest do lado servidor, que é parseado do fio.
pub struct HttpClientRequest {
pub method: string,
pub url: string,
pub headers: string, // linhas cruas "Name: Value\r\n"
pub body: string,
}
pub fn new(method: string, url: string) -> *HttpClientRequest— monta umpub fn set(self, name: string, value: string)— adiciona uma linha de header.pub fn body_str(self, s: string)— define o body e o header
envelope com headers e body vazios.
Múltiplas chamadas adicionam múltiplas linhas; não há deduplicação, então last-wins é responsabilidade de quem chama.
Content-Length correspondente em um único passo.
let req: *HttpClientRequest = HttpClientRequest::new("PATCH", "https://api.example.com/me");
req.set("Authorization", "Bearer xyz");
req.set("Content-Type", "application/json");
req.body_str("{\"name\":\"new\"}");
let r: !*HttpResponse = HttpClient::new().do(req);
if r.ok { println!(r.val.status); } // => 200
HttpClient
Configuração persistente do cliente. Mantenha uma instância por perto para compartilhar padrões entre várias requisições; para chamadas únicas as funções livres http_get / http_post_json montam um cliente padrão por chamada.
pub struct HttpClient {
pub user_agent: string, // enviado como User-Agent; padrão "glide/0.1"
pub max_redirects: i32, // saltos que `do` vai seguir; padrão 5
pub jar: *CookieJar, // null = sem tratamento de cookies
pub tls_insecure: bool, // pula verificação de certificado (só dev); padrão false
pub timeout_ms: i32, // timeout total por requisição; 0 = desabilitado
pub http2: bool, // tenta h2 sobre TLS via ALPN; padrão false
}
Semântica dos campos:
user_agent— valor do header de requisiçãoUser-Agent.max_redirects— número máximo de saltos de redirecionamento quedoseguejar— atribua umCookieJar::new()para habilitar a ingestão automática detls_insecure— quandotrue, pula a validação do certificado TLS. Apenastimeout_ms— timeout total por requisição em milissegundos (connect + send +http2— tenta HTTP/2 sobre TLS via ALPN, caindo de volta para HTTP/1.1
antes de retornar err("http_client: too many redirects"). do_once ignora isso.
Set-Cookie mais a anexação do header Cookie: entre requisições. Permanece null (sem tratamento de cookies) por padrão.
para dev local; código de produção DEVE deixar isso desligado.
receive), aplicado via timeouts de send/receive do socket. 0 o desabilita.
quando o servidor não negocia h2. Não tem efeito em URLs http:// (o upgrade h2c não está implementado).
Métodos:
pub fn new() -> *HttpClient— constrói com os padrões acima.pub fn do(self, req: *HttpClientRequest) -> !*HttpResponse— envia e seguepub fn do_once(self, req: *HttpClientRequest) -> !*HttpResponse— enviapub fn get(self, url: string) -> !*HttpResponse— GET único viado.pub fn post(self, url: string, body: string, content_type: string) -> !*HttpResponsepub fn post_json(self, url: string, body: string) -> !*HttpResponse—pub fn put(self, url: string, body: string, content_type: string) -> !*HttpResponsepub fn delete(self, url: string) -> !*HttpResponse— DELETE.
redirecionamentos até max_redirects. Um 303 rebaixa o método para GET e descarta o body; 301/302 fazem o mesmo para métodos diferentes de GET/HEAD; 307/308 preservam método e body. Valores Location relativos são resolvidos contra a URL atual.
exatamente uma requisição, nunca seguindo redirecionamentos. Use para proxies e crawlers que precisam preservar respostas 30x verbatim.
— POST com um Content-Type explícito.
POST com Content-Type: application/json. Quem chama serializa o JSON.
— PUT com um Content-Type explícito.
Cliente configurado com headers customizados e tratamento de erro explícito:
let c: *HttpClient = HttpClient::new();
c.user_agent = "myapp/1.0";
let req: *HttpClientRequest = HttpClientRequest::new("GET", "https://api.example.com/health");
req.set("Accept", "application/json");
let r: !*HttpResponse = c.do(req);
if !r.ok {
println!("request failed:", r.err);
return;
}
let resp: *HttpResponse = r.val;
println!("status:", resp.status); // => 200
println!("type:", resp.header("Content-Type")); // => application/json
POST de JSON e leitura do status, de um header e do body da resposta:
let c: *HttpClient = HttpClient::new();
let r: !*HttpResponse = c.post_json("https://api.example.com/users",
"{\"name\":\"alice\"}");
if r.ok {
let resp: *HttpResponse = r.val;
println!(resp.status); // => 201
println!(resp.header("Location")); // => /users/7
println!(resp.body); // => {"id":7,"name":"alice"}
}
Seguindo redirecionamentos com um timeout e um cookie jar. Atribuir um jar captura todo Set-Cookie e reenvia o header Cookie: correspondente em requisições posteriores; timeout_ms limita cada salto; max_redirects limita a cadeia:
import stdlib::http::cookies::*;
let c: *HttpClient = HttpClient::new();
c.jar = CookieJar::new(); // persiste cookies entre requisições
c.timeout_ms = 5000; // 5s por requisição
c.max_redirects = 10; // segue até 10 saltos
let login: !*HttpResponse = c.post("https://example.com/login",
"user=a&pass=b",
"application/x-www-form-urlencoded");
if !login.ok { println!(login.err); return; }
// O cookie de sessão definido acima agora é enviado automaticamente e qualquer
// redirecionamento 30x de /dashboard é seguido de forma transparente.
let page: !*HttpResponse = c.get("https://example.com/dashboard");
if page.ok { println!(page.val.status); } // => 200
Para inspecionar um redirecionamento em vez de segui-lo, use do_once:
let req: *HttpClientRequest = HttpClientRequest::new("GET", "https://example.com/old");
let r: !*HttpResponse = HttpClient::new().do_once(req);
if r.ok && r.val.status == 301 {
println!("redirects to", r.val.header("Location"));
}
HTTP/2
Defina http2 = true para negociar h2 via ALPN em conexões HTTPS. A API permanece inalterada — as respostas voltam no mesmo formato HttpResponse — e o cliente cai de volta para HTTP/1.1 silenciosamente quando o servidor recusa h2:
let c: *HttpClient = HttpClient::new();
c.http2 = true;
let r: !*HttpResponse = c.get("https://example.com/");
if r.ok { println!(r.val.status); } // => 200 (sobre h2 se oferecido)
ParsedUrl
A forma parseada de uma URL de requisição. Produzida internamente pelo cliente; exposta para quem precisa da sua formatação de header Host:.
pub struct ParsedUrl {
pub scheme: string, // "http" ou "https"
pub host: string,
pub port: i32, // padrão 80 (http) / 443 (https)
pub path: string, // sempre começa com "/"
}
pub fn host_header(self) -> string— formata o valor do headerHost:,
omitindo a porta quando ela é o padrão do scheme (80 para http, 443 para https) e mantendo-a caso contrário (ex.: example.com:8443).
Funções livres
Helpers de uso único que montam um HttpClient padrão por chamada. Para múltiplas requisições ou estado compartilhado (cookies, timeouts), construa HttpClient::new() uma vez e reutilize-o.
pub fn http_get(url: string) -> !*HttpResponse— GET com um cliente padrão.pub fn http_post_json(url: string, body: string) -> !*HttpResponse— POST de
JSON com um cliente padrão.
let r: !*HttpResponse = http_post_json("https://api.example.com/login",
"{\"user\":\"a\",\"pass\":\"b\"}");
if r.ok && r.val.status == 200 {
println!(r.val.body); // => {"token":"..."}
}
Servidor HTTP — HttpResponse
Um servidor HTTP/1.1 mínimo construído sobre o par assíncrono Listener / Conn. Um handler é apenas um fn(*HttpRequest) -> HttpResponse; os pontos de entrada recebem uma porta e esse handler. No Linux cada conexão roda numa coroutine estacionada, então um processo segura dezenas de milhares de clientes; no Windows / macOS / BSD o loop é serial (uma conexão por vez) até os reactors IOCP / kqueue chegarem.
import stdlib::http::*;
fn root(req: *HttpRequest) -> HttpResponse {
return HttpResponse::ok().body("hello, ".concat(req.path));
}
fn main() -> i32 {
http_listen(8080, root); // nunca retorna em caso de sucesso
return 0;
}
Limitações da v1: apenas HTTP/1.1 neste caminho (HTTP/2 e HTTP/3 têm seus próprios pontos de entrada), um único handler para todo path (o roteamento fica por cima — veja a seção do router), e o corpo é totalmente bufferizado antes de o handler rodar (sem uploads em streaming).
HttpRequest
Uma requisição de entrada, totalmente bufferizada. O tempo de vida do ponteiro é a chamada do handler — não o guarde.
pub struct HttpRequest {
pub method: string,
pub path: string,
pub version: string,
pub headers_block: string, // texto de fio cru "Name: Value\r\n…"
pub body: string,
pub params: *HashMap<string>, // params de path do router
pub queries: *HashMap<string>, // mapa de query parseado preguiçosamente
pub tls: bool, // true se chegou via https_listen
pub headers_cache: *HashMap<string>, // cache preguiçoso de lookup de header
}
Getters:
header(self: *HttpRequest, name: string) -> string— lookup de headerparam(self: *HttpRequest, name: string) -> string— parâmetro de path doquery(self: *HttpRequest, name: string) -> string— valor decodificado dapath_only(self: *HttpRequest) -> string—pathcom o segmento de query?...is_method(self: *HttpRequest, m: string) -> bool— checagem de métodobody_json(self: *HttpRequest) -> !*JsonValue— parseia o corpo como JSON;
case-insensitive, "" quando ausente. A primeira chamada parseia headers_block para um cache; chamadas posteriores são O(1). Headers repetidos mantêm o primeiro valor.
router, "" quando desconhecido ou despachado sem um router.
query-string, "" quando ausente. Parseado + cacheado no primeiro acesso.
removido.
case-insensitive.
err num corpo vazio ou inválido.
fn handler(req: *HttpRequest) -> HttpResponse {
if !req.is_method("POST") {
return HttpResponse::with_status(405).text("method not allowed");
}
let host: string = req.header("Host"); // ex.: "localhost:8080"
let q: string = req.query("q"); // "" quando não há ?q=
let v: !*JsonValue = req.body_json();
if !v.ok { return HttpResponse::with_status(400).text(v.err); }
let name: !string = v.val.get_string("name");
if !name.ok { return HttpResponse::with_status(400).text(name.err); }
return HttpResponse::ok().text("hi ".concat(name.val).concat(" @ ").concat(host));
}
HttpResponse
A resposta de saída. Construa com um construtor, depois encadeie os setters — cada setter retorna um HttpResponse novo (semântica de valor, não um ponteiro mutável).
pub struct HttpResponse {
pub status: i32,
pub headers_block: string,
pub body: string,
pub stream_src: *dyn ChunkSource, // null para respostas bufferizadas
pub file_path: string, // não-vazio ⇒ caminho de sendfile
}
Construtores (estáticos):
HttpResponse::with_status(code: i32) -> HttpResponse— status dado, semHttpResponse::ok() -> HttpResponse— 200, sem corpo.HttpResponse::not_found() -> HttpResponse— 404.HttpResponse::redirect(url: string) -> HttpResponse— 302 comHttpResponse::file(path: string, mime: string) -> HttpResponse— serve
corpo, sem headers.
Location: url.
um arquivo do disco via sendfile zero-copy; Content-Length é o tamanho do arquivo, mime preenche Content-Type.
Setters de corpo + content-type (cada um encadeável, retorna HttpResponse):
body(self, s: string)— define o corpo e oContent-Lengthcorrespondente.text(self, s: string)— corpo +Content-Type: text/plain; charset=utf-8.html(self, s: string)— corpo +Content-Type: text/html; charset=utf-8.json(self, s: string)— corpo +Content-Type: application/json.json_of(self, v: *JsonValue)— serializav.emit()como JSON.stream(self, source: *dyn ChunkSource)— emite
Transfer-Encoding: chunked, percorrendo source.next_chunk() até none(); body é ignorado quando uma fonte de stream é definida.
Setters de header + status:
status(self, code: i32)— substitui o código de status.set(self, name, value: string)— define um header, substituindo qualquer linhaadd(self, name, value: string)— anexa uma linha de header, preservandocookie(self, name, value: string)— anexa um headerSet-Cookie: name=value
existente de mesmo nome (case-insensitive). CR/LF em name/value é removido.
linhas de mesmo nome (para Set-Cookie, Vary, Link).
(passa por add).
Getters de header (recebem *HttpResponse):
header(self: *HttpResponse, name: string) -> string—headers(self: *HttpResponse, name: string) -> *Vector<string>— toda
lookup case-insensitive, "" quando ausente.
linha de header correspondente (para Set-Cookie).
let r: HttpResponse = HttpResponse::ok()
.set("X-App", "demo")
.cookie("session", "abc123")
.cookie("csrf", "xyz")
.text("ready");
r.header("X-App"); // => "demo"
r.headers("Set-Cookie").len(); // => 2
Um handler que retorna JSON, construído de duas formas:
fn api(req: *HttpRequest) -> HttpResponse {
// 1. string JSON escrita à mão
if req.path_only().eq("/ping") {
return HttpResponse::ok().json("{\"ok\":true}");
}
// 2. montada via JsonValue, serializada por json_of
let v: *JsonValue = JsonValue::object();
v.obj_set("path", JsonValue::string(req.path));
v.obj_set("ok", JsonValue::bool(true));
return HttpResponse::ok().json_of(v);
}
ChunkSource — corpos em streaming
Uma resposta pode transmitir seu corpo pedaço a pedaço em vez de bufferizá-lo. Implemente a trait ChunkSource numa struct que segura o estado do cursor; o servidor emite um frame chunked por next_chunk() e termina em none().
pub trait ChunkSource {
fn next_chunk(self: *Self) -> ?string;
}
pub struct Lines { pub items: *Vector<string>, pub idx: i32 }
impl ChunkSource for Lines {
fn next_chunk(self: *Lines) -> ?string {
if self.idx >= self.items.len() { return none(); }
let s: string = self.items.get(self.idx);
self.idx = self.idx + 1;
return some(s.concat("\n"));
}
}
fn handler(req: *HttpRequest) -> HttpResponse {
let src: *Lines = malloc(sizeof(Lines)) as *Lines;
src.items = ...; src.idx = 0;
return HttpResponse::ok().stream(src as *dyn ChunkSource);
}
Pontos de entrada
http_listen(port: i32, handler: fn(*HttpRequest) -> HttpResponse) -> !i32 faz o bind, imprime uma linha de escuta e roda em loop para sempre. Retorna err("bind failed") quando a porta não pode ser bindada; nunca retorna em caso de sucesso.
fn root(req: *HttpRequest) -> HttpResponse {
return HttpResponse::ok().body("echo: ".concat(req.path));
}
fn main() -> i32 {
let r: !i32 = http_listen(8080, root);
if !r.ok { eprintln(r.err); return 1; }
return 0;
}
https_listen(port: i32, cert_path: string, key_path: string, handler) -> !i32 é o irmão com TLS. cert_path / key_path são arquivos PEM. Ele anuncia h2,http/1.1 sobre ALPN — clientes com suporte a h2 negociam HTTP/2 de forma transparente, todos os outros caem para HTTP/1.1. req.tls é true dentro do handler.
fn main() -> i32 {
let r: !i32 = https_listen(8443, "cert.pem", "key.pem", root);
if !r.ok { eprintln(r.err); return 1; }
return 0;
}
Variantes multi-worker
http_listen_workers(port, n, handler) -> !i32 é o caminho rápido padrão: n workers pthread, cada um com seu próprio loop epoll + máquina de estados em C, com o handler invocado inline na thread do worker (sem spawn de coroutine). Na máquina de referência de 4 núcleos ele atinge ~159k req/s (~84% do Axum/Tokio no hello-world). Os handlers precisam ser não-bloqueantes — sem send/recv de chan, sem sleep_ms, sem I/O que estaciona; caso contrário o runtime aborta com uma mensagem apontando para a variante bloqueante. O caminho rápido é só Linux; outras plataformas roteiam automaticamente para a variante bloqueante.
http_listen_workers_blocking(port, n, handler) -> !i32 faz spawn de uma coro por conexão, então ops bloqueantes dentro do handler (queries de DB, HTTP downstream, sync de canal) estacionam apenas a coro daquela conexão. ~107k req/s na mesma máquina, handlers sem restrições.
fn root(req: *HttpRequest) -> HttpResponse {
return HttpResponse::ok().body("hello, ".concat(req.path));
}
fn main() -> i32 {
http_listen_workers(8080, 4, root); // 4 workers, caminho rápido
return 0;
}
Caminho de máquina de estados @leaf
Para os endpoints mais quentes há um caminho sem coroutine onde o handler roda inline na thread epoll do worker. A assinatura do handler é fn(string, string) -> string (método, path → corpo) e precisa ser marcada com @leaf (sem estacionar — o runtime aborta se estacionar). O framing embrulha o corpo retornado numa resposta 200 OK text/plain. ~70% mais rápido que http_listen_workers no hello-world; só Linux.
http_listen_sm_workers(port, n, h: fn(string, string) -> string) -> !i32 é a variante com handler de usuário:
@leaf
fn hello(method: string, _path: string) -> string {
return "hello\n";
}
fn main() -> i32 {
http_listen_sm_workers(8080, 4, hello);
return 0;
}
Duas baselines de bench completam a família (sem handler de usuário — sempre respondem com um corpo hello-world fixo):
http_listen_sm_hello(port: i32) -> !i32— single-worker.http_listen_sm_hello_workers(port: i32, n: i32) -> !i32—nworkers,
cada um com seu próprio listener SO_REUSEPORT.
Helpers de nível mais baixo
parse_http_request(raw: string) -> !HttpRequest— parseia uma requisiçãoformat_http_response(r: *HttpResponse) -> string— renderiza uma resposta
HTTP/1.1 completa a partir de bytes crus; err numa requisição malformada ou parcial.
para bytes de fio (sempre Connection: close). Exposta para que respostas possam ser encanadas para um logger ou recorder.
let r: !HttpRequest = parse_http_request("GET /x HTTP/1.1\r\nHost: a\r\n\r\n");
if r.ok { println!(r.val.method, r.val.path); } // GET /x
let bytes: string = format_http_response(&HttpResponse::ok().body("hi"));
Roteamento & handlers — Router
Router casa o método + caminho da URL de uma requisição de entrada contra uma tabela de padrões declarados antecipadamente e despacha o primeiro casamento. Os padrões suportam três formas de segmento:
/users— literal, precisa casar byte a byte./users/:id— param, captura um segmento, lido viareq.param("id")./static/*rest— wildcard, captura o resto do caminho (zero ou mais
segmentos) em req.param("rest"). Um wildcard só pode aparecer por último.
O despacho por método é exato (GET ≠ POST) e insensível a maiúsculas no registro; registre o mesmo padrão duas vezes para dois verbos, ou use any(...) para um catch-all.
Importe tanto os tipos http quanto o router:
import stdlib::http::*;
import stdlib::http::router::*;
Constantes de tipo de segmento
pub const RS_LITERAL: i32 = 0; // segmento de texto literal
pub const RS_PARAM: i32 = 1; // captura `:name`
pub const RS_WILDCARD: i32 = 2; // captura `*name` do resto do caminho
pub const ANY_METHOD: string = "*"; // verbo sentinela para `any(...)`
RouteSegment
Uma parte compilada de um padrão de rota.
pub struct RouteSegment {
pub kind: i32, // RS_LITERAL | RS_PARAM | RS_WILDCARD
pub text: string, // texto literal, ou o nome da captura para param/wildcard
}
Route
Uma linha da tabela de roteamento.
pub struct Route {
pub method: string,
pub pattern: string,
pub segments: *Vector<RouteSegment>,
pub handler: fn(*HttpRequest) -> HttpResponse,
pub local_mws: *Vector<fn(*HttpRequest, *Chain) -> HttpResponse>,
}
local_mws é a cadeia de middleware por rota, preenchida quando a rota foi montada através de Router::scope(prefix, sub). O middleware pai roda primeiro, depois local_mws na ordem de registro, depois o handler.
Router
pub struct Router {
pub routes: *Vector<Route>,
pub mw: *Vector<fn(*HttpRequest, *Chain) -> HttpResponse>,
pub not_found: fn(*HttpRequest) -> HttpResponse,
}
As rotas são armazenadas na ordem de declaração; o primeiro casamento vence. mw roda antes do handler casado na ordem de registro. not_found cobre um despacho sem casamento e tem por padrão um 404 simples com corpo "not found".
Construção & registro
Router::new() -> *Router— router vazio com o handler 404 padrão.route(self, method, pattern, handler)— registrahandlerparaget / post / put / delete / patch / head / options (self, pattern, handler)any(self, pattern, handler)— casa todo método HTTP empattern.route_with(self, method, pattern, mws, handler)— comoroutemas com umanot_found_with(self, handler)— sobrescreve o handler 404 padrão.free(self)— libera a tabela de rotas e a struct do router; combine com
method + pattern. O método é armazenado em maiúsculas; passe ANY_METHOD (ou "*") para todos os verbos.
— atalhos de verbo para route(...).
lista de middleware por rota (o mw no nível do router roda primeiro, depois mws, depois o handler).
defer.
Um handler tem a forma fn(*HttpRequest) -> HttpResponse. Leia os parâmetros capturados com req.param("name"):
import stdlib::http::*;
import stdlib::http::router::*;
fn root(req: *HttpRequest) -> HttpResponse {
return HttpResponse::ok().text("hi");
}
fn get_user(req: *HttpRequest) -> HttpResponse {
let id: string = req.param("id"); // capturado de `:id`
return HttpResponse::ok().json("{\"id\":\"".concat(id).concat("\"}"));
}
fn create_user(req: *HttpRequest) -> HttpResponse {
return HttpResponse::with_status(201).json(req.body);
}
fn main() -> i32 {
let r: *Router = Router::new();
defer r.free();
r.get("/", root);
r.get("/users/:id", get_user);
r.post("/users", create_user);
return r.listen(8080).val;
}
Uma rota wildcard captura tudo após o prefixo:
fn serve_static(req: *HttpRequest) -> HttpResponse {
let rest: string = req.param("rest"); // `/static/css/app.css` -> "css/app.css"
return HttpResponse::ok().text(rest);
}
let r: *Router = Router::new();
r.get("/static/*rest", serve_static);
r.any("/health", fn(req: *HttpRequest) -> HttpResponse {
return HttpResponse::ok().text("ok"); // casa GET/POST/HEAD/...
});
dispatch rejeita qualquer caminho contendo um segmento .. com um 400 bad path antes de consultar a tabela, então um handler de servidor de arquivos com wildcard não consegue ler fora da sua raiz.
dispatch
dispatch(self, req) -> HttpResponse percorre a tabela, e no primeiro casamento preenche req.params, roda a cadeia de middleware, depois chama o handler. Sem casamento ele roda o handler de not-found. Ele muta req.params no lugar, então passe uma requisição recém-construída.
let req: HttpRequest = ...;
let resp: HttpResponse = r.dispatch(&req); // => HttpResponse do handler
Estado da aplicação
state(self, p: *void) -> *Router armazena um ponteiro de estado da aplicação para todo o processo. Os handlers o alcançam através do extractor State<T> sem captura de closure. A v1 tem um único slot global. Retorna self para encadeamento.
let r: *Router = Router::new();
r.state(db as *void); // db: *Db
r.get("/users/:id", get_user);
Servindo
listen(self, port) -> !i32— loop de coroutine-por-conexão. No Linux usa olisten_workers(self, port, n) -> !i32— roteia através do servidor HTTP em Clisten_workers_blocking(self, port, n) -> !i32— coroutine-por-conexão
reactor + coroutines por conexão; Windows / macOS / BSD rodam serialmente até seus reactors chegarem.
com máquina de estados para máxima vazão. Os handlers PRECISAM ser não-bloqueantes (sem chan.send/recv, sem sleep_ms, sem I/O com parking).
através de n workers (balanceado por SO_REUSEPORT onde suportado) para handlers que genuinamente precisam bloquear.
Todos os três retornam !i32; uma falha de bind surge no braço err:
let r: *Router = Router::new();
r.get("/", root);
let res: !i32 = r.listen(8080);
if !res.ok { println!("listen failed: ", res.err); }
Middleware & grupos de rotas — Chain
O middleware tem a forma fn(*HttpRequest, *Chain) -> HttpResponse. Cada um ou chama chain_next(c) para avançar, ou retorna sem chamá-lo para curto-circuitar (o resto da cadeia e o handler da rota são pulados).
pub struct Chain {
pub idx: i32,
pub mws: *Vector<fn(*HttpRequest, *Chain) -> HttpResponse>,
pub handler: fn(*HttpRequest) -> HttpResponse,
pub req: *HttpRequest,
}
pub fn chain_next(c: *Chain) -> HttpResponse
chain_next(c) roda o próximo middleware, ou o handler da rota quando a lista se esgota. A resposta retornada pode ser inspecionada ou mutada antes do middleware retorná-la (comportamento estilo "after").
use_mw(self, mw) -> *Router— registra um middleware no nível do router; rodascope(self, prefix, sub) -> *Router— monta toda rota desubsob
antes do handler na ordem de registro. Retorna self para encadeamento.
prefix. O mw do sub-router vira middleware por rota que roda após a cadeia do pai e antes de cada handler. Copia as entradas de rota, então sub pode ser descartado ou reutilizado depois. Retorna self.
fn logger(req: *HttpRequest, c: *Chain) -> HttpResponse {
println!("[", req.method, "] ", req.path);
let resp: HttpResponse = chain_next(c);
println!(" -> ", resp.status);
return resp;
}
fn auth(req: *HttpRequest, c: *Chain) -> HttpResponse {
if req.header("Authorization").len() == 0 {
return HttpResponse::with_status(401).text("no auth"); // curto-circuito
}
return chain_next(c);
}
let api: *Router = Router::new();
api.use_mw(auth);
api.get("/users/:id", get_user);
let r: *Router = Router::new();
r.use_mw(logger);
r.scope("/api/v1", api);
// GET /api/v1/users/42 roda: logger -> auth -> get_user
r.listen(8080);
Handlers tipados — o proc-macro @handler
@handler dá a uma fn parâmetros de extractor tipados e um retorno flexível, depois emite um wrapper fn(*HttpRequest) -> HttpResponse que o router aceita inalterado. Despacho de parâmetro, em ordem:
req: *HttpRequest— passagem direta, sem extração.Json<T>—req.body_json()+T::from_json(400 no parse, 422 no bind).Path<T>—Path::extract(req, "<param-name>"); o ident do parâmetro é aQuery<T>—Query::extract(req, "<param-name>")contra a query string.State<T>—State::extract(req), lendo o slot definido porRouter::state.- qualquer outro
T—T::from_request(req)via a traitFromRequest.
chave do caminho.
Forma de retorno: HttpResponse passa direto; Json<T> é auto-encapsulado via .into_response().
@handler
fn show(id: Path<i32>) -> Json<User> {
return Json::wrap(load_user(id.val)); // .into_response() automático
}
let r: *Router = Router::new();
r.state(db as *void);
r.get("/users/:id", show); // o wrapper tem a forma de fn-ptr
Roteamento por atributo — @get/@post/… + routes!
Os atributos de rota (@route(METHOD, "/path") mais os açúcares @get, @post, @put, @delete, @patch, @head, @options, @any) encapsulam a fn da mesma forma que @handler faz e emitem uma função _route_register_<name>(r: *Router). routes!(r) percorre o programa e emite o registro r.<method>(...) correspondente para cada fn com atributo. Os nomes :param e *wildcard do caminho são checados em tempo de compilação contra os parâmetros Path<T> do handler (a menos que o handler receba um único req: *HttpRequest, que desativa isso).
import stdlib::http::*;
import stdlib::http::router::*;
@get("/users/:id")
fn get_user(id: Path<i32>) -> Json<User> {
return Json::wrap(load_user(id.val));
}
@post("/users")
fn create_user(body: Json<NewUser>) -> Json<User> {
return Json::wrap(insert_user(body.val)); // 422 se o corpo falhar no bind
}
fn main() -> i32 {
let r: *Router = Router::new();
defer r.free();
routes!(r); // registra get_user + create_user
return r.listen(8080).val;
}
@middleware(a, b) empilha middleware com escopo de rota que o registro gerado encadeia através de route_with, então a cadeia global use_mw é preservada e a, b rodam após ela. O atributo @listen(port) / @listen_workers(port, n) em fn main constrói o router, roda routes!, e chama listen / listen_workers para você.
Binding de requisição — FromRequest
Parâmetros de handler podem ser extractors tipados em vez de um *HttpRequest cru. Qualquer tipo que implemente FromRequest (ou, para os wrappers genéricos Json<T> / Authorization<S> / Path<T> / Query<T> / State<T>, exponha um from_request/extract inerente) pode ficar numa assinatura @handler; a macro o extrai da requisição e, em caso de falha, transforma a string de err numa resposta.
Extractors codificam o status HTTP desejado como um prefixo "<code>:<message>" na sua string de err (ex.: "401:missing token"). HttpResponse::from_extract_err faz o parse desse prefixo; um err simples (sem prefixo) usa 422 como padrão.
A trait FromRequest
pub trait FromRequest {
fn from_request(req: *HttpRequest) -> !Self;
}
Implemente uma vez e o tipo passa a ser usável como parâmetro de handler. A string de err carrega o código de status como prefixo.
import stdlib::http::*;
import stdlib::http::extract::*;
pub struct ApiKey { pub key: string }
impl FromRequest for ApiKey {
fn from_request(req: *HttpRequest) -> !ApiKey {
let v: string = req.header("X-Api-Key");
if v.len() == 0 { return err("401:missing X-Api-Key"); }
return ok(ApiKey { key: v });
}
}
// Chame diretamente, ou deixe o @handler fazer:
let r: !ApiKey = ApiKey::from_request(req);
if !r.ok { return HttpResponse::from_extract_err(r.err); } // resposta 401
let key: string = r.val.key;
*HttpRequest em si implementa FromRequest como um passthrough identidade, então um handler ainda pode receber a requisição crua.
Bearer — Authorization: Bearer <token>
pub struct Bearer {
pub token: string,
}
Remove o prefixo Bearer ; erra 401 quando o header está ausente ou usa outro esquema.
@handler
fn protected(auth: Bearer) -> HttpResponse {
return HttpResponse::ok().text("token=".concat(auth.token));
}
Headers — saco completo de headers
pub struct Headers {
pub req: *HttpRequest,
}
get(self: *Headers, name: string) -> string— busca case-insensitive;""quando ausente.has(self: *Headers, name: string) -> bool—truequando o header está presente e não-vazio.
@handler
fn echo_ua(headers: Headers) -> HttpResponse {
if !headers.has("User-Agent") { return HttpResponse::with_status(400); }
return HttpResponse::ok().text(headers.get("User-Agent"));
}
Authorization<S> — esquema de auth genérico
pub trait AuthScheme {
fn parse(value: string) -> !Self;
fn name() -> string;
}
pub struct Authorization<S> {
pub scheme: S,
}
Authorization<S: AuthScheme> lê o header Authorization (ausente → 401) e entrega o valor cru (com prefixo incluso) para S::parse. O extractor estático tem o formato Authorization::extract via o método inerente:
from_request(req: *HttpRequest) -> !Authorization<S>
Bearer e Basic ambos implementam AuthScheme, então Authorization<Bearer> e Authorization<Basic> funcionam de imediato.
@handler
fn whoami(auth: Authorization<Bearer>) -> HttpResponse {
return HttpResponse::ok().text(auth.scheme.token);
}
Basic — usuário + senha
pub struct Basic {
pub user: string,
pub pass: string,
}
Basic implementa AuthScheme. Nota: a v1 não faz decode base64 — ela trata o valor após Basic como o literal user:pass, separando no primeiro :.
let r: !Basic = Basic::parse("Basic alice:s3cret");
if r.ok {
// r.val.user => "alice", r.val.pass => "s3cret"
}
State<T> — estado de aplicação compartilhado
pub struct State<T> {
pub val: *T,
}
Estado registrado com Router::state(p) é alcançável sem globais. O extractor faz cast do slot para *T; um slot vazio erra 500.
extract(req: *HttpRequest) -> !State<T>
let db: *Db = open_db();
let r: *Router = Router::new();
r.state(db as *void);
@handler
fn list_users(state: State<Db>) -> HttpResponse {
return HttpResponse::ok().text(state.val.summary());
}
Path<T> e Query<T> — escalares tipados
pub trait FromPath {
fn from_path(s: string) -> !Self;
}
pub struct Path<T> {
pub val: T,
}
pub struct Query<T> {
pub val: T,
}
FromPath é implementado para string, i32 e bool. A macro @handler usa o ident do parâmetro como chave de busca: Path<T> lê req.param(name) (ausente → 404), Query<T> lê req.query(name) (ausente → 400); ambos convertem via T::from_path (valor inválido → 400).
Path::extract(req: *HttpRequest, name: string) -> !Path<T>Query::extract(req: *HttpRequest, name: string) -> !Query<T>
// rota /users/:id • GET /users/7?page=2
@handler
fn get_user(id: Path<i32>, page: Query<i32>) -> HttpResponse {
return HttpResponse::ok().text(
"user ".concat(id.val.to_string()).concat(" page ").concat(page.val.to_string()));
}
Chamando um extractor manualmente:
let r: !Path<i32> = Path::extract(req, "id");
if !r.ok { return HttpResponse::from_extract_err(r.err); } // 404 ou 400
let id: i32 = r.val.val;
Form — extractor de body urlencoded
pub struct Form {
pub data: *FormData,
}
Extrai um body POST application/x-www-form-urlencoded. Erra 415 quando o Content-Type não bate, 400 quando o body falha no parse.
get(self: *Form, name: string) -> string— primeiro valor paraname;""quando ausente.has(self: *Form, name: string) -> bool—truequando ao menos um campo bate.
@handler
fn submit(form: Form) -> HttpResponse {
if !form.has("user") { return HttpResponse::with_status(400); }
return HttpResponse::ok().text("welcome, ".concat(form.get("user")));
}
Cookies — saco de cookies da requisição
pub struct Cookies {
pub header: string,
}
Envolve o header de requisição Cookie: (somente leitura).
get(self: *Cookies, name: string) -> string— valor paraname;""quando ausente.has(self: *Cookies, name: string) -> bool—truequando presente.
@handler
fn me(cookies: Cookies) -> HttpResponse {
let sid: string = cookies.get("session");
if sid.eq("") { return HttpResponse::with_status(401).text("login"); }
return HttpResponse::ok().text("session=".concat(sid));
}
MultipartForm — multipart/form-data de entrada
pub struct UploadedFile {
pub name: string,
pub filename: string,
pub content_type: string,
pub body: string,
}
pub struct MultipartForm {
pub fields: *HashMap<string>,
pub files: *Vector<UploadedFile>,
}
MultipartForm implementa FromRequest: verifica que o Content-Type é multipart/form-data, lê o boundary= e divide o body em fields simples e files enviados. Erra 400 em not multipart / no boundary / malformed.
@handler
fn upload(mp: MultipartForm) -> HttpResponse {
if mp.files.len() > 0 {
let f: UploadedFile = mp.files.get(0);
return HttpResponse::ok().text(
f.filename.concat(": ").concat(f.body.len().to_string()).concat(" bytes"));
}
return HttpResponse::ok().text("name=".concat(mp.fields.get("user")));
}
HttpResponse::from_extract_err
pub fn from_extract_err(err_str: string) -> HttpResponse
Transforma a string de err de um extractor numa resposta. Um prefixo "<code>:<message>" define o status e usa a mensagem como body; uma string sem prefixo usa 422 como padrão.
let r: !Bearer = Bearer::from_request(req);
if !r.ok { return HttpResponse::from_extract_err(r.err); }
Binding de JSON tipado — Json<T>
Json<T> carrega um valor tipado de e para uma resposta JSON. O tipo do valor precisa implementar JsonBind (from_json / to_json).
Json<T> e json_respond
pub struct Json<T> {
pub val: T,
}
Json::wrap(v: T) -> Json<T>— envolve um valorT: JsonBind.json_respond<T: JsonBind>(v: T) -> HttpResponse— equivalente em uma linha aJson::wrap(v).into_response(); emite um200com o body JSON eContent-Type: application/json.
import stdlib::http::*;
import stdlib::http::typed::*;
import stdlib::json::*;
pub struct StructLegal {
pub nome: string,
pub idade: ?i32,
}
impl JsonBind for StructLegal {
fn from_json(v: *JsonValue) -> !StructLegal { /* ... */ }
fn to_json(self: *StructLegal) -> *JsonValue { /* ... */ }
}
fn create(req: *HttpRequest) -> HttpResponse {
let user: StructLegal = StructLegal { nome: "alice", idade: some(30) };
return json_respond(user); // 200 + {"nome":"alice","idade":30}
}
IntoResponse
pub trait IntoResponse {
fn into_response(self: Self) -> HttpResponse;
}
Implementado para HttpResponse (identidade) e para Json<T> quando T: JsonBind (serializa T.to_json() num 200). Use para ergonomia na cauda do handler:
let r: HttpResponse = Json::wrap(user).into_response();
Para fazer binding de um body JSON de entrada, valide req.body_json() contra T::from_json manualmente (o typer ainda não aceita um destructure de parâmetro de fn Json<T> nem um retorno de handler Json<T>), e então responda com json_respond.
Forms URL-encoded — FormData
pub struct FormData { /* pares (name, value) opacos */ }
Uma lista ordenada de pares (name, value) para bodies application/x-www-form-urlencoded. A ordem é preservada e chaves duplicadas são mantidas.
FormData::new() -> *FormData— form vazio.set(self: *FormData, name: string, value: string)— anexa um par.len(self: *FormData) -> i32— contagem de pares.name_at(self: *FormData, i: i32) -> string/value_at(self: *FormData, i: i32) -> string— percorre por índice.encode(self: *FormData) -> string— constrói a string de fiok=v&k=vpercent-encoded.
Funções livres:
form_decode(s: string) -> !*FormData— faz parse de um bodyk=v&k=v; erra em percent-encoding inválido.http_post_form(url: string, form: *FormData) -> !*HttpResponse— POST urlencoded de uma só vez.
import stdlib::http::form::*;
let f: *FormData = FormData::new();
f.set("user", "alice");
f.set("token", "abc xyz=99");
let body: string = f.encode(); // "user=alice&token=abc%20xyz%3D99"
let r: !*FormData = form_decode(body);
if r.ok {
for let i: i32 = 0; i < r.val.len(); i++ {
println!(r.val.name_at(i), "=", r.val.value_at(i));
}
}
let resp: !*HttpResponse = http_post_form("https://api.example.com/login", f);
if resp.ok { println!(resp.val.status); }
Bodies multipart — Multipart
pub struct Multipart { /* boundary + parts opacos */ }
Constrói um body multipart/form-data de saída (RFC 7578).
Multipart::new() -> *Multipart— boundary aleatório de 24 chars.Multipart::with_boundary(boundary: string) -> *Multipart— boundary fixo (testes determinísticos).add_field(self: *Multipart, name: string, value: string)— campo de texto simples.add_file(self: *Multipart, name: string, filename: string, content_type: string, data: string)— parte de arquivo (bytes crus).content_type(self: *Multipart) -> string—multipart/form-data; boundary=...para o header Content-Type.body(self: *Multipart) -> string— serializa todas as partes para o body de fio.
import stdlib::http::multipart::*;
import stdlib::http::client::*;
let mp: *Multipart = Multipart::new();
mp.add_field("user", "alice");
mp.add_file("avatar", "pic.png", "image/png", bytes);
let c: *HttpClient = HttpClient::new();
let r: !*HttpResponse = c.post(url, mp.body(), mp.content_type());
if r.ok { println!(r.val.status); }
O lado de recebimento é o MultipartForm (acima), parseado via sua impl FromRequest.
Cookies de cliente — CookieJar
pub struct Cookie {
pub name: string,
pub value: string,
pub domain: string,
pub path: string,
pub secure: bool,
pub http_only: bool,
}
pub struct CookieJar { /* lista de cookies opaca */ }
CookieJar é o store do lado do cliente (estilo RFC 6265). Anexe a um HttpClient para que respostas Set-Cookie sejam absorvidas e requisições de saída carreguem Cookie: automaticamente.
Cookie:
Cookie::new(name: string, value: string) -> *Cookie— escopo padrão (path="/", sem restrição de domínio, não secure, não http-only). Ajuste os campos diretamente.
CookieJar:
CookieJar::new() -> *CookieJar— jar vazio.ingest(self: *CookieJar, request_host: string, set_cookie_lines: *Vector<string>)— faz parse e armazena toda linhaSet-Cookie, escopada arequest_host; mesmo(name, domain, path)substitui.header_value(self: *CookieJar, host: string, path: string, is_secure: bool) -> string— constrói o valorCookie:de saída;""quando nenhum se aplica.all(self: *CookieJar) -> *Vector<*Cookie>— snapshot de todo cookie.clear(self: *CookieJar)— esquece todo cookie.
import stdlib::http::client::*;
import stdlib::http::cookies::*;
let jar: *CookieJar = CookieJar::new();
let c: *HttpClient = HttpClient::new();
c.jar = jar;
// Login: o Set-Cookie do servidor é absorvido.
let _ = c.post_json("https://api.example.com/login", body);
// Requisições subsequentes carregam Cookie: automaticamente.
let r: !*HttpResponse = c.get("https://api.example.com/me");
// Inspecione o jar diretamente:
let h: string = jar.header_value("api.example.com", "/me", true);
// h pode ser "session=abc123"
jar.clear(); // logout
Recursos HTTP — CORS, SSE, estáticos, compressão, proxy, JWT
Um conjunto de middlewares e helpers HTTP plug-and-play construídos sobre o pipeline Router / Chain e os tipos HttpRequest / HttpResponse. Cada um vive em seu próprio submódulo stdlib::http::* e é pub-importado individualmente:
import stdlib::http::*;
import stdlib::http::router::*;
import stdlib::http::cors::*; // CorsConfig / install_cors / cors_mw
import stdlib::http::sse::*; // SseEvent / SseChannel / sse_response ...
import stdlib::http::static::*; // StaticOpts / serve_dir
import stdlib::http::compress::*; // gzip_mw
import stdlib::http::proxy::*; // ReverseProxyConfig / reverse_proxy ...
import stdlib::http::jwt::*; // JwtClaims / jwt_verify
Middlewares compartilham a assinatura fn(req: *HttpRequest, c: *Chain) -> HttpResponse e são registrados com r.use_mw(...). Cada um chama chain_next(c) para invocar o resto do pipeline e então pós-processa o resultado. Helpers que tocam o sistema de arquivos ou a rede retornam !T e expõem falhas através de .ok / .err.
CORS — CorsConfig
CorsConfig guarda os valores emitidos como cabeçalhos de resposta Access-Control-Allow-*. install_cors os armazena num slot global do processo; cors_mw é o middleware que os carimba em cada resposta (e curto-circuita requisições preflight OPTIONS com um 204).
pub struct CorsConfig {
pub origin: string, // Access-Control-Allow-Origin
pub methods: string, // Access-Control-Allow-Methods
pub headers: string, // Access-Control-Allow-Headers
pub credentials: bool, // emite Allow-Credentials: true quando definido
pub max_age: i32, // segundos de Access-Control-Max-Age (0 = omitir)
}
| Símbolo | Assinatura | Descrição |
|---|---|---|
CorsConfig::permissive |
pub fn permissive() -> CorsConfig |
Origem *, todos os métodos/cabeçalhos comuns, sem credenciais, sem max-age. |
install_cors |
pub fn install_cors(cfg: CorsConfig) |
Armazena a config no slot CORS global do processo lido por cors_mw. |
cors_mw |
pub fn cors_mw(req: *HttpRequest, c: *Chain) -> HttpResponse |
Middleware: responde OPTIONS com 204, caso contrário adiciona os cabeçalhos CORS a chain_next(c). |
import stdlib::http::*;
import stdlib::http::router::*;
import stdlib::http::cors::*;
fn hello(_req: *HttpRequest) -> HttpResponse {
return HttpResponse::ok().text("hi");
}
fn main() -> i32 {
install_cors(CorsConfig::permissive());
let r: *Router = Router::new();
r.use_mw(cors_mw);
r.get("/hello", hello);
// GET /hello -> 200 + Access-Control-Allow-Origin: *
// OPTIONS /hello -> 204 + o bloco de cabeçalhos CORS
return r.listen_workers(8080, 4).val;
}
Para uma política restrita, monte a config à mão:
install_cors(CorsConfig {
origin: "https://app.example.com",
methods: "GET, POST",
headers: "Content-Type, Authorization",
credentials: true,
max_age: 600,
});
// respostas carregam Allow-Credentials: true e Max-Age: 600
Server-Sent Events — SseEvent
SSE transmite frames text/event-stream sobre chunked transfer. Um handler retorna um HttpResponse cujo corpo é um *dyn ChunkSource; o servidor puxa um evento por vez e o enquadra. SseEvent é o modelo de fio.
pub struct SseEvent {
pub event: string, // event: nome ("" = sem nome)
pub id: string, // id: valor ("" = omitir)
pub data: string, // data: payload (multi-linha dividido conforme spec)
pub retry: i32, // retry: ms (0 = omitir)
}
| Símbolo | Assinatura | Descrição |
|---|---|---|
SseEvent::data |
pub fn data(payload: string) -> SseEvent |
Evento só de dados, sem nome/id/retry. |
SseEvent::named |
pub fn named(name: string, payload: string) -> SseEvent |
Evento nomeado (evtSource.addEventListener(name, ...)). |
sse_format |
pub fn sse_format(e: SseEvent) -> string |
Serializa um evento em bytes de fio; um evento vazio é uma única linha em branco. |
sse_comment |
pub fn sse_comment(text: string) -> string |
Uma linha de comentário : text\n\n (heartbeat). |
sse_keepalive |
pub fn sse_keepalive() -> string |
Frame de heartbeat mínimo :\n\n. |
sse_last_event_id |
pub fn sse_last_event_id(req: *HttpRequest) -> string |
O cabeçalho Last-Event-ID do cliente ("" quando ausente). |
sse_response |
pub fn sse_response(source: *dyn ChunkSource) -> HttpResponse |
Envolve um ChunkSource com o content-type SSE + cabeçalhos no-buffer. |
SSE_END |
pub const SSE_END: string |
Nome de evento sentinela que fecha um stream SseChannel ("__close__"). |
import stdlib::http::*;
import stdlib::http::sse::*;
fn main() -> i32 {
let e: SseEvent = SseEvent::named("tick", "1");
println!(sse_format(e)); // "event: tick\ndata: 1\n\n"
println!(sse_format(SseEvent::data("hi"))); // "data: hi\n\n"
println!(sse_keepalive()); // ":\n\n"
return 0;
}
Um stream dirigido por produtor implementa ChunkSource::next_chunk e retorna some(sse_format(...)) por evento, none() no fim:
import stdlib::http::*;
import stdlib::http::sse::*;
pub struct Pulse { pub idx: i32 }
impl ChunkSource for Pulse {
fn next_chunk(self: *Pulse) -> ?string {
if self.idx >= 5 { return none(); }
self.idx = self.idx + 1;
return some(sse_format(SseEvent::data(self.idx.to_string())));
}
}
fn ticks(_req: *HttpRequest) -> HttpResponse {
let p: *Pulse = malloc(sizeof(Pulse)) as *Pulse;
p.idx = 0;
return sse_response(p as *dyn ChunkSource); // transmite data: 1..5
}
SseChannel distribui um chan<SseEvent> de broadcast para um cliente. Cada valor enviado ao chan vira um frame; enviar um evento cujo .event seja igual a SSE_END encerra o stream.
pub struct SseChannel { pub ch: *chan<SseEvent> }
| Símbolo | Assinatura | Descrição |
|---|---|---|
SseChannel::new |
pub fn new(ch: *chan<SseEvent>) -> *SseChannel |
Envolve um chan como um ChunkSource. |
import stdlib::http::*;
import stdlib::http::sse::*;
let events: chan<SseEvent> = make_chan(64);
fn stream(_req: *HttpRequest) -> HttpResponse {
let src: *SseChannel = SseChannel::new(&events);
return sse_response(src as *dyn ChunkSource);
}
// em outro lugar: events.send(SseEvent::named("ping", "1"));
// para fechar: events.send(SseEvent { event: SSE_END, id: "", data: "", retry: 0 });
Arquivos estáticos — StaticOpts
serve_dir monta um diretório num prefixo de URL e serve arquivos através de HttpResponse::file (sendfile zero-copy). Ele adiciona proteção contra path-traversal (rejeita .., / inicial, bytes NUL), uma ETag fraca a partir do tamanho do arquivo com suporte a If-None-Match → 304, um Cache-Control opcional e index.html para caminhos de diretório.
pub struct StaticOpts {
pub root: string, // diretório no disco
pub url_prefix: string, // segmento de URL sob o qual os arquivos montam
pub cache_control: string, // cabeçalho Cache-Control ("" = omitir)
}
| Símbolo | Assinatura | Descrição |
|---|---|---|
serve_dir |
pub fn serve_dir(r: *Router, opts: StaticOpts) |
Registra GET <url_prefix>/*filepath servindo <root>/.... |
import stdlib::http::*;
import stdlib::http::router::*;
import stdlib::http::static::*;
fn main() -> i32 {
let r: *Router = Router::new();
serve_dir(r, StaticOpts {
root: "./public",
url_prefix: "/static",
cache_control: "public, max-age=3600",
});
// GET /static/css/site.css -> ./public/css/site.css (+ ETag, Cache-Control)
// GET /static/ -> ./public/index.html
// GET /static/../etc -> 403 forbidden
return r.listen_workers(8080, 4).val;
}
Compressão — gzip_mw
gzip_mw comprime o corpo da resposta com gzip (zlib, linkado -lz) após rodar o resto da chain. Ele só atua quando a requisição anunciou Accept-Encoding: gzip, o status é 2xx, nenhum Content-Encoding já está definido, o corpo é ≥ 1024 bytes e o Content-Type não é um tipo de mídia já comprimido (image/video/audio, zip, gzip, octet-stream). Quando comprime, ele substitui o corpo e define Content-Encoding: gzip.
| Símbolo | Assinatura | Descrição |
|---|---|---|
gzip_mw |
pub fn gzip_mw(req: *HttpRequest, c: *Chain) -> HttpResponse |
Aplica gzip na resposta downstream quando as condições acima valem. |
import stdlib::http::*;
import stdlib::http::router::*;
import stdlib::http::compress::*;
fn page(_req: *HttpRequest) -> HttpResponse {
return HttpResponse::ok()
.set("Content-Type", "text/html; charset=utf-8")
.text(big_html()); // > 1024 bytes
}
fn main() -> i32 {
let r: *Router = Router::new();
r.use_mw(gzip_mw);
r.get("/", page);
// requisição com Accept-Encoding: gzip -> corpo com gzip, Content-Encoding: gzip
// requisições pequenas / sem aceitar gzip -> corpo passa intacto
return r.listen_workers(8080, 4).val;
}
Reverse proxy — ReverseProxyConfig
reverse_proxy encaminha uma requisição de entrada para uma origem upstream e rebaixa a resposta upstream de volta para o HttpResponse do servidor. Ele remove cabeçalhos hop-by-hop, carimba X-Forwarded-*, opcionalmente remove um prefixo de caminho e transforma erros de rede/parsing em 502 Bad Gateway.
pub struct ReverseProxyConfig {
pub upstream: string, // origem, ex. "http://127.0.0.1:9000"
pub strip_prefix: string, // remove do caminho antes de encaminhar
pub preserve_host: bool, // mantém o cabeçalho Host do cliente no upstream
pub rewrite_location: bool, // reescreve Location do upstream para public_base
pub public_base: string, // base usada por rewrite_location
pub timeout_ms: i32, // timeout da requisição upstream
pub tls_insecure: bool, // pula verificação TLS do upstream
}
| Símbolo | Assinatura | Descrição |
|---|---|---|
ReverseProxyConfig::to |
pub fn to(upstream: string) -> ReverseProxyConfig |
Config com defaults conservadores (timeout de 30s, sem strip de prefixo, verifica TLS). |
reverse_proxy |
pub fn reverse_proxy(cfg: *ReverseProxyConfig, req: *HttpRequest) -> HttpResponse |
Encaminha req upstream; 502 em caso de falha. |
proxy_build_upstream_request |
pub fn proxy_build_upstream_request(cfg: *ReverseProxyConfig, req: *HttpRequest) -> *HttpClientRequest |
Constrói (mas não envia) a requisição de cliente upstream. |
proxy_target_url |
pub fn proxy_target_url(cfg: *ReverseProxyConfig, path: string) -> string |
Calcula a URL upstream para um caminho de requisição (aplica strip_prefix). |
proxy_filter_request_headers |
pub fn proxy_filter_request_headers(block: string, preserve_host: bool) -> string |
Descarta hop-by-hop (e Host a menos que preservado) de um bloco de cabeçalhos de requisição. |
proxy_filter_response_headers |
pub fn proxy_filter_response_headers(block: string) -> string |
Descarta cabeçalhos hop-by-hop / de framing de um bloco de resposta upstream. |
proxy_rewrite_location |
pub fn proxy_rewrite_location(loc: string, upstream: string, public_base: string) -> string |
Reescreve um Location upstream absoluto para public_base; os relativos passam intactos. |
import stdlib::http::*;
import stdlib::http::proxy::*;
fn main() -> i32 {
let cfg: ReverseProxyConfig = ReverseProxyConfig::to("http://127.0.0.1:9000");
http_listen(8080, fn(req) { return reverse_proxy(&cfg, req); });
// GET /users -> http://127.0.0.1:9000/users, resposta repassada de volta
return 0;
}
Monte um upstream sob um prefixo local e reescreva redirecionamentos para o host público:
import stdlib::http::*;
import stdlib::http::proxy::*;
fn main() -> i32 {
let cfg: ReverseProxyConfig = ReverseProxyConfig {
upstream: "http://127.0.0.1:9000",
strip_prefix: "/api",
preserve_host: false,
rewrite_location: true,
public_base: "https://edge.example.com",
timeout_ms: 10000,
tls_insecure: false,
};
// /api/users?x=1 -> http://127.0.0.1:9000/users?x=1
println!(proxy_target_url(&cfg, "/api/users?x=1"));
// "http://127.0.0.1:9000/users?x=1"
http_listen(8080, fn(req) { return reverse_proxy(&cfg, req); });
return 0;
}
Verificação JWT — JwtClaims
jwt_verify checa um JWT HS256 compacto contra um segredo compartilhado: valida a estrutura de três partes, recalcula a assinatura HMAC-SHA-256 sobre header.payload, exige uma claim sub string e rejeita tokens expirados (exp no passado). Em sucesso, retorna as claims decodificadas; erros carregam um prefixo de status HTTP ("400:...", "401:...").
pub struct JwtClaims {
pub sub: string, // claim de subject (obrigatória)
pub exp: i32, // segundos epoch de expiração (0 = sem claim exp)
pub iat: i32, // segundos epoch de issued-at (0 = ausente)
pub raw: *JsonValue, // objeto de payload decodificado completo
}
| Símbolo | Assinatura | Descrição |
|---|---|---|
jwt_verify |
pub fn jwt_verify(token: string, key: string) -> !JwtClaims |
Verifica um token HS256; err em malformado/assinatura-ruim/expirado. |
import stdlib::http::*;
import stdlib::http::jwt::*;
fn main() -> i32 {
let token: string = req_token(); // "<header>.<payload>.<sig>"
let r: !JwtClaims = jwt_verify(token, "my-shared-secret");
if !r.ok {
println!(r.err); // "401:bad signature" / "401:expired"
return 1;
}
let claims: JwtClaims = r.val;
println!(claims.sub); // ex. "user-42"
println!(claims.exp.to_string()); // ex. "1900000000"
return 0;
}
Usado dentro de um handler, o erro com prefixo de status mapeia de forma limpa para uma resposta:
import stdlib::http::*;
import stdlib::http::jwt::*;
fn protected(req: *HttpRequest) -> HttpResponse {
let auth: string = req.header("Authorization"); // "Bearer <token>"
let token: string = auth.substring(7, auth.len());
let r: !JwtClaims = jwt_verify(token, "my-shared-secret");
if !r.ok {
return HttpResponse::with_status(401).text("unauthorized");
}
return HttpResponse::ok().text("welcome ".concat(r.val.sub));
}
HTTP/2 & HTTP/3 — H2Conn
Dois transportes mais recentes ficam por cima das camadas TLS e UDP vistas antes neste capítulo. HTTP/2 (stdlib::http::h2) é Glide puro: framing, uma máquina de estados de conexão e compressão de headers HPACK, com tanto um cliente (H2Conn) quanto um loop de servidor (h2_serve / h2_listen). HTTP/3 (stdlib::http::h3) embrulha QUIC via libngtcp2 + libnghttp3 e expõe um pequeno conjunto de funções livres que espelham o formato do cliente HTTP/1.1.
Ambos negociam o protocolo através do TLS ALPN. O cliente opta por "h2,http/1.1" (ou "h3") para que um servidor que não fale a nova versão faça fallback de forma transparente. A v1 de cada transporte é orientada a GET-e-POST-simples: sem PUSH, sem obedecer a priority, sem trailers.
Ressalva de plataforma para HTTP/3
HTTP/3 linka -lngtcp2 -lngtcp2_crypto_ossl -lnghttp3 -lssl -lcrypto e precisa da API QUIC do OpenSSL 3.5+ mais o shim estático ngtcp2_crypto_ossl. Essa combinação está presente no sysroot windows-gnu que vem com o release, mas não nos sysroots musl (Linux) ou macOS hoje. Na prática, HTTP/3 é exclusivo de Windows nos sysroots distribuídos — no Linux/macOS o linker faz fallback para as libs do sistema, que só resolvem se você instalou um toolchain OpenSSL 3.5+ / ngtcp2 por conta própria. HTTP/2 não tem tal dependência; funciona em todos os targets.
FrameType e FrameFlag — opcodes do HTTP/2
Frames HTTP/2 carregam um comprimento de 24 bits, um tipo de 8 bits, flags de 8 bits e um stream id de 31 bits (RFC 7540 §4.1). FrameType e FrameFlag são structs marcadoras vazias cujos métodos estáticos retornam as constantes da spec — use-os em vez de inteiros literais.
Métodos de FrameType (cada um retorna i32):
FrameType::data() // 0
FrameType::headers() // 1
FrameType::priority() // 2
FrameType::rst_stream() // 3
FrameType::settings() // 4
FrameType::push_promise() // 5
FrameType::ping() // 6
FrameType::goaway() // 7
FrameType::window_update() // 8
FrameType::continuation() // 9
Métodos de FrameFlag (cada um retorna i32, combine com |):
FrameFlag::end_stream() // 1
FrameFlag::end_headers() // 4
FrameFlag::padded() // 8
FrameFlag::priority() // 32
FrameFlag::ack() // 1 (compartilha o bit 1 com end_stream; nunca no mesmo frame)
FrameHeader — um header de frame decodificado
pub struct FrameHeader {
pub length: i32, // comprimento do payload, 24 bits
pub frame_type: i32, // tipo, 8 bits
pub flags: i32, // flags, 8 bits
pub stream_id: i32, // stream id, 31 bits (bit do topo reservado)
}
Funções de encode / decode de frame
write_frame(buf: *ByteBuffer, frame_type: i32, flags: i32, stream_id: i32, payload: string)— anexa um header de 9 bytes + o payload string. Trunca no primeiro NUL (ostringdo Glide é uma cstring), então use-o apenas para payloads de texto.write_frame_bytes(buf: *ByteBuffer, frame_type: i32, flags: i32, stream_id: i32, payload: *ByteBuffer)— o mesmo, mas o payload vem de umByteBuffercom comprimento explícito. Seguro para binário; use isto para blocos de header HPACK.read_frame_header(raw: *ByteBuffer, pos: i32) -> !*FrameHeader— faz parse de um header de 9 bytes empos.h2_preface() -> string— o preâmbulo de conexão de 24 bytes"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".settings_payload_empty() -> string— um payload SETTINGS vazio (usado para dar ACK nos settings do peer).settings_pair_to(buf: *ByteBuffer, key: i32, value: i32)— anexa uma entrada SETTINGS de 6 bytes.window_update_to(buf: *ByteBuffer, increment: i32)— anexa um payload WINDOW_UPDATE de 4 bytes.
import stdlib::bytes::*;
import stdlib::http::h2::*;
let buf: *ByteBuffer = ByteBuffer::new();
defer buf.free();
write_frame(buf, FrameType::ping(), 0, 0, "ABCDEFGH"); // PING de 8 bytes
let hr: !*FrameHeader = read_frame_header(buf, 0);
if hr.ok {
let h: *FrameHeader = hr.val;
println!(h.frame_type, h.length, h.stream_id); // => 6 8 0
}
HpackEncoder / HpackDecoder — compressão de headers HPACK
HPACK (RFC 7541) comprime campos de header HTTP/2 contra uma tabela estática de 61 entradas mais uma tabela dinâmica por direção. O encoder nunca emite literais Huffman (legal pela spec, H=0 sempre); o decoder suporta Huffman por completo porque servidores reais o emitem. Reutilize um encoder e um decoder durante toda a vida da conexão para que a tabela dinâmica se acumule.
pub struct HpackEncoder { pub max_table_size: i32, /* tabelas internas */ }
pub struct HpackDecoder { pub max_table_size: i32, /* tabelas internas */ }
/// Um campo de header decodificado, na ordem do fio.
pub struct HpackHeader {
pub name: string,
pub value: string,
}
HpackEncoder:
HpackEncoder::new() -> *HpackEncoder— cap de 4 KiB na tabela dinâmica.encode_header(self, buf: *ByteBuffer, name: string, value: string)— anexa um header abuf.
HpackDecoder:
HpackDecoder::new() -> *HpackDecoder— cap de 4 KiB na tabela dinâmica.decode_block(self, raw: string, len: i32) -> !*Vector<*HpackHeader>— decodifica um bloco de header completo.lené explícito porque blocos HPACK frequentemente contêm bytes NUL.
import stdlib::bytes::*;
import stdlib::net::ip::*; // _bytes_to_string
import stdlib::http::hpack::*;
let enc: *HpackEncoder = HpackEncoder::new();
let block: *ByteBuffer = ByteBuffer::new();
defer block.free();
enc.encode_header(block, ":method", "GET");
enc.encode_header(block, ":path", "/");
// Round-trip pelo decoder.
let dec: *HpackDecoder = HpackDecoder::new();
let raw: string = _bytes_to_string(block.data, block.len);
let r: !*Vector<*HpackHeader> = dec.decode_block(raw, block.len);
if r.ok {
for let i: i32 = 0; i < r.val.len(); i++ {
let h: *HpackHeader = r.val.get(i);
println!(h.name, "=", h.value); // => :method = GET / :path = /
}
}
H2Conn — conexão cliente HTTP/2
H2Conn embrulha um TlsStream já com handshake feito que negociou ALPN h2. Ele envia o preâmbulo de conexão e um SETTINGS + WINDOW_UPDATE inicial na construção, depois request abre um stream, envia headers (+ body opcional) e remonta a resposta.
pub struct H2Conn {
pub next_stream_id: i32, // próximo stream id ímpar do cliente
/* tls, codecs HPACK e rx ring são internos */
}
Métodos:
H2Conn::over(tls: *TlsStream) -> !*H2Conn— adota um stream TLSh2com handshake feito; envia preâmbulo + SETTINGS (ENABLE_PUSH=0, INITIAL_WINDOW_SIZE=1 MiB) + um WINDOW_UPDATE de conexão.request(self, method: string, path: string, authority: string, scheme: string, extra_headers: *Vector<*HpackHeader>, body: string) -> !*H2Response— envia uma requisição, lê a resposta completa.close(self)— envia GOAWAY e fecha o stream TLS subjacente.
A resposta:
pub struct H2Response {
pub status: i32,
pub headers: *Vector<*HpackHeader>,
pub body: string,
}
Uma requisição cliente completa. Note a negociação ALPN: só prossiga com H2Conn::over se o servidor realmente selecionou h2.
import stdlib::net::tls::*;
import stdlib::http::hpack::*;
import stdlib::http::h2::*;
fn fetch_h2() -> i32 {
let cfg: *TlsConfig = TlsConfig::client();
cfg.alpn = "h2,http/1.1";
let ts: !*TlsStream = TlsStream::connect("nghttp2.org", 443, cfg);
if !ts.ok { return 1; }
if !ts.val.alpn().eq("h2") {
// Servidor fez fallback para HTTP/1.1 — conduza pelo cliente 1.1.
ts.val.close();
return 2;
}
let h2r: !*H2Conn = H2Conn::over(ts.val);
if !h2r.ok { return 3; }
let h2: *H2Conn = h2r.val;
defer h2.close();
let extras: *Vector<*HpackHeader> = Vector::new(); // sem headers extras
let r: !*H2Response = h2.request("GET", "/", "nghttp2.org", "https", extras, "");
if !r.ok { return 4; }
println!("status:", r.val.status); // => status: 200
println!("bytes:", r.val.body.len());
return 0;
}
Para adicionar headers de requisição, empurre entradas HpackHeader no vetor extra_headers. A conexão permanece aberta após request retornar, então uma segunda chamada reutiliza a mesma tabela dinâmica HPACK:
let extras: *Vector<*HpackHeader> = Vector::new();
let h: *HpackHeader = malloc(sizeof(HpackHeader)) as *HpackHeader;
h.name = "accept";
h.value = "application/json";
extras.push(h);
let r1: !*H2Response = h2.request("GET", "/v1/a", "api.example.com", "https", extras, "");
let r2: !*H2Response = h2.request("GET", "/v1/b", "api.example.com", "https", extras, "");
A maioria dos usuários chega ao HTTP/2 pelo cliente de alto nível em vez disso: defina c.http2 = true em um HttpClient e ele negocia h2 via ALPN para URLs https://, fazendo fallback para HTTP/1.1 quando o servidor recusa.
import stdlib::http::*;
let c: *HttpClient = HttpClient::new();
c.http2 = true;
let r: !*HttpResponse = c.get("https://nghttp2.org/");
if r.ok { println!(r.val.status); } // => 200
H2ServerStream + h2_serve — servidor HTTP/2
O lado servidor conduz toda a vida de uma conexão cliente: valida o preâmbulo, troca SETTINGS, depois despacha cada requisição totalmente recebida para um handler. O handler roda de forma síncrona dentro do loop de leitura; streams concorrentes em uma conexão são serializados na etapa do handler.
/// Estado por stream que o servidor rastreia enquanto uma requisição chega.
pub struct H2ServerStream {
pub id: i32,
pub header_block: *ByteBuffer,
pub headers_done: bool,
pub body: *ByteBuffer,
pub closed_remote: bool,
}
h2_serve(s: *TlsStream, handler: fn(*HttpRequest) -> HttpResponse)— roda o loop do servidor HTTP/2 em um stream TLSh2com handshake feito. Fecha o stream quando a conexão termina; o chamador não chamas.close().
O handler é o mesmo formato fn(*HttpRequest) -> HttpResponse usado em todo o restante deste capítulo, então um handler escrito para HTTP/1.1 funciona sem alterações:
import stdlib::net::tls::*;
import stdlib::http::*;
import stdlib::http::h2::*;
fn handle(req: *HttpRequest) -> HttpResponse {
return HttpResponse::ok().body("served over h2");
}
// Dentro do seu próprio loop de accept TLS, uma vez que o ALPN negociou `h2`:
fn serve_conn(client: *TlsStream) {
h2_serve(client, handle); // assume a posse; fecha quando termina
}
HTTP/3 sobre QUIC
HTTP/3 é exposto como funções livres mais um tipo de cache de sessão. Lembre-se da ressalva de sysroot exclusivo de Windows acima. URLs devem ser https:// (ou h3://); a porta padrão é 443.
Requisições de cliente
http3_get(url: string, tls_insecure: bool) -> !*HttpResponse— GET de tiro único.http3_request(method: string, url: string, extra_headers: string, body: string, tls_insecure: bool) -> !*HttpResponse— requisição genérica de tiro único.extra_headersé um blocoName: Valueunido por CRLF.
tls_insecure = true desabilita a verificação de certificado (apenas dev local com self-signed). Ambos retornam o familiar HttpResponse.
import stdlib::http::*;
import stdlib::http::h3::*;
fn get_over_quic() -> i32 {
let r: !*HttpResponse = http3_get("https://cloudflare-quic.com/", false);
if !r.ok { println!("h3 failed:", r.err); return 1; }
println!("status:", r.val.status); // => status: 200
println!(r.val.body);
return 0;
}
let r: !*HttpResponse = http3_request(
"POST", "https://api.example.com/x",
"Content-Type: application/json\r\n",
"{\"hi\":1}", false);
if r.ok { println!(r.val.status); }
ParsedH3Url
O parser de URL usado internamente também é público:
pub struct ParsedH3Url {
pub host: string,
pub port: i32,
pub path: string,
}
H3SessionCache — retomada de sessão 0-RTT
Passar um H3SessionCache através de chamadas repetidas de http3_request_cached permite que a segunda conexão em diante para o mesmo (host, port) retome a sessão TLS e despache a requisição como early data 0-RTT (economiza ~1 RTT). Em memória por padrão; save/load o persistem em um arquivo binário entre reinicializações.
pub struct H3SessionCache { /* handle opaco */ }
Métodos:
H3SessionCache::new() -> *H3SessionCachefree(self)— libera o cache.attempts(self) -> i32— conexões que encontraram e instalaram uma sessão em cache (tentaram 0-RTT).accepted(self) -> i32— conexões cujos dados 0-RTT o servidor aceitou.attempts - acceptedé a contagem de rejeições.save(self, path: string) -> bool— serializa para um arquivo binário.load(self, path: string) -> i32— relê as entradas (descarta as expiradas; retorna a contagem carregada, 0 num arquivo ausente/corrompido).
E o ponto de entrada de requisição com cache:
http3_request_cached(method: string, url: string, extra_headers: string, body: string, tls_insecure: bool, cache: *H3SessionCache) -> !*HttpResponse— mesmo formato dehttp3_requestmas passando o cache. Thread-safe (mutex interno).
import stdlib::http::*;
import stdlib::http::h3::*;
fn resume_demo(url: string) -> i32 {
let c: *H3SessionCache = H3SessionCache::new();
defer c.free();
c.load("h3-sessions.bin"); // 0 na primeira execução; seguro antes de save rodar
let r1: !*HttpResponse = http3_request_cached("GET", url, "", "", false, c);
if !r1.ok { return 1; } // handshake completo; ticket em cache
let r2: !*HttpResponse = http3_request_cached("GET", url, "", "", false, c);
if !r2.ok { return 2; } // retoma a sessão anterior, pode usar 0-RTT
println!("0-RTT attempts:", c.attempts(), "accepted:", c.accepted());
c.save("h3-sessions.bin");
return 0;
}
Servidor HTTP/3
h3_make_self_signed_cert(cert_path: string, key_path: string, cn: string, days: i32) -> !i32— escreve um cert + chave PEM self-signed ECDSA-P256 (SAN cobreDNS:localhosteIP:127.0.0.1), válido pordaysdias. Útil para dev local / testes de loopback.http3_listen(port: i32, cert_path: string, key_path: string, handler: fn(*HttpRequest) -> HttpResponse) -> !i32— roda um servidor HTTP/3 naportUDP. Uma única thread leitora em C faz demux dos pacotes de entrada entre até 64 conexões; a requisição de cada conexão aceita é despachada parahandlerem sua própria pthread.
cert_path / key_path são arquivos PEM (mesmo layout de https_listen). A assinatura do handler é idêntica à dos servidores HTTP/1.1 e HTTP/2.
import stdlib::http::*;
import stdlib::http::h3::*;
fn root(req: *HttpRequest) -> HttpResponse {
return HttpResponse::ok().body("hello over QUIC");
}
fn main() -> i32 {
let cert: !i32 = h3_make_self_signed_cert("cert.pem", "key.pem", "localhost", 365);
if !cert.ok { println!(cert.err); return 1; }
let r: !i32 = http3_listen(4443, "cert.pem", "key.pem", root);
if !r.ok { println!(r.err); return 1; } // err em bind / cert ruim
return 0;
}
Limites do servidor v1: um stream de requisição por conexão, sem HTTP/3 PUSH, sem descoberta de path-MTU (assume um default seguro de 1200 bytes). Early data 0-RTT é aceita no servidor mas apenas para métodos idempotentes — requisições não-GET/HEAD chegando como 0-RTT são rejeitadas com 425 Too Early para que o cliente retente sobre 1-RTT.
Codificação de URL — url_encode
Percent-encoding e os structs de URL parseada que os clientes HTTP e WebSocket constroem internamente. O codec vive em stdlib::url; os structs parseados acompanham seus respectivos clientes (http::ParsedUrl, net::WsParsedUrl).
import stdlib::url::*;
url_encode / url_decode
pub fn url_encode(s: string) -> string
pub fn url_decode(s: string) -> !string
url_encode aplica percent-encoding em s conforme a RFC 3986 §2.3. O conjunto unreserved — letras ASCII, dígitos e - _ . ~ — passa intacto; todo outro byte vira %XX com hex em maiúsculas. UTF-8 multibyte é codificado byte a byte.
url_encode("hello world"); // => "hello%20world"
url_encode("a&b=c"); // => "a%26b%3Dc"
url_encode("test_~-."); // => "test_~-." (tudo unreserved)
url_encode("héllo"); // => "h%C3%A9llo" (bytes UTF-8)
url_decode é o inverso. Percorre a string, expandindo cada %XX de volta para um byte e copiando todo o resto na íntegra. Note que + não é tratado como espaço — ele é preservado literalmente, casando com o que url_encode produz (um espaço vira %20, nunca +). Um % no fim da string ou seguido por um dígito não-hex é um erro fatal, então a função retorna !string: leia .ok, depois .val ou .err.
let r: !string = url_decode("hello%20world");
if r.ok { println!(r.val); } // "hello world"
url_decode("%c3%a9").val; // => "é" (hex em minúsculas aceito)
url_decode("a+b").val; // => "a+b" (plus mantido literal)
let bad: !string = url_decode("%ZZ");
if !bad.ok { println!(bad.err); } // "url_decode: malformed percent escape"
let short: !string = url_decode("%2");
if !short.ok { println!(short.err); } // malformado (escape truncado)
O round-trip encode/decode é sem perdas sobre entrada arbitrária:
let src: string = "azAZ09-_.~/?:#[]@!$&'()*+,;= hé";
let enc: string = url_encode(src);
let dec: !string = url_decode(enc);
if dec.ok {
println!(dec.val.eq(src)); // => true
}
Construindo e parseando query strings
Não há um construtor dedicado de query-map em stdlib::url — uma query string é apenas um componente comum de URL, então você compõe uma codificando cada chave e valor com url_encode e juntando você mesmo:
let q: string = "q=".concat(url_encode("hello world"))
.concat("&lang=").concat(url_encode("en"));
// q => "q=hello%20world&lang=en"
No lado do servidor, a query string é parseada pra você: um http::HttpRequest expõe .query(name) (coberto na seção do servidor HTTP), então código de aplicação raramente decodifica parâmetros de query à mão.
ParsedUrl — URLs HTTP
O cliente HTTP parseia uma URL http:// ou https:// em um ParsedUrl. O struct é público pra você poder ler os componentes pra onde uma requisição foi roteada; o parser em si é interno (construído por HttpClient), então você normalmente obtém um indiretamente em vez de construí-lo.
pub struct ParsedUrl {
pub scheme: string, // "http" ou "https"
pub host: string, // host puro (literais IPv6 desembrulhados de [..])
pub port: i32, // porta explícita, senão 80 (http) / 443 (https)
pub path: string, // path + query, nunca vazio ("/" quando ausente)
}
port assume o padrão a partir do scheme (80 para http, 443 para https) e é sobrescrito por um :port explícito. Hosts IPv6 são aceitos em forma de colchetes (https://[::1]:8443/) e os colchetes são removidos de host.
host_header
pub fn host_header(self: *ParsedUrl) -> string
Formata o valor para um header HTTP Host:. A porta é omitida quando é igual ao padrão do scheme, e incluída caso contrário.
// https://example.com/ -> host_header() == "example.com"
// https://example.com:8443/x -> host_header() == "example.com:8443"
WsParsedUrl — URLs WebSocket
O cliente WebSocket usa um struct paralelo para URLs ws:// / wss://. Ele carrega uma flag booleana is_wss em vez de uma string de scheme, mas no restante tem a mesma forma de ParsedUrl.
pub struct WsParsedUrl {
pub is_wss: bool, // false = ws, true = wss
pub host: string,
pub port: i32, // porta explícita, senão 80 (ws) / 443 (wss)
pub path: string, // nunca vazio ("/" quando ausente)
}
host_header
pub fn host_header(self: *WsParsedUrl) -> string
Mesmo contrato de ParsedUrl::host_header: descarta a porta quando ela casa com o padrão do scheme (80 para ws, 443 para wss), mantém caso contrário.
// wss://example.com/ -> host_header() == "example.com"
// wss://example.com:8443/ -> host_header() == "example.com:8443"
ParsedUrl e WsParsedUrl têm deliberadamente os mesmos quatro campos (host / port / path mais um discriminador de scheme) pra que a lógica de conexão em ambos os clientes seja idêntica: resolver host, discar port, enviar path e emitir host_header() como o header Host:. A única diferença é quais portas padrão e nomes de scheme se aplicam.