الطرق العامة في الصدأ: كيف تحول Exonum من الحديد إلى Actix-web

النظام البيئي الصدأ لا يزال ينمو. نتيجة لذلك ، يتم إصدار مكتبات جديدة ذات وظائف محسنة بشكل متكرر في مجتمع المطورين ، بينما تصبح المكتبات القديمة قديمة. عندما صممنا Exonum في البداية ، استخدمنا شبكة الويب Iron. في هذه المقالة ، وصفنا كيف قمنا بتحويل إطار عمل Exonum إلى شبكة actix باستخدام البرمجة العامة.



إكسونوم على الحديد


في منصة Exonum ، تم استخدام إطار Iron بدون أي تجريدات. لقد قمنا بتثبيت معالجات لبعض الموارد وحصلنا على معلمات الطلب من خلال تحليل عناوين URL باستخدام طرق مساعدة ؛ تم إرجاع النتيجة ببساطة في شكل سلسلة.

بدا العملية (تقريبًا) كما يلي:

fn set_blocks_response(self, router: &mut Router) { let blocks = move |req: &mut Request| -> IronResult<Response> { let count: usize = self.required_param(req, "count")?; let latest: Option<u64> = self.optional_param(req, "latest")?; let skip_empty_blocks: bool = self.optional_param(req, "skip_empty_blocks")? .unwrap_or(false); let info = self.blocks(count, latest.map(Height), skip_empty_blocks)?; self.ok_response(&::serde_json::to_value(info).unwrap()) }; router.get("/v1/blocks", blocks, "blocks"); } 

بالإضافة إلى ذلك ، استخدمنا بعض المكونات الإضافية للبرامج الوسيطة في شكل رؤوس CORS. استخدمنا جبل لدمج جميع معالجات في API واحدة.

قرارنا بالابتعاد عن الحديد


كان الحديد مكتبة جيدة ، مع الكثير من الإضافات. ومع ذلك ، فقد كتب في الأيام التي كانت فيها مشاريع مثل العقود الآجلة وتوكيو غير موجودة.

تتضمن بنية Iron معالجة الطلبات المتزامنة ، والتي يمكن أن تتأثر بسهولة بعدد كبير من الاتصالات المفتوحة في وقت واحد. لكي تكون قابلة للتطوير ، يحتاج الحديد إلى أن يصبح غير متزامن ، الأمر الذي قد يتضمن إعادة التفكير في الإطار بالكامل وإعادة كتابته. نتيجةً لذلك ، فقد رأينا خروجًا تدريجيًا عن استخدام Iron بواسطة مهندسي البرامج.

لماذا اخترنا Actix ويب


Actix-web هو إطار شائع يحتل مرتبة عالية في معايير TechEmpower . يحتوي على مجتمع مطور نشط ، على عكس Iron ، ويحتوي على واجهة برمجة تطبيقات مصممة تصميماً جيدًا وتنفيذ عالي الجودة يعتمد على إطار عمل ممثل actix. تتم معالجة الطلبات بشكل غير متزامن بواسطة تجمع مؤشر الترابط؛ إذا طلب معالجة الذعر ، يتم إعادة تشغيل الفاعل تلقائيًا.

سابقا ، أثيرت مخاوف من أن actix-web يحتوي على الكثير من التعليمات البرمجية غير الآمنة. ومع ذلك ، تم تقليل مقدار التعليمات البرمجية غير الآمنة بشكل كبير عند إعادة كتابة الإطار بلغة برمجة آمنة - Rust. لقد راجع مهندسو Bitfury هذا الرمز بأنفسهم ويشعرون بالثقة في استقراره على المدى الطويل.

بالنسبة لإطار Exonum ، فإن التحول إلى actix حل مشكلة استقرار العملية. قد يفشل إطار حديد إذا كان هناك عدد كبير من الاتصالات. لقد وجدنا أيضًا أن واجهة برمجة تطبيقات actix-web أبسط وأكثر إنتاجية وتوحيدًا. نحن على ثقة من أن المستخدمين والمطورين سيكون لديهم وقت أسهل في استخدام واجهة برمجة Exonum ، والتي يمكن أن تعمل الآن بشكل أسرع بفضل تصميم الويب actix.

ما نطلبه من إطار ويب


