Programação de Redes
Introdução:
Inicialmente precisa conceituar o que é socket. A comunicação entre processos de software tornou-se indispensável nos sistemas atuais.
O elo entre os processos do servidor e do cliente é o socket. Ele é a “porta” na qual os processos enviam e recebem mensagens. De acordo com JAMES F KUROSE: “socket é a interface entre a camada de aplicação e a camada de transporte dentro de uma máquina”.
Então foram desenvolvidas diversas aplicações cliente/servidor onde cliente(s) e servidor poderiam estar em máquinas diferentes, distantes umas das outras. Os aplicativos do cliente e do servidor utilizam protocolos de transporte para se comunicarem. Quando um aplicativo interage com o software de protocolo, ele deve especificar detalhes, como por exemplo se é um servidor ou um cliente. Além disso, os aplicativos que se comunicam devem especificar detalhes adicionais (por exemplo, o remetente deve especificar os dados a serem enviados, e o receptor deve especificar onde os dados recebidos devem ser colocados).
Analisando o esquema acima percebemos que tudo acima da interface do socket, na camada de aplicação, é controlado pelo criador da aplicação. O controle da camada de transporte é feito pelo Sistema Operacional.
Temos dois tipos de serviços de transporte via socket: o confiável orientado a cadeia de bytes (byte steam) e os datagramas não confiáveis. O protocolo na qual é implementado o primeiro é o TCP, já o segundo é implementado no protocolo UDP.
Padrão de arquitetura Reactor e Proactor (Reativo e Proativo)
Reactor:
"Um padrão comportamental de objeto para desmultiplexação e distribuição de identificadores para eventos síncronos." - Douglas C. Schmidt
O padrão de arquitetura do Reactor permite que aplicativos controlados por eventos desmultiplexem e despachem solicitações de serviço que são entregues a um aplicativo por um ou mais clientes. A estrutura introduzida pelo padrão do Reactor inverte o fluxo de controle dentro de um aplicativo.
É responsabilidade de um componente designado, chamado reactor, não um aplicativo, aguardar eventos de indicação de forma síncrona, desmultiplexá-los para manipuladores de eventos associados que são responsáveis pelo processamento desses eventos e, em seguida, despachar o método de gancho apropriado no manipulador de eventos. Em particular, um reactor despacha manipuladores de eventos que reagem à ocorrência de um evento específico. Portanto, os desenvolvedores de aplicativos são responsáveis apenas pela implementação de manipuladores de eventos concretos e podem reutilizar os mecanismos de desmultiplexação e despacho do reactor. Embora o padrão do Reactor seja relativamente simples de programar e usar, ele possui várias restrições que podem limitar sua aplicabilidade. Em particular, ele não é dimensionado para suportar um grande número de clientes simultâneos e/ou solicitações de clientes de longa duração, pois serializa todo o processamento do manipulador de eventos na camada de desmultiplexação de eventos.
Para ilustrar o padrão do Reactor, considere o servidor acionado por eventos para um serviço de log distribuído mostrado na Figura abaixo. Os aplicativos clientes usam o serviço de log para registrar informações sobre seu status em um ambiente distribuído. Essas informações de status geralmente incluem notificações de erro, rastreamentos de depuração e relatórios de desempenho. Os registros de log são enviados para um servidor de log central, que pode gravar os registros em vários dispositivos de saída, como um console, uma impressora, um arquivo ou um banco de dados de gerenciamento de rede.
O servidor de log lida com registros de log e solicitações de conexão enviadas pelos clientes. Registros de log e solicitações de conexão podem chegar simultaneamente em vários identificadores. Um identificador identifica os recursos de comunicação de rede gerenciados em um SO.
Figura 1: Serviço de Log Distribuído
O servidor de log se comunica com os clientes usando um protocolo orientado a conexão, como o TCP. Os clientes que desejam registrar dados devem primeiro enviar uma solicitação de conexão ao servidor. O servidor aguarda essas solicitações de conexão usando um identificador de fábrica que escuta em um endereço conhecido pelos clientes. Quando uma solicitação de conexão chega, a fábrica de identificadores estabelece uma conexão entre o cliente e o servidor, criando um novo identificador que representa um ponto de extremidade da conexão. Esse identificador é retornado ao servidor, que aguarda as solicitações de serviço ao cliente chegarem no identificador. Depois que os clientes estão conectados, eles podem enviar registros simultaneamente ao servidor. O servidor recebe esses registros através dos identificadores de soquete conectados.
Talvez a maneira mais intuitiva de desenvolver um servidor de log simultâneo seja usar vários threads que possam processar vários clientes simultaneamente, como mostra abaixo. Essa abordagem aceita sincronicamente conexões de rede e gera um thread por conexão para manipular os registros de log do cliente.

Figura 2: Servidor de log multithread
No entanto, o uso de multithread para implementar o processamento de registros de log no servidor falha ao resolver as seguintes forças:
- Eficiência: a thread pode levar a um desempenho ruim devido à alternância de contexto, sincronização e movimentação de dados;
- Simplicidade de programação: a thread pode exigir esquemas complexos de controle de simultaneidade;
- Portabilidade: a thread não está disponível em todas as plataformas de SO.
Proactor:
"Um Padrão Comportamental de Objetos para desmultiplexar e despachar manipuladores para eventos assíncronos." - Douglas C. Schmidt
O padrão de arquitetura Proactor permite que os aplicativos controlados por eventos desmultiplexem e despachem solicitações de serviços com eficiência, acionadas pela conclusão de operações assíncronas. Oferece os benefícios de desempenho da simultaneidade sem incorrer em alguns de seus passivos.
No padrão Proactor, os componentes do aplicativo são representados por clientes e manipuladores de conclusão que são entidades proativas. Diferentemente do padrão Reactor, que espera passivamente a chegada de eventos de indicação e reage, clientes e manipuladores de conclusão no padrão Proactor instigam o controle e o fluxo de dados dentro de um aplicativo iniciando uma ou mais solicitações de operação assíncrona proativamente em um processador de operação assíncrono.
Quando essas operações assíncronas são concluídas, o processador de operação assíncrona e um componente proactor designado colaboram para desmultiplexar os eventos de conclusão resultantes para seus manipuladores de conclusão associados e despachar os métodos de gancho desses manipuladores. Após o processamento de um evento de conclusão, um manipulador de conclusão pode iniciar novas solicitações de operação assíncrona de maneira proativa.
O padrão Proactor deve ser aplicado quando os aplicativos exigirem os benefícios de desempenho da execução simultânea de operações, sem as restrições síncrona ou reativa ou multithread. Para ilustrar esses benefícios, considere um aplicativo de rede que precise executar várias operações simultaneamente. Por exemplo, um servidor Web de alto desempenho deve processar simultaneamente solicitações HTTP enviadas de vários clientes. A Figura abaixo mostra uma interação típica entre navegadores Web e um servidor Web. Quando um usuário instrui um navegador a abrir uma URL, ele envia uma solicitação HTTP GET ao servidor da Web. Após o recebimento, o servidor analisa e valida a solicitação e envia os arquivos especificados de volta ao navegador.

