gRPC-rs:從 C 到 Rust

介紹

在上篇文章中,我們講到 TiKV 為了支持 [gRPC],我們造了個輪子 [gRPC-rs],這篇文章簡要地介紹一下這個庫。首先我們來聊聊什么是 gRPC。gRPC 是 Google 推出的基于 [HTTP2] 的開源 RPC 框架,希望通過它使得各種微服務之間擁有統一的 RPC 基礎設施。它不僅支持常規的平臺如 Linux,Windows,還支持移動設備和 IoT,現有十幾種語言的實現,現在又多了一種語言 Rust。

gRPC 之所以有如此多的語言支持,是因為它有一個 C 寫的核心庫(gRPC core),因此只要某個語言兼容 C ABI,那么就可以通過封裝,寫一個該語言的 gRPC 庫。Rust 對 C 有良好的支持,gRPC-rs 就是對 gRPC core ABI 的 Rust 封裝。

Core 能異步處理 RPC 請求,在考慮到 Rust 中已有較為成熟的異步框架 [Futures],我們決定將 API 設計成 Future 模式。

gRPC-rs 架構圖

我們將根據架構圖從底向上地講一下,在上一篇文章中已經討論過傳輸層和協議,在這就不再贅述。

gRPC Core

Core 中有幾個比較重要的對象:

  • Call 以及 4 種類型 RPC: Call 代表了一次 RPC,可以派生出四種類型 RPC,

    • Unary: 這是最簡單的一種 RPC 模式,即一問一答,客戶端發送一個請求,服務端返回一個回復,該輪 RPC 結束。

    • Client streaming: 這類的 RPC 會創建一個客戶端到服務端的流,客戶端可以通過這個流,向服務端發送多個請求,而服務端只會返回一個回復。

    • Server streaming: 與上面的類似,不過它會創建一個服務端到客戶端的流,服務端可以發送多個回復,

    • Bidirectional streaming: 如果說上面兩類是單工,那么這類就是雙工了,客戶端和服務端可以同時向對方發送消息。

    值得一提的是由于 gRPC 基于 HTTP2,它利用了 HTTP2 多路復用特性,使得一個 TCP 連接上可以同時進行多個 RPC,一次 RPC 即為 HTTP2 中的一個 Stream。

  • Channel: 它是對底層鏈接的抽象,具體來說一個 Channel 就是一條連著遠程服務器的 TCP 鏈接。

  • Server: 顧名思義,它就是 gRPC 服務端封裝,可以在上面注冊我們的服務。

  • Completion queue: 它是 gRPC 完成事件隊列,事件可以是收到新的回復,可以是新來的請求。

簡要介紹一下 Core 庫的實現,Core 中有一個 [Combiner] 的概念,Combiner 中一個函數指針或稱組合子(Combinator)隊列。每個組合子都有特定的功能,通過不同的組合可以實現不同的功能。下面的偽碼大概說明了 Combiner 的工作方式。

class combiner {mpscq q; // multi-producer single-consumer queue can be made non-blockingstate s; // is it empty or executingrun(f) {if (q.push(f)) {// q.push returns true if it's the first thingwhile (q.pop(&f)) { // modulo some extra work to avoid racesf();}}}
}

Combiner 里面有一個 mpsc 的無鎖隊列 q,由于 q 只能有一個消費者,這就要求在同一時刻只能有一個線程去調用隊列里面的各個函數。調用的入口是 run() 方法,在 run() 中各個函數會被序列地執行。當取完 q 時,該輪調用結束。假設一次 RPC 由六個函數組成,這樣的設計使這組函數(RPC)可以在不同的線程上運行,這是異步化 RPC 的基礎。

Completion queue(以下簡稱 CQ)就是一個 Combiner,它暴露出了一個 next()借口,相當于 Combiner 的 run()。由于接口的簡單,Core 內部不用開啟額外線程,只要通過外部不斷調用 next() 就能驅動整個 Core。

所有的 HTTP2 處理,Client 的 RPC 請求和 Server 的 RPC 連接全是通過一個個組合子的不同組合而構成的。下面是一次 Unary 的代碼。它由6個組合子組成,這些組合子作為一個 batch 再加上 Call 用于記錄狀態,兩者構成了這次的 RPC。