خلال هذه العملية ، أدركنا أنه من المهم بالنسبة لنا ليس مجرد تغيير الأطر ، ولكن أيضًا إنشاء بنية API جديدة مستقلة عن أي إطار ويب محدد. تسمح هذه البنية بإنشاء معالجات ، دون أي قلق بشأن تفاصيل الويب ، ونقلها إلى أي واجهة خلفية. يمكن تنفيذ هذا المفهوم عن طريق كتابة الواجهة الأمامية التي من شأنها تطبيق الأنواع والسمات الأساسية.

لفهم الشكل الذي تحتاجه هذه الواجهة الأمامية ، دعونا نحدد ما هي واجهة برمجة تطبيقات HTTP بالفعل:

  • يتم تقديم الطلبات حصريًا من قبل العملاء ؛ يستجيب الخادم لهم فقط (لا يبدأ الخادم الطلبات).
  • يطلب إما قراءة البيانات أو تغيير البيانات.
  • نتيجة لمعالجة الطلب ، يعرض الخادم استجابة ، والتي تحتوي على البيانات المطلوبة ، في حالة النجاح ؛ أو معلومات حول الخطأ ، في حالة الفشل.

إذا أردنا تحليل جميع طبقات التجريد ، اتضح أن أي طلب HTTP هو مجرد استدعاء دالة:

 fn request(context: &ServiceContext, query: Query) -> Result<Response, ServiceError> 

كل شيء آخر يمكن اعتباره امتدادًا لهذا الكيان الأساسي. وبالتالي ، لكي نكون مستقلين عن تنفيذ محدد لإطار عمل على الويب ، نحتاج إلى كتابة معالجات بأسلوب مشابه للمثال أعلاه.

سمة "نقطة النهاية" للمعالجة العامة لطلبات HTTP


سيكون الأسلوب الأكثر بساطة ومباشرة هو إعلان سمة "نقطة النهاية" ، التي تصف تطبيقات طلبات محددة:

 // A trait describing GET request handlers. It should be possible to call each of the handlers from any freed // thread. This requirement imposes certain restrictions on the trait. Parameters and request results are // configured using associated types. trait Endpoint: Sync + Send + 'static { type Request: DeserializeOwned + 'static; type Response: Serialize + 'static; fn handle(&self, context: &Context, request: Self::Request) -> Result<Self::Response, io::Error>; } 

نحن الآن بحاجة إلى تنفيذ هذا المعالج في إطار محدد. على سبيل المثال ، في actix-web يبدو كما يلي:

 // Response type in actix-web. Note that they are asynchronous, even though `Endpoint` assumes that // processing is synchronous. type FutureResponse = actix_web::FutureResponse<HttpResponse, actix_web::Error>; // A raw request handler for actix-web. This is what the framework ultimately works with. The handler // receives parameters from an arbitrary context, through which the request parameters are passed. type RawHandler = dyn Fn(HttpRequest<Context>) -> FutureResponse + 'static + Send + Sync; // For convenience, let's put everything we need from the handler into a single structure. #[derive(Clone)] struct RequestHandler { /// The name of the resource. pub name: String, /// HTTP method. pub method: actix_web::http::Method, /// The raw handler. Note that it will be used from multiple threads. pub inner: Arc<RawHandler>, } 

يمكننا استخدام الهياكل لتمرير معلمات الطلب من خلال السياق. يمكن لـ Actix-web إلغاء تسلسل المعلمات تلقائيًا باستخدام serde. على سبيل المثال ، يتم إلغاء تحويل a = 15 & b = hello إلى هيكل مثل هذا:

 #[derive(Deserialize)] struct SimpleQuery { a: i32, b: String, } 

توافق وظيفة إلغاء التسلسل هذه بشكل جيد مع طلب النوع المرتبط من سمة "نقطة النهاية".