Figura 3: Arquitetura típica de software de comunicação para servidor Web
O desenvolvimento de servidores Web de alto desempenho requer a resolução das seguintes forças:
- Simultaneidade: O servidor deve executar várias solicitações do cliente simultaneamente;
- Eficiência: O servidor deve minimizar a latência, maximizar a taxa de transferência e evitar a utilização desnecessária da CPU.
- Simplicidade de programação: O design do servidor deve simplificar o uso de estratégias de concorrência eficientes;
- Adaptabilidade: A integração de protocolos de transporte novos ou aprimorados (como HTTP 1.1) deve resultar em custos mínimos de manutenção.
Um servidor Web pode ser implementado usando várias estratégias de simultaneidade, incluindo várias threads síncronos, envio de evento síncrono reativo e envio de evento assíncrono proativo. Abaixo, examinamos as desvantagens das abordagens convencionais e explicamos como o padrão Proactor fornece uma técnica poderosa que suporta uma estratégia eficiente e flexível de despacho de eventos assíncronos para aplicativos simultâneos de alto desempenho.
Acceptor-Connector:
O padrão de projeto Acceptor-Connector desacopla a conexão e a inicialização dos serviços de ponto de cooperação em um sistema em rede do processamento que eles executam depois de conectados e inicializados. O Acceptor-Connector permite que os aplicativos configurem suas topologias de conexão de uma maneira amplamente independente dos serviços que eles fornecem. O padrão pode ser colocado em camadas na parte superior do Reactor para manipular eventos associados ao estabelecimento da conectividade entre serviços.
Reactor vs Proactor:
Em geral, os mecanismos de multiplexação de E/S dependem de um desmultiplexador de eventos, um objeto que despacha eventos de E/S de um número limitado de fontes para os manipuladores de eventos de leitura e gravação apropriados. O desenvolvedor registra interesse em eventos específicos e fornece manipuladores de eventos ou retornos de chamada. O desmultiplexador de eventos entrega os eventos solicitados aos manipuladores de eventos.
Dois padrões que envolvem desmultiplexadores de eventos são chamados Reactor e Proactor. Os padrões do reactor envolvem E/S síncrona, enquanto o padrão Proactor envolve E/S assíncrona. No Reactor, o desmultiplexador de eventos aguarda os eventos que indicam quando um descritor ou socket de arquivo está pronto para uma operação de leitura ou gravação. O desmultiplexador passa esse evento para o manipulador apropriado, responsável por executar a leitura ou gravação real.
No padrão Proactor, por outro lado, o manipulador ou o desmultiplexador de eventos em nome do manipulador e inicia operações de leitura e gravação assíncronas. A própria operação de E/S é executada pelo sistema operacional (SO). Os parâmetros passados para o sistema operacional incluem os endereços dos buffers de dados definidos pelo usuário, dos quais o sistema operacional obtém dados para gravação ou nos quais o sistema operacional coloca dados lidos. O desmultiplexador de eventos aguarda eventos que indicam a conclusão da operação de E/S e encaminha esses eventos para os manipuladores apropriados. Por exemplo, no Windows, um manipulador pode iniciar operações de E/S assíncronas (sobrepostas na terminologia da Microsoft), e o desmultiplexador de eventos pode esperar pelos eventos de IOCompletion. A implementação desse padrão assíncrono é baseada em uma API assíncrona no nível do SO, e chamaremos essa implementação de assíncrona "no nível do sistema" ou "true", porque o aplicativo depende totalmente do SO para executar a E/S real.
Um exemplo ajudará você a entender a diferença entre o Reactor e o Proactor. Vamos nos concentrar na operação de leitura aqui, pois a implementação de gravação é semelhante. Aqui está uma leitura no Reactor:
- Um manipulador de eventos declara interesse em eventos de E/S que indicam prontidão para leitura em um socket específico
- O desmultiplexador de eventos aguarda eventos
- Um evento chega e ativa o desmultiplexador e o desmultiplexador chama o manipulador apropriado
- O manipulador de eventos executa a operação de leitura real, manipula a leitura de dados, declara interesse renovado em eventos de E/S e retorna o controle ao expedidor.
Por comparação, aqui está uma operação de leitura no Proactor (true async):
- Um manipulador inicia uma operação de leitura assíncrona (nota: o sistema operacional deve suportar E/S assíncrona). Nesse caso, o manipulador não se importa com eventos de prontidão de E/S, mas, em vez disso, registra o interesse em receber eventos de conclusão.
- O desmultiplexador de eventos aguarda até que a operação seja concluída
- Enquanto o desmultiplexador de eventos aguarda, o SO executa a operação de leitura em um thread paralelo do kernel, coloca os dados em um buffer definido pelo usuário e notifica o desmultiplexador de eventos de que a leitura está concluída
- O desmultiplexador de eventos chama o manipulador apropriado;
- O manipulador de eventos manipula os dados do buffer definido pelo usuário, inicia uma nova operação assíncrona e retorna o controle ao desmultiplexador de eventos.
Fonte: POSA2
Concorrência & Paralelismo
Threads:
É um fluxo seqüencial de controle dentro de um programa. Basicamente, consiste em uma unidade básica de utilização da CPU, compreendendo um ID, um contador de programa, um conjunto de registradores e uma pilha. Um processo tradicional tem uma única thread de controle. Se o processo possui múltiplas threads de controle, ele pode realizar mais do que uma tarefa a cada momento. Essa possibilidade abre portas para um novo modelo de programação.
Green Threads:
Green threads resolvem um problema comum na programação. Você não deseja que seu código bloqueie a CPU, impedindo que ela faça um trabalho significativo. Resolvemos isso usando multitarefa, o que nos permite suspender a execução de um pedaço de código enquanto retomamos outro e alternamos entre 'contextos'.
Isso não deve ser confundido com paralelismo, embora fácil confundir, são duas coisas diferentes. Pense dessa maneira: green threads nos permite trabalhar de maneira mais inteligente e eficiente e, assim, usar nossos recursos com mais eficiência, e o paralelismo é como jogar mais recursos no problema.
Geralmente, existem duas maneiras de fazer isso:
- Multitarefa preemptivo;
- Multitarefa não preemptivo (ou multitarefa cooperativa).
MultiTasking (MultiTarefa):
É o processo de executar várias tarefas ao mesmo tempo em um único processador. Ele é usado para permitir que várias tarefas sejam executadas de forma simultânea e para aumentar a eficiência do processador. Existem várias técnicas de multitasking, como multitasking baseado em tempo, multitasking baseado em eventos e multitasking baseado em processos.
Tipos de tarefas:
-
Preemptivo: Ocorre quando uma tarefa é interrompida por algum agendador externo e executa outra antes de voltar. A tarefa não tem nada a haver sobre esse assunto, a decisão é tomada pelo agendador. O kernel usa isso em sistemas operacionais, ou seja, para permitir que você use a interface do usuário enquanto executa a CPU para fazer cálculos em sistemas de thread único.
-
Não-Preemptivo: Uma tarefa decide por si mesma quando é melhor a CPU fazer outra coisa do que esperar que algo aconteça na tarefa atual. Geralmente isso ocorre quando
yieldrepassa o controle ao agendador. Um caso de uso normal para isso é gerar controle quando algo que irá bloquear a execução ocorre. Um exemplo disso são as operações de E/S. Quando o controle é gerado, um agendador central direciona a CPU para retomar o trabalho em outra tarefa que está pronta para realmente fazer outra coisa além de apenas bloquear.
Síncrono (sync) e assíncrono (async):
São dois termos que se referem ao modo como as operações são executadas em um programa de computador.
Sync: são aquelas que são executadas de forma sequencial, ou seja, uma operação é executada após a conclusão da operação anterior. Isso significa que o programa é bloqueado enquanto a operação está sendo executada e não pode continuar até que a operação seja concluída.
Async: são aquelas que são executadas de forma independente, ou seja, uma operação é iniciada e o programa continua a executar outras operações enquanto a operação assíncrona está sendo executada. Isso significa que o programa não é bloqueado enquanto a operação assíncrona está sendo executada e pode continuar a executar outras operações enquanto aguarda o término da operação assíncrona.
Operações síncronas são geralmente mais fáceis de programar e entender, mas podem ser menos eficientes em situações em que é necessário aguardar o término de uma operação para continuar a execução. Operações assíncronas, por outro lado, são geralmente mais eficientes, mas podem ser mais complexas de programar e entender.
Bloqueante e não-bloqueante:
Blocking e non-blocking são termos que se referem ao modo como as operações são executadas em um programa de computador.
Operações blocking são aquelas que bloqueiam o programa enquanto aguardam o término de uma operação. Isso significa que o programa não pode continuar a executar outras operações enquanto aguarda o término da operação blocking.
Operações non-blocking, por outro lado, são aquelas que não bloqueiam o programa enquanto aguardam o término de uma operação. Isso significa que o programa pode continuar a executar outras operações enquanto aguarda o término da operação non-blocking.
Operações blocking são geralmente mais fáceis de programar e entender, mas podem ser menos eficientes em situações em que é necessário aguardar o término de uma operação para continuar a execução. Operações non-blocking, por outro lado, são geralmente mais eficientes, mas podem ser mais complexas de programar e entender.
Exclusão Mútua (Mutex)
Mutex (mutual exclusion, exclusão mútua em inglês): é um mecanismo de sincronização que é usado para garantir que apenas uma thread (rotina) de um programa de computador tenha acesso a uma região crítica de código por vez. Ele é usado para evitar conflitos de acesso entre threads que podem ocorrer quando várias threads tentam acessar e modificar o mesmo dado ao mesmo tempo. O mutex bloqueia a região crítica de código enquanto uma thread está acessando-a, garantindo que outras threads aguardem o término da operação antes de tentarem acessar a região crítica de código.
Epoll/Kqueue/IOCP/IO_uring:
Epoll, Kqueue, IOCP e IO_uring são todos mecanismos de E/S event-driven (E/S com base em eventos) que são usados para monitorar e gerenciar operações de entrada e saída assíncronas em sistemas operacionais diferentes. Eles são usados para permitir que as aplicações sejam notificadas quando os dados estão disponíveis para leitura ou quando os dados podem ser escritos sem bloquear o processador.
Epoll: é um mecanismo de E/S com base em eventos disponível no Linux. Ele permite que as aplicações monitorem vários descritores de arquivo para verificar se eles estão prontos para leitura ou escrita.
Kqueue: é um mecanismo de E/S com base em eventos disponível no FreeBSD, NetBSD e OpenBSD. Ele funciona de maneira semelhante ao Epoll, permitindo que as aplicações monitorem descritores de arquivo para verificar se eles estão prontos para leitura ou escrita.
IOCP: é um mecanismo de E/S com base em eventos disponível no Windows. Ele permite que as aplicações monitorem vários handles de arquivo e sockets para verificar se eles estão prontos para leitura ou escrita.
IO_uring: é um mecanismo de E/S com base em eventos disponível no Linux. Ele foi projetado para ser mais rápido e eficiente do que o Epoll e oferece uma interface mais simples para as aplicações.
Coroutine (Corrotina):
Na ciência da computação, as rotinas são definidas como uma sequência de operações. A execução de rotinas forma um relacionamento pai-filho e o filho termina sempre antes do pai. Corrotinas (o termo foi introduzido por Melvin Conway) são uma generalização de rotinas (Donald Knuth). A principal diferença entre corrotinas e rotinas é que uma corrotina permite suspender e retomar explicitamente seu progresso por meio de operações adicionais, preservando o estado de execução e, portanto, fornecendo um fluxo de controle aprimorado (mantendo o contexto de execução).
As características de uma corrotina são:
- Os valores dos dados locais persistem entre chamadas sucessivas (alternância de contexto);
- A execução é suspensa quando o controle sai da rotina e é retomada em algum momento posterior;
- Mecanismo de controle-transferência;
- Simétrico ou assimétrico;
- Objeto de primeira classe (pode ser passado como argumento, retornado por procedimentos, armazenado em uma estrutura de dados para ser usado posteriormente ou livremente manipulado pelo desenvolvedor);
- Stackful (com pilha) ou Stackless (sem pilha).
Stackfulness (com pilha ou sem pilha)
-
Stackful (com pilha): pode ser suspensa de dentro de um stackframe (quadro de pilha) aninhado. A execução continua exatamente no mesmo ponto no código em que foi suspensa antes.
-
Stackless (sem pilha): apenas a rotina de nível superior pode ser suspensa. Qualquer rotina chamada por essa rotina de nível superior pode não ser suspensa. Isso proíbe o fornecimento de operações de suspensão ou retomada de rotinas dentro de uma biblioteca de uso geral.
As coroutines stackless são implementadas sem usar uma pilha de chamadas de função, enquanto as coroutines stackful são implementadas com uma pilha de chamadas de função. As coroutines stackless são geralmente mais eficientes em termos de uso de memória, mas são mais difíceis de implementar e menos flexíveis do que as coroutines stackful.
Fiber (Fibra):
Uma fibra pode salvar o estado de execução atual, incluindo todos os registradores e sinalizadores da CPU, o ponteiro de instruções e o ponteiro de pilha e, posteriormente, restaurar esse estado. A idéia é ter vários caminhos de execução em execução em uma única thread usando o planejamento cooperativo (ao contrario das threads, que são agendados preventivamente). A fibra em execução decide explicitamente quando deve permitir que outra fibra seja executada (alternância de contexto).
O controle é passado cooperativamente entre as fibras lançadas em um determinado segmento. Em um determinado momento, em um determinada thread, no máximo uma fibra está em execução.
A geração de fibras adicionais em um determinada thread não distribui seu programa por mais núcleos de hardware, embora possa fazer um uso mais eficaz do núcleo no qual está sendo executado.
Por outro lado, uma fibra pode acessar com segurança qualquer recurso pertencente exclusivamente a seu thread pai, sem precisar explicitamente defender esse recurso contra o acesso simultâneo de outras fibras na mesma thread. Você já está garantido que nenhuma outra fibra nessa thread está acessando simultaneamente neste mesmo recurso. Isso pode ser particularmente importante ao introduzir a concorrência no código legado. Você pode gerar fibras com segurança executando código antigo, usando E/S assíncrona para intercalar a execução.
Com efeito, as fibras fornecem uma maneira natural de organizar o código simultâneo com base em E/S assíncronas. Em vez de encadear manipuladores de conclusão, o código executado em uma fibra pode fazer o que parece ser uma chamada de função de bloqueio normal. Essa chamada pode suspender de forma barata a fibra de chamada, permitindo que outras fibras no mesma thread sejam executadas. Quando a operação é concluída, a fibra suspensa é retomada, sem a necessidade de salvar ou restaurar explicitamente seu estado. Suas variáveis de pilha local persistem na chamada.
Instalando Asio
Existem duas versões principais disponíveis:
-
Asio Standalone (sem Boost) — Versão mais recente: 1.38.0 (dez/2025). Requer C++11 ou posterior. Recomendado para projetos novos que não dependem do Boost.
-
Boost.Asio — Faz parte do ecossistema Boost. Versão atual do Boost: 1.87.0 (2025). Mais comum em projetos que já usam Boost.
-
Networking-TS — Baseado no Asio/Boost.Asio, proposto para a biblioteca padrão como
std::net(ainda em discussão no ISO).
O Asio é uma biblioteca header-only — não requer compilação prévia. Basta adicionar o diretório de headers ao seu projeto.
Asio Standalone
Linux
Ubuntu/Debian:
sudo apt install libasio-dev
Arch Linux:
sudo pacman -S asio
Para compilar sem Boost:
g++ -std=c++20 client.cpp -o client -pthread -DASIO_STANDALONE
macOS
brew install asio
Windows (vcpkg)
vcpkg install asio
CMake com FetchContent (sem Boost, qualquer plataforma)
Esta é a forma recomendada para projetos CMake — garante sempre a versão correta:
cmake_minimum_required(VERSION 3.14)
project(meu_projeto)
include(FetchContent)
FetchContent_Declare(
asio
GIT_REPOSITORY https://github.com/chriskohlhoff/asio.git
GIT_TAG asio-1-38-0
SOURCE_SUBDIR asio
)
FetchContent_MakeAvailable(asio)
add_executable(meu_programa main.cpp)
target_include_directories(meu_programa PRIVATE ${asio_SOURCE_DIR}/asio/include)
target_compile_definitions(meu_programa PRIVATE ASIO_STANDALONE)
target_link_libraries(meu_programa PRIVATE pthread)
No código, use:
#include <asio.hpp> // Standalone Asio
// em vez de:
// #include <boost/asio.hpp> // Boost.Asio
Boost.Asio
Linux
Ubuntu/Debian:
sudo apt install libboost-dev
Arch Linux:
sudo pacman -S boost
Para compilar:
g++ -std=c++20 client.cpp -o client -pthread -lboost_system
BSD
OpenBSD:
pkg_add boost
Para compilar:
c++ -std=c++20 -L/usr/local/lib client.cpp -o client -lboost_system
Windows
vcpkg (recomendado):
# x64
vcpkg install boost-asio:x64-windows
# x86
vcpkg install boost-asio:x86-windows
MinGW (MSYS2):
# x86_64
pacman -S mingw-w64-x86_64-boost
# i686
pacman -S mingw-w64-i686-boost
Nota: O ambiente MSYS2 não requer
sudopara instalação de pacotes.
Conan 2.x:
conan install --requires="boost/1.87.0" --build=missing
Atenção: A sintaxe antiga
Boost/1.72.0@bincrafters/stablepertence ao Conan 1.x e ao Bintray, que encerrou em maio de 2021. Use sempre a sintaxe do Conan 2.x mostrada acima.
MSVC (linha de comando):
cl /EHsc /std:c++20 /I "C:\path\to\boost" example.cpp
Diferença entre Standalone e Boost.Asio
| Aspecto | Asio Standalone | Boost.Asio |
|---|---|---|
| Dependência | Nenhuma | Boost |
| Header principal | <asio.hpp> | <boost/asio.hpp> |
| Namespace | asio:: | boost::asio:: |
| Error codes | asio::error_code | boost::system::error_code |
| Macro necessária | ASIO_STANDALONE | — |
| Versão atual | 1.38.0 (dez/2025) | Parte do Boost 1.87.0 |
Dica: As duas versões compartilham praticamente a mesma API. Migrar entre elas é simples: basta trocar os includes, o namespace e a macro de compilação.
asio::io_context
O Asio possui uma classe chamada asio::io_context que é usada para gerenciar operações de entrada e saída assíncronas em um programa de computador. Ela é usada para monitorar e gerenciar operações de entrada e saída em vários descritores de arquivo e sockets, permitindo que o programa seja notificado quando os dados estão disponíveis para leitura ou quando os dados podem ser escritos sem bloquear o processador.
A asio::io_context é geralmente usada em conjunto com um objeto executor_work_guard, que é responsável por manter o loop de eventos da asio::io_context rodando mesmo quando não há operações pendentes.
O conceito é baseado na API de rede do Unix. O Asio também possui o conceito "socket", mas isso não é suficiente: um objeto io_context é necessário para se comunicar com os serviços de E/S do sistema operacional.
Nota histórica: A classe
io_servicefoi o nome antigo deio_context. Ela foi renomeada e está obsoleta — use sempreio_contextem código novo.
A imagem abaixo mostrará a estrutura da Arquitetura Asio:

io_context deriva de execution_context:
class io_context
: public execution_context
{
......
}
Enquanto execution_context deriva de noncopyable:
class execution_context
: private noncopyable
{
......
}
Isso significa que o objeto io_context não pode ser copiado. Portanto, durante a inicialização do socket, ou seja, associar o socket ao io_context, o io_context deve ser passado como referência.
Ex.:
asio::io_context io_context;
asio::ip::tcp::socket socket{io_context}; // io_context passado por referência
Além de gerenciar operações de entrada e saída assíncronas, a asio::io_context também fornece uma série de outras funcionalidades úteis. Por exemplo, ela permite que o programa agende operações para serem executadas em um momento futuro, permitindo que o programa execute tarefas de forma assíncrona de acordo com um cronograma. Ela também permite que o programa cancele operações que estão em andamento.
A asio::io_context pode ser configurada para escalonar operações em vários threads, permitindo que elas sejam executadas em paralelo e aproveitando ao máximo o poder de processamento do computador:
// Sugestão de concorrência para o io_context — pode otimizar internamente
asio::io_context ctx{std::thread::hardware_concurrency()};
A seguir, estão descritas todas as funções comuns do asio::io_context:
-
run: Inicia o loop de eventos doasio::io_context. Bloqueia o thread atual até que todas as operações agendadas tenham sido concluídas ou oio_contextseja interrompido. -
poll: Similar à funçãorun, mas não bloqueia o thread atual. Em vez disso, processa todas as operações prontas noasio::io_contexte retorna imediatamente. -
run_one: Similar à funçãorun, mas processa apenas uma operação pendente antes de retornar. -
poll_one: Similar à funçãopoll, mas processa apenas uma operação pronta antes de retornar. -
stop: Interrompe o loop de eventos doasio::io_context. Útil quando o programa precisa sair antes que todas as operações pendentes sejam concluídas. -
restart: Reinicia oasio::io_contextapós ter sido interrompido comstop(), permitindo que ele aceite novas operações. Deve ser chamado antes de uma nova chamada arun().
Atenção: Em versões antigas do Asio, esta função se chamava
reset(). O nomereset()está depreciado — userestart()em código novo.
-
stopped: Retornatruese oasio::io_contextfoi interrompido comstop(). -
get_executor: Retorna um objeto executor que pode ser usado para agendar operações para serem executadas noasio::io_context.
asio::post e asio::dispatch
Em versões modernas do Asio, as operações de agendamento são realizadas através de funções livres que recebem um executor como primeiro parâmetro:
-
asio::post(executor, fn): Agenda a funçãofnpara ser executada de forma assíncrona no contexto do executor. A função nunca é executada diretamente na chamada — sempre é enfileirada. -
asio::dispatch(executor, fn): Agenda a funçãofnpara ser executada no contexto do executor. Se o chamador já estiver dentro do contexto do executor, a função pode ser executada imediatamente (in-line).
asio::io_context ctx;
// Agenda uma tarefa para execução assíncrona
asio::post(ctx, [] {
std::cout << "Executando de forma assíncrona\n";
});
ctx.run();
Atenção: As formas antigas
io_context.post(fn)eio_context.dispatch(fn)estão depreciadas. Useasio::post(ctx, fn)easio::dispatch(ctx, fn).
executor_work_guard
A classe asio::executor_work_guard é usada para manter o loop de eventos da asio::io_context rodando mesmo quando não há operações pendentes. Isso é essencial quando você quer que a thread do io_context continue viva aguardando trabalho futuro (por exemplo, em um servidor que usa múltiplas threads).
#include <asio.hpp>
#include <thread>
#include <iostream>
int main()
{
asio::io_context ctx;
// Mantém o io_context vivo mesmo sem operações pendentes
auto guard = asio::make_work_guard(ctx);
std::thread worker([&ctx] {
ctx.run(); // bloqueia aqui até o guard ser liberado
});
// Faz algum trabalho...
asio::post(ctx, [] { std::cout << "Tarefa executada!\n"; });
// Libera o guard para permitir que ctx.run() retorne
guard.reset();
worker.join();
return 0;
}
Atenção: A classe
asio::io_context::worke a macro-based work guard estão depreciadas. Useasio::make_work_guard(ctx)que retorna umexecutor_work_guard<io_context::executor_type>.
Buffers
Fundamentalmente, E/S envolve a transferência de dados para e de regiões contíguas da memória, chamadas buffers. Esses buffers podem ser simplesmente expressos como uma tupla que consiste em um ponteiro e um tamanho em bytes. No entanto, para permitir o desenvolvimento de aplicativos de rede eficientes, o Asio inclui suporte para operações de coleta de dispersão. Essas operações envolvem um ou mais buffers:
- Uma scatter-read recebe dados em vários buffers.
- Uma gather-write transmite vários buffers.
Portanto, exigimos uma abstração para representar uma coleção de buffers. A abordagem usada no Asio é definir um tipo (na verdade, dois tipos) para representar um único buffer. Eles podem ser armazenados em um contêiner, que pode ser passado para as operações de coleta de dispersão.
Além de especificar buffers como ponteiro e medir o tamanho em bytes, o Asio faz uma distinção entre memória modificável (mutável) e memória não modificável (onde a última é criada a partir do armazenamento para uma variável qualificada de const). Esses dois tipos podem, portanto, ser definidos da seguinte maneira:
typedef std::pair<void*, std::size_t> mutable_buffer;
typedef std::pair<const void*, std::size_t> const_buffer;
Um mutable_buffer seria conversível em um const_buffer, mas a conversão na direção oposta não é válida.
No entanto, o Asio não usa as definições acima como estão, mas define duas classes: mutable_buffer e const_buffer. O objetivo deles é fornecer uma representação opaca da memória contígua, onde:
-
Os tipos se comportam como
std::pairnas conversões. Ou seja, ummutable_bufferé conversível em umconst_buffer, mas a conversão oposta é desabilitada. -
There is protection against buffer overruns. Given a buffer instance, a user can only create another buffer representing the same range of memory or a sub-range of it. To provide further safety, the library also includes mechanisms for automatically determining the size of a buffer from an array, boost::array or std::vector of POD elements, or from a std::string.
-
A memória subjacente é acessada explicitamente usando a função membro
data(). Em geral, um aplicativo nunca deve precisar fazer isso, mas é necessário que a implementação da biblioteca passe a memória não processada para as funções subjacentes do sistema operacional.
Finalmente, vários buffers podem ser passados para operações (como read() ou write()) colocando os objetos do buffer em um contêiner. Os conceitos MutableBufferSequence e ConstBufferSequence foram definidos para que contêineres como std::vector, std::list, std::array ou boost::array possam ser usados.
Streambuf para integração com Iostreams
A classe boost::asio::basic_streambuf é derivada de std::basic_streambuf para associar a sequência de entrada e a saída a um ou mais objetos de algum tipo de matriz de caracteres, cujos elementos armazenam valores arbitrários. Esses objetos da matriz de caracteres são internos ao objeto streambuf, mas é fornecido acesso direto aos elementos da matriz para permitir que sejam utilizados com operações de E/S, como as operações de envio ou recebimento de um socket:
-
A sequência de entrada do streambuf é acessível através da função membro
data(). O tipo de retorno dessa função atende aos requisitosConstBufferSequence. -
A sequência de saída do streambuf é acessível através da função membro
prepare(). O tipo de retorno dessa função atende aos requisitos deMutableBufferSequence. -
Os dados são transferidos sequência frontal de saída para a parte de trás da sequência de entrada chamando a função membro
commit(). -
Os dados são removidos da sequência frontal de entrada chamando a função de membro
consume().
O construtor streambuf aceita um argumento size_t especificando a soma máximo dos tamanhos da sequência de entrada e de saída. Qualquer operação que, se for bem-sucedida, aumentará os dados internos além desse limite, lançará uma exceção std::length_error.
Tipos de Buffers
O Asio fornece vários tipos de buffers que podem ser usados para representar conjuntos de dados que podem ser lidos ou escritos de forma assíncrona. A seguir, uma lista de alguns dos tipos de buffers disponíveis no Asio:
-
asio::const_buffer: Representa um conjunto de dados que serão lidos, mas não alterados. Ele é útil quando você deseja ler os dados de uma fonte externa, como uma conexão de rede, sem alterá-los. -
asio::mutable_buffer: Representa um conjunto de dados que serão lidos e alterados. Ele é útil quando você deseja ler os dados de uma fonte externa e alterá-los antes de enviá-los para outro lugar. -
asio::buffer: É uma função que cria um buffer a partir de um objeto de tipo T. Ele pode ser usado para criar buffers a partir de qualquer tipo de dado, como strings, arrays ou estruturas de dados. -
asio::buffer_cast: É uma função que retorna um ponteiro para os dados subjacentes de um buffer. Ele é útil para acessar os dados de um buffer de forma mais conveniente. -
asio::buffer_size: É uma função que retorna o tamanho de um buffer em bytes. Ela é útil para determinar quantos dados podem ser lidos ou escritos em um buffer.
Esses são apenas alguns dos tipos de buffers disponíveis no Asio. Existem outros tipos de buffers disponíveis para uso em situações específicas.
O que é um Executor
Um executor em C++ é um objeto ou mecanismo que é responsável por gerenciar o agendamento e a execução de tarefas ou operações em uma ou mais threads. Ele pode ser usado para gerenciar a concorrência e a sincronização entre as tarefas, garantindo que elas sejam executadas de forma eficiente e consistente.
Um executor em C++ pode ser implementado de várias maneiras diferentes, dependendo das necessidades da aplicação. Algumas possibilidades incluem:
-
Usando threads: um executor pode ser implementado como um conjunto de threads que são responsáveis por executar as tarefas adicionadas a ele. Isso pode ser útil se a aplicação precisa de muitas tarefas sendo executadas concorrentemente e se houver recursos suficientes para suportar essas threads.
-
Usando um pool de threads: um executor pode ser implementado como um pool de threads que são compartilhados por todas as tarefas adicionadas a ele. Isso pode ajudar a gerenciar o uso de recursos, permitindo que mais tarefas sejam executadas concorrentemente, mas sem precisar criar novas threads para cada tarefa.
-
Usando um event loop: um executor pode ser implementado usando um event loop para gerenciar a execução de tarefas. Isso pode ser útil se a aplicação precisa lidar com múltiplos eventos simultâneos e se as tarefas só precisam ser executadas quando um evento ocorre.
O event loop é uma estrutura de loop de execução que é usada para gerenciar a entrada e a saída de eventos em uma aplicação. Ele é comumente usado em aplicações que precisam lidar com múltiplos eventos simultâneos, como entrada do usuário, atualizações de redes ou operações de tempo de espera. Em C++, um event loop pode ser implementado de várias maneiras diferentes, dependendo das necessidades da aplicação. Algumas possibilidades incluem:
-
Usando um loop infinito: um event loop pode ser implementado como um loop infinito que verifica periodicamente por eventos. Isso pode ser útil se a aplicação precisa verificar por eventos com frequência, mas não precisa de uma resposta muito rápida.
-
Usando notificações assíncronas: um event loop pode ser implementado usando notificações assíncronas para ser avisado quando um evento ocorre. Isso pode ser útil se a aplicação precisa de uma resposta rápida aos eventos e se houver muitos eventos ocorrendo com pouco tempo de espera entre eles.
Resumidamente, um executor pode ser usado em conjunto com um event loop para gerenciar a execução de tarefas que são disparadas por eventos. Por exemplo, se o event loop detecta um evento de entrada de usuário, ele pode adicionar uma tarefa ao executor para ser executada em uma thread separada, permitindo que a aplicação continue processando outros eventos enquanto a tarefa é executada. Isso pode ajudar a garantir que a aplicação responda de forma rápida e responsiva, mesmo quando há muitas tarefas a serem executadas.
Surgimento do assunto em torno do C++ STL
A proposta P2300, oficialmente denominada std::execution, introduz um modelo de programação assíncrona baseado em "senders" e "receivers" (remetentes e receptores). Ela foi projetada para ser uma alternativa genérica e componível aos callbacks e futures/promises.
Status atual: A proposta P2300 (std::execution) foi aceita para o C++26 (aprovada em 2024). Ela representa um modelo diferente dos executores do Asio — baseado em sender/receiver em vez de callbacks diretos.
O modelo sender/receiver de P2300 funciona assim:
- Um sender descreve um trabalho que ainda não foi iniciado
- Um receiver é o destino do resultado desse trabalho
- Um scheduler determina onde/quando o trabalho será executado
Este modelo é diferente dos executores do Asio, mas as duas abordagens são complementares. O Asio continua sendo a escolha prática para programação de redes assíncronas em C++ hoje.
A proposta anterior P0443R14 ("The Unified Executor for C++") foi o precursor do P2300 e influenciou muito o design atual.
Por quê Executores?
C++ sempre careceu de infraestrutura de programação simultânea disponível, e a infraestrutura recém-introduzida desde C++11, bem como a melhoria de bibliotecas de terceiros, como boost e folly, têm mais ou menos problemas e certas limitações.
std::async não é assíncrono
std::async é uma função do C++ Standard Library que é usada para iniciar uma tarefa assíncrona em um ponto específico no tempo. Ela é usada para criar uma tarefa assíncrona que será executada em uma thread separada e retorna um objeto std::future que pode ser usado para obter o resultado da tarefa quando ela for concluída.
Quando chamamos std::async, ela pode ou não iniciar a execução imediatamente, dependendo da política de execução fornecida como argumento (std::launch::async ou std::launch::defer). No entanto, a função sempre retorna um objeto std::future imediatamente, independentemente de a computação ter sido concluída ou não.
O objeto std::future representa um valor que pode não estar disponível imediatamente. Ao chamar um método em um objeto std::future, como std::future::get(), ele pode bloquear a thread atual até que o valor esteja pronto. Isso significa que, se chamarmos std::future::get() imediatamente após std::async, a chamada pode bloquear até que a computação seja concluída.
Diferentemente dostd::async, a expressão co_await [C++20] é usada para suspender a avaliação de uma função assíncrona (coroutine) enquanto aguarda a conclusão de uma computação representada pela expressão operando.
A ideia por trás da função std::async e std::future é permitir que o programador inicie uma tarefa assíncrona e continue a trabalhar em outras partes do código enquanto aguarda o resultado. No entanto, o programador precisa estar ciente de que o comportamento de bloqueio pode ocorrer se ele chamar std::future::get() imediatamente após std::async.
Apesar disso, std::async pode ser útil em situações em que é necessário iniciar uma tarefa assíncrona de forma fácil e rápida. Ele é especialmente útil quando é necessário obter o resultado da tarefa assíncrona de forma síncrona, usando a sintaxe de await do C++20 ou esperando pelo objeto std::future retornado por std::async.
A seguir, um exemplo de uso da função std::async para iniciar uma tarefa assíncrona e obter o resultado da tarefa de forma síncrona:
#include <iostream>
#include <future>
// Função que será executada assincronamente
int long_running_task(int x, int y) {
// Simulando um processamento demorado
std::this_thread::sleep_for(std::chrono::seconds(2));
return x + y;
}
int main() {
// Iniciando a tarefa assíncrona com std::async
std::future<int> result = std::async(long_running_task, 10, 20);
// Obtendo o resultado da tarefa síncronamente com std::future::get
int sum = result.get();
std::cout << "Resultado da tarefa assíncrona: " << sum << std::endl;
return 0;
}
Neste exemplo, a função long_running_task é iniciada de forma assíncrona com std::async e o resultado da tarefa é obtido síncronamente com std::future::get. Isso significa que o código que chama std::async será bloqueado até que a tarefa seja concluída e o resultado esteja disponível.
Observe que, apesar de usarmos std::async para iniciar a tarefa assíncrona, o código não pode ser escrito de forma assíncrona usando a sintaxe de await do C++20. Para escrever código assíncrono de forma mais simples e clara, é recomendável usar outras bibliotecas de tempo de execução, como Asio ou Libunifex.
Em resumo, std::async é uma função do C++ Standard Library que é usada para iniciar uma tarefa assíncrona em uma thread separada. Ela não é uma função assíncrona no sentido tradicional da palavra e não pode ser usada com a sintaxe de await do C++20, mas pode ser útil em situações em que é necessário iniciar uma tarefa assíncrona de forma fácil e rápida.
Modelo de Evolução do Future/Promise
No C++11, o modelo future/promise é um meio de permitir que uma thread espere por um valor a ser produzido por outra thread de maneira assíncrona. Ele é composto pelos seguintes elementos:
-
Promise: um objeto que permite que um valor seja definido em algum momento no futuro. A promessa possui um método setValue para definir o valor e um métodosetExceptionpara definir uma exceção a ser lançada quando o valor for solicitado. -
Future: um objeto que permite que uma thread espere por um valor produzido por outra thread. O futuro possui um método wait que faz a thread que o chama esperar até que o valor esteja disponível. Além disso, o futuro possui métodos comotheneonErrorque permitem que ações sejam executadas quando o valor estiver disponível ou uma exceção for lançada.
A implementação de um Future/Promise típico em C++ é mostrada na figura abaixo:

Para usar o modelo de futuros e promessas, é preciso criar um objeto promessa e obter um objeto futuro a partir dele. Em seguida, a thread que produzirá o valor deve definir o valor na promessa usando o método setValue ou setException. A thread que estiver esperando pelo valor pode usar o método wait do futuro para esperar até que o valor esteja disponível.
Aqui está um exemplo de como usar o modelo de futuros e promessas em C++:
#include <future>
#include <iostream>
int main() {
// Cria uma promessa e um futuro
std::promise<int> promise;
std::future<int> future = promise.get_future();
// Define o valor da promessa em uma thread separada
std::thread([&promise] {
std::this_thread::sleep_for(std::chrono::seconds(1));
promise.set_value(42);
}).detach();
// Espera pelo valor a ser definido e imprime-o
std::cout << future.get() << std::endl;
return 0;
}
Este código cria um objeto std::promise e um objeto std::future, e define o valor da std::promise em uma thread separada usando um std::thread. O objeto std::future é então usado para esperar pelo valor a ser definido, e o valor é impresso no console.
Essas classes são mais básicas do que as oferecidas pelas bibliotecas folly e asio, mas são parte da Biblioteca Padrão de C++ e, portanto, estão disponíveis em qualquer compilador C++ padrão.
As classes std::future e std::promise fornecem um conjunto similar de funcionalidades às classes folly::Future e folly::Promise da biblioteca folly, mas são parte da Biblioteca Padrão de C++ e não exigem dependências adicionais.
Executores do Asio
Os executores são componentes do asio que definem o contexto de execução de uma função ou um bloco de código. Eles podem ser usados para controlar como e quando uma função ou um bloco de código é executado, e permitem que você aproveite os recursos de concorrência fornecidos pelo asio para executar tarefas de forma assíncrona e concorrente.
O asio fornece várias formas de se trabalhar com executores, incluindo a possibilidade de especificar o contexto de execução de uma função ou um bloco de código usando o template asio::execution, ou usando funções como asio::post, que permitem agendar a execução de uma função ou um bloco de código em um determinado contexto de execução.
asio::execution é um modelo C++ que representa um contexto de execução, ou um objeto que define como uma função ou um bloco de código deve ser executado. É usado como um parâmetro de tipo em vários componentes asio, como asio::strand e asio::spawn, para especificar o contexto de execução no qual uma função ou um bloco de código deve ser executado.
Existem vários tipos de executores que podem ser usados com asio::execution. Estes incluem:
-
asio::io_context::executor_type: Este é o tipo de executor padrão paraasio::io_contexte representa o contexto de execução fornecido por um objetoasio::io_context. As funções ou blocos de código executados usando este executor serão executados no contexto do loop de eventos do io_context. -
asio::strand<Executor>: Este é um executor decorador que envolve outro executor, Executor, e garante que as funções ou blocos de código executados com ele sejam executados de forma serializada, ou seja, apenas um de cada vez. Isso pode ser útil para sincronizar o acesso a recursos compartilhados. -
asio::system_executor: Este é um tipo de executor especial que representa o contexto de execução fornecido pelo sistema operacional. As funções ou blocos de código executados usando este executor serão executados no contexto da thread pool do sistema operacional. -
asio::thread_pool: Este é um tipo de executor que representa um pool (grupo) de threads que podem serem usados para executar funções ou blocos de código, ou também para executar tarefas de forma concorrente em múltiplas threads. -
asio::post: Esta é uma função que leva uma função ou um bloco de código e um executor e agenda a função ou o bloco de código para ser executado no contexto do executor especificado.
Em geral, asio::execution é um conceito poderoso que permite especificar o contexto de execução no qual uma função ou um bloco de código deve ser executado e aproveitar os vários tipos de executores fornecidos por asio para controlar a execução do seu código.
Asio executores em comparação com outras alternativas
std::execution (C++26)
O namespace std::execution (aprovado para C++26 via P2300) fornece um modelo de programação assíncrona baseado em sender/receiver. É diferente dos executores de política de execução do C++17 (std::execution::sequenced_policy, std::execution::parallel_policy) — esses últimos controlam apenas algoritmos paralelos como std::sort, não operações de rede.
O modelo P2300 funciona com schedulers, senders e receivers:
// Exemplo conceitual com std::execution (C++26 / implementações experimentais)
// Nota: requer implementação como libunifex ou stdexec
namespace ex = std::execution;
auto result = ex::sync_wait(
ex::schedule(scheduler)
| ex::then([] { return 42; })
);
Importante:
std::execution::executenão existe como função padrão neste modelo. O P2300 usaex::start()e composição de senders. Não confundir com os executores do Asio, que têm API diferente.
O Asio continua sendo a escolha prática para programação de redes assíncronas em C++, independente do P2300. As duas abordagens são complementares.
Libunifex
Libunifex e Asio são duas bibliotecas diferentes que são utilizadas para criar aplicações de redes de forma assíncrona em C++.
ASIO é uma biblioteca de tempo de execução que oferece suporte para a comunicação assíncrona entre sistemas de computador. Ela é amplamente utilizada para a criação de aplicações de redes, como servidores de rede e clientes de rede. Asio fornece uma série de recursos, como sockets de rede, temporizadores e sinais de interrupção, que podem ser usados para criar aplicações de rede de forma assíncrona.
Por outro lado, Libunifex é uma biblioteca de executores para C++ que oferece uma interface uniforme para a execução de tarefas assíncronas em diferentes plataformas e bibliotecas de tempo de execução, como Asio Standalone e Boost.ASIO. Ela permite que você escreva código assíncrono de forma mais portável, pois você pode usar a mesma interface para trabalhar com diferentes bibliotecas de tempo de execução sem precisar se preocupar com as diferenças entre elas.
Cppcoro
Cppcoro foi uma biblioteca pioneira de corrotinas para C++ criada por Lewis Baker. Ela fornecia primitivas para escrever código assíncrono usando corrotinas do C++20.
Atenção: O cppcoro está largamente sem manutenção desde 2021. Não é recomendado para novos projetos. Alternativas ativas incluem:
Em resumo, o Asio é a biblioteca mais completa e ativa para criar aplicações de rede assíncronas em C++, com suporte tanto a callbacks quanto a corrotinas C++20. O Libunifex é experimental e voltado à pesquisa do modelo sender/receiver do P2300.
Sockets (Soquetes)
A classe asio::basic_stream_socket do Asio é uma classe genérica que é usada como base para a criação de classes de socket específicas para diferentes protocolos de rede. Ela fornece uma interface comum para realizar operações de entrada e saída em sockets e é a classe base para a criação de classes de socket para protocolos como TCP, UDP, ICMP e SCTP.
A asio::basic_stream_socket fornece uma série de funções de leitura e escrita assíncronas que podem ser usadas para enviar e receber dados através de um socket de forma assíncrona. Ela também oferece funções para estabelecer conexões com outros hosts na rede e para fechar conexões existentes.
Para usar a asio::basic_stream_socket, é preciso criar uma classe derivada que especifique o tipo de socket que deseja criar, como um socket TCP, UDP, ICMP ou SCTP. Em seguida, é possível criar um objeto da classe derivada e passar um objeto de resolução de endereço e um objeto de io_context para o construtor. Em seguida, é possível chamar as funções de leitura e escrita fornecidas pela asio::basic_stream_socket para enviar e receber dados através do socket.
Existem 4 tipos de sockets:
(1) basic_stream_socket:
Este socket fornece fluxos de bytes baseados em conexão bidirecional, confiável e sequencial. tcp::socket é uma instância deste socket:
class tcp
{
......
/// The TCP socket type.
typedef basic_stream_socket<tcp> socket;
......
}
(2) basic_datagram_socket:
Este socket fornece serviço de datagrama sem garantias de conexão e não confiável. udp::socket é uma instância deste socket:
class udp
{
......
/// The UDP socket type.
typedef basic_datagram_socket<udp> socket;
......
}
(3) basic_raw_socket:
Este socket fornece acesso a protocolos e interfaces de rede interno. O icmp::socket é uma instância deste socket:
class icmp
{
......
/// The ICMP socket type.
typedef basic_raw_socket<icmp> socket;
......
}
(4) basic_seq_packet_socket:
Este socket combina fluxo(stream) e datagrama: fornece um serviço de datagramas com conexão bidirecional, confiável e bidirecional. SCTP é um exemplo deste tipo de serviço.
Todos esses 4 sockets derivam da classe basic_socket e precisam ser associados a um io_context durante a inicialização. Veja tcp::socket como exemplo:
boost::asio::io_context io_context;
boost::asio::ip::tcp::socket socket{io_context};
Observe que o io_context deve ser passado por referência no construtor do socket (consulte io_context).
Todos os sockets derivam de basic_socket, que requer um io_context na inicialização:
// Criação de socket TCP — io_context passado por referência
asio::io_context io_context;
asio::ip::tcp::socket socket{io_context};
Os sockets não suportam cópia (copy construction/copy assignment são deletados), mas suportam move (move construction/move assignment). Por isso, ao transferi-los entre funções, use std::move:
// Correto: transferir ownership com move
asio::ip::tcp::socket outro_socket = std::move(socket);
// Incorreto: sockets não podem ser copiados
// asio::ip::tcp::socket copia = socket; // erro de compilação
Isso garante que apenas um objeto possui o descritor de arquivo subjacente por vez, evitando problemas de gerenciamento de recursos.
Além das funções de leitura e escrita assíncronas, a asio::basic_stream_socket também oferece uma série de outras funcionalidades úteis. Por exemplo, ela permite que o programa configure opções de socket, como o timeout de leitura e escrita, o tamanho do buffer de leitura e escrita e o uso de Keepalives. Ela também permite que o programa obtenha informações sobre o socket, como o endereço local e remoto, o estado da conexão e o número de bytes enviados e recebidos.
Em resumo, a asio::basic_stream_socket é uma classe genérica do Asio que é usada como base para a criação de classes de socket específicas para diferentes protocolos de rede. Ela fornece uma interface comum para realizar operações de entrada e saída em sockets e oferece uma série de funcionalidades úteis, como configuração de opções de socket e obtenção de informações sobre o socket.
Strands: Usar threads sem bloqueio explícito
A classe asio::strand é usada para garantir que operações de entrada e saída sejam executadas de forma serial em um objeto de io_context. Isso é útil em situações em que é importante que as operações de entrada e saída sejam realizadas em uma determinada ordem ou em que é importante evitar conflitos entre operações simultâneas.
Para usar a asio::strand, é preciso criar um objeto da classe e passar um objeto de io_context para o construtor. Em seguida, é possível chamar as funções de leitura e escrita assíncronas fornecidas pela asio::strand e passar um objeto de completion token para elas. Quando uma operação de entrada e saída é agendada através de uma asio::strand, ela é adicionada a uma fila de operações e é garantido que as operações na fila sejam executadas de forma serial.
Além de garantir que as operações de entrada e saída sejam executadas de forma serial, a asio::strand também fornece uma série de outras funcionalidades úteis. Por exemplo, ela permite que o programa cancele operações que estão em andamento e permite que o programa obtenha informações sobre o número de operações pendentes na fila.
Em resumo, asio::strand é usada para garantir que operações de entrada e saída sejam executadas de forma serial em um objeto de io_context. Ela oferece uma série de funcionalidades úteis, como cancelamento de operações em andamento e obtenção de informações sobre o número de operações pendentes na fila.
Uma thread é definida como uma chamada estritamente sequencial de manipuladores de eventos[event handler] (ou seja, nenhuma chamada simultânea). O uso de asio::strand permite a execução de código em um programa multithread sem a necessidade de bloqueio explícito (por exemplo, usando mutex[exclusão mútua]). E pode ser usado para sincronizar mais cenários.
Strand agrupa tarefas. Tarefas do mesmo asio::strand não podem rodar em paralelo, mas quando suspensas, podem ser executadas em outra thread quando acordarem da próxima vez.
As strands podem ser implícitas ou explícitas, conforme ilustrado pelas seguintes abordagens alternativas:
-
Utilizando
asio::io_context::run()em apenas uma thread significa que todos os manipuladores de eventos são executados em uma thread implícita, devido à garantia doasio::io_contextde que os manipuladores[handlers] são invocados somente de dentro da funçãorun(). -
Onde existe uma única cadeia de operações assíncronas associadas a uma conexão (por exemplo, em uma implementação de protocolo half-duplex como HTTP), não há possibilidade de execução simultânea dos manipuladores. Neste caso seria um
strandimplícito. -
Um
strandexplícito é uma instânciastrand<>ouasio::io_context::strand. Todos os objetos de função do manipulador[handler] de eventos precisam ser vinculados ao strand usandoasio::bind_executor()ou de outra forma postados/despachados através do objeto strand.
Strand é genérico e pode ser usado para sincronizar mais cenários.
-
Primeiro, que
defer()só consegue enfileirar as tarefas de uma única cadeia de operações. Se você tem um canal duplex (ex.:read()ewrite()num único socket), entãodefer()já não oferece garantias o suficiente pra sincronizar o acesso aos dados compartilhados, e ainda tem que usarstrands. Por esse motivo não posso fazer uso da otimização dedefer()na lib de fibras, porque tem sempre um segundo canal assíncrono de notificações que representa a cadeia de cancelamento da fibra. -
Segundo,
strandspodem ser usados no cenário onde há objetos trafegando entre múltiplos io_executors, masdefer()falha se você agenda, a partir de um contexto de execução, uma tarefa em outro contexto esperando que isso vá sincronizar/enfileirar/serializar algum trabalho.
O executor associado deve atender aos requisitos do executor. Ele será usado pela operação assíncrona para enviar manipuladores intermediários e finais para execução.
O executor pode ser customizado para um tipo de manipulador específico, especificando um tipo aninhado executor_type e a função membro get_executor().
Veja o exemplo abaixo:
class my_handler
{
public:
// Custom implementation of Executor type requirements.
typedef my_executor executor_type;
// Return a custom executor implementation.
executor_type get_executor() const noexcept
{
return my_executor();
}
void operator()() { ... }
};
Corrotina
Corrotinas são uma técnica de programação que permite que uma função seja dividida em várias partes, cada uma delas sendo executada de forma separada. Elas são usadas para criar funções assíncronas, ou seja, funções que podem ser "pausadas" e retomadas posteriormente, permitindo que o programa execute outras tarefas enquanto aguarda a conclusão de uma operação.
Por exemplo, suponha que você tenha uma função que faz uma chamada de rede para obter os dados de um determinado recurso. Usando corrotinas, você pode escrever essa função de forma síncrona, como se a chamada de rede fosse imediatamente concluída, e depois usar as corrotinas para "pausar" a execução da função enquanto aguarda a resposta. Isso permite que o programa execute outras tarefas enquanto aguarda, em vez de ficar "bloqueado".
Como usar corrotinas no C++
No C++20, o suporte a corrotinas foi adicionado diretamente à linguagem com o cabeçalho <coroutine>. Ele é amplamente suportado pelos compiladores modernos (GCC 10+, Clang 12+, MSVC 19.25+) e é a base sobre a qual o Asio constrói sua integração com corrotinas.
A biblioteca Asio aproveita o mecanismo de corrotinas do C++20 para permitir que você escreva código assíncrono de forma sequencial e legível, usando co_await para suspender a execução enquanto aguarda operações de E/S.
A seguir, um exemplo de servidor TCP usando corrotinas Asio com standalone Asio:
#include <asio.hpp>
#include <asio/co_spawn.hpp>
#include <asio/detached.hpp>
#include <iostream>
using asio::awaitable;
using asio::co_spawn;
using asio::detached;
using asio::use_awaitable;
namespace ip = asio::ip;
using ip::tcp;
// Corrotina que trata uma conexão de cliente
awaitable<void> tratar_cliente(tcp::socket socket)
{
try {
char dados[1024];
for (;;) {
// co_await suspende a corrotina até que haja dados disponíveis
std::size_t n = co_await socket.async_read_some(
asio::buffer(dados), use_awaitable);
// Eco: envia de volta o que foi recebido
co_await asio::async_write(socket,
asio::buffer(dados, n), use_awaitable);
}
} catch (std::exception& e) {
std::cout << "Conexão encerrada: " << e.what() << '\n';
}
}
// Corrotina do servidor que aceita conexões
awaitable<void> servidor(asio::io_context& ctx, unsigned short porta)
{
tcp::acceptor acceptor(ctx, {tcp::v4(), porta});
std::cout << "Servidor ouvindo na porta " << porta << '\n';
for (;;) {
tcp::socket socket = co_await acceptor.async_accept(use_awaitable);
// Lança uma corrotina independente para cada cliente
co_spawn(ctx, tratar_cliente(std::move(socket)), detached);
}
}
int main()
{
asio::io_context ctx;
co_spawn(ctx, servidor(ctx, 8080), detached);
ctx.run();
return 0;
}
Ao ter o primeiro contato com corrotinas em C++ (com Asio) você encontrará novas palavras-chave que precisará compreender:
-
co_spawn: é uma função da biblioteca Asio que permite criar e iniciar uma corrotina de forma assíncrona. Ela "lança" uma corrotina em um dado executor, permitindo que a corrotina execute tarefas assíncronas de forma independente. -
co_yield: é uma palavra-chave do C++ que permite que uma corrotina seja "pausada" e permita que outras corrotinas sejam executadas. Quando uma corrotina é pausada comco_yield, ela é suspensa temporariamente e permite que outras tarefas sejam processadas. -
co_await: é uma palavra-chave do C++ que permite que uma corrotina aguarde a conclusão de uma operação assíncrona. Quando uma corrotina encontra umco_await, ela é suspensa até que a operação termine, liberando a thread para executar outras tarefas. -
co_return: é uma palavra-chave do C++ que permite que uma corrotina retorne um valor ao encerrar. É usado para finalizar a execução de uma corrotina e retornar o resultado para quem a aguarda. -
detached: é um completion token especial que indica que a corrotina deve ser executada de forma independente — ninguém irá aguardar seu resultado. Útil para tarefas de servidor do tipo "disparar e esquecer". -
awaitable<T>: é o tipo de retorno de uma corrotina Asio. Uma função que retornaawaitable<void>é uma corrotina que pode ser pausada comco_awaite não produz valor ao concluir.
Atenção: Uma função que usa
co_awaitinternamente deve retornarawaitable<T>(ou outro tipo de corrotina). Declarar a função comovoide usarco_awaitdentro dela é um erro de compilação.
Completion Tokens
Em Asio, um completion token é um tipo de dado usado para especificar como uma operação assíncrona deve ser completada. Ele é passado como último parâmetro para funções assíncronas e determina como o resultado é entregue ao chamador.
Existem vários tipos de completion token disponíveis no Asio:
asio::use_awaitable: transforma a operação em uma expressão que pode ser usada comco_awaitdentro de uma corrotina. É a opção recomendada para código moderno com C++20.asio::use_future: transforma a operação em umstd::future, permitindo obter o resultado de forma síncrona ou em outra thread.asio::detached: indica que o resultado da operação não será observado (disparar e esquecer).asio::redirect_error(token, ec): captura erros noerror_codefornecido em vez de lançar exceção.
Adaptadores de Token de Completude
Ao usar use_awaitable, por padrão os erros são convertidos em exceções asio::system_error. Em código de produção, é comum preferir tratamento explícito de erros sem exceções. O Asio oferece adaptadores para isso:
redirect_error — captura o erro sem exceção
#include <asio.hpp>
#include <asio/co_spawn.hpp>
#include <asio/redirect_error.hpp>
#include <iostream>
using asio::awaitable;
using asio::use_awaitable;
awaitable<void> ler_com_tratamento(asio::ip::tcp::socket& socket)
{
char buffer[1024];
asio::error_code ec;
// O erro é capturado em 'ec' em vez de ser lançado como exceção
std::size_t n = co_await socket.async_read_some(
asio::buffer(buffer),
asio::redirect_error(use_awaitable, ec));
if (ec) {
if (ec == asio::error::eof)
std::cout << "Conexão encerrada pelo par\n";
else
std::cout << "Erro: " << ec.message() << '\n';
co_return;
}
std::cout << "Recebidos " << n << " bytes\n";
}
as_tuple — retorna (error_code, resultado) como tupla
#include <asio.hpp>
#include <asio/experimental/as_tuple.hpp>
#include <iostream>
using asio::awaitable;
using asio::use_awaitable;
using asio::experimental::as_tuple;
awaitable<void> ler_com_tupla(asio::ip::tcp::socket& socket)
{
char buffer[1024];
// Desestrutura automaticamente o resultado em (ec, bytes)
auto [ec, n] = co_await socket.async_read_some(
asio::buffer(buffer),
as_tuple(use_awaitable));
if (ec) {
std::cout << "Erro: " << ec.message() << '\n';
co_return;
}
std::cout << "Recebidos " << n << " bytes\n";
}
Dica: Prefira
redirect_errorouas_tupleem código de produção para ter controle explícito sobre os erros. Reserve o tratamento por exceção para cenários onde a corrotina deve abortar completamente ao encontrar um erro.
Composição de Corrotinas
O Asio suporta a composição de corrotinas usando os operadores && e || sobre objetos awaitable<>. Isso permite executar múltiplas operações em paralelo de forma elegante:
#include <asio.hpp>
#include <asio/experimental/awaitable_operators.hpp>
#include <iostream>
using namespace asio::experimental::awaitable_operators;
using asio::awaitable;
using asio::use_awaitable;
awaitable<void> aguardar_timeout(asio::steady_timer& timer,
std::chrono::seconds segundos)
{
timer.expires_after(segundos);
co_await timer.async_wait(use_awaitable);
}
awaitable<void> ler_com_timeout(asio::ip::tcp::socket& socket,
asio::io_context& ctx)
{
asio::steady_timer timer(ctx);
char buffer[1024];
// Executa as duas corrotinas em paralelo:
// retorna quando a PRIMEIRA terminar (leitura ou timeout)
co_await (
socket.async_read_some(asio::buffer(buffer), use_awaitable)
|| aguardar_timeout(timer, std::chrono::seconds(5))
);
}
- Operador
||: aguarda a primeira corrotina a terminar e cancela as demais. - Operador
&&: aguarda todas as corrotinas terminarem antes de prosseguir.
Asio Corrotina comparado com Cppcoro
O cppcoro foi uma biblioteca pioneira de corrotinas para C++ que fornecia primitivas para escrever código assíncrono de forma legível. Ela foi projetada para funcionar com o padrão de corrotinas do C++20 e serviu como referência importante para o design moderno de corrotinas em C++.
Atenção: O cppcoro está largamente sem manutenção desde 2021, quando Lewis Baker saiu da Microsoft. Para novos projetos, recomenda-se usar diretamente as corrotinas do Asio com
use_awaitable, ou considerar alternativas ativas como libcoro.
Principais diferenças:
- O Asio é uma biblioteca completa de E/S de rede com suporte a corrotinas integradas ao seu modelo de executores — ideal para programação de redes.
- O cppcoro era focado exclusivamente em primitivas de corrotinas (sem integração de E/S de rede direta) e não recebe mais atualizações.
- Ambos seguem como referência a proposta técnica P1056R0, que descreve o suporte a corrotinas no C++.
Endpoint (Ponto de Extremidade)
O Asio fornece outros tipos de objetos endpoint para representar pontos finais de conexões de rede de outros protocolos, além do TCP. Por exemplo, o objeto asio::ip::udp::endpoint é usado para representar um ponto final de uma conexão de rede UDP. Ele é composto por um endereço IP e uma porta, e funciona de maneira similar ao objeto asio::ip::tcp::endpoint, mas é usado para protocolos UDP em vez de TCP.
O objeto asio::ip::tcp::endpoint é usado para representar um ponto final de uma conexão de rede TCP. Ele é composto por um endereço IP e uma porta, que são usados para identificar o host remoto ou o servidor que o programa deseja se conectar ou se comunicar.
Para usar o objeto asio::ip::tcp::endpoint, é preciso incluir o cabeçalho <boost/asio/ip/tcp.hpp> no seu código e criar um objeto da classe passando um endereço IP e uma porta para o construtor. Em seguida, é possível usar o objeto asio::ip::tcp::endpoint para identificar o host remoto ou o servidor que o programa deseja se conectar ou se comunicar.
O objeto asio::ip::tcp::endpoint também fornece uma série de outras funcionalidades úteis. Por exemplo, é possível usar as funções address e port para obter o endereço IP e a porta do objeto, respectivamente. Além disso, é possível usar a função to_string para obter uma string que representa o objeto asio::ip::tcp::endpoint.
Além disso, o Asio fornece outros tipos de objetos endpoint para representar pontos finais de conexões de outros protocolos, como o objeto asio::ip::icmp::endpoint para o protocolo ICMP e o objeto asio::local::stream_protocol::endpoint para o protocolo Unix local. Cada um desses objetos endpoint é composto por um endereço e uma porta, e fornece uma série de funcionalidades úteis para trabalhar com os respectivos protocolos de rede.
basic_endpoint(const boost::asio::ip::address& addr, unsigned short port_num)
: impl_(addr, port_num)
{
//TODO
}
O cliente usa o endpoint para designar o endereço do servidor, e o aplicativo do servidor usa o endpoint para identificar qual endereço será usado para escutar e aceitar conexões. Um exemplo de TCP endpoint abaixo:
boost::asio::ip::tcp::endpoint endpoint{
boost::asio::ip::make_address("127.0.0.1"), 3303};
Normalmente, o servidor precisa escutar(listen) todos os endereços da máquina atual e pode recorrer a outro construtor de basic_endpoint:
basic_endpoint(const InternetProtocol& internet_protocol,
unsigned short port_num)
: impl_(internet_protocol.family(), port_num)
{
}
Um exemplo de servidor UDP que escuta todos os endereços IPv4 & IPv6:
//IPv6
boost::asio::ip::udp::endpoint endpoint{
boost::asio::ip::udp::v6(),
3303};
...
//IPv4
boost::asio::ip::udp::endpoint endpoint{
boost::asio::ip::udp::v4(),
3306};
Em resumo, o Asio fornece uma série de objetos endpoint para representar pontos finais de conexões de rede de diferentes protocolos. Cada um desses objetos endpoint é composto por um endereço e uma porta, e fornece uma série de funcionalidades úteis para trabalhar com os respectivos protocolos de rede.
DNS Query
O objeto asio::ip::tcp::resolver é usado para resolver nomes de domínio em endereços IP. Ele é útil em situações em que o programa precisa se conectar a um host remoto usando um nome de domínio, mas precisa do endereço IP para estabelecer a conexão de rede.
Para usar o objeto asio::ip::tcp::resolver, inclua o cabeçalho <asio/ip/tcp.hpp> no seu código e crie um objeto da classe passando um asio::io_context para o construtor. Em seguida, use o método resolve para resolver o nome de domínio em um endereço IP.
Resolução Síncrona
O método resolve retorna um range de resultados que pode ser percorrido com range-for moderno:
#include <asio.hpp>
#include <iostream>
int main()
{
try
{
asio::io_context io_context;
asio::ip::tcp::resolver resolver{io_context};
// resolve(host, serviço) — serviço pode ser nome ("https") ou porta ("443")
auto endpoints = resolver.resolve("google.com", "https");
// Iteração moderna com range-for
for (const auto& entry : endpoints)
{
std::cout << entry.endpoint() << '\n';
}
}
catch (std::exception& e)
{
std::cerr << e.what() << '\n';
}
return 0;
}
O resultado da execução será algo como:
142.250.184.206:443
[2800:3f0:4004:811::200e]:443
O tipo asio::ip::tcp::resolver::results_type é um range de basic_resolver_entry:
template <typename InternetProtocol>
class basic_resolver_entry
{
public:
typedef InternetProtocol protocol_type;
typedef typename InternetProtocol::endpoint endpoint_type;
// Conversão implícita para endpoint — permite uso direto com connect()
operator endpoint_type() const { return endpoint_; }
......
}
Como ele possui o operador de conversão endpoint_type(), pode ser passado diretamente para funções como asio::connect():
// O resolver results pode ser passado diretamente para connect
asio::ip::tcp::socket socket{io_context};
asio::connect(socket, endpoints); // itera automaticamente todos os endpoints
Resolução Assíncrona
O método async_resolve resolve o nome de domínio de forma assíncrona, sem bloquear a thread. Isso é essencial em servidores que precisam resolver múltiplos nomes simultaneamente:
#include <asio.hpp>
#include <iostream>
int main()
{
asio::io_context io_context;
asio::ip::tcp::resolver resolver{io_context};
// async_resolve aceita um callback (ou use_awaitable em corrotinas)
resolver.async_resolve("google.com", "https",
[](const asio::error_code& ec,
asio::ip::tcp::resolver::results_type results)
{
if (ec) {
std::cerr << "Erro na resolução: " << ec.message() << '\n';
return;
}
for (const auto& entry : results)
std::cout << entry.endpoint() << '\n';
});
io_context.run(); // processa a operação assíncrona
return 0;
}
Resolução Assíncrona com Corrotinas
Com corrotinas C++20, a resolução assíncrona fica ainda mais legível:
#include <asio.hpp>
#include <asio/co_spawn.hpp>
#include <asio/detached.hpp>
#include <iostream>
using asio::awaitable;
using asio::use_awaitable;
using asio::ip::tcp;
awaitable<void> resolver_dns(asio::io_context& ctx,
const std::string& host)
{
tcp::resolver resolver{ctx};
// co_await suspende a corrotina até a resolução completar
auto endpoints = co_await resolver.async_resolve(host, "https",
use_awaitable);
for (const auto& entry : endpoints)
std::cout << entry.endpoint() << '\n';
}
int main()
{
asio::io_context ctx;
asio::co_spawn(ctx,
resolver_dns(ctx, "google.com"),
asio::detached);
ctx.run();
return 0;
}
Cancelamento
O objeto resolver também permite cancelar uma resolução em andamento com o método cancel():
resolver.cancel(); // cancela todas as operações pendentes no resolver
Isso é útil em aplicações com timeout, onde a resolução não deve bloquear indefinidamente.
Atenção: A classe
resolver::queryfoi depreciada no Asio moderno. Useresolver.resolve(host, serviço)diretamente, sem criar um objetoqueryintermediário.
Exceção
O Asio fornece duas formas de lidar com erros nas operações de rede: por exceção (comportamento padrão) e por código de erro (sem exceção, usando error_code). Conhecer as duas formas é fundamental para escrever código robusto.
Tratamento por Exceção
Por padrão, as funções síncronas do Asio lançam asio::system_error (standalone) ou boost::system::system_error (Boost.Asio) quando ocorre um erro. Ambas derivam de std::runtime_error.
O Asio fornece uma série de exceções de erro que incluem:
asio::error::address_family_not_supported: o tipo de endereço especificado não é suportado pela plataforma.asio::error::address_in_use: o endereço especificado já está em uso por outro processo.asio::error::connection_aborted: a conexão foi abortada pelo host remoto.asio::error::connection_refused: a conexão foi recusada pelo host remoto.asio::error::connection_reset: a conexão foi reiniciada pelo host remoto.asio::error::eof: o par encerrou a conexão (fim de arquivo/stream).asio::error::timed_out: a operação excedeu o tempo limite.asio::error::host_not_found: o nome do host não pôde ser resolvido.
Essas exceções são lançadas pelos métodos do Asio que realizam operações de rede síncronas e podem ser capturadas e tratadas de acordo com o erro específico:
#include <asio.hpp>
#include <iostream>
int main()
{
try {
asio::io_context io_context;
asio::ip::tcp::resolver resolver{io_context};
// Lança asio::system_error se a resolução falhar
auto endpoints = resolver.resolve("host.invalido.exemplo", "https");
for (const auto& ep : endpoints)
std::cout << ep.endpoint() << '\n';
} catch (const asio::system_error& e) {
// e.code() retorna o error_code específico
std::cerr << "Erro Asio [" << e.code() << "]: " << e.what() << '\n';
} catch (const std::exception& e) {
std::cerr << "Exceção: " << e.what() << '\n';
}
return 0;
}
A função membro what() retorna as informações detalhadas da exceção, enquanto code() retorna o error_code correspondente.
Tratamento por Código de Erro (sem exceção)
A maioria das funções síncronas do Asio possui uma sobrecarga que recebe um asio::error_code por referência. Nesse caso, nenhuma exceção é lançada — o erro é armazenado no error_code e a função retorna normalmente:
#include <asio.hpp>
#include <iostream>
int main()
{
asio::io_context io_context;
asio::ip::tcp::socket socket{io_context};
asio::ip::tcp::endpoint endpoint{
asio::ip::make_address("192.168.1.1"), 8080};
// Sobrecarga sem exceção: erro armazenado em 'ec'
asio::error_code ec;
socket.connect(endpoint, ec);
if (ec) {
if (ec == asio::error::connection_refused)
std::cerr << "Conexão recusada!\n";
else
std::cerr << "Erro ao conectar: " << ec.message() << '\n';
return 1;
}
std::cout << "Conectado com sucesso!\n";
return 0;
}
Verificando erros em operações assíncronas
Nos callbacks de operações assíncronas, o primeiro parâmetro é sempre um error_code:
socket.async_receive(asio::buffer(buffer),
[](const asio::error_code& ec, std::size_t bytes) {
if (ec) {
if (ec == asio::error::eof) {
std::cout << "Conexão encerrada pelo par\n";
} else {
std::cerr << "Erro: " << ec.message() << '\n';
}
return;
}
// Processar bytes recebidos...
std::cout << "Recebidos " << bytes << " bytes\n";
});
Tabela de Erros Comuns
| Código de Erro | Significado | Causa Comum |
|---|---|---|
asio::error::eof | Fim da conexão | Par encerrou a conexão normalmente |
asio::error::connection_refused | Conexão recusada | Porta fechada no servidor |
asio::error::connection_reset | Conexão reiniciada | Desconexão abrupta |
asio::error::connection_aborted | Conexão abortada | Timeout ou erro de rede |
asio::error::address_in_use | Endereço em uso | Porta já ocupada |
asio::error::host_not_found | Host não encontrado | Falha de DNS |
asio::error::timed_out | Tempo esgotado | Servidor não respondeu |
asio::error::operation_aborted | Operação cancelada | cancel() ou stop() chamado |
Standalone vs Boost.Asio
| Aspecto | Standalone Asio | Boost.Asio |
|---|---|---|
| Tipo de erro | asio::error_code | boost::system::error_code |
| Exceção | asio::system_error | boost::system::system_error |
| Categoria | asio::error::get_system_category() | boost::system::system_category() |
Dica: Em código de produção, prefira a sobrecarga com
error_codepara operações críticas, pois o tratamento explícito de erros é mais eficiente e previsível do que o mecanismo de exceções.
Conectar Servidor
O cliente pode usar os endpoints retornados por DNS para conectar o aplicativo servidor. Veja o código abaixo:
#include <boost/asio.hpp>
#include <iostream>
int main()
{
try
{
boost::asio::io_context io_context;
boost::asio::ip::tcp::resolver resolver{io_context};
boost::asio::ip::tcp::resolver::results_type endpoints =
resolver.resolve("google.com", "https");
boost::asio::ip::tcp::socket socket{io_context};
auto endpoint = boost::asio::connect(socket, endpoints);
std::cout << "Connect to " << endpoint << " successfully!\n";
}
catch (std::exception& e)
{
std::cerr << e.what() << '\n';
return -1;
}
return 0;
}
O resultado da execução será:
Connect to 172.217.194.101:443 successfully!
Observe que o boost::asio::connect requer o iterador de endpoints. Se você quiser apenas um endpoint específico, poderá usar a função membro connect do socket. Verifique o código abaixo:
#include <boost/asio.hpp>
#include <iostream>
int main()
{
try
{
boost::asio::io_context io_context;
boost::asio::ip::tcp::resolver resolver{io_context};
boost::asio::ip::tcp::resolver::results_type endpoints =
resolver.resolve("google.com", "https");
boost::asio::ip::tcp::socket socket{io_context};
auto eit = endpoints.cbegin();
for (; eit != endpoints.cend(); eit++)
{
boost::system::error_code ec;
boost::asio::ip::tcp::endpoint endpoint = *eit;
socket.connect(endpoint, ec);
if (!ec)
{
std::cout << "Connect to " << endpoint << " successfully!\n";
break;
}
}
if (eit == endpoints.cend())
{
std::cout << "Connect failed!\n";
return -1;
}
}
catch (std::exception& e)
{
std::cerr << e.what() << '\n';
return -1;
}
return 0;
}
O resultado da execução será:
Connect to 172.217.194.139:443 successfully!
Accept connections
Para que o servidor aceite a conexão dos clientes. O servidor precisará criar um acceptor:
......
boost::asio::io_context io_context;
boost::asio::ip::tcp::acceptor acceptor{
io_context,
boost::asio::ip::tcp::endpoint{boost::asio::ip::tcp::v6(), 3303}};
......
boost::asio::ip::tcp::acceptor é uma instância de basic_socket_acceptor:
class tcp
{
......
/// The TCP acceptor type.
typedef basic_socket_acceptor<tcp> acceptor;
......
}
O construtor basic_socket_acceptor combinará criação de socket, configuração de endereço de reutilização, funções binding & listening:
basic_socket_acceptor(boost::asio::io_context& io_context,
const endpoint_type& endpoint, bool reuse_addr = true)
: basic_io_object<BOOST_ASIO_SVC_T>(io_context)
{
......
}
Então o acceptor aceitará as conexões dos clientes. O código abaixo mostra o endereço do cliente e fecha a conexão:
#include <boost/asio.hpp>
#include <iostream>
int main()
{
try
{
boost::asio::io_context io_context;
boost::asio::ip::tcp::acceptor acceptor{
io_context,
boost::asio::ip::tcp::endpoint{boost::asio::ip::tcp::v6(), 3303}};
while (1)
{
boost::asio::ip::tcp::socket socket{io_context};
acceptor.accept(socket);
std::cout << socket.remote_endpoint() << " connects to " << socket.local_endpoint() << '\n';
}
}
catch (std::exception& e)
{
std::cerr << e.what() << '\n';
return -1;
}
return 0;
}
O resultado da execução será:
[::ffff:10.217.242.61]:39290 connects to [::ffff:192.168.35.145]:3303
......
Operações síncronas E/S
Depois que a conexão é estabelecida, o cliente e o servidor podem se comunicar. Como na API sockets do UNIX, o boost::asio também fornece as funções send e receive Use basic_stream_socket como exemplo e um par de implementações assim:
template <typename ConstBufferSequence>
std::size_t send(const ConstBufferSequence& buffers)
{
......
}
......
template <typename MutableBufferSequence>
std::size_t receive(const MutableBufferSequence& buffers)
{
......
}
Observe que os tipos de buffer de send/receive são ConstBufferSequence/MutableBufferSequence, e podemos usar a função boost::asio::buffer para construir tipos relacionados.
Abaixo está um programa cliente que envia "Hello world!" Para o servidor após o estabelecimento da conexão:
#include <boost/asio.hpp>
#include <iostream>
int main()
{
try
{
boost::asio::io_context io_context;
boost::asio::ip::tcp::endpoint endpoint{
boost::asio::ip::make_address("10.217.242.61"),
3303};
boost::asio::ip::tcp::tcp::socket socket{io_context};
socket.connect(endpoint);
std::cout << "Connect to " << endpoint << " successfully!\n";
socket.send(boost::asio::buffer("Hello world!"));
}
catch (std::exception& e)
{
std::cerr << e.what() << '\n';
return -1;
}
return 0;
}
O programa servidor que aguarda o recebimento da saudação do cliente:
#include <boost/asio.hpp>
#include <iostream>
int main()
{
try
{
boost::asio::io_context io_context;
boost::asio::ip::tcp::acceptor acceptor{
io_context,
boost::asio::ip::tcp::endpoint{boost::asio::ip::tcp::v4(), 3303}};
while (1)
{
boost::asio::ip::tcp::socket socket{io_context};
acceptor.accept(socket);
std::cout << socket.remote_endpoint() << " connects to " << socket.local_endpoint() << '\n';
char recv_str[1024] = {};
socket.receive(boost::asio::buffer(recv_str));
std::cout << "Receive string: " << recv_str << '\n';
}
}
catch (std::exception& e)
{
std::cerr << e.what() << '\n';
return -1;
}
return 0;
}
Compile e execute os programas. O cliente exibirá o seguinte:
$ ./client
Connect to 10.217.242.61:3303 successfully!
Servidor exibirá seguinte:
$ ./server
10.217.242.21:64776 connects to 10.217.242.61:3303
Receive string: Hello world!
Se nenhum erro ocorrer, o send pode garantir que pelo menos um byte seja enviado com sucesso, e você deve verificar o valor de retorno para ver se todos os bytes foram enviados com sucesso ou não. receive é semelhante a send. O boost::asio::basic_stream_socket também fornece read_some e write_some, que têm as mesmas funções que receive e send.
Se não nos preocuparmos em verificar o estado do meio (bytes parciais são enviados com sucesso), e apenas nos importarmos se todos os bytes serão enviados com sucesso ou não, podemos usar o boost::asio::write que usa o write_some por baixo. Da mesma forma, não é difícil adivinhar o que boost::asio::read faz.
Operações Assíncronas E/S
Diferentemente da API sockets do UNIX, o Asio possui habilidades de leitura & gravação (read/write) assíncronas inclusas. Usando basic_stream_socket como exemplo, as assinaturas públicas das funções assíncronas são:
// Envio assíncrono — retorna imediatamente, handler chamado ao completar
template <typename ConstBufferSequence, typename WriteHandler>
void async_send(const ConstBufferSequence& buffers, WriteHandler&& handler);
// Recebimento assíncrono — retorna imediatamente, handler chamado ao completar
template <typename MutableBufferSequence, typename ReadHandler>
void async_receive(const MutableBufferSequence& buffers, ReadHandler&& handler);
Como as funções async_send e async_receive retornam imediatamente e não bloqueiam a thread atual, você deve passar uma função de retorno de chamada (callback) ou completion token como parâmetro. O callback recebe o resultado das operações:
void handler(
const asio::error_code& error, // resultado da operação
std::size_t bytes_transferred // bytes processados
)
Dica: Em código moderno, prefira lambdas a
std::bindpara callbacks — o código fica mais legível e o compilador pode otimizar melhor.
Há um exemplo simples de cliente/servidor. Abaixo está o código do cliente:
#include <boost/asio.hpp>
#include <functional>
#include <iostream>
#include <memory>
void callback(
const boost::system::error_code& error,
std::size_t bytes_transferred,
std::shared_ptr<boost::asio::ip::tcp::socket> socket,
std::string str)
{
if (error)
{
std::cout << error.message() << '\n';
}
else if (bytes_transferred == str.length())
{
std::cout << "Message is sent successfully!" << '\n';
}
else
{
socket->async_send(
boost::asio::buffer(str.c_str() + bytes_transferred, str.length() - bytes_transferred),
std::bind(callback, std::placeholders::_1, std::placeholders::_2, socket, str));
}
}
int main()
{
try
{
boost::asio::io_context io_context;
boost::asio::ip::tcp::endpoint endpoint{
boost::asio::ip::make_address("192.168.35.145"),
3303};
std::shared_ptr<boost::asio::ip::tcp::socket> socket{new boost::asio::ip::tcp::socket{io_context}};
socket->connect(endpoint);
std::cout << "Connect to " << endpoint << " successfully!\n";
std::string str{"Hello world!"};
socket->async_send(
boost::asio::buffer(str),
std::bind(callback, std::placeholders::_1, std::placeholders::_2, socket, str));
io_context.run();
}
catch (std::exception& e)
{
std::cerr << e.what() << '\n';
return -1;
}
return 0;
}
Vamos analisar o código:
(1) Como o objeto sockets é non-copyable (sockets), sockets é criado como um ponteiro inteligente de memória compartilhada (shared_pointer):
......
std::shared_ptr<boost::asio::ip::tcp::socket> socket{new boost::asio::ip::tcp::socket{io_context}};
......
(2) Como o callback possui apenas dois parâmetros, ele precisa usar std::bind para passar parâmetros adicionais:
......
std::bind(callback, std::placeholders::_1, std::placeholders::_2, socket, str)
......
(3) async_send não garante que todos os bytes sejam enviados (boost::asio::async_write retorna todos os bytes enviados com sucesso ou ocorre um erro), portanto, é necessário reemitir async_send no callback:
......
if (error)
{
......
}
else if (bytes_transferred == str.length())
{
......
}
else
{
socket->async_send(......);
}
(4) A função io_context.run será bloqueada até que todo o trabalho termine e não haja mais handlers(manipuladores) a serem despachados, ou até que o io_context seja interrompido:
socket->get_executor().context().run();
Se não houver a função io_context.run, o programa será encerrado imediatamente.
Verifique o código do servidor que usa async_receive:
#include <ctime>
#include <functional>
#include <iostream>
#include <string>
#include <boost/asio.hpp>
void callback(
const boost::system::error_code& error,
std::size_t,
char recv_str[]) {
if (error)
{
std::cout << error.message() << '\n';
}
else
{
std::cout << recv_str << '\n';
}
}
int main()
{
try
{
boost::asio::io_context io_context;
boost::asio::ip::tcp::acceptor acceptor(
io_context,
boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), 3303));
for (;;)
{
boost::asio::ip::tcp::socket socket(io_context);
acceptor.accept(socket);
char recv_str[1024] = {};
socket.async_receive(
boost::asio::buffer(recv_str),
std::bind(callback, std::placeholders::_1, std::placeholders::_2, recv_str));
io_context.run();
io_context.restart();
}
}
catch (std::exception& e)
{
std::cerr << e.what() << std::endl;
}
return 0;
}
Há duas advertências às quais você precisa prestar atenção:
(1) Apenas para fins de demonstração: para cada cliente, o callback é chamado apenas uma vez;
(2) O io_context.restart deve ser chamado para chamar outro io_context.run.
Da mesma forma, você também pode verificar como usar o boost::asio::async_read.
Compile e execute os programas.
O cliente exibirá o seguinte:
$ ./client
Connect to 192.168.35.145:3303 successfully!
Message is sent successfully!
Servidor emitirar o seguinte:
$ ./server
Hello world!
Protocolo ICMP
A classe asio::ip::icmp é uma classe em C++ que faz parte da biblioteca Asio (Asynchronous Input/Output) e é usada para implementar a comunicação usando o protocolo ICMP (Internet Control Message Protocol).
O protocolo ICMP é um protocolo de nível de rede que é usado para enviar mensagens de erro e de controle entre dispositivos de rede. Ele é comumente usado para testar a conectividade entre dispositivos de rede, como o comando ping em sistemas operacionais.
A classe asio::ip::icmp fornece uma interface para criar e gerenciar sockets ICMP. Ela é usada para criar sockets ICMP, que são usados para enviar e receber mensagens ICMP. Ela também fornece métodos para enviar e receber mensagens ICMP através de um socket, bem como para gerenciar a conexão e desconexão de clientes.
A classe asio::ip::icmp é derivada da classe asio::basic_socket<Protocol>, que é uma classe genérica que representa um socket de rede. Ela fornece uma interface para criar e gerenciar sockets de rede usando qualquer protocolo de rede suportado pelo Asio. A classe asio::ip::icmp é uma especialização da classe asio::basic_socket<Protocol> para o protocolo ICMP.
A classe asio::ip::icmp fornece os seguintes métodos e funcionalidades:
connect: estabelece uma conexão com um host especificado através de um endpoint ICMP.close: fecha o socket e interrompe qualquer conexão existente.read: lê dados de um socket e armazena os dados em um buffer de saída.write: escreve dados de um buffer em um socket.
Além disso, a classe asio::ip::icmp fornece várias configurações de socket, como opções de buffer de entrada e saída, opções de tempo de espera e opções de recurso. Essas opções podem ser ajustadas usando os métodos set_option e get_option da classe asio::ip::icmp.
Aqui está um exemplo de como a classe asio::ip::icmp pode ser usada para implementar um programa em C++ que envia uma mensagem ICMP para um host especificado e aguarda por uma resposta:
#include <iostream>
#include <chrono>
#include <asio.hpp>
// Constantes para os campos protocol e type
const unsigned char IPPROTO_ICMP = 1;
const unsigned char ICMP_ECHO = 8;
const unsigned char ICMP_ECHOREPLY = 0;
const unsigned char ICMP_DEST_UNREACH = 3;
const unsigned char ICMP_TIME_EXCEEDED = 11;
struct iphdr
{
unsigned char ihl:4;
unsigned char version:4;
unsigned char tos;
unsigned short tot_len;
unsigned short id;
unsigned short frag_off;
unsigned char ttl;
unsigned char protocol;
unsigned short check;
unsigned int saddr;
unsigned int daddr;
};
struct icmphdr
{
unsigned char type;
unsigned char code;
unsigned short checksum;
union un
{
struct echo
{
unsigned short id;
unsigned short sequence;
};
unsigned int gateway;
struct frag
{
unsigned short __unused;
unsigned short mtu;
};
};
};
int main()
{
// Cria um objeto io_context doAsio
asio::io_context io_context;
// Cria um socket ICMP
asio::ip::icmp::socket socket(io_context);
// Configura um endpoint ICMP para o host
asio::ip::icmp::endpoint host_endpoint("www.example.com", 0);
// Conecta o socket ao host
socket.connect(host_endpoint);
// Envia um ping para o host
std::vector<unsigned char> ping(sizeof(icmphdr) + sizeof(iphdr) + 8);
iphdr* ip_header = reinterpret_cast<iphdr*>(ping.data());
ip_header->ihl = 5;
ip_header->version = 4;
ip_header->tot_len = htons(ping.size());
ip_header->protocol = IPPROTO_ICMP;
ip_header->saddr = inet_addr("127.0.0.1");
ip_header->daddr = inet_addr("www.example.com");
icmphdr* icmp_header = reinterpret_cast<icmphdr*>(ping.data() + sizeof(iphdr));
icmp_header->type = ICMP_ECHO;
icmp_header->code = 0;
icmp_header->un.echo.id = htons(1234);
icmp_header->un.echo.sequence = htons(1);
*reinterpret_cast<unsigned long*>(ping.data() + sizeof(icmphdr) + sizeof(iphdr)) = htonl(time(nullptr));
// Calcula o checksum do ping
icmp_header->checksum = 0;
icmp_header->checksum = asio::ip::icmp::checksum(ping.data(), ping.size());
// Armazena o tempo de envio do ping
auto send_time = std::chrono::high_resolution_clock::now();
// Envia o ping para o host
asio::write(socket,asio::buffer(ping));
// Aguarda por uma resposta de ping do host
std::vector<unsigned char> reply(1024);
size_t bytes_received = asio::read(socket, asio::buffer(reply));
// Verifica se o tipo de mensagem recebida é um ICMP_ECHOREPLY
iphdr* reply_ip_header = reinterpret_cast<iphdr*>(reply.data());
icmphdr* reply_icmp_header = reinterpret_cast<icmphdr*>(reply.data() + (reply_ip_header->ihl * 4));
if (reply_icmp_header->type == ICMP_ECHOREPLY)
{
// Calcula o tempo de viagem do ping
auto trip_time = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - send_time).count();
// Imprime o resultado do ping
std::cout << "Recebida resposta de ping do host em " << trip_time << " ms" << std::endl;
}
else
{
std::cout << "Tipo de mensagem não reconhecido" << std::endl;
}
return 0;
}
Protocolo Raw
Uma das funcionalidades que aAsio oferece é a possibilidade de enviar e receber pacotes de dados usando o protocolo de rede a baixo nível. Isso é conhecido como "envio/recebimento de pacotes raw", ou simplesmente "raw sockets".
A classe asio::generic::raw_protocol é usada para implementar a comunicação de pacotes e também fornece uma interface para criar e gerenciar sockets de pacotes raw. Ela também fornece métodos para enviar e receber pacotes raw através de um socket, bem como para gerenciar a conexão e desconexão de clientes.
Para usar raw sockets com aAsio, é necessário incluir o cabeçalho <asio/generic/raw_socket.hpp> e criar uma instância de asio::generic::raw_protocol, que é a classe responsável por gerenciar a conexão de rede.
Os pacotes raw são pacotes de rede que são enviados e recebidos diretamente, sem qualquer tipo de encapsulamento ou formatação adicional. Eles são usados para implementar protocolos de nível inferior, como o protocolo ICMP, ou para fazer debug de aplicações de rede.
Exemplo de código para enviar um pacote raw:
#include <iostream>
#include <boost/asio/io_context.hpp>
#include <boost/asio/generic/raw_socket.hpp>
#include <boost/asio/ip/udp.hpp>
namespace asio = boost::asio;
int main() {
asio::io_context io_context;
asio::basic_raw_socket<asio::generic::raw_protocol> socket(io_context);
// Cria um buffer com os dados a serem enviados
std::vector<std::uint8_t> data = {0x01, 0x02, 0x03, 0x04};
asio::const_buffer buffer(data.data(), data.size());
// Envia o pacote para o endereço IP e porta especificados
socket.send_to(buffer, asio::ip::udp::endpoint(asio::ip::make_address("127.0.0.1"), 1234));
return 0;
}
O exemplo acima mostra como enviar um pacote raw para o endereço IP "127.0.0.1" na porta 1234. O pacote é criado como um buffer de dados e enviado através da chamada ao método send_to() do socket para enviar o buffer de dados para o endereço IP e porta especificados usando o tipo asio::ip::udp::endpoint.
Para receber pacotes raw, basta chamar o método receive_from() do socket, passando um buffer para armazenar os dados recebidos.
#include <iostream>
#include <boost/asio/io_context.hpp>
#include <boost/asio/generic/raw_socket.hpp>
#include <boost/asio/ip/udp.hpp>
namespace asio = boost::asio;
int main() {
asio::io_context io_context;
asio::basic_raw_socket<asio::generic::raw_protocol> socket(io_context);
// Liga o soquete a uma porta específica
socket.bind(asio::ip::udp::endpoint(asio::ip::make_address("127.0.0.1"), 1234));
// Recebe os dados através do soquete raw
char recv_buf[1024];
asio::ip::udp::endpoint sender_endpoint;
size_t bytes_received = socket.receive_from(
asio::buffer(recv_buf, sizeof(recv_buf)), sender_endpoint);
// Imprime os dados do endpoint
std::cout << "Received " << bytes_received << " bytes from ";
std::cout.write(reinterpret_cast<char*>(sender_endpoint.data()), sender_endpoint.size());
std::cout << std::endl;
std::cout << "Data: " << recv_buf << std::endl;
return 0;
}
No exemplo acima, o socket é conectado ao endereço IP "127.0.0.1" na porta 1234 e cria um buffer para armazenar os dados recebidos. Em seguida, é chamado o método receive_from() do socket, que bloqueia a execução do programa até que um pacote seja recebido. Quando o pacote é recebido, usamos a função data para obter um ponteiro para os dados do endpoint e a função size para obter o tamanho dos dados. Em seguida, usamos a função write da classe ostream para escrever os dados na tela. Porém, o tipo basic_endpoint<raw_protocol>::data_type é um ponteiro para um sockaddr, enquanto o tipo esperado pela função write é um ponteiro para o tipo caractere (char*). Para corrigir isso, basta usar o operador reinterpret_cast para converter o ponteiro para sockaddr em um ponteiro para caractere. Isso permite que a função write seja chamada com os dados do endpoint.
É importante notar que, ao trabalhar com pacotes raw, é necessário se preocupar com os detalhes da camada de rede (como cabeçalhos de protocolo, endereçamento, etc.), o que pode ser complexo e trabalhoso. Além disso, o envio/recebimento de pacotes raw geralmente só é necessário em casos especiais, como quando é preciso implementar um protocolo de rede customizado ou realizar testes de baixo nível. Em muitos casos, é mais conveniente usar um protocolo de rede mais alto nível, como o TCP ou o UDP, que já fornecem muitas das funcionalidades necessárias para a comunicação de rede.
Protocolo UDP
Nós discutimos como se comunicar através do TCP o suficiente, então é hora de mudar para o UDP agora. O UDP é um protocolo sem conexão e não confiável, mas é mais fácil de usar que o TCP.
Há um exemplo de Cliente/Servidor.
Cliente:
#include <boost/asio.hpp>
#include <iostream>
int main()
{
try
{
boost::asio::io_context io_context;
boost::asio::ip::udp::socket socket{io_context};
socket.open(boost::asio::ip::udp::v4());
socket.send_to(
boost::asio::buffer("Welcome to C++ Networking."),
boost::asio::ip::udp::endpoint{boost::asio::ip::make_address("192.168.35.145"), 3303});
}
catch (std::exception& e)
{
std::cerr << e.what() << '\n';
return -1;
}
return 0;
}
Embora não seja necessário chamar a função socket.connect, você precisa chamar explicitamente o socket.open. Além disso, o endpoint do servidor precisa ser especificado ao chamar socket.send_to.
Servidor:
#include <ctime>
#include <functional>
#include <iostream>
#include <string>
#include <boost/asio.hpp>
int main()
{
try
{
boost::asio::io_context io_context;
for (;;)
{
boost::asio::ip::udp::socket socket(
io_context,
boost::asio::ip::udp::endpoint{boost::asio::ip::udp::v4(), 3303});
boost::asio::ip::udp::endpoint client;
char recv_str[1024] = {};
socket.receive_from(
boost::asio::buffer(recv_str),
client);
std::cout << client << ": " << recv_str << '\n';
}
}
catch (std::exception& e)
{
std::cerr << e.what() << std::endl;
}
return 0;
}
Muito fácil, não é? Crie e execute o cliente e servidor. Então o seguinte log será impresso no lado do servidor:
$ ./servidor
10.217.242.21:63838: Welcome to C++ Networking.
10.217.242.21:61259: Welcome to C++ Networking.
SSL/TLS
SSL (Secure Sockets Layer) e TLS (Transport Layer Security) são dois protocolos de segurança amplamente utilizados na internet para proteger as comunicações entre dispositivos. Ambos são baseados em certificados de segurança e chaves criptográficas, que são usados para criptografar e decifrar os dados transmitidos.
A principal diferença entre SSL e TLS é que TLS é uma versão atualizada e aprimorada do SSL. O SSL foi originalmente criado pela Netscape nos anos 1990 como uma maneira de proteger as comunicações na internet. No entanto, com o tempo, problemas de segurança foram encontrados no SSL, levando ao desenvolvimento do TLS como substituto.
Atualmente, SSLv2 e SSLv3 são considerados inseguros e devem ser desabilitados. A maioria dos sistemas usa TLS 1.2 como mínimo, com TLS 1.3 como versão preferida por ser mais rápida e segura. O Asio suporta TLS 1.3 via OpenSSL 1.1.1+.
Asio SSL
A biblioteca Asio SSL fornece uma API que permite aos desenvolvedores criar e gerenciar conexões seguras usando TLS. Ela inclui funções para realizar handshakes SSL/TLS, criptografar e decifrar dados e verificar a validade de certificados de segurança.
Para usar SSL/TLS com Asio, inclua <asio/ssl.hpp> (standalone) ou <boost/asio/ssl.hpp> (Boost.Asio) e vincule com a biblioteca OpenSSL.
Exemplo: cliente SSL/TLS assíncrono (abordagem moderna)
O padrão moderno usa async_handshake() em vez do handshake síncrono:
#include <asio.hpp>
#include <asio/ssl.hpp>
#include <iostream>
#include <string>
namespace ssl = asio::ssl;
using tcp = asio::ip::tcp;
int main()
{
asio::io_context io_context;
// Use 'tls_client' para conexões de cliente (ou 'tls' para uso genérico)
// Evite 'sslv23' — está depreciado
ssl::context ctx(ssl::context::tls_client);
// Opções de segurança: desabilitar protocolos antigos e vulneráveis
ctx.set_options(
ssl::context::default_workarounds
| ssl::context::no_sslv2
| ssl::context::no_sslv3
| ssl::context::single_dh_use);
// Verificação de certificado do servidor (recomendado em produção)
ctx.set_verify_mode(ssl::verify_peer);
ctx.set_default_verify_paths();
// Socket SSL sobre TCP
ssl::stream<tcp::socket> socket(io_context, ctx);
// Resolver e conectar ao host
tcp::resolver resolver(io_context);
auto endpoints = resolver.resolve("exemplo.com", "443");
asio::connect(socket.lowest_layer(), endpoints);
// SNI (Server Name Indication) — necessário para servidores que hospedam
// múltiplos certificados no mesmo IP
SSL_set_tlsext_host_name(socket.native_handle(), "exemplo.com");
// Handshake assíncrono (abordagem recomendada)
socket.async_handshake(ssl::stream_base::client,
[&socket](const asio::error_code& ec) {
if (ec) {
std::cerr << "Erro no handshake: " << ec.message() << '\n';
return;
}
std::cout << "Handshake TLS bem-sucedido!\n";
// Enviar dados após o handshake
std::string request = "GET / HTTP/1.1\r\nHost: exemplo.com\r\n\r\n";
asio::async_write(socket, asio::buffer(request),
[](const asio::error_code& ec, std::size_t bytes) {
if (!ec)
std::cout << "Enviados " << bytes << " bytes\n";
});
});
io_context.run();
return 0;
}
Atenção:
asio::ssl::context::sslv23está depreciado. Usessl::context::tls(genérico),ssl::context::tls_client(para clientes) oussl::context::tls_server(para servidores).
Exemplo: servidor SSL/TLS assíncrono
#include <asio.hpp>
#include <asio/ssl.hpp>
#include <iostream>
#include <memory>
namespace ssl = asio::ssl;
using tcp = asio::ip::tcp;
class SessaoSSL : public std::enable_shared_from_this<SessaoSSL>
{
public:
SessaoSSL(tcp::socket socket, ssl::context& ctx)
: socket_(std::move(socket), ctx) {}
void iniciar()
{
auto self = shared_from_this();
// Handshake assíncrono como servidor
socket_.async_handshake(ssl::stream_base::server,
[self](const asio::error_code& ec) {
if (!ec) self->ler();
});
}
private:
void ler()
{
auto self = shared_from_this();
socket_.async_read_some(asio::buffer(dados_),
[self](const asio::error_code& ec, std::size_t n) {
if (!ec) self->escrever(n);
});
}
void escrever(std::size_t n)
{
auto self = shared_from_this();
asio::async_write(socket_, asio::buffer(dados_, n),
[self](const asio::error_code& ec, std::size_t) {
if (!ec) self->ler();
});
}
ssl::stream<tcp::socket> socket_;
char dados_[1024];
};
int main()
{
asio::io_context ctx_io;
ssl::context ctx_ssl(ssl::context::tls_server);
ctx_ssl.set_options(
ssl::context::default_workarounds
| ssl::context::no_sslv2
| ssl::context::no_sslv3
| ssl::context::single_dh_use);
ctx_ssl.use_certificate_chain_file("server.crt");
ctx_ssl.use_private_key_file("server.key", ssl::context::pem);
ctx_ssl.use_tmp_dh_file("dh2048.pem");
tcp::acceptor acceptor(ctx_io, {tcp::v4(), 443});
std::function<void()> aceitar;
aceitar = [&]() {
acceptor.async_accept(
[&](const asio::error_code& ec, tcp::socket socket) {
if (!ec)
std::make_shared<SessaoSSL>(std::move(socket), ctx_ssl)->iniciar();
aceitar(); // aceita a próxima conexão
});
};
aceitar();
ctx_io.run();
return 0;
}
TLS 1.3
O TLS 1.3 está disponível no Asio através do OpenSSL 1.1.1 ou superior. Para restringir a versões modernas:
ssl::context ctx(ssl::context::tls);
// Permitir apenas TLS 1.2 e 1.3 (desabilitar versões antigas)
ctx.set_options(
ssl::context::no_sslv2
| ssl::context::no_sslv3
| ssl::context::no_tlsv1
| ssl::context::no_tlsv1_1);
Dica: TLS 1.3 oferece handshake mais rápido (1-RTT e até 0-RTT para reconexões), forward secrecy obrigatório e remoção de algoritmos criptográficos obsoletos. Prefira TLS 1.3 sempre que possível.
Compilando com suporte a SSL
# Standalone Asio com OpenSSL
g++ -std=c++20 servidor.cpp -o servidor -pthread -lssl -lcrypto -DASIO_STANDALONE
# Boost.Asio com OpenSSL
g++ -std=c++20 servidor.cpp -o servidor -pthread -lssl -lcrypto -lboost_system
Conexão Serial
A conexão serial é um tipo de conexão de comunicação que permite que dois dispositivos se comuniquem por meio de uma porta serial. A porta serial é uma interface física que permite que um dispositivo envie e receba dados por meio de um par de fios. Ela é comumente usada para se comunicar com dispositivos externos, como Arduinos, dispositivos de comunicação industrial e dispositivos de automação.
Asio
A biblioteca Asio inclui suporte para trabalhar com portas seriais, o que permite aos programadores escrever aplicativos que se comunicam com dispositivos seriais, como Arduino e dispositivos de comunicação industrial. Isso é útil quando é preciso enviar ou receber dados de um dispositivo por meio de uma porta serial, como em aplicativos de automação industrial ou em projetos de robótica.
Para usar a biblioteca Asio para trabalhar com portas seriais, é necessário incluir o cabeçalho #include <boost/asio.hpp> no início do seu código. Em seguida, é preciso criar uma instância de um objeto serial_port, que representa a porta serial a ser usada, e passar a ela as informações sobre a porta, como o nome da porta (por exemplo, "COM1" no Windows ou "/dev/ttyUSB0" no Linux) e a taxa de transmissão (baud rate).
Aqui está um exemplo de código que abre uma porta serial e envia uma mensagem pelo Arduino:
#include <boost/asio.hpp>
#include <iostream>
namespace asio = boost::asio;
int main() {
// Cria um objeto boost::asio::io_context para gerenciar as operações de entrada/saída.
asio::io_context io;
// Cria um objeto boost::asio::serial_port para representar a porta serial.
asio::serial_port serial(io, "/dev/ttyUSB0");
// Configura a porta serial com a taxa de transmissão desejada.
serial.set_option(asio::serial_port_base::baud_rate(9600));
// Envia uma mensagem pelo Arduino.
asio::write(serial, asio::buffer("Hello, Arduino!\n"));
return 0;
}
Além de enviar e receber dados, você também pode configurar várias opções da porta serial usando a biblioteca Asio. Por exemplo, você pode definir o número de bits de dados, a paridade e o número de bits de parada usando as opções serial_port_base::character_size, serial_port_base::parity e serial_port_base::stop_bits, respectivamente. Aqui está um exemplo de como fazer isso:
serial.set_option(boost::asio::serial_port_base::character_size(8));
serial.set_option(boost::asio::serial_port_base::parity(boost::asio::serial_port_base::parity::none));
serial.set_option(boost::asio::serial_port_base::stop_bits(boost::asio::serial_port_base::stop_bits::one));
Você também pode definir o modo de fluxo de dados da porta serial usando as opções serial_port_base::flow_control. Por exemplo, para habilitar o controle de fluxo hardware (RTS/CTS), você pode usar o seguinte código:
serial.set_option(boost::asio::serial_port_base::flow_control(boost::asio::serial_port_base::flow_control::hardware));
Além disso, a biblioteca permite que você trate eventos de interrupção da porta serial usando a classe serial_port_service. Isso é útil quando você precisa ser notificado quando a porta serial for interrompida, por exemplo, quando um dispositivo conectado à porta envia um sinal de interrupção. Para usar essa funcionalidade, você precisa criar um objeto serial_port_service e registrar um manipulador de eventos de interrupção usando a função async_wait_for_interrupt(). Aqui está um exemplo de como fazer isso:
#include <boost/asio/serial_port_service.hpp>
// Define uma função de callback para ser chamada quando a interrupção ocorrer.
void interrupt_callback(const boost::system::error_code& error) {
if (!error) {
// A interrupção ocorreu. Faça alguma coisa aqui.
std::cout << "Interrupt received!" << std::endl;
}
}
int main() {
// Cria um objeto boost::asio::io_context para gerenciar as operações de entrada/saída.
boost::asio::io_context io;
// Cria um objeto boost::asio::serial_port para representar a porta serial.
boost::asio::serial_port serial(io, "/dev/ttyUSB0");
// Cria um objeto boost::asio::serial_port_service para tratar eventos de interrupção da porta serial
boost::asio::serial_port_service serial_service(io);
// Registra o manipulador de eventos de interrupção.
serial_service.async_wait_for_interrupt(serial, interrupt_callback);
// Executa o loop de eventos da biblioteca Asio. Isso fará com que o manipulador de interrupção seja chamado quando a interrupção ocorrer.
io.run();
}
Essa é uma maneira de tratar eventos de interrupção da porta serial usando a biblioteca Asio. Note que você precisa executar o loop de eventos (chamando a função io_context::run()) para que os manipuladores de eventos sejam chamados quando os eventos ocorrerem.
Exemplos
Cliente NTP
Nota: Nos formatos da data e o timestamp, a base era 0, resultando inicialmente no seguinte horário: 0h de 1 de janeiro de 1900 UTC, quando todos os bits são zero.
Referência: RFC 5902
Para exibir o horário atual, precisará alterar o timestamp!
#include <array>
#include <boost/asio.hpp>
#include <iostream>
#include <chrono>
namespace asio = boost::asio;
using asio::ip::udp;
int main(int argc, char *argv[]) {
try {
if (argc != 2) {
std::cerr << "Usage: ntp_client <host>" << std::endl;
return 1;
}
asio::io_context io_context;
udp::resolver resolver(io_context);
udp::endpoint receiver_endpoint =
*resolver.resolve(udp::v4(), argv[1], "123").begin();
udp::socket socket(io_context);
socket.open(udp::v4());
std::array<char, 48> send_buf = {0x1b, 0, 0, 0, 0, 0, 0, 0, 0};
socket.send_to(asio::buffer(send_buf), receiver_endpoint);
std::array<char, 48> recv_buf;
udp::endpoint sender_endpoint;
size_t len = socket.receive_from(asio::buffer(recv_buf), sender_endpoint);
std::cout << "received " << len << " bytes from " << sender_endpoint
<< std::endl;
// Extrair o NTP timestamp da resposta (do servidor)
unsigned long long int ntp_timestamp =
(unsigned long long int)(recv_buf[40]) << 24 |
(unsigned long long int)(recv_buf[41]) << 16 |
(unsigned long long int)(recv_buf[42]) << 8 |
(unsigned long long int)(recv_buf[43]);
// Converter o timestamp para std::chrono::system_clock::time_point
std::chrono::system_clock::time_point time_point =
std::chrono::system_clock::time_point(
std::chrono::seconds(ntp_timestamp - 2208988800ull));
// ntp_timestamp - unix_timestamp
// Converter o time_point para std::time_t e exibir na tela
std::time_t time = std::chrono::system_clock::to_time_t(time_point);
std::cout << std::ctime(&time) << std::endl;
} catch (std::exception &e) {
std::cerr << e.what() << std::endl;
}
return 0;
}
DNS resolver (ShowMeIP)
Nota: A classe
asio::ip::tcp::resolver::queryfoi depreciada no Asio moderno. Useresolver.resolve(host, serviço)diretamente.
#include <iostream>
#include <string>
#include <asio.hpp>
int main(int argc, char* argv[])
{
// Verificar o número de argumentos
if (argc != 2) {
std::cerr << "Usage: showip hostname" << std::endl;
return 1;
}
// Descobrir o endereço IP por baixo do link mencionado no argv[1]
asio::io_context io_context;
asio::ip::tcp::resolver resolver(io_context);
// Uso moderno: resolve(host, serviço) - resolver::query está depreciado
auto results = resolver.resolve(argv[1], "");
// Iterar todos os IPs detectados e exibi-los na tela.
std::cout << "IP addresses for " << argv[1] << ":" << std::endl << std::endl;
for (const auto& result : results) {
std::cout << " " << result.endpoint().address().to_string() << std::endl;
}
return 0;
}
QuickSort com Corrotinas
Referência: Zap/Cpp benchmark - An asynchronous runtime with a focus on performance and resource efficiency.
#include <algorithm>
#include <asio.hpp>
#include <chrono>
#include <iostream>
#include <random>
#include <vector>
using namespace std::chrono;
using asio::awaitable;
using asio::co_spawn;
using asio::detached;
awaitable<void> quickSort(asio::io_context &ctx,
std::vector<int>::iterator begin,
std::vector<int>::iterator end) {
if (std::distance(begin, end) <= 32) {
// Use std::sort for small inputs
std::sort(begin, end);
} else {
auto pivot = begin + std::distance(begin, end) - 1;
auto i = std::partition(begin, pivot, [=](int x) { return x <= *pivot; });
std::swap(*i, *pivot);
co_await quickSort(ctx, begin, i);
co_await quickSort(ctx, i + 1, end);
}
co_return;
}
void shuffle(std::vector<int> &arr) {
std::mt19937 rng(std::random_device{}());
std::shuffle(std::begin(arr), std::end(arr), rng);
}
int main() {
std::vector<int> arr(10'000'000);
std::cout << "filling" << std::endl;
std::iota(std::begin(arr), std::end(arr), 0);
std::cout << "shuffling" << std::endl;
shuffle(arr);
std::cout << "running" << std::endl;
const int num_threads = std::thread::hardware_concurrency();
asio::io_context ctx{num_threads};
const auto start = high_resolution_clock::now();
co_spawn(
ctx,
[&]() -> awaitable<void> {
co_await quickSort(ctx, std::begin(arr), std::end(arr));
},
detached);
// Run the io_context to process the posted tasks
ctx.run();
const auto elapsed =
duration_cast<milliseconds>(high_resolution_clock::now() - start);
std::cout << "took " << elapsed.count() << "ms" << std::endl;
if (!is_sorted(std::begin(arr), std::end(arr))) {
throw std::runtime_error("array not sorted");
}
}
Servidor TCP com WolfSSL (base)
Nota: Apenas ilustrativo. Requer aprimoramento complementar!
#include <iostream>
#include <string>
#include <boost/asio.hpp>
#include <wolfssl/ssl.h>
namespace asio = boost::asio;
using asio::ip::tcp;
class wolfSSL_context
{
public:
wolfSSL_context(asio::io_context& io_context,
asio::ssl::context::method method)
: context_(io_context, method)
{
context_.set_options(
asio::ssl::context::default_workarounds
| asio::ssl::context::no_sslv2
| asio::ssl::context::single_dh_use);
// Utilizar os certificados.
context_.use_certificate_chain_file("server.crt");
context_.use_private_key_file("server.key", asio::ssl::context::pem);
context_.use_tmp_dh_file("dh2048.pem");
}
asio::ssl::context& context()
{
return context_;
}
private:
asio::ssl::context context_;
};
class wolfSSL_stream
: public asio::ssl::stream<tcp::socket>
{
public:
wolfSSL_stream(asio::io_context& io_context, wolfSSL_context& context)
: asio::ssl::stream<tcp::socket>(io_context, context.context())
{
}
};
class wolfSSL_server
{
public:
wolfSSL_server(asio::io_context& io_context,
unsigned short port)
: acceptor_(io_context, tcp::endpoint(tcp::v4(), port)),
context_(io_context, asio::ssl::context::tlsv12)
{
start_accept();
}
private:
void start_accept()
{
wolfSSL_stream new_stream(acceptor_.get_io_context(), context_);
acceptor_.async_accept(new_stream.next_layer(),
std::bind(&wolfSSL_server::handle_accept, this,
std::placeholders::_1,
std::move(new_stream)));
}
void handle_accept(const asio::error_code& error,
wolfSSL_stream stream)
{
if (!error)
{
stream.handshake(asio::ssl::stream_base::server);
// Executar o Handshake com SSL/TLS e ler os dados do cliente.
// ...
start_accept();
}
}
tcp::acceptor acceptor_;
wolfSSL_context context_;
};
int main()
{
try
{
asio::io_context io_context;
wolfSSL_server server(io_context, 443);
io_context.run();
}
catch (std::exception& e)
{
std::cerr << e.what() << std::endl;
}
return 0;
}
Noise-C com Asio
Cliente:
#include <iostream>
#include <array>
#include <boost/asio.hpp>
#include <noise/protocol.h>
#include <noise/handshake.h>
using boost::asio::ip::tcp;
int main(int argc, char *argv[]) {
// Declara as chaves públicas estáticas local e remota
std::array<uint8_t, NOISE_PUBLIC_KEY_LEN> local_static_public_key;
local_static_public_key.fill(0x55);
std::array<uint8_t, NOISE_PUBLIC_KEY_LEN> remote_static_public_key;
remote_static_public_key.fill(0xAA);
// Declara as chaves públicas efêmeras local e remota
std::array<uint8_t, NOISE_PUBLIC_KEY_LEN> local_ephemeral_public_key;
std::array<uint8_t, NOISE_PUBLIC_KEY_LEN> remote_ephemeral_public_key;
// Declara as chaves privadas efêmeras local e remota
std::array<uint8_t, NOISE_PUBLIC_KEY_LEN> local_ephemeral_private_key;
std::array<uint8_t, NOISE_PUBLIC_KEY_LEN> remote_ephemeral_private_key;
// Declara o estado da negociação de chave
NoiseHandshakeState *state = 0;
// Inicializa o estado da negociação de chave
int result = noise_handshakestate_new_by_name(
&state, "Noise_NN_25519_ChaChaPoly_BLAKE2s", 0);
if (result != NOISE_ERROR_NONE) {
std::cerr << "Error initializing handshake state" << std::endl;
return 1;
}
// Define as chaves públicas estáticas local e remota
noise_handshakestate_set_local_static_public_key(state, local_static_public_key.data(), local_static_public_key.size());
noise_handshakestate_set_remote_static_public_key(state, remote_static_public_key.data(), remote_static_public_key.size());
// Gera o par de chaves efêmeras local
result = noise_handshakestate_generate_local_keypair(state, local_ephemeral_private_key.data(), local_ephemeral_private_key.size());
if (result != NOISE_ERROR_NONE) {
std::cerr << "Error generating local ephemeral key pair" << std::endl;
return 1;
}
noise_handshakestate_get_local_public_key(state, local_ephemeral_public_key.data(), local_ephemeral_public_key.size());
// Cria um objeto boost::asio::io_context
boost::asio::io_context io_context;
// Cria um objeto boost::asio::ip::tcp::socket
tcp::socket socket(io_context);
// Conecta ao servidor
boost::asio::connect(socket, tcp::resolver(io_context).resolve({ "localhost", "1234" }));
// Envia a chave pública efêmera local ao servidor
boost::asio::write(socket, boost::asio::buffer(local_ephemeral_public_key));
// Recebe a chave pública efêmera remota do servidor
boost::asio::read(socket, boost::asio::buffer(remote_ephemeral_public_key));
noise_handshakestate_set_remote_ephemeral_public_key(state, remote_ephemeral_public_key.data(), remote_ephemeral_public_key.size());
// Gera a chave privada efêmera remota
result = noise_handshakestate_generate_remote_key(state, remote_ephemeral_private_key.data(), remote_ephemeral_private_key.size());
if (result != NOISE_ERROR_NONE) {
std::cerr << "Error generating remote ephemeral private key" << std::endl;
return 1;
}
// Realiza a negociação de chave (handshake)
size_t message_len;
std::array<uint8_t, MAX_HANDSHAKE_MESSAGE_LEN> message;
result = noise_handshakestate_start(state, message.data(), &message_len);
if (result != NOISE_ERROR_NONE) {
std::cerr << "Error starting handshake" << std::endl;
return 1;
}
// Envia a mensagem ao servidor
boost::asio::write(socket, boost::asio::buffer(message, message_len));
// Recebe a resposta do servidor
boost::asio::read(socket, boost::asio::buffer(remote_ephemeral_public_key));
result = noise_handshakestate_write_message(state, remote_ephemeral_public_key.data(), remote_ephemeral_public_key.size(), message.data(), &message_len);
if (result != NOISE_ERROR_NONE) {
std::cerr << "Error writing handshake message" << std::endl;
return 1;
}
// Envia a mensagem ao servidor
boost::asio::write(socket, boost::asio::buffer(message, message_len));
// Recebe a resposta do servidor
boost::asio::read(socket, boost::asio::buffer(remote_ephemeral_public_key));
result = noise_handshakestate_read_message(state, message.data(), message_len, remote_ephemeral_public_key.data(), &remote_ephemeral_public_key.size());
if (result != NOISE_ERROR_NONE) {
std::cerr << "Error reading handshake message" << std::endl;
return 1;
}
// Libera o estado do handshake
noise_handshakestate_free(state);
// Encerra socket
socket.close();
return 0;
}
Servidor:
#include <iostream>
#include <array>
#include <boost/asio.hpp>
#include <noise/protocol.h>
#include <noise/handshake.h>
using boost::asio::ip::tcp;
int main(int argc, char *argv[]) {
// Declara as chaves públicas estáticas local e remota
std::array<uint8_t, NOISE_PUBLIC_KEY_LEN> local_static_public_key;
local_static_public_key.fill(0x55);
std::array<uint8_t, NOISE_PUBLIC_KEY_LEN> remote_static_public_key;
remote_static_public_key.fill(0xAA);
// Declara as chaves públicas efêmeras local e remota
std::array<uint8_t, NOISE_PUBLIC_KEY_LEN> local_ephemeral_public_key;
std::array<uint8_t, NOISE_PUBLIC_KEY_LEN> remote_ephemeral_public_key;
// Declara as chaves privadas efêmeras local e remota
std::array<uint8_t, NOISE_PUBLIC_KEY_LEN> local_ephemeral_private_key;
std::array<uint8_t, NOISE_PUBLIC_KEY_LEN> remote_ephemeral_private_key;
// Declara o estado da negociação de chave
NoiseHandshakeState *state = 0;
// Inicializa o estado da negociação de chave
int result = noise_handshakestate_new_by_name(
&state, "Noise_NN_25519_ChaChaPoly_BLAKE2s", 0);
if (result != NOISE_ERROR_NONE) {
std::cerr << "Error initializing handshake state" << std::endl;
return 1;
}
// Define as chaves públicas estáticas local e remota
noise_handshakestate_set_local_static_public_key(state, local_static_public_key.data(), local_static_public_key.size());
noise_handshakestate_set_remote_static_public_key(state, remote_static_public_key.data(), remote_static_public_key.size());
// Gera o par de chaves efêmeras local
result = noise_handshakestate_generate_local_keypair(state, local_ephemeral_private_key.data(), local_ephemeral_private_key.size());
if (result != NOISE_ERROR_NONE) {
std::cerr << "Error generating local ephemeral key pair" << std::endl;
return 1;
}
noise_handshakestate_get_local_public_key(state, local_ephemeral_public_key.data(), local_ephemeral_public_key.size());
// Cria um objeto boost::asio::io_context
boost::asio::io_context io_context;
// Cria um objeto boost::asio::ip::tcp::acceptor
tcp::acceptor acceptor(io_context, tcp::endpoint(tcp::v4(), 1234));
// Aguarda uma conexão do cliente
tcp::socket socket(io_context);
acceptor.accept(socket);
// Recebe a chave pública efêmera local do cliente
boost::asio::read(socket, boost::asio::buffer(remote_ephemeral_public_key));
noise_handshakestate_set_remote_ephemeral_public_key(state, remote_ephemeral_public_key.data(), remote_ephemeral_public_key.size());
// Gera o par de chaves efêmeras local
result = noise_handshakestate_generate_local_keypair(state, local_ephemeral_private_key.data(), local_ephemeral_private_key.size());
if (result != NOISE_ERROR_NONE) {
std::cerr << "Error generating local ephemeral private key" << std::endl;
return 1;
}
noise_handshakestate_get_local_public_key(state, local_ephemeral_public_key.data(), local_ephemeral_public_key.size());
// Envia a chave pública efêmera local ao cliente
boost::asio::write(socket, boost::asio::buffer(local_ephemeral_public_key));
// Realiza a negociação de chave (handshake)
size_t message_len;
std::array<uint8_t, MAX_HANDSHAKE_MESSAGE_LEN> message;
// Recebe a primeira mensagem do cliente
boost::asio::read(socket, boost::asio::buffer(message));
result = noise_handshakestate_read_message(state, message.data(), message.size(), remote_ephemeral_public_key.data(), &remote_ephemeral_public_key.size());
if (result != NOISE_ERROR_NONE) {
std::cerr << "Error to read first handshake message" << std::endl;
return 1;
}
result = noise_handshakestate_write_message(state, remote_ephemeral_public_key.data(), remote_ephemeral_public_key.size(), message.data(), &message_len);
if (result != NOISE_ERROR_NONE) {
std::cerr << "Error to write first handshake message" << std::endl;
return 1;
}
// Envia a primeira mensagem ao cliente
boost::asio::write(socket, boost::asio::buffer(message, message_len));
// Recebe a segunda mensagem do cliente
boost::asio::read(socket, boost::asio::buffer(message));
result = noise_handshakestate_read_message(state, message.data(), message.size(), remote_ephemeral_public_key.data(), &remote_ephemeral_public_key.size());
if (result != NOISE_ERROR_NONE) {
std::cerr << "Error to read second handshake message" << std::endl;
return 1;
}
result = noise_handshakestate_write_message(state, remote_ephemeral_public_key.data(), remote_ephemeral_public_key.size(), message.data(), &message_len);
if (result != NOISE_ERROR_NONE) {
std::cerr << "Error to write first handshake message" << std::endl;
return 1;
}
// Envia a segunda mensagem ao cliente
boost::asio::write(socket, boost::asio::buffer(message, message_len));
// Gera a chave privada efêmera remota
result = noise_handshakestate_generate_remote_key(state, remote_ephemeral_private_key.data(), remote_ephemeral_private_key.size());
if (result != NOISE_ERROR_NONE) {
std::cerr << "Error generating remote ephemeral private key" << std::endl;
return 1;
}
// Libera o estado do handshake
noise_handshakestate_free(state);
// Encerra socket
socket.close();
return 0;
}
Conclusão 🎉
Espero que tenha esclarecido o uso da programação de rede com Asio para você. Definitivamente, este pequeno livro apenas introduz a idéia básica. Para melhorar sua habilidade de codificação, você precisa ler mais a documentação junto com o código-fonte e praticar mais. 👀
Bons estudos e divirta-se praticando! 😉
Como contribuir
Para contribuir, basta abrir uma issue no repositório do projeto, explicando como exatamente você poderia contribuir. Exemplos e sugestões de possíveis contribuições incluem:
- Revisão: Precisamos tanto de revisões técnicas quanto de português. Sinalize que você deseja ser um revisor abrindo uma issue falando do seu interesse!
- Escrita: Abra uma issue ou PR com um capítulo, seção ou parágrafo de exemplo em qualquer parte do livro, e explique por que ela é relevante.
- Infraestrutura: O livro atualmente está escrito totalmente em markdown. Sugestões e ajuda para melhorar a infraestrutura de leitura é bem vinda.
- Sugestões de conteúdo: Abra uma issue contando por que você acha que o novo conteúdo deveria estar presente no livro.
Licença
O material disponível diretamente neste repositório está sob licença CC-0.
Contribuinte(s)
Meus agradecimentos são para:
- Christopher M. Kohlhoff - Autor do Asio
- Nan Xiao - Autor do projeto original (boost-asio network programming - little book)