歡迎向Rust中文社群投稿,投稿地址,好文將在以下地方直接展示
- Rust中文社群首頁
- Rust中文社群Rust文章欄目
- 知乎專欄Rust語言
Rust生態系統仍在增長。因此,具有改進功能的新庫經常被髮布到開發人員社群中,而較舊的庫變得過時。當我們最初設計Exonum時,我們使用了Iron Web框架。我們現在決定將Exonum平臺轉移到actix-web框架。
在本文中,我們將介紹如何使用泛型程式設計將Exonum框架移植到actix-web
Exonum on Iron
在Exonum平臺中,Iron框架在沒有任何抽象的情況下使用。我們為某些資源安裝了handlers,並通過使用輔助方法解析URL來獲取請求引數; 結果只是以字串的形式返回。
另外,我們以CORS頭的形式使用了一些中介軟體外掛。我們使用mount將所有處理程式合併到一個API中。
我們決定擺脫 Iron
Iron是一個很好的庫,有很多外掛。然而,它是在future
和tokio
等專案不存在的日子寫的。
Iron的體系結構涉及同步請求處理,這很容易受到大量同時開啟的連線(併發)的影響。為了實現可擴充套件性,Iron需要變為非同步,這將涉及重新思考和重寫整個框架。結果,我們看到軟體工程師逐漸不使用Iron。
為什麼我們選擇Actix-Web
Actix-web是一個受歡迎的框架,在TechEmpower基準測試中排名很高。它擁有一個活躍的開發人員社群,與Iron不同,它擁有精心設計的API和基於actix actor框架的高質量實現。執行緒池非同步處理請求; 如果請求處理panics,則actor會自動重啟。
以前,人們擔心actix-web包含許多不安全的程式碼。但是,當框架逐漸以安全的程式語言(Rust)重寫時,不安全程式碼的數量顯著減少。Bitfury的工程師(Exonum框架的工程師)自己對這些程式碼進行了審查,並對其長期穩定性充滿信心。
對於Exonum框架,轉向actix解決了操作穩定性問題。如果存在大量連線,則Iron框架可能會失敗。我們還發現actix-web API更簡單,更高效,更統一。我們相信,使用者和開發人員可以更輕鬆地使用Exonum程式設計介面,由於採用了actix-web設計,現在可以更快地執行。
我們對Web框架的要求
在此過程中,我們意識到,不僅要簡單地轉換框架,而且要設計獨立於任何特定Web框架的新API體系結構,這對我們非常重要。這種架構允許建立處理程式,幾乎不關心Web細節,並將它們轉移到任何後端。這個概念可以通過編寫一個應用基本型別和trait
的前端來實現。
要了解這個前端需要看起來像什麼,讓我們定義任何HTTP API的真正含義:
- 請求僅由客戶提出; 伺服器只響應它們(伺服器不發起請求)。
- 請求讀取資料或更改資料。
- 作為請求處理的結果,伺服器在成功的情況下返回包含所需資料的響應; 如果失敗,或者有關錯誤的資訊。
如果我們要分析所有抽象層,事實證明任何HTTP請求只是一個函式呼叫:
fn request(context:& ServiceContext,query:Query) - > Result <Response,ServiceError>
其他一切都可以被視為這個基本實體的延伸。因此,為了獨立於Web框架的特定實現,我們需要以類似於上面示例的樣式編寫處理程式。
用於HTTP請求的範型處理的 trait“Endpoint”
最簡單直接的方法是宣告Endpoint
trait,它描述了特定請求的實現:
// 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” trait的相關型別請求很好地吻合。
接下來,讓我們設計一個介面卡,它將特定的Endpoint
實現包裝到actix-web
的RequestHandler
中。請注意,在執行此操作時,請求和響應型別的資訊將消失。這種技術稱為型別擦除 – 它將靜態排程轉換為動態排程。
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請求新增處理程式就足夠了,因為我們建立了一個獨立於實現細節的trait。 但是,我們發現這個解決方案還不夠先進。
“Endpoint” trait的缺點
編寫處理程式時會生成大量輔助程式碼:
// 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
trait。 輸入將僅包含具有HTTP請求處理程式的閉包。
引數和閉包的結果可以有不同的型別,因此我們必須在這裡使用方法過載。 Rust不支援直接過載,但允許使用Into
和From
trait進行模擬。
此外,閉包值的返回型別不必與Endpoint
實現的返回值匹配。 要操縱此型別,必須從接收到的閉包的型別中提取它。
從“Fn” trait中獲取型別
在Rust中,每個閉包都有自己獨特的型別,無法在程式中明確指出。 對於帶閉包的操作,我們使用Fn
trait。 trait包含函式的簽名以及引數型別和返回值,但是,分別檢索這些元素並不容易。
主要思想是使用以下形式的輔助結構:
/// 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,因為Rust要求所有範型引數都在結構的定義中指出。 但是,閉包或函式F本身的型別不是範型的(儘管它實現了範型的Fn
trait)。 型別引數A和B不直接使用。
正是Rust型別系統的這種限制使得我們無法通過直接為閉包實現Endpoint
trait來應用更簡單的策略:
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可以描述“From”的轉換。 這種轉換允許我們儲存任何函式並提取其引數的型別:
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.", q.b, q.a)
}
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 {}", q.b, q.a, c)
};
let closure_extractor = SimpleExtractor::from(my_closure);
專業化和標記型別
現在我們有一個帶有顯式引數化引數型別的函式,可以使用它來代替Endpoint
trait。 例如,我們可以輕鬆實現從SimpleExtractor
到RequestHandler
的轉換。 不過,這不是一個完整的解決方案。 我們需要以某種方式區分型別級別(以及同步和非同步處理程式之間)的GET
和POST
請求的處理程式。 在此任務中,標記型別可以幫助我們。
首先,讓我們重寫SimpleExtractor
,以便它可以區分同步和非同步結果。 同時,我們將為每個案例實施“From” trait。 注意,可以針對範型結構的特定變體實現 trait。
/// 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;
現在我們可以為模板引數R和K的所有組合(處理程式的返回值和請求的型別)定義“From”特徵的四種不同實現。
// 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
方法應該考慮特定後端的實現 trait。 此限制阻止我們在旅途中新增後端,但很少需要此功能。
另一個問題是我們無法在沒有其他引數的情況下定義處理程式的特化。 換句話說,如果我們編寫以下程式碼,它將不會被編譯,因為它與現有的範型實現相沖突:
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字串null
,該字串被反序列化為()
。 這個問題可以通過C ++風格的專業化來解決,但是現在它只在編譯器的夜間版本中可用,並且不清楚何時它將成為一個穩定的 trait。
同樣,返回值的型別也不能專門化。 即使請求沒有暗示某種型別的返回值,它仍然會傳遞帶有null
的JSON。
在GET
請求中解碼URL
查詢也對引數型別施加了一些不明顯的限制,但是這個問題與serde-urlencoded
實現的 trait有關。
結論
如上所述,我們已經實現了一個改進的API,它允許簡單而清晰地建立處理程式,而無需擔心Web細節。 這些處理程式可以與任何後端一起使用,甚至可以同時使用多個後端。