بعد ذلك ، يتيح استنباط محول يلف تنفيذًا محددًا لـ "Endpoint" في RequestHandler لـ actix-web. انتبه إلى حقيقة أنه أثناء القيام بذلك ، تختفي المعلومات الموجودة في أنواع الطلبات والاستجابة. وتسمى هذه التقنية محو الكتابة - فهي تحول الإرسال الثابت إلى ديناميكي.

 impl RequestHandler { fn from_endpoint<E: Endpoint>(name: &str, endpoint: E) -> RequestHandler { let index = move |request: HttpRequest<Context>| -> FutureResponse { let context = request.state(); let future = Query::from_request(&request, &()) .map(|query: Query<E::Request>| query.into_inner()) .and_then(|query| endpoint.handle(context, query).map_err(From::from)) .and_then(|value| Ok(HttpResponse::Ok().json(value))) .into_future(); Box::new(future) }; Self { name: name.to_owned(), method: actix_web::http::Method::GET, inner: Arc::from(index) as Arc<RawHandler>, } } } 

في هذه المرحلة ، يكفي فقط إضافة معالجات لطلبات POST ، لأننا أنشأنا سمة مستقلة عن تفاصيل التنفيذ. ومع ذلك ، وجدنا أن هذا الحل لم يكن متقدمًا بدرجة كافية.

عيوب خاصية "نقطة النهاية"


يتم إنشاء كمية كبيرة من التعليمات البرمجية المساعدة عند كتابة معالج:

 // A structure with the context of the handler. struct ElementCountEndpoint { elements: Rc<RefCell<Vec<Something>>>, } // Implementation of the `Endpoint` trait. impl Endpoint for ElementCountEndpoint { type Request = (); type Result = usize; fn handle(&self, context: &Context, _request: ()) -> Result<usize, io::Error> { Ok(self.elements.borrow().len()) } } // Installation of the handler in the backend. let endpoint = ElementCountEndpoint::new(elements.clone()); let handler = RequestHandler::from_endpoint("/v1/element_count", endpoint); actix_backend.endpoint(handler); 

من الناحية المثالية ، يجب أن نكون قادرين على اجتياز عملية إغلاق بسيطة كمعامل ، وبالتالي تقليل مقدار الضوضاء النحوية بشكل كبير.

 let elements = elements.clone(); actix_backend.endpoint("/v1/elements_count", move || { Ok(elements.borrow().len()) }); 

أدناه سنناقش كيف يمكن القيام بذلك.

غمر خفيف في البرمجة العامة


نحتاج إلى إضافة القدرة على إنشاء محول تلقائيًا يقوم بتنفيذ سمة `Endpoint` مع الأنواع المرتبطة الصحيحة. سيتكون الإدخال فقط من الإغلاق باستخدام معالج طلب HTTP.

يمكن أن يكون للمناقشات ونتائج الإغلاق أنواع مختلفة ، لذلك يتعين علينا التعامل مع أساليب التحميل الزائد هنا. الصدأ لا يدعم التحميل الزائد بشكل مباشر ولكنه يسمح بمضاهاة باستخدام السمات "Into" و "From".

بالإضافة إلى ذلك ، لا يجب أن يتطابق النوع الذي تم إرجاعه من قيمة الإغلاق مع القيمة التي تم إرجاعها لتطبيق `Endpoint`. لمعالجة هذا النوع ، يجب استخراجه من نوع الإغلاق المستلم.

جلب الأنواع من سمة 'Fn`


في Rust ، لكل إغلاق نوع فريد خاص به ، لا يمكن الإشارة إليه صراحة في البرنامج. للتلاعب مع عمليات الإغلاق ، نستخدم خاصية "Fn". تحتوي السمة على توقيع الدالة مع أنواع الوسائط والقيمة التي تم إرجاعها ، ومع ذلك ، فإن استرداد هذه العناصر بشكل منفصل لا يتم بسهولة.

الفكرة الرئيسية هي استخدام بنية مساعدة من النموذج التالي:

 /// Simplified example of extracting types from an F closure: Fn(A) -> B. struct SimpleExtractor<A, B, F> { // The original function. inner: F, _a: PhantomData<A>, _b: PhantomData<B>, } 

يجب أن نستخدم PhantomData ، لأن الصدأ يتطلب الإشارة إلى جميع المعلمات العامة في تعريف الهيكل. ومع ذلك ، فإن نوع الإغلاق أو الوظيفة F نفسها ليس نوعًا عامًا (على الرغم من أنه يطبق سمة "Fn" عامة). لا يتم استخدام معلمات النوع A و B فيه مباشرة.

