Intercambio asíncrono de datos con una aplicación remota a través de SSH

Buen día amigos y colegas. Mi nombre sigue siendo Dmitry Smirnov y sigo siendo, para mi gran placer, el desarrollador de ISPsystem. Hace algún tiempo, comencé a trabajar en un proyecto completamente nuevo que me inspiró mucho, porque el nuevo es en nuestro caso la falta de código heredado y soporte para compiladores antiguos. Hola, Boost, C ++ 17 y todas las otras alegrías del desarrollo moderno.

Dio la casualidad de que todos mis proyectos pasados ​​fueron multiproceso, respectivamente, tenía muy poca experiencia con soluciones asincrónicas. Esto es lo que se convirtió en lo más agradable para mí en este desarrollo, además de las herramientas modernas y potentes.

Una de las últimas tareas relacionadas fue la necesidad de escribir un contenedor sobre la biblioteca libssh2 en las realidades de una aplicación asincrónica usando Boost.Asio , y capaz de generar no más de dos hilos. Te contaré sobre esto.



Nota: el autor supone que el lector está familiarizado con los conceptos básicos del desarrollo asincrónico y boost :: asio.

Desafío


En términos generales, la tarea era la siguiente: conectarse a un servidor remoto utilizando una clave rsa o nombre de usuario y contraseña; cargar un script en la máquina remota y ejecutarlo; lea sus respuestas y envíele comandos a través de la misma conexión. En este caso, por supuesto, sin bloquear el flujo (que es la mitad del conjunto total posible).

Descargo de responsabilidad : Sé que Poco trabaja con SSH, pero no encontré una manera de casarlo con Asio, y fue más interesante escribir algo propio :-).

Inicialización


Para inicializar y minimizar la biblioteca, decidí usar el singleton habitual:

Init ()
class LibSSH2 { public: static void Init() { static LibSSH2 instance; } private: explicit LibSSH2() { if (libssh2_init(0) != 0) { throw std::runtime_error("libssh2 initialization failed"); } } ~LibSSH2() { std::cout << "shutdown libssh2" << std::endl; libssh2_exit(); } }; 



Hay, por supuesto, escollos en esta decisión, de acuerdo con mi manual favorito, "Mil y una maneras de dispararle una pierna en C ++". Si alguien genera una transmisión que se olvida de pinchar, y la principal termina antes, pueden surgir efectos especiales interesantes. Pero en este caso, no tendré en cuenta esta posibilidad.

Entidades clave


Después de analizar el ejemplo , queda claro que para nuestra pequeña biblioteca necesitamos tres entidades simples: socket, sesión y canal. Como es bueno tener herramientas sincrónicas, dejaremos a Asio a un lado por ahora.

Comencemos con un zócalo simple:

Zócalo
 class Socket { public: explicit Socket() : m_sock(socket(AF_INET, SOCK_STREAM, 0)) { if (m_sock == -1) { throw std::runtime_error("failed to create socket"); } } ~Socket() { close(m_sock); } private: int m_sock = -1; } 


Ahora sesión:

La sesión
 class Session { public: explicit Session(const bool enable_compression) : m_session(libssh2_session_init()) { if (m_session == nullptr) { throw std::runtime_error("failed to create libssh2 session"); } libssh2_session_set_blocking(m_session, 0); if (enable_compression) { libssh2_session_flag(m_session, LIBSSH2_FLAG_COMPRESS, 1); } } ~Session() { const std::string desc = "Shutting down libssh2 session"; libssh2_session_disconnect(m_session, desc.c_str()); libssh2_session_free(m_session); } private: LIBSSH2_SESSION *m_session; } 


Como ahora tenemos un socket y una sesión, sería bueno escribir una función de espera para un socket en las realidades de libssh2:

Zócalo de espera
 int WaitSocket() const { pollfd fds{}; fds.fd = sock; fds.events = 0; if ((libssh2_session_block_directions(session) & LIBSSH2_SESSION_BLOCK_INBOUND) != 0) { fds.events |= POLLIN; } if ((libssh2_session_block_directions(session) & LIBSSH2_SESSION_BLOCK_OUTBOUND) != 0) { fds.events |= POLLOUT; } return poll(&fds, 1, 10); } 


En realidad, esto no es prácticamente diferente del ejemplo anterior, excepto que utiliza select en lugar de sondeo.

El canal permanece. Hay varios tipos de canales en libssh2: simple, SCP, tcp directo. Nos interesa el canal más simple y básico:

Canal
 class SimpleChannel { public: explicit SimpleChannel(session) { while ((m_channel = libssh2_channel_open_session(session) == nullptr && GetSessionLastError() == LIBSSH2_ERROR_EAGAIN) { WaitSocket(); } if (m_channel == nullptr) { throw std::runtime_error("Critical error while opening simple channel"); } } void SendEof() { while (libssh2_channel_send_eof(m_channel) == LIBSSH2_ERROR_EAGAIN) { WaitSocket(); } while (libssh2_channel_wait_eof(m_channel) == LIBSSH2_ERROR_EAGAIN) { WaitSocket(); } } ~SimpleChannel() { CloseChannel(); } private: void CloseChannel() { int rc; while ((rc = libssh2_channel_close(m_channel)) == LIBSSH2_ERROR_EAGAIN) { WaitSocket(); } libssh2_channel_free(m_channel); } LIBSSH2_CHANNEL *m_channel; }; 


Ahora que todas las herramientas básicas están listas, queda por establecer una conexión con el host y realizar las manipulaciones necesarias. La grabación asíncrona en el canal y la sincrónica, por supuesto, serán muy diferentes, pero el proceso de establecer una conexión no lo es.

Por lo tanto, escribimos la clase base:

Conexión básica
 class BaseConnectionImpl { protected: explicit BaseConnectionImpl(const SshConnectData &connect_data) ///<    ,     : m_session(connect_data.enable_compression) , m_connect_data(connect_data) { LibSSH2::Init(); ConnectSocket(); HandShake(); ProcessKnownHosts(); Auth(); } ///       bool CheckSocket(int type) const { pollfd fds{}; fds.fd = m_sock; fds.events = type; return poll(&fds, 1, 0) == 1; } bool WantRead() const { return CheckSocket(POLLIN); } bool WantWrite() const { return CheckSocket(POLLOUT); } /*   ,   ,       *  - . */ void ConnectSocket() {...} void HandShake() {...} void Auth() {...} class Socket m_sock; class Session m_session; class SimpleChannel; SshConnectData m_connect_data; }; 


Ahora estamos listos para escribir la clase más simple para conectarse al host remoto y ejecutar cualquier comando en él:

Conexión sincrónica
 class Connection::Impl : public BaseConnectionImpl { public: explicit Impl(const SshConnectData &connect_data) : BaseConnectionImpl(connect_data) {} template <typename Begin> void WriteToChannel(LIBSSH2_CHANNEL *channel, Begin ptr, size_t size) { do { int rc; while ((rc = libssh2_channel_write(channel, ptr, size)) == LIBSSH2_ERROR_EAGAIN) { WaitSocket(); } if (rc < 0) { break; } size -= rc; ptr += rc; } while (size != 0); } void ExecuteCommand(const std::string &command, const std::string &in = "") { SimpleChannel channel(*this); int return_code = libssh2_channel_exec(channel, command.c_str()); if (return_code != 0 && return_code != LIBSSH2_ERROR_EAGAIN) { throw std::runtime_error("Critical error while executing ssh command"); } if (!in.empty()) { WriteToChannel(channel, in.c_str(), in.size()); channel.SendEof(); } std::string response; for (;;) { int rc; do { std::array<char, 4096> buffer{}; rc = libssh2_channel_read(channel, buffer.data(), buffer.size()); if (rc > 0) { boost::range::copy(boost::adaptors::slice(buffer, 0, rc), std::back_inserter(response)); } else if (rc < 0 && rc != LIBSSH2_ERROR_EAGAIN) { throw std::runtime_error("libssh2_channel_read error (" + std::to_string(rc) + ")"); } } while (rc > 0); if (rc == LIBSSH2_ERROR_EAGAIN) { WaitSocket(); } else { break; } } } }; 


Hasta ahora, todo lo que escribimos ha sido una simple reducción de ejemplos de libssh2 a una forma más civilizada. Pero ahora, teniendo todas las herramientas simples para escribir datos en el canal de forma sincrónica, podemos pasar a Asio.

Tener un socket estándar es bueno, pero no demasiado práctico si necesita esperar asincrónicamente para que lea / escriba mientras realiza su propio negocio en el proceso. Aquí boost :: asio :: ip :: tcp :: socket viene al rescate, con un método maravilloso:

 async_wait(wait_type, WaitHandler) 

Está maravillosamente construido a partir de un socket normal, para el cual ya configuramos la conexión de antemano y potenciamos :: asio :: io_context, el contexto de ejecución de nuestra aplicación.

Constructor de conexiones asíncronas
 class AsyncConnection::Impl : public BaseConnectionImpl, public std::enable_shared_from_this<AsyncConnection::Impl> { public: Impl(boost::asio::io_context &context, const SshConnectData &connect_data) : BaseConnectionImpl(connect_data) , m_tcp_socket(context, tcp::v4(), m_sock.GetSocket()) { m_tcp_socket.non_blocking(true); } }; 



Ahora necesitamos comenzar la ejecución de algún comando en el host remoto y, tan pronto como lleguen los datos, enviarlo a alguna devolución de llamada.

 void AsyncRun(const std::string &command, CallbackType &&callback) { m_read_callback = std::move(callback); auto ec = libssh2_channel_exec(*m_channel, command.c_str()); TryRead(); } 

Por lo tanto, al ejecutar el comando, transferimos el control al método TryRead ().

 void TryRead() { if (m_read_in_progress) { return; } m_tcp_socket.async_wait(tcp::socket::wait_read, [this, self = shared_from_this()](auto ec) { if (WantRead()) { ReadHandler(ec); } if (m_complete) { return; } TryRead(); }); } 

En primer lugar, verificamos si el proceso de lectura ya se está ejecutando en alguna llamada anterior. Si no, entonces comenzamos a esperar la disponibilidad del socket para la lectura. Una lambda normal con la captura de shared_from_this () se usa como controlador de espera.

Presta atención a la llamada a WantRead (). Async_wait, como resultó, también tiene sus defectos, y simplemente puede regresar por tiempo de espera. Para evitar acciones innecesarias en este caso, decidí verificar el socket a través de una encuesta sin tiempo de espera: ¿el socket realmente quiere leer ahora? Si no, entonces ejecutamos TryRead () nuevamente y esperamos. De lo contrario, comenzamos inmediatamente a leer y transferir datos a la devolución de llamada.

 void ReadHandler(const boost::system::error_code &error) { if (error != boost::system::errc::success) { return; } m_read_in_progress = true; int ec = LIBSSH2_ERROR_EAGAIN; std::array<char, 4096> buffer {}; while ((ec = libssh2_channel_read(*m_channel, buffer.data(), buffer.size())) > 0) { std::string tmp; boost::range::copy(boost::adaptors::slice(buffer, 0, ec), std::back_inserter(tmp)); if (m_read_callback != nullptr) { m_read_callback(tmp); } } m_read_in_progress = false; } 

Por lo tanto, se inicia un ciclo de lectura asíncrono sin fin desde la aplicación en ejecución. El siguiente paso para nosotros será enviar instrucciones a la aplicación:

 void AsyncWrite(const std::string &data, WriteCallbackType &&callback) { m_input += data; m_write_callback = std::move(callback); TryWrite(); } 

Los datos y la devolución de llamada transferidos a la grabación asincrónica se guardarán dentro de la conexión. Y ejecuta el siguiente ciclo, solo que esta vez las entradas:

Ciclo de grabación
 void TryWrite() { if (m_input.empty() || m_write_in_progress) { return; } m_tcp_socket.async_wait(tcp::socket::wait_write, [this, self = shared_from_this()](auto ec) { if (WantWrite()) { WriteHandler(ec); } if (m_complete) { return; } TryWrite(); }); } void WriteHandler(const boost::system::error_code &error) { if (error != boost::system::errc::success) { return; } m_write_in_progress = true; int ec = LIBSSH2_ERROR_EAGAIN; while (!m_input.empty()) { auto ptr = m_input.c_str(); auto read_size = m_input.size(); while ((ec = libssh2_channel_write(*m_channel, ptr, read_size)) > 0) { read_size -= ec; ptr += ec; } AssertResult(ec); m_input.erase(0, m_input.size() - read_size); if (ec == LIBSSH2_ERROR_EAGAIN) { break; } } if (m_input.empty() && m_write_callback != nullptr) { m_write_callback(); } m_write_in_progress = false; } 


Por lo tanto, escribiremos datos en el canal hasta que todos se transfieran con éxito. Luego devolveremos el control a la persona que llama para que se pueda transferir un nuevo dato. De esta manera, no solo puede enviar instrucciones a alguna aplicación en el host, sino también, por ejemplo, cargar archivos de cualquier tamaño en pequeñas porciones, sin bloquear el hilo, lo cual es importante.

Usando esta biblioteca, pude ejecutar con éxito un script en un servidor remoto que rastrea los cambios en el sistema de archivos, mientras leía su salida y enviaba varios comandos. En general: una experiencia muy valiosa en la adaptación de la biblioteca de estilo si para un proyecto moderno de C ++ con Boost.

Estaré encantado de leer los consejos de los usuarios más experimentados de Boost.Asio para obtener más información y mejorar mi solución :-).

Source: https://habr.com/ru/post/es430488/


All Articles