Selamat siang, teman dan kolega. Nama saya masih Dmitry Smirnov, dan saya masih, sangat senang, pengembang ISPsystem. Beberapa waktu lalu, saya mulai mengerjakan proyek yang sama sekali baru, yang sangat menginspirasi saya, karena yang baru dalam kasus kami adalah kurangnya kode warisan dan dukungan untuk kompiler lama. Halo, Boost, C ++ 17 dan semua kesenangan lain dari perkembangan modern.
Kebetulan semua proyek masa lalu saya adalah multi-threaded, masing-masing, saya memiliki sedikit pengalaman dengan solusi asinkron. Inilah yang menjadi yang paling menyenangkan bagi saya dalam pengembangan ini, di samping alat-alat canggih modern.
Salah satu tugas terkait terakhir adalah kebutuhan untuk menulis pembungkus atas
libssh2 perpustakaan dalam realitas aplikasi asinkron menggunakan
Boost.Asio , dan mampu
memunculkan tidak lebih dari dua utas. Saya akan memberi tahu Anda tentang ini.

Catatan: penulis berasumsi bahwa pembaca sudah terbiasa dengan dasar-dasar pengembangan asinkron dan meningkatkan :: asio.
Tantangan
Secara umum, tugasnya adalah sebagai berikut: terhubung ke server jauh menggunakan kunci rsa atau nama pengguna dan kata sandi; unggah skrip ke mesin jarak jauh dan jalankan; baca jawabannya dan kirim perintah kepadanya melalui koneksi yang sama. Dalam hal ini, tentu saja, tanpa menghalangi aliran (yang merupakan setengah dari total kumpulan mungkin).
Penafian : Saya tahu bahwa Poco bekerja dengan SSH, tetapi saya tidak menemukan cara untuk menikah dengannya dengan Asio, dan lebih menarik untuk menulis sesuatu tentang saya sendiri :-).
Inisialisasi
Untuk menginisialisasi dan meminimalkan perpustakaan, saya memutuskan untuk menggunakan singleton biasa:
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(); } };
Tentu saja ada jebakan dalam keputusan ini, menurut buku pegangan favorit saya, "Seribu Satu Cara untuk Menembak Kaki Anda di C ++". Jika seseorang menghasilkan aliran yang mereka lupa tusuk, dan yang utama berakhir lebih awal, efek khusus yang menarik mungkin muncul. Tetapi dalam hal ini, saya tidak akan memperhitungkan kemungkinan ini.
Entitas Kunci
Setelah menganalisis
contoh , menjadi jelas bahwa untuk perpustakaan kecil kami, kami membutuhkan tiga entitas sederhana: soket, sesi, dan saluran. Karena senang memiliki alat sinkronisasi, kami akan mengesampingkan Asio untuk saat ini.
Mari kita mulai dengan soket sederhana:
Soket 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; }
Sesi sekarang:
Sesi 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; }
Karena sekarang kita memiliki soket dan sesi, alangkah baiknya untuk menulis fungsi tunggu untuk sebuah socket dalam realitas libssh2:
Soket menunggu 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); }
Sebenarnya, ini praktis tidak berbeda dari contoh di atas, kecuali bahwa ia menggunakan pilih daripada polling.
Saluran tetap ada. Ada beberapa jenis saluran di libssh2: sederhana, SCP, tcp langsung. Kami tertarik pada saluran dasar yang paling sederhana:
Saluran 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; };
Sekarang semua alat dasar sudah siap, ia tetap membuat koneksi dengan host dan melakukan manipulasi yang diperlukan. Rekaman asinkron ke saluran dan sinkron, tentu saja, akan sangat berbeda, tetapi proses membangun koneksi tidak.
Oleh karena itu, kami menulis kelas dasar:
Koneksi dasar class BaseConnectionImpl { protected: explicit BaseConnectionImpl(const SshConnectData &connect_data)
Sekarang kita siap menulis kelas paling sederhana untuk terhubung ke host jarak jauh dan menjalankan perintah apa pun di atasnya:
Koneksi sinkron 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; } } } };
Sejauh ini, semua yang kami tulis merupakan pengurangan sederhana dari contoh libssh2 ke bentuk yang lebih beradab. Tetapi sekarang, dengan memiliki semua alat sederhana untuk menulis data ke saluran secara serempak, kita dapat beralih ke Asio.
Memiliki soket standar adalah baik, tetapi tidak terlalu praktis jika Anda harus menunggu secara tidak sinkron untuk membaca / menulis saat melakukan bisnis Anda sendiri dalam proses. Sini boost :: asio :: ip :: tcp :: socket datang untuk menyelamatkan, memiliki metode yang luar biasa:
async_wait(wait_type, WaitHandler)
Itu luar biasa dibangun dari soket biasa, di mana kita sudah mengatur koneksi di muka dan meningkatkan :: asio :: io_context - konteks eksekusi aplikasi kita.
Konstruktor koneksi asinkron 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); } };
Sekarang kita perlu memulai eksekusi beberapa perintah pada host jarak jauh dan, segera setelah data darinya tiba, kirimkan ke beberapa panggilan balik.
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(); }
Dengan demikian, dengan menjalankan perintah, kami mentransfer kontrol ke metode 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(); }); }
Pertama-tama, kami memeriksa apakah proses membaca sudah berjalan oleh beberapa panggilan sebelumnya. Jika tidak, maka kita mulai mengharapkan kesiapan soket untuk membaca. Lambda biasa dengan penangkapan shared_from_this () digunakan sebagai penangan menunggu.
Perhatikan panggilan ke WantRead (). Async_wait, ternyata, juga memiliki kekurangannya, dan dapat kembali dengan waktu habis. Untuk menghindari tindakan yang tidak perlu dalam kasus ini, saya memutuskan untuk memeriksa soket melalui jajak pendapat tanpa batas waktu - apakah soket benar-benar ingin membaca sekarang. Jika tidak, maka kita jalankan lagi TryRead () dan tunggu. Jika tidak, kami segera mulai membaca dan mentransfer data ke panggilan balik.
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; }
Dengan demikian, siklus baca asinkron tak berujung dimulai dari aplikasi yang sedang berjalan. Langkah selanjutnya bagi kami adalah mengirimkan instruksi ke aplikasi:
void AsyncWrite(const std::string &data, WriteCallbackType &&callback) { m_input += data; m_write_callback = std::move(callback); TryWrite(); }
Data dan panggilan balik yang ditransfer ke rekaman asinkron akan disimpan di dalam koneksi. Dan jalankan siklus berikutnya, kali ini saja entri:
Siklus rekaman 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; }
Jadi, kami akan menulis data ke saluran sampai semuanya berhasil ditransfer. Kemudian kami akan mengembalikan kontrol ke pemanggil sehingga sepotong data baru dapat ditransfer. Dengan cara ini Anda tidak hanya dapat mengirim instruksi ke beberapa aplikasi pada host, tetapi juga, misalnya, mengunggah file dengan ukuran apa pun dalam porsi kecil, tanpa memblokir utas, yang penting.
Menggunakan perpustakaan ini, saya berhasil menjalankan skrip pada server jauh yang melacak perubahan sistem file, pada saat yang sama membaca outputnya dan mengirim berbagai perintah. Secara umum: pengalaman yang sangat berharga dalam mengadaptasi perpustakaan gaya si untuk proyek C ++ modern menggunakan Boost.
Saya akan senang membaca tips pengguna Boost.Asio yang lebih berpengalaman untuk mempelajari lebih lanjut dan meningkatkan solusi saya :-).