grpc_call_error grpcwarp_call_start_unary(grpc_call *call, grpcsharp_batch_context *tag) {grpc_op ops[6];ops[0].op = GRPC_OP_SEND_INITIAL_METADATA;...ops[1].op = GRPC_OP_SEND_MESSAGE;...ops[2].op = GRPC_OP_SEND_CLOSE_FROM_CLIENT;...ops[3].op = GRPC_OP_RECV_INITIAL_METADATA;...ops[4].op = GRPC_OP_RECV_MESSAGE;...ops[5].op = GRPC_OP_RECV_STATUS_ON_CLIENT;return grpcwrap_call_start_batch(call, ops, tag);
}

用 Rust 封裝 Core

<!-- 適當擴展該節? -->
介紹完 Core,現在說一下如何用 Rust 封裝它。這一層封裝并不會產生額外的開銷,不像有的語言在調用 C 時會有類型的轉換或者 runtime 會有較大開銷,在 Rust 中開銷微乎其微,這得益于 Rust 用 llvm 做編譯器后端,它對 C 有良好的支持,Rust 調用 C ABI 就像調用一個普通的函數,可以做到 Zero-cost。

同時用 Rust 封裝 C ABI 是一件很簡單的事情,簡單到像黑魔法。比如封裝 CQ next():

C:

grpc_event grpc_completion_queue_next(grpc_completion_queue *cq,gpr_timespec deadline,void *reserved);

Rust:

extern "C" {pub fn grpc_completion_queue_next(cq: *mut GrpcCompletionQueue,deadline: GprTimespec,reserved: *mut c_void)-> GrpcEvent;
}

接著我們看看如何封裝 C 的類型。繼續以 next() 為例子:

C:

// CQ 指針
grpc_completion_queue *cq;// grpc_event 結構體
struct grpc_event {grpc_completion_type type;int success;void *tag;
};

Rust:

pub enum GrpcCompletionQueue {}#[repr(C)]
pub struct GrpcEvent {pub event_type: GrpcCompletionType,pub success: c_int,pub tag: *mut c_void,
}

CQ 在 Core 的 ABI 中傳遞的形式是指針,Rust Wraper 無須知道 CQ 具體的內部結構。對于這種情況,Rust 推薦用無成員的枚舉體表示,具體好處有兩個,第一,由于沒有成員,我們無法在 Rust 中構建該枚舉體的實例,第二,Type safe,當傳遞了一個錯誤類型的指針時編譯器會報錯。

#[repr(C)] 也是 Rust 的黑魔法之一。加上了這個標簽的結構體,在內存中的布局和對齊就和 C 一樣了,這樣的結構體可以安全地傳遞給 C ABI。

Futures in gRPC-rs

經過上一節的封裝,我們已經得到了一個可用但是非常裸的 Rust gRPC 庫了,[grpc-sys]。在實踐中,我們不推薦直接用 [grpc-sys],直接用它就像在 Rust 中寫 C 一樣,事倍功半,Rust 語言的諸多特性無法得到施展,例如泛型,Trait,Ownership 等,也無法融入 Rust 社區。

上面說過 Core 能異步處理 RPC,那么如何用 Rust 來做更好的封裝呢? [Futures]!它是一個成熟的異步編程庫,同時有一個活躍的社區。 Futures 非常適用于 RPC 等一些 IO 操作頻繁的場景。Futures 中也有組合子概念,和 Core 中的類似,但是使用上更加方便,也更加好理解。舉一個栗子:

use futures::{future, Future};fn double(i: i64) -> i64 { i * 2 }let ans = future::ok(1).map(double).and_then(|i| Ok(40 + i));println!("{:?}", ans.wait().unwrap());

你覺得輸出的答案是多少呢?沒錯就是 42。在 Core 那節說過不同的組合子組織在一起可以干不同的事,在 Future 中我們可以這么理解,一件事可以分成多個步驟,每個步驟由一個組合子完成。比如上例,map 完成了翻倍的動作,and_then 將輸入加上 40。 現在來看看 gRPC-rs 封裝的 API。