هذا التقييد لنظام الصدأ هو الذي يمنعنا من تطبيق إستراتيجية أبسط من خلال تطبيق سمة "Endpoint" مباشرة للإغلاق:

 impl<A, B, F> Endpoint for F where F: Fn(&Context, A) -> B { type Request = A; type Response = B; fn handle(&self, context: &Context, request: A) -> Result<B, io::Error> { // ... } } 

في الحالة أعلاه ، يقوم المترجم بإرجاع خطأ:

 error[E0207]: the type parameter `A` is not constrained by the impl trait, self type, or predicates --> src/main.rs:10:6 | 10 | impl<A, B, F> Endpoint for F where F: Fn(&Context, A) -> B { | ^ unconstrained type parameter 

الهيكل المساعد SimpleExtractor يجعل من الممكن وصف تحويل `من`. يتيح لنا هذا التحويل حفظ أي وظيفة واستخراج أنواع الوسائط الخاصة بها:

 impl<A, B, F> From<F> for SimpleExtractor<A, B, F> where F: Fn(&Context, A) -> B, A: DeserializeOwned, B: Serialize, { fn from(inner: F) -> Self { SimpleExtractor { inner, _a: PhantomData, _b: PhantomData, } } } 

ترجمة التعليمات البرمجية التالية بنجاح:

 #[derive(Deserialize)] struct Query { a: i32, b: String, }; // Verification of the ordinary structure. fn my_handler(_: &Context, q: Query) -> String { format!("{} has {} apples.", qb, qa) } let fn_extractor = SimpleExtractor::from(my_handler); // Verification of the closure. let c = 15; let my_closure = |_: &Context, q: Query| -> String { format!("{} has {} apples, but Alice has {}", qb, qa, c) }; let closure_extractor = SimpleExtractor::from(my_closure); 

أنواع التخصص والعلامة


الآن لدينا وظيفة مع أنواع وسيطة ذات معلمات صريحة ، والتي يمكن استخدامها بدلا من سمة "نقطة النهاية". على سبيل المثال ، يمكننا بسهولة تنفيذ التحويل من SimpleExtractor إلى RequestHandler. لا يزال ، هذا ليس حلا كاملا. نحتاج إلى التمييز بطريقة ما بين معالجات طلبات GET و POST على مستوى الكتابة (وبين معالجات متزامن وغير متزامن). في هذه المهمة ، تأتي أنواع العلامات لمساعدتنا.

أولاً ، دعنا نعيد كتابة SimpleExtractor بحيث يمكن التمييز بين النتائج المتزامنة وغير المتزامنة. في الوقت نفسه ، سوف نقوم بتنفيذ سمة "من" لكل حالة. لاحظ أنه يمكن تنفيذ السمات لمتغيرات محددة من الهياكل العامة.

 /// Generic handler for HTTP-requests. pub struct With<Q, I, R, F> { /// A specific handler function. pub handler: F, /// Structure type containing the parameters of the request. _query_type: PhantomData<Q>, /// Type of the request result. _item_type: PhantomData<I>, /// Type of the value returned by the handler. /// Note that this value can differ from the result of the request. _result_type: PhantomData<R>, } // Implementation of an ordinary synchronous returned value. impl<Q, I, F> From<F> for With<Q, I, Result<I>, F> where F: Fn(&ServiceApiState, Q) -> Result<I>, { fn from(handler: F) -> Self { Self { handler, _query_type: PhantomData, _item_type: PhantomData, _result_type: PhantomData, } } } // Implementation of an asynchronous request handler. impl<Q, I, F> From<F> for With<Q, I, FutureResult<I>, F> where F: Fn(&ServiceApiState, Q) -> FutureResult<I>, { fn from(handler: F) -> Self { Self { handler, _query_type: PhantomData, _item_type: PhantomData, _result_type: PhantomData, } } } 

نحتاج الآن إلى الإعلان عن البنية التي ستجمع بين معالج الطلب واسمه ونوعه:

 #[derive(Debug)] pub struct NamedWith<Q, I, R, F, K> { /// The name of the handler. pub name: String, /// The handler with the extracted types. pub inner: With<Q, I, R, F>, /// The type of the handler. _kind: PhantomData<K>, } 

بعد ذلك ، نعلن العديد من الهياكل الفارغة التي ستكون بمثابة أنواع علامات. تسمح لنا العلامات بتنفيذ التعليمات البرمجية الخاصة بكل معالج لتحويل المعالج إلى RequestHandler الموضحة مسبقًا.

 /// A handler that does not change the state of the service. In HTTP, GET-requests correspond to this // handler. pub struct Immutable; /// A handler that changes the state of the service. In HTTP, POST, PUT, UPDATE and other similar //requests correspond to this handler, but for the current case POST will suffice. pub struct Mutable; 

يمكننا الآن تحديد أربعة تطبيقات مختلفة لسمات `From` لجميع مجموعات معلمات القالب R و K ​​(القيمة التي تم إرجاعها للمعالج ونوع الطلب).

 // Implementation of a synchronous handler of GET requests. impl<Q, I, F> From<NamedWith<Q, I, Result<I>, F, Immutable>> for RequestHandler where F: Fn(&ServiceApiState, Q) -> Result<I> + 'static + Send + Sync + Clone, Q: DeserializeOwned + 'static, I: Serialize + 'static, { fn from(f: NamedWith<Q, I, Result<I>, F, Immutable>) -> Self { let handler = f.inner.handler; let index = move |request: HttpRequest| -> FutureResponse { let context = request.state(); let future = Query::from_request(&request, &()) .map(|query: Query<Q>| query.into_inner()) .and_then(|query| handler(context, query).map_err(From::from)) .and_then(|value| Ok(HttpResponse::Ok().json(value))) .into_future(); Box::new(future) }; Self { name: f.name, method: actix_web::http::Method::GET, inner: Arc::from(index) as Arc<RawHandler>, } } } // Implementation of a synchronous handler of POST requests. impl<Q, I, F> From<NamedWith<Q, I, Result<I>, F, Mutable>> for RequestHandler where F: Fn(&ServiceApiState, Q) -> Result<I> + 'static + Send + Sync + Clone, Q: DeserializeOwned + 'static, I: Serialize + 'static, { fn from(f: NamedWith<Q, I, Result<I>, F, Mutable>) -> Self { let handler = f.inner.handler; let index = move |request: HttpRequest| -> FutureResponse { let handler = handler.clone(); let context = request.state().clone(); request .json() .from_err() .and_then(move |query: Q| { handler(&context, query) .map(|value| HttpResponse::Ok().json(value)) .map_err(From::from) }) .responder() }; Self { name: f.name, method: actix_web::http::Method::POST, inner: Arc::from(index) as Arc<RawHandler>, } } } // Implementation of an asynchronous handler of GET requests. impl<Q, I, F> From<NamedWith<Q, I, FutureResult<I>, F, Immutable>> for RequestHandler where F: Fn(&ServiceApiState, Q) -> FutureResult<I> + 'static + Clone + Send + Sync, Q: DeserializeOwned + 'static, I: Serialize + 'static, { fn from(f: NamedWith<Q, I, FutureResult<I>, F, Immutable>) -> Self { let handler = f.inner.handler; let index = move |request: HttpRequest| -> FutureResponse { let context = request.state().clone(); let handler = handler.clone(); Query::from_request(&request, &()) .map(move |query: Query<Q>| query.into_inner()) .into_future() .and_then(move |query| handler(&context, query).map_err(From::from)) .map(|value| HttpResponse::Ok().json(value)) .responder() }; Self { name: f.name, method: actix_web::http::Method::GET, inner: Arc::from(index) as Arc<RawHandler>, } } } // Implementation of an asynchronous handler of POST requests. impl<Q, I, F> From<NamedWith<Q, I, FutureResult<I>, F, Mutable>> for RequestHandler where F: Fn(&ServiceApiState, Q) -> FutureResult<I> + 'static + Clone + Send + Sync, Q: DeserializeOwned + 'static, I: Serialize + 'static, { fn from(f: NamedWith<Q, I, FutureResult<I>, F, Mutable>) -> Self { let handler = f.inner.handler; let index = move |request: HttpRequest| -> FutureResponse { let handler = handler.clone(); let context = request.state().clone(); request .json() .from_err() .and_then(move |query: Q| { handler(&context, query) .map(|value| HttpResponse::Ok().json(value)) .map_err(From::from) }) .responder() }; Self { name: f.name, method: actix_web::http::Method::POST, inner: Arc::from(index) as Arc<RawHandler>, } } } 

واجهة للخلفية


الخطوة الأخيرة هي إنشاء واجهة تقبل عمليات الإغلاق وإضافتها إلى الواجهة الخلفية المقابلة. في الحالة المحددة ، لدينا خلفية واحدة - actix-web. ومع ذلك ، هناك إمكانية تنفيذ إضافية وراء الواجهة. على سبيل المثال: مولد مواصفات Swagger.

 pub struct ServiceApiScope { actix_backend: actix::ApiBuilder, } impl ServiceApiScope { /// This method adds an Immutable handler to all backends. pub fn endpoint<Q, I, R, F, E>(&mut self, name: &'static str, endpoint: E) -> &mut Self where // Here we list the typical restrictions which we have encountered earlier: Q: DeserializeOwned + 'static, I: Serialize + 'static, F: Fn(&ServiceApiState, Q) -> R + 'static + Clone, E: Into<With<Q, I, R, F>>, // Note that the list of restrictions includes the conversion from NamedWith into RequestHandler // we have implemented earlier. RequestHandler: From<NamedWith<Q, I, R, F, Immutable>>, { self.actix_backend.endpoint(name, endpoint); self } /// A similar method for Mutable handlers. pub fn endpoint_mut<Q, I, R, F, E>(&mut self, name: &'static str, endpoint: E) -> &mut Self where Q: DeserializeOwned + 'static, I: Serialize + 'static, F: Fn(&ServiceApiState, Q) -> R + 'static + Clone, E: Into<With<Q, I, R, F>>, RequestHandler: From<NamedWith<Q, I, R, F, Mutable>>, { self.actix_backend.endpoint_mut(name, endpoint); self } 

لاحظ كيف يتم اشتقاق أنواع معلمات الطلب ونوع نتيجة الطلب وتزامن / تزامن المعالج تلقائيًا من توقيعه. بالإضافة إلى ذلك ، نحتاج إلى تحديد اسم ونوع الطلب بشكل صريح.

عيوب النهج


النهج الموصوف أعلاه ، على الرغم من كونه فعال للغاية ، له عيوبه. على وجه الخصوص ، يجب أن تراعي طرق endpoint و endpoint_mut خصوصيات التنفيذ لواجهات خلفية محددة . يمنعنا هذا التقييد من إضافة خلفية أثناء التنقل ، على الرغم من أن هذه الوظيفة نادراً ما تكون مطلوبة.

هناك مشكلة أخرى وهي أنه لا يمكننا تحديد تخصص معالج ما دون وسيطات إضافية . بمعنى آخر ، إذا كتبنا الكود التالي ، فلن يتم تجميعه لأنه يتعارض مع التطبيق العام الحالي:

 impl<(), I, F> From<F> for With<(), I, Result<I>, F> where F: Fn(&ServiceApiState) -> Result<I>, { fn from(handler: F) -> Self { Self { handler, _query_type: PhantomData, _item_type: PhantomData, _result_type: PhantomData, } } } 

نتيجة لذلك ، يجب أن تقبل الطلبات التي لا تحتوي على أي معلمات سلسلة JSON خالية ، والتي يتم إلغاء تسلسلها إلى (). يمكن حل هذه المشكلة عن طريق التخصص في نمط C ++ ، لكنها متوفرة حاليًا فقط في الإصدار الليلي من المحول البرمجي وليس من الواضح متى ستصبح ميزة مستقرة.

وبالمثل ، لا يمكن تخصيص نوع القيمة المرتجعة . حتى إذا كان الطلب لا يتضمن نوعًا معينًا من القيمة التي يتم إرجاعها ، فسيظل يمر على JSON بلا قيمة.

كما أن فك شفرة استعلام URL في طلبات GET يفرض أيضًا بعض القيود غير الواضحة على نوع المعلمات ، لكن هذه المشكلة تتعلق بالأخصائية الخاصة بتطبيق serde-urlencoded.

الخاتمة


كما هو موضح أعلاه ، قمنا بتطبيق واجهة برمجة تطبيقات محسّنة ، تتيح إنشاء معالجات بسيطة وواضحة ، دون الحاجة إلى القلق بشأن تفاصيل الويب. يمكن لهذه المعالجات العمل مع أي خلفية أو حتى مع عدة واجهات في وقت واحد.





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


All Articles