---
title: "Zero-Copy em Zig: sendfile para Arquivos e Sockets"
url: "https://ziglang.com.br/artigos/zig-sendfile-zero-copy-arquivos-sockets/"
markdown_url: "https://ziglang.com.br/artigos/zig-sendfile-zero-copy-arquivos-sockets.MD"
description: "Guia prático de zero-copy em Zig com sendfile: como enviar arquivos por sockets, tratar escrita parcial, fallback portátil, backpressure e benchmarks."
date: "2026-09-02"
author: ""
---

# Zero-Copy em Zig: sendfile para Arquivos e Sockets

Guia prático de zero-copy em Zig com sendfile: como enviar arquivos por sockets, tratar escrita parcial, fallback portátil, backpressure e benchmarks.


Para **enviar um arquivo por um socket em Zig com menos cópias**, use `sendfile` quando o target e o tipo de conexão permitirem, mas trate a operação como streaming: mantenha um offset, aceite transferências parciais, respeite backpressure e tenha um fallback com `read` + `write`. O benefício vem de evitar que a aplicação copie cada bloco do kernel para um buffer no user space e depois de volta para o kernel.

Esse padrão é útil em servidores de arquivos, proxies internos, caches, distribuição de artefatos e endpoints que entregam objetos grandes sem transformá-los. Ele não é um botão universal de performance. TLS, compressão, ranges, arquivos pequenos e diferenças entre Linux, BSD e macOS mudam a decisão. O objetivo deste guia é mostrar o modelo operacional correto, não apenas uma chamada isolada de syscall.

Se você ainda está montando a camada de rede, leia primeiro o guia de [sockets TCP e UDP em Zig](/artigos/zig-networking-sockets-tcp-udp/). Para arquivos grandes acessados aleatoriamente, compare também com [mmap em Zig](/artigos/zig-mmap-arquivos-memory-mapped/).

## Resposta rápida

| Cenário | Melhor ponto de partida |
|---|---|
| Arquivo grande para socket TCP sem TLS | `sendfile` com loop e offset |
| HTTPS com TLS no user space | buffer reutilizável + escrita pela biblioteca TLS |
| Conteúdo comprimido ou transformado em tempo real | streaming tradicional |
| Arquivo pequeno | resposta normal; meça antes de otimizar |
| Socket não bloqueante | `sendfile` + readiness (`poll`/event loop) |
| Target multiplataforma | interface comum + implementação por plataforma |
| Resposta com HTTP Range | calcule offset e quantidade antes do loop |
| Benchmark | compare CPU, throughput, p95 e bytes transferidos |

## O que “zero-copy” significa na prática

O caminho tradicional para enviar um arquivo é conceitualmente simples:

1. a aplicação chama `read`;
2. o kernel coloca os bytes em um buffer visível ao processo;
3. a aplicação chama `write` no socket;
4. o kernel copia ou referencia os dados para a pilha de rede.

Em pseudocódigo Zig:

```zig
var buffer: [64 * 1024]u8 = undefined;
while (true) {
    const n = try file.read(&buffer);
    if (n == 0) break;
    try writeAllToSocket(socket, buffer[0..n]);
}
```

Esse código pode ser perfeitamente bom. É portátil, fácil de instrumentar e permite transformar, comprimir, criptografar ou calcular um hash sobre os bytes. O custo é que cada bloco atravessa o buffer da aplicação.

Com `sendfile`, você fornece ao kernel um descritor de entrada, um descritor de saída, um offset e uma quantidade. Em plataformas que suportam esse caminho, o kernel consegue aproveitar o page cache e alimentar a pilha de rede sem entregar o conteúdo inteiro a um buffer controlado pela aplicação.

“Zero-copy” não significa literalmente zero movimentação física em qualquer hardware. Drivers, checksums, DMA, fragmentação e a própria implementação do sistema operacional ainda participam. O termo descreve a redução de cópias e transições evitáveis no caminho da aplicação.

## A arquitetura recomendada em Zig

Não espalhe chamadas de `sendfile` pelo handler HTTP. Crie uma operação de domínio pequena, com contrato explícito:

```zig
const TransferOptions = struct {
    offset: u64 = 0,
    count: u64,
};

const TransferResult = struct {
    bytes_sent: u64,
    completed: bool,
};

fn transferFile(
    file: *std.fs.File,
    socket: Socket,
    options: TransferOptions,
) !TransferResult {
    // Seleciona sendfile ou fallback conforme plataforma e conexão.
}
```

Essa separação resolve quatro problemas:

- a API pública não depende da assinatura de uma syscall específica;
- Linux pode usar um caminho otimizado sem contaminar o resto do projeto;
- TLS e plataformas sem suporte usam o fallback;
- testes conseguem validar offset, limite e falhas sem subir um servidor completo.

Como o Zig ainda evolui antes da versão 1.0, nomes de funções e conversões de handles podem mudar. Fixe a toolchain do projeto e confira a versão da stdlib usada no CI. O contrato acima deve permanecer seu; o adaptador para o sistema operacional é que acompanha a versão.

## Loop correto: transferência parcial é normal

O erro mais comum é assumir que uma chamada transfere tudo:

```zig
// Conceitualmente incorreto: o retorno pode ser menor que count.
_ = try sendfile(socket_fd, file_fd, offset, count);
```

A lógica robusta mantém o progresso:

```zig
fn sendFileRegion(
    out_fd: FileDescriptor,
    in_fd: FileDescriptor,
    initial_offset: u64,
    count: u64,
) !u64 {
    var offset = initial_offset;
    var remaining = count;

    while (remaining > 0) {
        const sent = sendfilePlatform(out_fd, in_fd, offset, remaining) catch |err| {
            switch (err) {
                error.WouldBlock => {
                    try waitUntilWritable(out_fd);
                    continue;
                },
                error.Interrupted => continue,
                else => return err,
            }
        };

        if (sent == 0) break;
        offset += sent;
        remaining -= sent;
    }

    return count - remaining;
}
```

O snippet é deliberadamente uma camada de adaptação: `FileDescriptor`, `sendfilePlatform` e `waitUntilWritable` devem mapear para as APIs da versão e do target adotados pelo projeto.

Três regras são obrigatórias:

1. **Retorno curto não é erro.** Continue a partir do novo offset.
2. **`EINTR` não reinicia seu estado.** Repita preservando o progresso.
3. **`EAGAIN` não autoriza busy loop.** Espere o socket ficar gravável via `poll`, `epoll`, `kqueue` ou event loop.

Em um servidor bloqueante simples, a própria chamada pode esperar espaço no socket. Em um servidor com milhares de conexões, bloquear uma thread por cliente pode destruir a escalabilidade. O modelo de concorrência precisa combinar com a operação.

## Como obter offset e tamanho com segurança

Antes de transferir, descubra o tamanho do arquivo e valide o intervalo solicitado. Para uma resposta completa:

```text
offset = 0
count = tamanho_do_arquivo
```

Para um range HTTP inclusivo `bytes=1000-1999`:

```text
offset = 1000
count = 1999 - 1000 + 1 = 1000
```

Valide antes de fazer qualquer cast:

- `offset` não pode ser maior que o tamanho do arquivo;
- o fim do range não pode ser menor que o início;
- a soma deve ser checada contra overflow;
- `count` deve caber nos tipos aceitos pela plataforma;
- o arquivo pode mudar entre o `stat` e a transferência.

Se os arquivos são mutáveis, decida a semântica. Uma opção é abrir o arquivo e usar os metadados daquele handle, evitando resolver o caminho novamente. Para artefatos imutáveis e nomes por hash, o problema fica menor.

Nunca concatene um caminho recebido do usuário diretamente ao diretório de arquivos. Resolva a rota para um identificador interno ou use operações relativas a um diretório já aberto. Isso reduz path traversal e condições de corrida com links simbólicos.

## Fallback portátil com buffer reutilizável

O fallback não é código de segunda classe. Ele cobre TLS, transformações e targets sem `sendfile`, portanto precisa de testes e métricas.