// helloworld.proto
service Greeter {// An unary RPC, sends a greetingrpc SayHello (HelloRequest) returns (HelloReply) {}
}impl GreeterClient {pub fn say_hello_async(&self, req: HelloRequest) -> ClientUnaryReceiver<HelloReply> {self.client.unary_call_async(&METHOD_GREETER_SAY_HELLO, req, CallOption::default())}...
}

以 [helloworld.proto] 為例,GreeterClient::say_hello_async() 向遠程 Server 發送一個請求 (HelloRequest),Server 返回給一個結果 (HelloReply)。由于是異步操作,這個函數會立即返回,返回的 ClientUnaryReceiver 實現了 Future,當它完成時就會得到 HelloReply。在一般的異步編程中都會有 Callback,用于處理異步的返回值,在這個 RPC 中就是 HelloReply,在 Future 中可以用組合子來寫,比如 and_then,再舉一個栗子,現有一次完整的 RPC 邏輯,拿到回復后打印到日志。下面就是 gRPC-rs 的具體用法。

// 同步
let resp = client.say_hello(req);
println!("{:?}", resp);// 異步
let f = client.say_hello_async(req).and_then(|resp| {println!("{:?}", resp);Ok(())});
executer.spawn(f); // 類似 Combiner,// 用于異步執行 Future,// 常用的有 tokio-core。

Unary RPC

gRPC-rs 根據 service 在 proto 文件中的定義生成對應的代碼,包括 RPC 方法的定義(Method)、客戶端和服務端代碼,生成的代碼中會使用 gRPC-rs 的 API。那么具體是怎么做的呢?這節還是以 helloworld.proto 為例,來講講客戶端 Unary RPC 具體的實現。首先,SayHelloMethod 記錄了 RPC 類型,全稱以及序列化反序列化函數。為什么要序列化反序列化函數呢?因為 Core 本身不涉及消息的序列化,這一部分交由封裝層解決。在生成的客戶端中可以會調用 gRPC-rs 的 API,根據 Method 的定義發起 RPC。

// 生成的代碼
const METHOD_GREETER_SAY_HELLO: Method<HelloRequest, HelloReply> = Method {ty: MethodType::Unary,name: "/helloworld.Greeter/SayHello",req_mar: Marshaller { ser: pb_ser, de: pb_de },resp_mar: Marshaller { ser: pb_ser, de: pb_de },
};impl GreeterClient {// An unary RPC, sends a greetingpub fn say_hello_async(&self, req: HelloRequest)-> ClientUnaryReceiver<HelloReply> {self.client.unary_call_async(&METHOD_GREETER_SAY_HELLO, req)}...
}// gRPC-rs 的 API。該函數立即返回,不會等待 RPC 完成。省略部分代碼。
pub fn unary_async<P, Q>(channel: &Channel,method: &Method<P, Q>,req: P)-> ClientUnaryReceiver<Q> {let mut payload = vec![];(method.req_ser())(&req, &mut payload);            // 序列化消息let call = channel.create_call(method, &opt);      // 新建 Calllet cq_f = unsafe {grpc_sys::grpcwrap_call_start_unary(call.call, // 發起 RPCpayload,tag)};ClientUnaryReceiver::new(call, cq_f, method.resp_de()) // 收到回復后再反序列化
}

寫在最后

這篇簡單介紹了 gRPC Core 的實現和 gRPC-rs 的封裝,詳細的用法,在這就不做過多介紹了,大家如果感興趣可以查看 [examples]。 gRPC-rs 深入使用了 Future,里面有很多神奇的用法,比如 Futures in gRPC-rs 那節最后的 executer, gRPC-rs 利用 CQ 實現了一個能并發執行 Future 的 executer(類似 furtures-rs 中的 [Executer]),大幅減少 context switch,性能得到了顯著提升。如果你對 gRPC 和 rust 都很感興趣,歡迎參與開發,目前還有一些工作沒完成,詳情請點擊 https://github.com/pingcap/grpc-rs

參考資料:

gRPC open-source universal RPC framework

The rust language implementation of gRPC

[Hypertext Transfer Protocol Version 2 (HTTP/2)
][HTTP2]

Zero-cost Futures in Rust

深入了解 gRPC:協議

gRPC, Combiner Explanation

Rust, Representing opaque structs

Rust repr(), alternative representations

