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
yield
repassa 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
Inicialmente existe duas versões disponíveis para download:
-
Asio Standalone (sem boost) - Recomendação: C++11[
std::system_error
] ou posterior; -
Boost Asio (normalmente mais utilizado).
-
Networking-TS - Baseado no asio/boost::asio, proposto pela ISO para standard library
std::net
.
Instalar Asio não é difícil. Pois ele possui apenas arquivos headers.
Os exemplos abaixo citarão a instalação do boost::asio
.
Linux
Ubuntu
:
$ sudo apt-get install boost
Arch Linux
:
$ sudo pacman -S boost
No linux para compilar um programa, o parâmetro -pthread
se faz necessário:
$ g++ client.cpp -o client -pthread -lboost_system
BSD
OpenBSD
:
$ pkg_add boost
Quando estiver compilando um programa, favor vincular as bibliotecas boost
.
Ex.:
$ c++ -L/usr/local/lib client.cpp -o client -lboost_system
Windows
Se você utiliza MSVC, então poderá optar por diversas opções:
-
Baixar pré-compiladas: Sourceforge
-
Usar
vcpkg
com o seguinte parâmetro, ex.:x86:
vcpkg install boost:x86-windows
ou
x64:
vcpkg install boost:x64-windows
-
Usar
conan
(Requer: python) com o seguinte parâmetro, ex.:Versão [1.72]:
conan install Boost/1.72.0@bincrafters/stable
Nota: Para que a instalação com conan funcione neste repositório precisará utilizar este comando antes:
conan remote add bincrafters https://api.bintray.com/conan/bincrafters/public-conan
Para compilar no Windows usando MSVC no prompt de comando use:
cl /EHsc /I C:\Program Files\boost\boost_1_72_0 example.cpp /link /LIBPATH:C:\Program Files\boost\boost_1_72_0\lib
Caso queira utilizar MinGW(Minimal GNU for Windows) terá duas opções:
-
x86:
$ pacman -S mingw-w64-i386-boost
x86_64:
$ pacman -S mingw-w64-x86_64-boost
Nota: Por mais que parece ser um ambiente linux (minimalista), não requer uso do
sudo
na instalação -
Compilar manualmente seguindo a documentação boost: Windows - Getting Start
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 de work, que é responsável por manter o loop de eventos da asio::io_context
rodando. O loop de eventos monitora os descritores de arquivo e sockets gerenciados pela asio::io_context
e notifica o programa quando os dados estão disponíveis para leitura ou quando os dados podem ser escritos.
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
(a classe io_service
está obsoleta agora) é necessário para se comunicar com os serviços da E/S do sistema operacional. 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
{
......
}
Observe a classe noncopyable
:
class noncopyable
{
protected:
noncopyable() {}
~noncopyable() {}
private:
noncopyable(const noncopyable&);
const noncopyable& operator=(const noncopyable&);
};
Isso significa que o objeto io_context
não pode ser utilizado como copy constructed/copy assignment/move constructed/move assignment. 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.:
template <typename Protocol
BOOST_ASIO_SVC_TPARAM_DEF1(= datagram_socket_service<Protocol>)>
class basic_datagram_socket
: public basic_socket<Protocol BOOST_ASIO_SVC_TARG>
{
public:
......
explicit basic_datagram_socket(boost::asio::io_context& io_context)
: basic_socket<Protocol BOOST_ASIO_SVC_TARG>(io_context)
{
}
......
}
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, permitindo que o programa interrompa tarefas que não são mais necessárias.
Outra funcionalidade útil da asio::io_context
é a capacidade de escalonar operações em vários threads. Isso é útil em situações em que é necessário realizar várias operações de entrada e saída ao mesmo tempo e é importante aproveitar ao máximo o poder de processamento do computador. 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.
Em resumo, a asio::io_context
é uma classe fundamental do Asio que é usada para gerenciar operações de entrada e saída assíncronas em um programa de computador. Ela monitora e gerencia 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. Além disso, a asio::io_context
fornece uma série de outras funcionalidades úteis, como agendamento de operações para serem executadas em um momento futuro, cancelamento de operações em andamento e escalonamento de operações em vários threads.
A seguir, estão descritas todas as funções comuns do asio::io_context
:
-
run
: A função run é usada para iniciar o loop de eventos doasio::io_context
. Ela bloqueia o thread atual até que todas as operações agendadas tenham sido concluídas ou oasio::io_context
seja interrompido. -
poll
: A função poll é similar à função run, mas não bloqueia o thread atual. Em vez disso, ela processa todas as operações pendentes noasio::io_context
e retorna imediatamente. -
run_one
: A função run_one é similar à função run, mas processa apenas uma operação pendente noasio::io_context
antes de retornar. -
stop
: A função stop é usada para interromper o loop de eventos doasio::io_context
. Isso é útil em situações em que o programa precisa sair do loop de eventos antes que todas as operações pendentes tenham sido concluídas. -
reset
: A função reset é usada para reiniciar oasio::io_context
. Isso é útil em situações em que o programa precisa começar a processar operações pendentes novamente após ter sido interrompido. -
poll_one
: A função poll_one é similar à função poll, mas processa apenas uma operação pendente no asio::io_context antes de retornar. -
poll_one_at
: A função poll_one_at é similar à função poll_one, mas retorna após o tempo especificado, mesmo se não houver operações pendentes para processar. -
stopped
: A função stopped retorna true se oasio::io_context
foi interrompido e false caso contrário. -
get_executor
: A função get_executor retorna um objeto executor que pode ser usado para agendar operações para serem executadas noasio::io_context
. -
dispatch
: A função dispatch é usada para agendar uma operação para ser executada de forma síncrona noasio::io_context
. Isso garante que a operação seja executada imediatamente, sem esperar por outras operações pendentes. -
post
: A função post é usada para agendar uma operação para ser executada de forma assíncrona noasio::io_context
. Isso permite que o programa continue rodando enquanto a operação é executada em segundo plano. -
work
: Isso informa aoasio::io_context
que há operações pendentes e garante que o loop de eventos continue rodando, mesmo quando não há operações pendentes. -
work_guard
: A classeasio::work_guard
é usada para gerenciar um objetoasio::work
adicionado aoasio::io_context
. Ela garante que o objetoasio::work
não seja removido doasio::io_context
enquanto o objeto asio::work_guard estiver em uso. Quando o objetoasio::work_guard
é destruído, o objetoasio::work
é removido doasio::io_context
e o loop de eventos pode parar, se não houver mais operações pendentes para processar. Além de gerenciar o objetoasio::work
,asio::work_guard
também fornece uma série de outras funcionalidades úteis. Por exemplo, é possível usar a função reset para adicionar um novo objetoasio::work
aoasio::io_context
, mesmo se o objeto asio::work_guard já estiver gerenciando um objetoasio::work
. Além disso, é possível usar a função get_executor para obter um objeto executor que pode ser usado para agendar operações para serem executadas noasio::io_context
.
Em resumo, asio::work_guard
é uma classe do Asio que é usada para gerenciar um objeto asio::work
adicionado ao asio::io_context
. Ela garante que o objeto asio::work
não seja removido do asio::io_context
enquanto o objeto asio::work_guard
estiver em uso, garantindo assim que o loop de eventos continue rodando, mesmo quando não há operações pendentes para processar. Além disso, a asio::work_guard
fornece uma série de outras funcionalidades úteis, como a possibilidade de adicionar um novo objeto asio::work
ao asio::io_context
e de obter um objeto executor para agendar operações para serem executadas no asio::io_context
. asio::work_guard
é especialmente útil em situações em que o programa precisa garantir que o asio::io_context continue rodando por um período prolongado de tempo, mesmo quando não há operações pendentes para processar. Isso é comum em programas que usam o Asio para implementar serviços de rede, como servidores web ou servidores de banco de dados.
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::pair
nas 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
Em 6 de julho de 2021, a proposta dos Executores foi atualizada com mais um bilhão de pontos. O novo documento, P2300,
oficialmente denominado std::execution
, em comparação com The Unified Executor for C++, P0443R14,
expõe mais sistematicamente as ideias de design dos Executors; dá mais instruções sobre implementação.
A biblioteca Executors praticada pelo autor em seu tempo livre acaba de concluir o conteúdo do P1879R3.
Unified Executors
propõe que o namespace std::execution
do C++ Standard Library que visa fornecer uma forma mais flexível e genérica de trabalhar com Executors. A proposta foi apresentada no Grupo de Trabalho 21 (WG21) do Comitê de Padrões do C++ como a Proposta de Padrão P1907R0.
Atualmente, o namespace std::execution
fornece vários tipos de Executors, como std::execution::sequenced_policy
e std::execution::parallel_policy
, que podem ser usados para controlar como as tarefas são agendadas e executadas. No entanto, esses Executors são bastante rígidos e não permitem muita flexibilidade na customização da forma como as tarefas são agendadas e executadas.
A proposta Unified Executors
visa fornecer uma forma mais flexível de trabalhar com Executors, permitindo que os programadores criem seus próprios Executors personalizados de acordo com suas necessidades específicas. Isso seria feito através da introdução de novos tipos e funções no namespace std::execution
, como std::execution::uniform_invocable
e std::execution::execute
, que permitiriam a criação de Executors personalizados de forma mais fácil e rápida.
A proposta de universal executors ainda está em fase de discussão no Grupo de Trabalho 21 (WG21) e ainda não foi adotada como parte do C++ Standard. No entanto, se aprovada, ela pode ser uma adição importante ao C++ Standard
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étodosetException
para 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 comothen
eonError
que 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_context
e 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
std::execution
é um namespace do C++ Standard Library que fornece tipos e funções relacionados à execução de tarefas assíncronas. Ele foi introduzido no C++17 e ampliado no C++20 para fornecer uma interface padronizada para a execução de tarefas assíncronas em diferentes plataformas e bibliotecas de tempo de execução.
O conceito de Executor é uma parte importante do namespace std::execution
. Ele é um tipo de modelo de classe que define uma interface para a execução de tarefas assíncronas. Um Executor é responsável por agendar tarefas para serem executadas em um determinado ponto no tempo, permitindo que o código assíncrono seja escrito de forma mais simples e clara.
O conceito de Executor é importante porque ele permite que você escreva código assíncrono de forma mais genérica, 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. Isso torna o código mais portável e facilita a manutenção e a expansão do código no futuro.
O namespace std::execution
fornece vários tipos de Executor, como std::execution::sequenced_policy
e std::execution::parallel_policy
, que podem ser usados para controlar como as tarefas são agendadas e executadas. Além disso, ele fornece funções como std::execution::execute
e std::execution::bulk_execute
, que podem ser usadas para executar tarefas de forma assíncrona de acordo com o Executor especificado.
Em resumo, std::execution
é um namespace do C++ Standard Library que fornece uma interface padronizada para a execução de tarefas assíncronas em diferentes plataformas e bibliotecas de tempo de execução, enquanto que Asio é uma biblioteca de tempo de execução que oferece recursos para criar aplicações de rede de forma assíncrona. Asio pode ser usado com o namespace std::execution
, mas também pode ser usado de forma independente. A escolha da biblioteca a ser usada depende das necessidades específicas de sua aplicação e de suas preferências de programação.
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 é uma alternativa a outras bibliotecas de tempo de execução para C++, como Asio e Libunifex, que também oferecem suporte para a programação assíncrona, porém com o uso de corrotinas ao invés de executores. Ela permite que você escreva código assíncrono de forma mais simples e clara, usando a sintaxe de corrotinas do C++20 STL.
Cppcoro usa a funcionalidade de coroutinas introduzida no C++20 para permitir que você escreva código assíncrono de forma mais fácil e natural. Ele fornece uma série de funções e tipos de dados que permitem que você crie, gerencie e execute coroutinas de forma mais eficiente. Além disso, ele fornece suporte para a execução de coroutinas em paralelo, o que pode ser útil em aplicações de alta performance.
Em resumo, Asio é uma biblioteca de tempo de execução que fornece recursos para criar aplicações de rede de forma assíncrona, enquanto que Libunifex é uma biblioteca de executores que oferece uma interface uniforme para trabalhar com diferentes bibliotecas de tempo de execução de forma mais portável.
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 uma referência no construtor do socket
(consulte io_context). Ainda use basic_socket
e uma instância, um de seus construtores é o seguinte:
explicit basic_socket(boost::asio::io_context& io_context)
: basic_io_object<BOOST_ASIO_SVC_T>(io_context)
{
}
Para a classe basic_io_object
, ele não suporta copy constructed/copy assignment:
......
private:
basic_io_object(const basic_io_object&);
void operator=(const basic_io_object&);
......
mas suporta move constructed/move assignment:
......
protected:
basic_io_object(basic_io_object&& other)
{
......
}
basic_io_object& operator=(basic_io_object&& other)
{
......
}
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_context
de 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
strand
implícito. -
Um
strand
explí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,
strands
podem 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 da chamada de rede. Isso permite que o programa execute outras tarefas enquanto aguarda a resposta, em vez de ficar "bloqueado" aguardando a conclusão da chamada de rede.
Como usar corrotinas no C++
No C++, existem duas maneiras de criar corrotinas: a primeira é usando a biblioteca Asio, e a segunda é usando o recurso std::coroutine
, que foi adicionado ao C++20. As corrotinas <coroutine>
são baseadas em uma nova sintaxe de linguagem do C++ e são mais fáceis de usar do que as corrotinas Asio, mas elas ainda são um recurso relativamente novo e podem não estar disponíveis em todas as implementações do C++.
Em resumo, as corrotinas Asio são uma maneira de criar e executar corrotinas no C++ usando a biblioteca Asio, enquanto as corrotinas <coroutine>
são uma maneira de criar corrotinas usando uma nova sintaxe de linguagem adicionada ao C++20. Ambas as opções permitem que você crie funções assíncronas de forma fácil e eficiente.
Uma vantagem das corrotinas Asio é que elas podem ser usadas com qualquer biblioteca ou sistema que suporte a biblioteca Asio, o que significa que elas são compatíveis com uma ampla variedade de plataformas de rede. Além disso, as corrotinas Asio fornecem uma maneira de escrever código assíncrono de forma mais clara e legível, pois permitem que você escreva código síncrono que é "convertido" em código assíncrono pelo próprio Asio.
Por outro lado, as corrotinas são baseadas em uma nova sintaxe de linguagem e, por esse motivo, podem ser mais fáceis de usar do que as corrotinas Asio. Além disso, elas podem ser mais eficientes em termos de desempenho, pois elas são implementadas diretamente na linguagem C++ e não dependem de uma biblioteca externa. No entanto, elas ainda são um recurso relativamente novo e podem não estar disponíveis em todas as implementações do C++.
Em resumo, as corrotinas Asio e std::coroutine
são duas maneiras diferentes de criar e executar corrotinas no C++. As corrotinas Asio são compatíveis com uma ampla variedade de plataformas e fornecem uma maneira de escrever código assíncrono de forma mais clara, enquanto as corrotinas std::coroutine
são baseadas em uma nova sintaxe de linguagem e podem ser mais eficientes em termos de desempenho. Qual das duas opções é a melhor para você depende das suas necessidades e da plataforma em que está trabalhando.
A seguir, um exemplo de como usar corrotinas Asio para fazer uma chamada HTTP GET assíncrona usando a biblioteca Asio:
#include <iostream>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/asio/connect.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <cstdlib>
namespace beast = boost::beast; // from <boost/beast.hpp>
namespace http = beast::http; // from <boost/beast/http.hpp>
namespace net = boost::asio; // from <boost/asio.hpp>
using tcp = net::ip::tcp; // from <boost/asio/ip/tcp.hpp>
// Realiza uma chamada HTTP GET assíncrona e imprime o corpo da resposta
void async_http_get(net::io_context& ioc, const std::string& host, const std::string& target)
{
// Cria um socket TCP
tcp::resolver resolver{ioc};
beast::tcp_stream stream{ioc};
// Realiza a resolução do nome do host e conecta ao servidor
co_await resolver.async_resolve(host, "http", net::use_awaitable);
co_await stream.async_connect(resolver.results(), net::use_awaitable);
// Cria uma solicitação HTTP e envia-a para o servidor
http::request<http::string_body> req{http::verb::get, target, 11};
req.set(http::field::host, host);
req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
co_await http::async_write(stream, req, net::use_awaitable);
// Recebe a resposta do servidor
beast::flat_buffer buffer;
http::response<http::string_body> res;
co_await http::async_read(stream, buffer, res, net::use_awaitable);
// Imprime o corpo da resposta
std::cout << res << std::endl;
}
int main()
{
net::io_context ioc;
// Inicia a chamada HTTP GET assíncrona em uma corrotina
asio::co_spawn(ioc, [&] {
co_return async_http_get(ioc, "www.example.com", "/");
}, asio::detached);
// Executa o event loop
ioc.run();
return EXIT_SUCCESS;
}
Ao ter o primeiro contato com corrotinas em C++ (com asio) suponho que conhecerá novas palavras-chave que necessitará compreender, que são:
-
co_spawn
: é uma função da biblioteca Asio que permite criar e iniciar uma corrotina de forma assíncrona. Ela é usada para "lançar" uma corrotina em uma determinada contexto de E/S, permitindo que a corrotina execute tarefas assíncronas como fazer chamadas de rede ou ler e escrever em arquivos. -
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 corrotinas sejam executadas. Quando outra corrotina termina sua execução, a corrotina "pausada" é retomada a partir do ponto onde foi interrompida. -
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 é "pausada" até que a operação assíncrona seja concluída, permitindo que outras corrotinas sejam executadas enquanto aguarda. -
co_return
: é uma palavra-chave do C++ que permite que uma corrotina retorne um valor quando ela é concluída. É usado para encerrar a execução de uma corrotina e retornar o valor especificado para quem a chamou. -
detached
: é um parâmetro opcional que pode ser usado com a função co_spawn da biblioteca Asio. Ele indica que a corrotina deve ser executada de forma assíncrona e não precisa ser aguardada para concluir sua execução. Isso é útil quando você deseja que a corrotina execute uma tarefa de forma independente e não precisa saber quando ela termina.
As corrotinas Asio são uma extensão da biblioteca Asio que fornece suporte nativo para a criação e execução de corrotinas no C++. Elas permitem que você escreva código síncrono que é "convertido" em código assíncrono pelo próprio Asio, o que torna mais fácil criar funções assíncronas de forma clara e legível.
Para usar corrotinas Asio, você precisa incluir o cabeçalho <asio/co_spawn.hpp>
e usar a palavra-chave co_await para "pausar" a execução da corrotina enquanto aguarda a conclusão de uma operação assíncrona. Você também pode usar a palavra-chave co_yield
para "pausar" a execução da corrotina e permitir que outras corrotinas sejam executadas.
Além disso, as corrotinas Asio podem ser "lançadas" em um contexto de E/S usando a função co_spawn
, que permite que a corrotina execute tarefas assíncronas como fazer chamadas de rede ou ler e escrever em arquivos. Você também pode usar a função async_write
e async_read
da biblioteca Asio para escrever e ler dados de forma assíncrona, respectivamente.
No entanto, é importante lembrar que as corrotinas Asio dependem da biblioteca Asio para funcionar, o que pode afetar o desempenho em comparação com outras opções, como as corrotinas std::coroutine
, que são implementadas diretamente na linguagem C++. Qual das duas opções é a melhor para você depende das suas necessidades e da plataforma em que está trabalhando.
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 um parâmetro para uma função assíncrona e é usado para determinar como a função deve notificar o chamador quando a operação for concluída.
Existem vários tipos de completion token disponíveis no Asio, como asio::use_future
e asio::use_awaitable
. Cada um desses tipos de completion token especifica uma forma diferente de completar a operação assíncrona.
Por exemplo, o completion token asio::use_future
é usado para completar a operação assíncrona retornando um objeto std::future
que pode ser usado para obter o resultado da operação. Isso permite que o chamador da função assíncrona aguarde o término da operação de forma síncrona, usando a sintaxe de await do C++20.
O completion token asio::use_awaitable
, por outro lado, é usado para completar a operação assíncrona retornando um objeto awaitable que pode ser usado para aguardar o término da operação de forma assíncrona. Isso permite que o chamador da função assíncrona aguarde o término da operação de forma assíncrona, usando a sintaxe de await do C++20.
Em resumo, os completion tokens são usados no Asio para especificar como uma operação assíncrona deve ser completada. Eles são passados como parâmetros para funções assíncronas e são usados para determinar como a função deve notificar o chamador quando a operação for concluída. Existem vários tipos de completion token disponíveis, cada um com suas próprias características e usos específicos.
Asio Corrotina comparado com Cppcoro
O cppcoro é uma biblioteca de corrotinas para C++ que fornece primitivas para escrever código assíncrono de maneira mais simples e legível. Ela foi projetada para funcionar em conjunto com o Asio, mas também pode ser usada com outras bibliotecas de E/S ou mesmo em aplicações sem E/S. A cppcoro utiliza o padrão de corrotinas do C++20 e é compatível com compiladores que já suportam esse padrão.
Ambas as bibliotecas permitem escrever código assíncrono de maneira mais simples, mas existem algumas diferenças entre elas:
-
O Asio é uma biblioteca mais ampla que fornece suporte para várias plataformas, incluindo sistemas operacionais diferentes e dispositivos de E/S. O Cppcoro, por outro lado, é focada apenas em corrotinas e foi projetado para ser usado como extensão do
std::coroutine
, mas também pode ser utilizado em conjunto com o Asio ou outras bibliotecas de E/S assíncronas (não obrigatório) ou independentemente delas em aplicações sem E/S. Ele fornece uma série de primitivas para escrever código assíncrono de maneira mais simples e legível, como a palavra-chaveco_yield
para produzir um valor e suspender a execução da corrotina. Portanto, a cppcoro pode ser considerada mais completa e consistente no uso de corrotinas. -
O Asio é mais amplamente usado em projetos em produção, enquanto que a Cppcoro é uma biblioteca mais nova e talvez menos conhecida. No entanto, ambas são opções válidas para escrever código assíncrono em C++.
Ambos seguem como referêcia a proposta técnica P1056R0 que descreve o progresso da especificação do suporte a corrotinas no C++, incluindo a sintaxe e as funções-membro para declarar e usar corrotinas, as palavras-chave co_await
e co_yield
para suspender e produzir valores na corrotina, e as classes coroutine_handle
e coroutine_traits
para gerenciar a execução de corrotinas. Ele também descreve o suporte a corrotinas em funções assíncronas, que permitem que corrotinas sejam usadas como uma alternativa ao modelo de callback functions para realizar operações assíncronas.
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 ou a um servidor 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
, é preciso incluir o cabeçalho <asio/ip/tcp.hpp>
no seu código e criar um objeto da classe passando um objeto asio::io_context
para o construtor. Em seguida, é possível usar o método resolve para resolver o nome de domínio em um endereço IP. O método resolve retorna um iterador que pode ser usado para percorrer a lista de endereços IP retornados.
O objeto asio::ip::tcp::resolver
também fornece uma série de outras funcionalidades úteis. Por exemplo, é possível usar o método async_resolve para resolver o nome de domínio de forma assíncrona, permitindo que o programa continue rodando enquanto a resolução é realizada em segundo plano. Além disso, é possível usar o método cancel para cancelar a resolução de um nome de domínio em andamento.
Veja boost::asio::ip::tcp::resolver
no exemplo 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");
for (auto it = endpoints.cbegin(); it != endpoints.cend(); it++)
{
boost::asio::ip::tcp::endpoint endpoint = *it;
std::cout << endpoint << '\n';
}
}
catch (std::exception& e)
{
std::cerr << e.what() << '\n';
}
return 0;
}
O resultado da execução será:
74.125.24.101:443
74.125.24.139:443
74.125.24.138:443
74.125.24.102:443
74.125.24.100:443
74.125.24.113:443
O elemento boost::asio::ip::tcp::resolver::results_type
é o iterador de basic_resolver_entry
:
template <typename InternetProtocol>
class basic_resolver_entry
{
......
public:
/// The protocol type associated with the endpoint entry.
typedef InternetProtocol protocol_type;
/// The endpoint type associated with the endpoint entry.
typedef typename InternetProtocol::endpoint endpoint_type;
......
/// Convert to the endpoint associated with the entry.
operator endpoint_type() const
{
return endpoint_;
}
......
}
Como ele possui o operador endpoint_type()
, ele pode ser convertido diretamente no endpoint:
boost::asio::ip::tcp::endpoint endpoint = *it;
Em resumo, o objeto asio::ip::tcp::resolver
do Asio é usado para resolver nomes de domínio em endereços IP. Ele fornece uma série de funcionalidades úteis, como a possibilidade de resolver o nome de domínio de forma assíncrona e de cancelar a resolução de um nome de domínio em andamento.
Exceção
O Asio fornece uma série de exceções de erro que podem ser lançadas em situações em que ocorrem erros durante a execução de operações de rede. Essas exceções são derivadas da classe boost::system::system_error
e incluem:
boost::asio::error::address_family_not_supported
: lançada quando o tipo de endereço especificado não é suportado pela plataforma.boost::asio::error::address_in_use
: lançada quando o endereço especificado já está em uso por outro processo.boost::asio::error::connection_aborted
: lançada quando a conexão é abortada pelo host remoto.boost::asio::error::connection_refused
: lançada quando a conexão é recusada pelo host remoto.boost::asio::error::connection_reset
: lançada quando a conexão é reiniciada pelo host remoto.
Essas exceções são lançadas pelos métodos do Asio que realizam operações de rede, como boost::asio::ip::tcp::acceptor::accept
ou boost::asio::ip::tcp::socket::connect
. Elas podem ser capturadas pelo programa e tratadas de acordo com o erro específico que ocorreu.
As funções do Asio
podem gerar a exceção boost::system::system_error
. Veja o resolve
no exemplo abaixo:
results_type resolve(BOOST_ASIO_STRING_VIEW_PARAM host,
BOOST_ASIO_STRING_VIEW_PARAM service, resolver_base::flags resolve_flags)
{
boost::system::error_code ec;
......
boost::asio::detail::throw_error(ec, "resolve");
return r;
}
Há duas sobrecargas de funções boost::asio::detail::throw_error
:
inline void throw_error(const boost::system::error_code& err)
{
if (err)
do_throw_error(err);
}
inline void throw_error(const boost::system::error_code& err,
const char* location)
{
if (err)
do_throw_error(err, location);
}
As diferenças dessas duas funções é que estão apenas incluindo a string "location" ("resolve
" no nosso exemplo) ou não. Assim, o do_throw_error
também tem duas sobrecargas, veja uma como exemplo:
void do_throw_error(const boost::system::error_code& err, const char* location)
{
boost::system::system_error e(err, location);
boost::asio::detail::throw_exception(e);
}
boost::system::system_error
deriva de std::runtime_error
:
class BOOST_SYMBOL_VISIBLE system_error : public std::runtime_error
{
......
public:
system_error( error_code ec )
: std::runtime_error(""), m_error_code(ec) {}
system_error( error_code ec, const std::string & what_arg )
: std::runtime_error(what_arg), m_error_code(ec) {}
......
const error_code & code() const BOOST_NOEXCEPT_OR_NOTHROW { return m_error_code; }
const char * what() const BOOST_NOEXCEPT_OR_NOTHROW;
......
}
A função membro chamado what()
retorna as informações detalhadas da exceção.
Em resumo, o Asio fornece uma série de exceções de erro que podem ser lançadas em situações em que ocorrem erros durante a execução de operações de rede. Essas exceções são derivadas da classe boost::system::system_error
ou asio::system_error
(standalone) e incluem erros comuns de rede, como conexão abortada, conexão recusada ou endereço em uso. Elas podem ser capturadas pelo programa e tratadas de acordo com o erro específico que ocorreu.
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::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::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. Ainda pode usar basic_stream_socket
como exemplo, e um par de implementações assim:
template <typename ConstBufferSequence, typename WriteHandler>
BOOST_ASIO_INITFN_RESULT_TYPE(WriteHandler,
void (boost::system::error_code, std::size_t))
async_send(const ConstBufferSequence& buffers,
BOOST_ASIO_MOVE_ARG(WriteHandler) handler)
{
.......
}
template <typename MutableBufferSequence, typename ReadHandler>
BOOST_ASIO_INITFN_RESULT_TYPE(ReadHandler,
void (boost::system::error_code, std::size_t))
async_receive(const MutableBufferSequence& buffers,
BOOST_ASIO_MOVE_ARG(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 como o parâmetro que recebe o resultado das operações de leitura & gravação:
void handler(
const boost::system::error_code& error, // Result of operation.
std::size_t bytes_transferred // Number of bytes processed.
)
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));
socket->get_executor().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));
socket.get_executor().context().run();
socket.get_executor().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 que são 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 decriptar 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, alguns problemas de segurança foram encontrados no SSL, o que levou ao desenvolvimento do TLS como uma substituição.
O TLS foi projetado para corrigir os problemas de segurança encontrados no SSL e fornecer um nível ainda maior de segurança nas comunicações na internet. Ele é compatível com o SSL, o que significa que os dispositivos que suportam o TLS também podem se comunicar com dispositivos que usam o SSL.
Apesar da similaridade entre SSL e TLS, a maioria dos sites da web e serviços de internet atualmente usa o TLS para proteger suas comunicações. Isso é porque o TLS é considerado mais seguro e atualizado do que o SSL. No entanto, o SSL ainda é amplamente utilizado em algumas aplicações, como em conexões seguras de email (SMTPS) e em alguns protocolos de VPN.
Asio SSL
A biblioteca Asio SSL fornece uma API que permite aos desenvolvedores criar e gerenciar conexões seguras usando o protocolo SSL (Secure Sockets Layer) ou TLS (Transport Layer Security). Ela inclui funções para realizar handshakes SSL/TLS, criptografar e decriptar dados usando chaves criptográficas e verificar a validade de certificados de segurança.
Aqui está um exemplo completo de código em C++ que demonstra como usar a biblioteca Asio SSL para estabelecer uma conexão segura com um servidor e enviar uma solicitação HTTP. Este exemplo supõe que você já tenha incluído os cabeçalhos relevantes e configurado o objeto asio::ssl::context de acordo com suas necessidades.
#include <asio/ssl.hpp>
#include <asio/ip/tcp.hpp>
#include <iostream>
#include <string>
#include <vector>
int main()
{
asio::io_context io_context;
// Criar um objeto asio::ssl::context com as configurações SSL/TLS desejadas
asio::ssl::context ctx(asio::ssl::context::sslv23);
ctx.set_options(asio::ssl::context::default_workarounds
| asio::ssl::context::no_sslv2
| asio::ssl::context::single_dh_use);
ctx.use_certificate_chain_file("certificate.pem");
ctx.use_private_key_file("key.pem", asio::ssl::context::pem);
// Criar um objeto asio::ssl::stream usando o objeto asio::ssl::context
asio::ssl::stream<asio::ip::tcp::socket> socket(io_context, ctx);
// Conectar ao servidor
socket.lowest_layer().connect({{}, 443});
// Realizar o handshake SSL como um cliente
socket.handshake(asio::ssl::stream_base::client);
// Enviar a solicitação HTTP
std::string request = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
asio::write(socket, asio::buffer(request));
// Receber a resposta do servidor
std::vector<char> response(1024);
asio::read(socket, asio::buffer(response));
std::cout << "Response from server: " << std::string(response.data(), response.size()) << std::endl;
return 0;
}
Neste exemplo, primeiro criamos um objeto asio::ssl::context
com as configurações SSL/TLS desejadas. Em seguida, criamos um objeto asio::ssl::stream
passando o objeto asio::ssl::context
como um parâmetro para o construtor.
Em seguida, usamos o método connect do objeto asio::ip::tcp::socket
subjacente para estabelecer a conexão com o servidor. Depois disso, realizamos o handshake SSL como um cliente usando o método handshake.
Depois disso, podemos usar os métodos de leitura e escrita padrão, como read e write, para enviar e receber dados através da conexão segura. No exemplo acima, enviamos uma solicitação HTTP simples usando o método write, e depois usamos o método read para ler a resposta do servidor.
Depois de ler a resposta do servidor, podemos exibir o conteúdo da resposta usando o operador de inserção de stream (<<
) e o método string da classe std::vector
.
Este é um exemplo básico de como usar a biblioteca Asio SSL em aplicativos C++. É importante lembrar que há muitos detalhes adicionais que podem ser considerados ao trabalhar com conexões seguras, como verificação de certificados de segurança, gerenciamento de erros e gerenciamento de sessões SSL/TLS.
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)
#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);
asio::ip::tcp::resolver::query query(argv[1], "");
auto results = resolver.resolve(query);
// Iterar todos os IPs detectados e exibi-los na tela.
std::cout << "IP addresses for " << argv[1] << ":" << std::endl << std::endl;
for (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_service, tcp::endpoint(tcp::v4(), 1234));
// Aguarda uma conexão do cliente
tcp::socket socket(io_service);
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)