Operações Assíncronas E/S

Diferentemente da API sockets do UNIX, o Asio possui habilidades de leitura & gravação (read/write) assíncronas inclusas. Usando basic_stream_socket como exemplo, as assinaturas públicas das funções assíncronas são:

// Envio assíncrono — retorna imediatamente, handler chamado ao completar
template <typename ConstBufferSequence, typename WriteHandler>
void async_send(const ConstBufferSequence& buffers, WriteHandler&& handler);

// Recebimento assíncrono — retorna imediatamente, handler chamado ao completar
template <typename MutableBufferSequence, typename ReadHandler>
void async_receive(const MutableBufferSequence& buffers, ReadHandler&& handler);

Como as funções async_send e async_receive retornam imediatamente e não bloqueiam a thread atual, você deve passar uma função de retorno de chamada (callback) ou completion token como parâmetro. O callback recebe o resultado das operações:

void handler(
    const asio::error_code& error,  // resultado da operação
    std::size_t bytes_transferred   // bytes processados
)

Dica: Em código moderno, prefira lambdas a std::bind para callbacks — o código fica mais legível e o compilador pode otimizar melhor.

Há um exemplo simples de cliente/servidor. Abaixo está o código do cliente:

	#include <boost/asio.hpp>
	#include <functional>
	#include <iostream>
	#include <memory>
	
	void callback(
	        const boost::system::error_code& error,
	        std::size_t bytes_transferred,
	        std::shared_ptr<boost::asio::ip::tcp::socket> socket,
	        std::string str)
	{
	    if (error)
	    {
	        std::cout << error.message() << '\n';
	    }
	    else if (bytes_transferred == str.length())
	    {
	        std::cout << "Message is sent successfully!" << '\n';
	    }
	    else
	    {
	        socket->async_send(
	                boost::asio::buffer(str.c_str() + bytes_transferred, str.length() - bytes_transferred),
	                std::bind(callback, std::placeholders::_1, std::placeholders::_2, socket, str));
	    }
	}	
	
	int main()
	{
	    try
	    {
	        boost::asio::io_context io_context;
	
	        boost::asio::ip::tcp::endpoint endpoint{
	                boost::asio::ip::make_address("192.168.35.145"),
	                3303};
	
	        std::shared_ptr<boost::asio::ip::tcp::socket> socket{new boost::asio::ip::tcp::socket{io_context}};
	        socket->connect(endpoint);
	
	        std::cout << "Connect to " << endpoint << " successfully!\n";
	
	        std::string str{"Hello world!"};
	        socket->async_send(
	                boost::asio::buffer(str),
	                std::bind(callback, std::placeholders::_1, std::placeholders::_2, socket, str));
	        io_context.run();
	    }
	    catch (std::exception& e)
	    {
	        std::cerr << e.what() << '\n';
	        return -1;
	    }
	
	    return 0;
	}

Vamos analisar o código:

(1) Como o objeto sockets é non-copyable (sockets), sockets é criado como um ponteiro inteligente de memória compartilhada (shared_pointer):

	......
	std::shared_ptr<boost::asio::ip::tcp::socket> socket{new boost::asio::ip::tcp::socket{io_context}};
	......

(2) Como o callback possui apenas dois parâmetros, ele precisa usar std::bind para passar parâmetros adicionais:

	......
	std::bind(callback, std::placeholders::_1, std::placeholders::_2, socket, str)
	......

(3) async_send não garante que todos os bytes sejam enviados (boost::asio::async_write retorna todos os bytes enviados com sucesso ou ocorre um erro), portanto, é necessário reemitir async_send no callback:

	......
	if (error)
	{
	    ......
	}
	else if (bytes_transferred == str.length())
	{
	    ......
	}
	else
	{
	    socket->async_send(......);
	}

(4) A função io_context.run será bloqueada até que todo o trabalho termine e não haja mais handlers(manipuladores) a serem despachados, ou até que o io_context seja interrompido:

	socket->get_executor().context().run();

Se não houver a função io_context.run, o programa será encerrado imediatamente.

Verifique o código do servidor que usa async_receive:

	#include <ctime>
	#include <functional>
	#include <iostream>
	#include <string>
	#include <boost/asio.hpp>
	
	void callback(
	        const boost::system::error_code& error,
	        std::size_t,
	        char recv_str[]) {
	    if (error)
	    {
	        std::cout << error.message() << '\n';
	    }
	    else
	    {
	        std::cout << recv_str << '\n';
	    }
	}
	
	int main()
	{
	    try
	    {
	        boost::asio::io_context io_context;
	
	        boost::asio::ip::tcp::acceptor acceptor(
	                                        io_context,
	                                        boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), 3303));
	
	        for (;;)
	        {
	            boost::asio::ip::tcp::socket socket(io_context);
	            acceptor.accept(socket);
	
	            char recv_str[1024] = {};
	            socket.async_receive(
	                    boost::asio::buffer(recv_str),
	                    std::bind(callback, std::placeholders::_1, std::placeholders::_2, recv_str));
	            io_context.run();
	            io_context.restart();
	        }
	    }
	    catch (std::exception& e)
	    {
	        std::cerr << e.what() << std::endl;
	    }
	
	    return 0;
	}

Há duas advertências às quais você precisa prestar atenção:

(1) Apenas para fins de demonstração: para cada cliente, o callback é chamado apenas uma vez;
(2) O io_context.restart deve ser chamado para chamar outro io_context.run.

Da mesma forma, você também pode verificar como usar o boost::asio::async_read.

Compile e execute os programas.

O cliente exibirá o seguinte:

$ ./client
Connect to 192.168.35.145:3303 successfully!
Message is sent successfully!

Servidor emitirar o seguinte:

$ ./server
Hello world!