gRPC - A solution for RPCs by Google

Tokio, A platform for writing fast networking code with Rust.

作者:沈泰寧

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/257948.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/257948.shtml
英文地址,請注明出處:http://en.pswp.cn/news/257948.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

紅帽linux無法進入tty,linux自啟腳本(以及無法進入tty控制臺)

1.建立需開機運行的腳本auto(可以不要后面的.sh后綴)2.放在/etc/init.d/目錄下 (操作系統復制命令&#xff0c;在當前文件夾下復制sudo cp auto /etc/init.d)[可能先要對init.d取得x權限]3.賦予權限&#xff0c;在init.d文件目錄下sudo chmod 775 ./auto4.執行&#xff0c;命…

【最短路】SDUT3034--炸學校

炸學校 Time Limit: 2000ms Memory limit: 65536K 有疑問&#xff1f;點這里^_^ 題目描述 “小兒么小二郎&#xff0c;背著那炸彈炸學校&#xff0c;不怕那太陽曬&#xff0c;也不怕那風雨狂。”估計這首歌我們大家都耳熟能詳了。于是就有一群小學生們商量著炸學校。要把本…

管控研發部門USB設備

前提背景&#xff1a;研發部門圖紙經常泄漏&#xff0c;領導說要管控USB,但是要能讀&#xff0c;只限制不能寫。本想大力推薦Devicelock&#xff0c;因費用原因沒了后話&#xff0c;只好使用最基本的域策略進行實施.51CTOblog傳圖片這么麻煩&#xff0c;還是全部寫文字好了。實…

linux系統編程練手項目,精選 22 個 C++ 項目,編程小白練手首選!

C/C 做為元老級的編程語言&#xff0c;任時光更迭依舊屹立不倒&#xff0c;哪怕現在煊赫一時的AI&#xff0c;其底層也是用其編寫。linux那么做為新手該如何快速上手 C 呢&#xff1f;固然是敲代碼啊&#xff01;一切不寫代碼的學編程都是瞎搞。下面為你們精選了 22 個 C 項目&…

Swift iOS : WebView緩存圖片的方法

廣告 Swift iOS開發小書 &#xff0c;幫你快速上手開發 www.ituring.com.cn/book/2413 正文 每次加載WebView內容&#xff0c;如果圖片可以緩存的話&#xff0c;速度就會非常快。默認情況下&#xff0c;WebView自己來加載圖片&#xff0c;緩存的策略也是自己定的。如想要自己緩…

linux怎么同時查看兩個文件,MultiTail - 在單個Linux終端中同時監視多個文件

無論是服務器管理員還是程序員&#xff0c;我們需要參考多個日志文件來有效地排除故障任務。 為了實現這一點&#xff0c;我們必須打開&#xff0c;拖尾或更少的不同shell中的每個日志文件。 但是&#xff0c;我們可以使用傳統的tail命令狀尾-f在/ var / log / messages文件或尾…

新一代藍牙5標準開啟 會成為物聯網的最佳選擇嗎

在過去&#xff0c;藍牙在生活中最常見的應用就是鍵盤、鼠標、音箱和藍牙耳機&#xff0c;這些傳輸對頻寬要求不高&#xff0c;藍牙技術的采用不僅節省了線材成本&#xff0c;還增加了產品的靈活性。藍牙技術聯盟(SIG)正式宣布推出新一代標準藍牙5(Bluetooth 5)&#xff0c;其主…

今日BBC

1、隨身英語 Dry January 新年戒酒一個月 link 2、地道英語 Hot potato 棘手的問題“燙手山芋” link 3、今日新聞 Brussels attacks: Belgian police arrest six suspects link The arrests were made in the Schaerbeek district. There is no word yet on the identitie…

c語言中的指針語法,C語言中指針的用法介紹

C語言中指針的用法介紹for(int i0;i{num*s;s;}return num;)這個例子中的函數 fun統計一個字符串中各個字符的 ASCII 碼值之和。前面說了&#xff0c;數組的名字也是一個指針。在函數調用中&#xff0c;當把 str 作為實參傳遞給形參 s后&#xff0c;實際是把 str 的值傳遞給了 s…

實驗吧 貌似有點難 偽造ip

解題鏈接&#xff1a; http://ctf5.shiyanbar.com/phpaudit/ 解答&#xff1a; 點擊View the source code —>代碼顯示IP為1.1.1.1即可得到KEY—>使用modify header偽造IP—>拿到flag 相關&#xff1a; modify header我也是第一次用&#xff0c;下面附上相關說明&…

用C語言用指針怎么算通用定積分,C語言:利用指針編寫程序,用梯形法計算給定的定積分實例...

題目要求利用指針編寫程序&#xff0c;用梯形法計算下列公式中的定積分&#xff1a;參考代碼首先說明一下指針的用處&#xff1a;因為所傳遞的參數均為數字&#xff0c;并不需要使用指針提高效率&#xff0c;故這里使用指針指向函數。請注意calc()函數中的這一語句&#xff1a;…

單點登錄系統cas資料匯總

http://jasig.github.io/cas/4.0.x/index.html 主頁https://jasigcas.herokuapp.com demohttps://wiki.jasig.org/display/CASUM/Home 4.x之前的文檔http://jasig.github.io/cas/4.1.x/index.html …

有限小數用c語言,分數化為有限小數或無限循環小數(c實現)

問題描述&#xff1a;將分數轉化為小數&#xff0c;相信很多人都會吧&#xff0e;那么&#xff0c;這里給定一個分數N/D,N為分子&#xff0c;D為分母(N,D均為整數)&#xff0c;試編程求出N/D的小數形式&#xff0c;當然如果這個小數為無限循環小數&#xff0c;則把循環的部分用…

你該把前端外包出來了

2019獨角獸企業重金招聘Python工程師標準>>> 移動熱潮慢慢褪去&#xff0c;大的幾個app已經霸占了所有的人桌面&#xff0c;而微信卻變得越來越重要。微信里面&#xff0c;提倡H5的應用&#xff0c;H5應用開發成本低、上線快、易調整、跨平臺等諸多優勢&#xff0c;…

R 統計學工具部署和使用

由于公司內部對于市場數據分析的需求&#xff0c;要求引入R統計工具&#xff0c;并集成到報表工具中。對于R的介紹&#xff0c;大家請百度一下&#xff0c;當然&#xff0c;最好能去看官方的說明 https://www.r-project.org/ 下面簡單介紹一下R工具的安裝和數據分析工具Spotfir…

USACO Dual Palindromes

輸出N個大于s的滿足條件的數&#xff0c; 對于滿足條件的數的定義是其2-10進制表示中&#xff0c;至少有兩種表示為回文串。。還是暴力&#xff1a; /*ID: m1500293LANG: CPROG: dualpal */ #include <cstdio> #include <cstring> #include <algorithm>using…

c語言庫函數fgets,C語言 標準I/O庫函數 fgets 使用心得

char *fgets(char *s, int n, FILE *stream);參數說明&#xff1a;s --指定存放所讀取的數據的位置n -- 指定所讀取數據的最大長度(這個最大長度包括了字符串結束符 \0所占據的存儲空間&#xff0c;因此&#xff0c;實際最大讀取的有效字符數是 n - 1)stream --數據源&#xff…

Android下創建一個輸入法

輸入法是一種可以讓用戶輸入文字的控件。Android提供了一套可擴展的輸入法框架&#xff0c;使得應用程序可以讓用戶選擇各種類型的輸入法&#xff0c;比如基于觸屏的鍵盤輸入或者基于語音。當安裝了特定輸入法之后&#xff0c;用戶即可在系統設置中選擇個輸入法&#xff0c;并在…

linux awk f,linux的awk詳情(上)

一丶awk介紹AWK是一種處理文本文件的語言&#xff0c;是一個強大的文本分析工具&#xff0c;可以報告生成器&#xff0c;格式化文本輸出1.常用語法awk [options] ‘program’ varvalue file…awk [options] -f programfile varvalue file…awk [options] BEGIN{ action;… } pa…

C#的async和await

C# 5.0中引入了async 和 await。這兩個關鍵字可以讓你更方便的寫出異步代碼。 看個例子&#xff1a; public class MyClass {public MyClass(){DisplayValue(); //這里不會阻塞System.Diagnostics.Debug.WriteLine("MyClass() End.");}public Task<double> Get…