```zig
fn copyRange(
    allocator: std.mem.Allocator,
    file: *std.fs.File,
    writer: anytype,
    offset: u64,
    count: u64,
) !u64 {
    try file.seekTo(offset);

    const buffer = try allocator.alloc(u8, 64 * 1024);
    defer allocator.free(buffer);

    var remaining = count;
    var total: u64 = 0;

    while (remaining > 0) {
        const wanted: usize = @intCast(@min(remaining, buffer.len));
        const n = try file.read(buffer[0..wanted]);
        if (n == 0) break;

        try writer.writeAll(buffer[0..n]);
        remaining -= n;
        total += n;
    }

    return total;
}
```

Em um servidor, evite alocar um novo buffer para cada bloco. Aloque uma vez por transferência, reutilize buffers de um pool limitado ou associe um buffer a cada worker. Um pool sem limite apenas troca pressão no heap por retenção excessiva de memória.

O tamanho ideal não é universal. Blocos de 16 KiB, 64 KiB e 256 KiB podem se comportar de forma diferente conforme armazenamento, rede e TLS. Meça com arquivos representativos.

## sendfile e HTTP

Em HTTP/1.1, envie os headers antes do corpo:

```text
HTTP/1.1 200 OK\r\n
Content-Length: 7340032\r\n
Content-Type: application/octet-stream\r\n
ETag: "..."\r\n
\r\n
```

Depois, transfira exatamente `Content-Length` bytes. Se a conexão cair após parte do corpo, não tente corrigir o status HTTP: os headers já foram enviados. Registre quantos bytes saíram e encerre a conexão.

Para `206 Partial Content`, inclua `Content-Range` e use o range calculado. Para requisição `HEAD`, envie os mesmos headers que enviaria no `GET`, mas não transfira o corpo.

Evite `sendfile` quando o corpo exige:

- compressão gzip ou Brotli em tempo real;
- template ou substituição de conteúdo;
- criptografia feita no processo;
- checksum calculado naquele momento;
- multiplexação por um protocolo que precisa enquadrar os bytes no user space.

Uma alternativa eficiente é pré-comprimir arquivos estáticos e selecionar a representação pronta conforme `Accept-Encoding`. Assim, o servidor pode enviar o `.br` ou `.gz` já existente sem transformar o conteúdo durante a requisição.

## O limite do TLS

No HTTPS tradicional, OpenSSL, BoringSSL, mbedTLS ou outra biblioteca no user space precisa receber o plaintext para produzir registros criptografados. Um `sendfile` direto do arquivo para o socket pula justamente essa etapa, então não serve para o caminho comum.

Existem tecnologias como kernel TLS em alguns ambientes, mas elas exigem suporte coordenado entre kernel, biblioteca, cifras e operação. Não trate isso como default portátil.

Arquiteturas práticas:

- **Nginx/Caddy termina TLS e serve arquivos:** seu processo Zig só cuida de autorização ou metadados;
- **proxy termina TLS, Zig envia por conexão interna:** avalie se o proxy pode buscar o arquivo por localização interna;
- **Zig termina TLS:** use o writer da biblioteca TLS e o fallback com buffer;
- **rede interna sem TLS:** `sendfile` pode ser adequado se o modelo de ameaça permitir.

Para desenho de borda, veja [Zig por trás de Nginx](/artigos/zig-nginx-proxy-reverso-load-balancing/) e o guia de [TLS, HTTPS e mTLS em Zig](/artigos/zig-tls-https-certificados-mtls/).

## Backpressure, cancelamento e timeouts

Um cliente lento não deve manter recursos para sempre. Defina:

- timeout para começar a escrever;
- timeout de inatividade entre progressos;
- deadline total opcional para a resposta;
- limite de transferências simultâneas;
- política de cancelamento quando o cliente desconecta.

O timeout mais útil não é “o download deve terminar em 10 segundos”, porque um arquivo grande em rede móvel pode ser legítimo. Prefira um prazo de **progresso**: se nenhum byte sair durante uma janela, cancele.

Em I/O não bloqueante, registre interesse em escrita somente enquanto houver dados pendentes. Manter todo socket sempre inscrito como gravável causa wakeups desnecessários, pois sockets saudáveis costumam estar prontos para escrita a maior parte do tempo.

## Portabilidade: Linux, BSD e macOS não são iguais

A ideia de `sendfile` existe em várias plataformas, mas assinaturas e semânticas variam. A posição dos descritores, a forma de passar offset, o retorno de bytes e o suporte a headers/trailers não são idênticos.

