29. 乾貨系列從零用Rust編寫正反向代理,非同步回撥(async trait)的使用

2023-11-21 12:03:09

wmproxy

wmproxy已用Rust實現http/https代理, socks5代理, 反向代理, 靜態檔案伺服器,四層TCP/UDP轉發,七層負載均衡,內網穿透,後續將實現websocket代理等,會將實現過程分享出來,感興趣的可以一起造個輪子

專案地址

國內: https://gitee.com/tickbh/wmproxy

github: https://github.com/tickbh/wmproxy

關於 ++trait++

  trait是Rust中的概念,類似於其他語言中的介面(interface)
  在Rust中不存在繼承的概念,所有關於結構體的拓展功能全部均由trait來代替。比如std::io::Read這是一個關於io的trait,在TcpStream中和在File中均實現了該功能,這樣子如果上層只關心讀操作的,我們就可以將其轉化成std::io::Read的一個物件,比如io: std::io::Read,後面我們也可以把他包裹成BufferReader等實現功能的轉化。

為什麼不在Rust中使用繼承

  • 繼承破壞了封裝性,父類別的改變會影響子類。如果父類別的出現發生變化,所有繼承自該父類別的子類都需要相應地進行修改,這會增加程式碼的維護成本。
  • Rust追求記憶體安全和無資料競爭,繼承不利於編譯器進行靜態檢查。
  • 繼承關係的耦合度高。子類和父類別之間是緊密耦合的關係,這會影響程式碼的靈活性和可移植性。
  • 繼承往往被過度使用,導致子類與父類別功能緊密耦合。

下面舉下例子,設計關於車的通用基礎類別,能跑能停等

public class BaseCar { 
  //... 省略其他屬性和方法... 
  public void run() { //... }
  public void stop() { //... }
}

一開始自行車都很完美,接下來設計摩托車,摩托車需要加油,那麼基礎類別被改成

public class BaseCar { 
  //... 省略其他屬性和方法... 
  public void run() { //... }
  public void stop() { //... }
  public void refuel() { //... }
}

但是自行車又沒有加油的需求

// 自行車
public class Bicycle extends BaseCar { 
 //... 省略其他屬性和方法... 
 public void refuel() { 
     throw new UnSupportedMethodException("我不需要加油!");
 }
}

如果接下來又有修理引擎的介面,那基礎類別又得加repairEngine的介面。自行車繼承這個基礎類別將會產生嚴重的負擔,不繼承又得重新寫一些關於基礎能力的函數,又會增加重複程式碼。

那麼接下來是以trait方案的實現

pub trait Base {
   fn run(&self); 
   fn stop(&self); 
}
pub trait Refuel {
   fn refuel(&mut self); 
}
pub trait RepairEngine {
   fn repair_engine(&mut self); 
}

那麼自行車只需要實現Base能力,然後摩托車在自行車的基礎上實現RefuelRepairEngine即可實現解耦。

非同步的trait

在程式中均使用的是非同步(async)程式設計,那麼我們可能需要將trait實現成:

pub trait Base {
   async fn run(&self); 
   async fn stop(&self); 
}

當我們如此寫的時候編譯器就會提示我們:

functions in traits cannot be declared `async`
`async` trait functions are not currently supported
consider using the `async-trait` crate: https://crates.io/crates/async-trait
see issue #91611 <https://github.com/rust-lang/rust/issues/91611> for more informationrustcClick for full compiler diagnostic

原來非同步的trait實現還沒有進入到stable階段,暫時只能有預覽版即nightly版本進行使用。

那麼本文將探討該功能在未stable前如何實現非同步的trait

假如返回一個非同步的Future

trait Base {
    type FetchData<'a>: std::future::Future<Output = String> + 'a where Self: 'a;
    fn run<'a>(&'a self) -> Self::FetchData<'a>;
}

那麼實現自行車的函數將為:

trait Base {
    type FetchData<'a>: /* 將要何種型別呢?? */;
    fn run<'a>(&'a self) -> Self::FetchData<'a>;
}

我們嘗試過各種型別,編譯器都無法通過編譯,所以我們需要進行返回值的修改,我們將通過執行時型別擦除來實現。

首先,我們可以通過用 擦除 future 型別來避免編寫 future 型別。以上面的例子為例,你可以這樣寫你的特徵:dyn

trait Base {
    fn run<'a>(&'a self) -> Pin<Box<dyn Future<Output = String> + Send + '_>>;
}

那麼實現將為:

impl Base for Bicycle {
    fn run<'a>(&'a self) -> std::pin::Pin<Box<dyn std::future::Future<Output = String> + Send + '_>> {
        Box::pin(async {
            "ok".to_string()
        })
    }
}

可以看出整個函數非常的冗餘,相當的讓人難受。

那麼此時我們可以藉助async-trait的宏處理庫,他將幫我們自動處理掉無用的資料,那麼我們的程式碼將變成如下:

#[async_trait]
trait Base {
    async fn run(&self) -> String;
}
#[async_trait]
impl Base for Bicycle {
    async fn run(&self) -> String {
        "ok".to_string()
    }
}

當然現在此方法會造成額外的開銷,像BoxSend等都會造成一定的效能損失,如果要零損失實現非同步還可以嘗試以下方案

手動實現Poll

需要零開銷或在no_std上下文中工作的特徵還有另一種選擇:它們可以從 Future 特徵中獲取輪詢的概念,並將其直接構建到它們的介面中。如果 future 已完成,並且 future 正在等待其他事件,則該方法將返回。Future::poll,Poll::Ready(Output),Poll::Pending

pub trait Base {
    type Item;
    fn poll_next(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>
    ) -> Poll<Option<Self::Item>>;
}

當然控制Poll的方式相當的麻煩,只要在對效能要求極高的情況下在進行此操作。

預期的官方實現

在最新的Beta或者nightly版本中可以用#![feature(async_fn_in_trait)]來啟用該能力,那麼我們就可以如下程式設計:

#![feature(async_fn_in_trait)]

trait Base {
    async fn run(&self) -> String;
}

impl Base for Bicycle {
    async fn run(&self) -> String {
        "ok".to_string()
    }
}

這樣子就和普通的實現沒有什麼差別了。

實現該功能的難點

理論上來說,一個非同步只有你在呼叫await的時候他才會真正的被呼叫,如果在此前有參照對話的存在,那麼他的生命週期管理才是比較麻煩的存在。

小結

當前的Rust版本為1.74.0,好訊息的是當前async trait已經Beta Channel了,如果不出意外的話下一次釋出版本的穩定版將會擁有該能力了。該功能的官方實現將會給非同步程式設計的帶來極大的方便。讓async/await能力越來越強。預期2023年末就可以直接使用了。下一章節我們將講async trait在專案中的應用。

點選 [關注][在看][點贊] 是對作者最大的支援