يوم جيد اصدقاء وزملاء. اسمي لا يزال ديمتري سميرنوف ، وما زلت ، من دواعي سروري البالغ ، مطور ISPsystem. منذ بعض الوقت ، بدأت العمل في مشروع جديد تمامًا ، ألهمني كثيرًا ، حيث أن المشروع الجديد في حالتنا هو عدم وجود رمز قديم ودعم للمترجمين القدامى. مرحبًا ، Boost ، C ++ 17 وجميع أفراح التطور الحديث.
حدث ذلك أن جميع مشاريعي السابقة كانت متعددة الخيوط ، على التوالي ، كانت لدي خبرة قليلة جدًا في الحلول غير المتزامنة. هذا ما أصبح أكثر متعة بالنسبة لي في هذا التطور ، بالإضافة إلى الأدوات القوية الحديثة.
كانت إحدى المهام ذات الصلة الأخيرة هي الحاجة إلى كتابة غلاف فوق مكتبة
libssh2 في حقائق التطبيق غير المتزامن باستخدام
Boost.Asio ، وقادرة على إنتاج أكثر من
خيطين . سأخبرك عن هذا.

ملاحظة: يفترض المؤلف أن القارئ على دراية بأساسيات التطوير غير المتزامن وتعزيز :: asio.
التحدي
بشكل عام ، كانت المهمة كما يلي: الاتصال بخادم بعيد باستخدام مفتاح rsa أو اسم المستخدم وكلمة المرور ؛ تحميل برنامج نصي على الجهاز البعيد وتشغيله ؛ قراءة إجاباته وإرسال أوامر له من خلال نفس الاتصال. في هذه الحالة ، بالطبع ، دون عرقلة التدفق (وهو نصف إجمالي التجمع الممكن).
إخلاء المسؤولية : أعلم أن Poco يعمل مع SSH ، لكنني لم أجد طريقة للزواج منه مع Asio ، وكان من المثير أكثر أن أكتب شيئًا خاصًا بي :-).
التهيئة
لتهيئة المكتبة وتصغيرها ، قررت استخدام المفرد المعتاد:
الأولي ()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(); } };
هناك بالطبع مآزق في هذا القرار ، وفقًا لكتيبي المفضل ، "ألف وطريقة لتصويب ساقك في C ++". إذا قام أحد الأشخاص بتوليد دفق ينسى نسيانه ، وينتهي الجزء الرئيسي في وقت سابق ، فقد تنشأ تأثيرات خاصة مثيرة للاهتمام. ولكن في هذه الحالة ، لن تأخذ في الاعتبار هذا الاحتمال.
الكيانات الرئيسية
بعد تحليل
المثال ، يصبح من الواضح أنه بالنسبة لمكتبتنا الصغيرة نحتاج إلى ثلاثة كيانات بسيطة: المقبس والجلسة والقناة. نظرًا لأنه من الجيد امتلاك أدوات متزامنة ، سنترك Asio جانبًا في الوقت الحالي.
لنبدأ بمقبس بسيط:
مقبس 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; }
الجلسة الآن:
الجلسة 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; }
نظرًا لأن لدينا الآن مقبسًا وجلسة ، سيكون من الجيد كتابة وظيفة انتظار لمقبس في حقائق libssh2:
انتظار مأخذ التوصيل 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); }
في الواقع ، لا يختلف هذا عمليا عن المثال أعلاه ، باستثناء أنه يستخدم التحديد بدلاً من الاستطلاع.
القناة لا تزال. هناك عدة أنواع من القنوات في libssh2: بسيطة ، SCP ، TCP مباشرة. نحن مهتمون بأبسط قناة أساسية:
القناة 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; };
الآن بعد أن أصبحت جميع الأدوات الأساسية جاهزة ، يبقى إنشاء اتصال مع المضيف وإجراء التلاعبات اللازمة. التسجيل غير المتزامن للقناة والمتزامن ، بالطبع ، سيكون مختلفًا تمامًا ، ولكن عملية إنشاء الاتصال ليست كذلك.
لذلك ، نكتب الفئة الأساسية:
اتصال أساسي class BaseConnectionImpl { protected: explicit BaseConnectionImpl(const SshConnectData &connect_data)
نحن الآن على استعداد لكتابة أبسط فئة للاتصال بالمضيف البعيد وتنفيذ أي أمر عليه:
اتصال متزامن 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; } } } };
حتى الآن ، كل ما كتبناه كان اختزال بسيط لأمثلة libssh2 إلى شكل أكثر تحضرا. ولكن الآن ، بعد امتلاك جميع الأدوات البسيطة لكتابة البيانات إلى القناة بشكل متزامن ، يمكننا الانتقال إلى Asio.
يعد وجود مقبس قياسي أمرًا جيدًا ، ولكنه ليس عمليًا للغاية إذا كنت بحاجة إلى الانتظار بشكل غير متزامن حتى تقرأ / تكتب أثناء القيام بأعمالك التجارية في هذه العملية. هنا دفعة :: asio :: ip :: tcp :: مقبس يأتي لإنقاذ ، طريقة رائعة:
async_wait(wait_type, WaitHandler)
إنه مبني بشكل رائع من مقبس عادي ، قمنا بالفعل بإعداد الاتصال مقدمًا وتعزيز :: asio :: io_context - سياق التنفيذ لتطبيقنا.
مُنشئ اتصال غير متزامن 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); } };
الآن نحن بحاجة إلى البدء في تنفيذ بعض الأوامر على المضيف البعيد ، وبمجرد وصول البيانات منه ، قم بإرساله إلى بعض الاستدعاء.
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(); }
وبالتالي ، بتشغيل الأمر ، نقوم بنقل التحكم إلى طريقة 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(); }); }
بادئ ذي بدء ، نتحقق مما إذا كانت عملية القراءة قيد التشغيل بالفعل بواسطة مكالمة سابقة. إذا لم يكن كذلك ، فإننا نبدأ في توقع جاهزية المقبس للقراءة. يتم استخدام لامدا عادية مع التقاط Shared_from_this () كمعالج انتظار.
انتبه إلى المكالمة إلى WantRead (). Async_wait ، كما اتضح ، لها عيوبها أيضًا ، ويمكنها ببساطة العودة بمرور الوقت. من أجل تجنب الإجراءات غير الضرورية في هذه الحالة ، قررت التحقق من المقبس من خلال الاستطلاع بدون مهلة - هل يريد المقبس حقًا القراءة الآن. إذا لم يكن الأمر كذلك ، فإننا نقوم بتشغيل TryRead () مرة أخرى وننتظر. خلاف ذلك ، نبدأ على الفور في قراءة البيانات ونقلها إلى رد الاتصال.
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; }
وبالتالي ، يتم بدء دورة قراءة غير متزامنة لا نهاية لها من التطبيق قيد التشغيل. الخطوة التالية لنا هي إرسال التعليمات إلى التطبيق:
void AsyncWrite(const std::string &data, WriteCallbackType &&callback) { m_input += data; m_write_callback = std::move(callback); TryWrite(); }
سيتم حفظ البيانات وإعادة الاتصال المحولة إلى التسجيل غير المتزامن داخل الاتصال. وقم بتشغيل الدورة التالية ، فقط هذه المرة الإدخالات:
دورة التسجيل 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; }
وبالتالي ، سنكتب البيانات إلى القناة حتى يتم نقلها جميعًا بنجاح. ثم سنعيد التحكم إلى المتصل حتى يمكن نقل جزء جديد من البيانات. بهذه الطريقة ، لا يمكنك فقط إرسال تعليمات إلى بعض التطبيقات على المضيف ، ولكن أيضًا ، على سبيل المثال ، تحميل ملفات من أي حجم في أجزاء صغيرة ، دون حظر سلسلة المحادثات ، وهو أمر مهم.
باستخدام هذه المكتبة ، تمكنت من تشغيل برنامج نصي بنجاح على خادم بعيد يتتبع تغييرات نظام الملفات ، وفي نفس الوقت قراءة مخرجاته وإرسال أوامر مختلفة. بشكل عام: تجربة قيّمة جدًا في تكييف مكتبة بأسلوب si لمشروع C ++ حديث باستخدام Boost.
سأكون سعيدًا بقراءة نصائح Boost.Asio الأكثر خبرة للمستخدمين لمعرفة المزيد وتحسين الحل :-).