Estruture o código por capacidade:

```zig
switch (builtin.os.tag) {
    .linux => return transferLinux(...),
    .freebsd, .macos => return transferBsdLike(...),
    else => return transferBuffered(...),
}
```

Não force a mesma syscall em todos os targets com condicionais espalhadas. Mantenha uma interface comum e implementações pequenas por plataforma. Seu pipeline deve compilar cada branch relevante; veja como montar isso no guia de [cross-compilation em Zig](/artigos/zig-cross-compilation-guia/).

Também diferencie “compila” de “funciona”. Testes de integração precisam criar um arquivo temporário, abrir um par de sockets, transferir um range e comparar os bytes recebidos.

## Como testar sem depender de benchmark manual

Crie uma matriz mínima:

1. arquivo vazio;
2. arquivo menor que um page size;
3. arquivo maior que o buffer do fallback;
4. range no começo, meio e fim;
5. cliente que lê devagar;
6. desconexão no meio;
7. socket não bloqueante que retorna `WouldBlock`;
8. arquivo truncado durante a transferência;
9. `count` maior que os bytes restantes;
10. fallback forçado mesmo em Linux.

Valide conteúdo e contagem, não apenas “não retornou erro”. Para ranges, compare o resultado com `arquivo[offset .. offset + count]`.

Testes unitários devem cobrir o cálculo do intervalo e os adapters de erro. Testes de integração cobrem descritores reais. Para organizar a suíte, use o [guia completo de testes em Zig](/artigos/zig-testes-guia-completo/).

## Benchmark: o que medir

Um benchmark útil compara pelo menos dois caminhos:

- buffer + `read`/`write`;
- `sendfile`.

Use arquivos de tamanhos variados, conexões persistentes e concorrências próximas da produção. Registre:

- throughput total em MiB/s;
- uso de CPU do processo e do sistema;
- latência p50, p95 e p99;
- trocas de contexto;
- memória residente;
- quantidade de syscalls;
- erros e desconexões.

Rode com cache quente e frio separadamente. Se o arquivo já está no page cache, você mede principalmente o caminho CPU/rede. Se está frio, o armazenamento domina e pode esconder a diferença.

Não conclua pelo throughput de uma única conexão em localhost. A vantagem pode aparecer sob concorrência, quando a redução de CPU libera capacidade para mais clientes. Também pode desaparecer quando TLS ou o limite da placa de rede domina.

O complemento natural é o tutorial de [profiling com perf e flamegraph em Zig](/artigos/zig-profiling-perf-flamegraph/).

## Checklist de produção

Antes de ativar zero-copy, confirme:

- [ ] o caminho atende somente arquivos autorizados;
- [ ] offset e count são validados contra overflow e tamanho;
- [ ] retornos parciais atualizam o progresso;
- [ ] `Interrupted` repete sem perder estado;
- [ ] `WouldBlock` espera readiness sem busy loop;
- [ ] cliente lento tem timeout de progresso;
- [ ] o número de arquivos e sockets abertos é limitado;
- [ ] o fallback com buffer está coberto por testes;
- [ ] TLS e compressão escolhem o caminho correto;
- [ ] logs registram bytes planejados, enviados e motivo do término;
- [ ] testes de range incluem limites e arquivo vazio;
- [ ] benchmarks comparam CPU e latência, não só throughput;
- [ ] cada target suportado compila e roda teste de integração.

## Conclusão

`sendfile` é uma otimização valiosa quando um serviço Zig precisa mover bytes imutáveis de um arquivo para um socket sem inspecioná-los. O desenho correto, porém, começa fora da syscall: interface por plataforma, offset explícito, loop para transferências parciais, integração com backpressure, timeout de progresso e fallback portátil.

Use zero-copy para o caso em que ele realmente reduz trabalho. Para TLS no user space, conteúdo dinâmico ou transformação em trânsito, um buffer reutilizável e uma boa política de streaming tendem a ser mais simples e corretos. Em ambos os caminhos, a vantagem do Zig é a mesma: ownership, erros, descritores e custos ficam visíveis no código — e, portanto, podem ser medidos, testados e operados com previsibilidade.
