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.mzph.cn/news/257948.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

linux系统编程练手项目,精选 22 个 C++ 项目,编程小白练手首选!

C/C 做为元老级的编程语言&#xff0c;任时光更迭依旧屹立不倒&#xff0c;哪怕现在煊赫一时的AI&#xff0c;其底层也是用其编写。linux那么做为新手该如何快速上手 C 呢&#xff1f;固然是敲代码啊&#xff01;一切不写代码的学编程都是瞎搞。下面为你们精选了 22 个 C 项目&…

linux怎么同时查看两个文件,MultiTail - 在单个Linux终端中同时监视多个文件

无论是服务器管理员还是程序员&#xff0c;我们需要参考多个日志文件来有效地排除故障任务。 为了实现这一点&#xff0c;我们必须打开&#xff0c;拖尾或更少的不同shell中的每个日志文件。 但是&#xff0c;我们可以使用传统的tail命令状尾-f在/ var / log / messages文件或尾…

今日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…

实验吧 貌似有点难 伪造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;…

你该把前端外包出来了

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…

Android下创建一个输入法

输入法是一种可以让用户输入文字的控件。Android提供了一套可扩展的输入法框架&#xff0c;使得应用程序可以让用户选择各种类型的输入法&#xff0c;比如基于触屏的键盘输入或者基于语音。当安装了特定输入法之后&#xff0c;用户即可在系统设置中选择个输入法&#xff0c;并在…

eclipse创建android工程,在eclipse创建android 工程

1.在工具栏选择"New".在弹出对话框里&#xff0c;开打android文件夹&#xff0c;选择"android application Project"&#xff0c;选择“Next”.2.Application Name: 应用程序名称。Projetc Name: 工程名称。Packet Name: 包名称. 注意&#xff0c;包名称…

android导航二级分类,Android实现腾讯新闻的新闻类别导航效果

效果图如下所示&#xff1a;1、在Adapter中加入如下代码private int clickTemp -1;//TODO 被选择的item下标/** * TODO 传入下标&#xff0c;设置被选择的item * * param position */public void setSelection(int position) {clickTemp position;}2、在Adapter的getView方法…

Linux下访问window挂载的磁盘

点击window挂在的磁盘,如下图左侧"文档". 出现如下错误: Error mounting /dev/sda3 .... Command-line mount -t "ntfs" -o "uhelperudisks2 修复办法: sudo ntfsfix /dev/sda6 参考资料: 1. win8安装ubuntu后不能访问windows其他磁盘转载于:https:…

(转)如果知道dll文件是面向32位系统还是面向64位系统的?

本文为转载文章&#xff0c;原文地址&#xff1a;http://www.cnblogs.com/qguohog/archive/2011/09/13/2174897.html&#xff0c;仅仅是记录供后续使用&#xff0c;如有侵权请通知删除。 在发布dll时&#xff0c;可以选择编译为x86模式、x64模式以及Any Cpu模式等。那么对于已经…

Spring Roo 简介

一直以来&#xff0c;Java/Spring开发被认为是笨重的代表&#xff0c;无法快速生成项目原型和骨架。所以&#xff0c;Spring推出了Spring Roo这个项目&#xff0c;帮助我们快速生成项目原型。本文参考自Spring Roo的官方文档&#xff0c;如果熟悉英文的话可以直接看原文档&…

Windows—JDK安装与环境变量配置

本文介绍JDK的安装与环境变量配置。 工具/原料 JDK1.8.0_65WIN7 32bitjdk-8u65-windows-i586.exe方法/步骤 安装JDK 选择安装目录 安装过程中会出现两次 安装提示 。第一次是安装 jdk &#xff0c;第二次是安装 jre 。建议两个都安装在同一个java文件夹中的不同文件夹中。&…

实现物联网项目,你需要提前知道的6件事情

目前为止&#xff0c;对于大多数寻求数字化与服务化转型的制造商来说&#xff0c;实现物联网应用项目仍然是一个很大的挑战。 我们此前做过一项研究&#xff0c;到2016年底,全球企业级物联网项目将超过10000个。但是其中大部分的项目都还是处于初期概念验证(PoC)阶段&#xff0…

Android笔记(六十七) 自定义控件

实际编程中&#xff0c;系统提供的控件往往无法满足我们的需求&#xff0c;一来是样子丑陋&#xff0c;二来是一些复杂的组合需要多次使用的话&#xff0c;每次都写一堆控件的组合会很耗费时间&#xff0c;所以我们将这些组件的组合自定义为一个新的控件&#xff0c;以后使用的…

Linux系统rootpassword改动

重新启动系统。 进入系统引导界面&#xff1a; 按下e键&#xff1a; 选择第二项。内核启动參数设置&#xff0c;按下e键&#xff1a; 在结尾处&#xff0c;输入数字 1或者 英文 " single"&#xff0c;再回车&#xff1a; 按下b键启动。此时以单用户模式级别引导启动程…

(转) Quartz学习——SSMM(Spring+SpringMVC+Mybatis+Mysql)和Quartz集成详解(四)

http://blog.csdn.net/u010648555/article/details/60767633 当任何时候觉你得难受了&#xff0c;其实你的大脑是在进化&#xff0c;当任何时候你觉得轻松&#xff0c;其实都在使用以前的坏习惯。 通过前面的学习&#xff0c;你可能大致了解了Quartz&#xff0c;本篇博文为你打…

被流氓360设置浏览器主页的解决办法(如果你也遇到了跟我一样的问题,不妨看一下是不是这个原因)...

最近电脑罢工&#xff0c;重装了系统&#xff1b;很多常用软件都不得不重新安装&#xff0c;其实这都不是事儿&#xff0c;现在基本上都是百兆光纤了&#xff0c;下载安装都很顺溜。 浏览器也在安装之列&#xff0c;因为搞开发所以谷歌火狐浏览器都是必装的&#xff1b;平时基本…

BZOJ1834 [ZJOI2010]network 网络扩容

网络流训练好题。。。但是要给差评&#xff01;蒟蒻表示这就是板子题&#xff0c;然后做了半个小时T T 先跑一边最大流&#xff0c;得到第一问答案ans。 第二问&#xff1a;原先的边不动&#xff0c;费用为0。 然后对每条边在上面再加一条边&#xff0c;流量为inf&#xff0c;费…