关于grpc:gRPC的理解与使用

5次阅读

共计 6734 个字符,预计需要花费 17 分钟才能阅读完成。

协定介绍

gRPC 是谷歌开源的一套 RPC 协定框架,底层应用 HTTP/2 协定,次要有两局部,数据编码以及申请映射

数据编码是将内存对象编码为可传输的字节流,也包含把字节流转化为内存对象,常见的蕴含 json, msgpack, xml, protobuf,其中该编码效率比json 高一些,grpc抉择应用protobuf

gRPC为什么基于HTTP2

HTTP1.1遇到的问题

  • 协定繁琐,蕴含很多的细节设计,也预留了很多将来扩大选项,所以没有软件实现了协定中提及的全副细节
  • 协定规定是一发一收这种模式,相当于一个先进先出的串行队列,HTTP Pipelining 把多个 HTTP 申请放到一个 TCP 连贯中来发送,发送过程中不须要服务器对前一个申请的响应,然而在客户端,还是会依照发送的程序来接管响应申请,导致 HTTP 头阻塞(Head-of-line blocking)

HTTP2的个性与组成

  • HEAD 头数据压缩: 对 HTTP 头字段进行数据压缩,因为 HTTP 头蕴含了大量冗余数据,HTTP2对这些数据进行了压缩,压缩后对于申请大小的影响显著,能够将多个申请压缩到一个包中,减小传输负载
  • 多路复用: 每个 HTTP 申请 / 应答在各自的流(stream,每个流都是互相独立,有一个整数 ID 标识,是存在于TCP 连贯中的一个虚构连贯通道,能够承载双向音讯)中实现数据交换,如果一个申请 / 应答阻塞或者速度很慢,也不会影响其它流中的申请 / 应答解决,在一个 TCP 连贯中就能够传输多个流数据而无需建设多个连贯
  • 流量管制和优先级机制: 能够无效利用流的多路复用机制,流量管制能够确保只有接收者应用的数据会被传输,优先级机制能够确保重要的资源被优先传输
  • 服务端推送: 即服务端能够推送应答给客户端
  • 消息报文二进制编码
  • 最小传输单元 帧(frame)HTTP2 定义了很多类型的帧,每个帧服务于不同的目标,数据帧中有 1 个要害数据,这个帧属于哪个资源,音讯由一个或多个帧形成

json

全称 JavaScript Object Notation,一种轻量级的数据交换格局,具备良好的可读和便于疾速编写的个性。可在不同平台之间进行数据交换,在json 呈现以前,罕用的是 xml(Extensiable Markup Language) 进行文件传输

xmljson 的独特长处

  • 可读性好,构造清晰
  • 分层存储(档次嵌套)
  • 都可作为Ajax s 传输数据
  • 都跨平台,可作为数据传输格局

json的长处

  • 数据格式简略,易读易写,且数据都是压缩的,文件较小,便于传输
  • json解析难度较低,而 xml 须要循环遍历 DOM 进行解析,效率较低
  • 服务端和客户端能够间接应用 json,便于保护,而不同客户端解析xml 可能应用不同办法
  • json 已成为以后服务器与 web 利用之间数据传输的公认规范

xml的应用领域

  • xml格局较为谨严,可读性更强,更易于拓展,能够良好的做配置文件
  • 呈现较早,在各个领域有宽泛的利用,具备广泛的流行性

json语法规定

json语法是 JavaScript 语法的 子集 ,而json 个别也是用来传输 对象 数组 。也就是json 语法是 JavaScript 语法的一部分 (满足特定语法的JavaScript 语法)

  • 数据保留在名称、值对中,数据由逗号分隔
  • 花括号示意对象
  • 中括号示意数组

json名称 / 值

json 数据的书写格局为:"名称":"值"
对应 JavaScript 的概念就是:名称 ="值"
json 的格局和 JavaScript 对象格局还是有所区别:

  • JavaScript对象的名称能够不加引号,也能够单引号,也能够双引号,但 json 字符串的名称只能加双引号的字符示意。
  • JavaScript对象的键值能够是除 json 值之外还能够是函数等其余类型数据,而 json 字符串的值对只能是 数字、字符串(要双引号)、逻辑值、对象(加大括号)、数组(中括号)、null

json对象

json有两种示意构造—对象和数组 ,通过这两种示意构造能够示意更简单的构造。比照java 的话 json 数组和 json 对象就好比 java 的列表 / 数组 (Object 类型)和对象 (Map) 一样的关系。并且很多状况其对象值可能互相嵌套多层,对对象中存在对象,对象中存在数组,数组中存在对象

JavaScript对象 / json对象 / json字符串

//JavaScript 对象, 除了字符串、数字、true、false、null 和 undefined 之外,JavaScript 中的值都是对象
var a1={name:"pky" , sex:"man", value: 12345};
var a2={'name':'pky' , 'sex':'man', 'value': 12345};
// 满足 json 格局的 JavaScript 对象, json 对象
var a3={"name":"pky" , "sex":"man", "value": 12345};
//json 字符串
var a4='{"name":"pky","sex":"man","value": 12345}';

json次要毛病是非字符串的编码效率比拟低,下面的数据比方 value 字段的值,在内存中是 12345,占用 2 字节,json 编码转变为 json 字符串 之后占用 5 字节

Protobuf

Protobuf 一方面选用了 VarInts 对数字进行编码,解决了效率问题;另一方面给每个字段指定一个整数编号,传输的时候只传字段编号,解决冗余问题

数据编码

protobuf应用 .proto 文件作为编号与字段映射关系的对照表

message Demo {
  int32 i = 1;
  string s = 2;
  bool b = 3;
}

每个字段前面的数字是tag,不能反复,和字段一一对应

Protobuf 提供了一系列工具,为 proto 形容的 message 生成各种语言的代码

申请映射

proto文件作为 IDL,能够做到RPC 形容,比方最简略的一个 hello.proto 文件如下

package demo.hello;

service Greeter {rpc SayHello (HelloRequest) returns (HelloReply) {}}

message HelloRequest {string name = 1;}

message HelloReply {string message = 1;}

定义了一个 Greeter 服务,其中有一个 SayHello 的办法,承受 HelloRequest 音讯并返回 HelloReply 音讯

一个gRPC 定义蕴含三个局部,包名、服务名和接口名,连贯规定如下

/${包名}.${服务名}/${接口名}

上述 hello.proto 的包名是demo.hello,服务名是Greeter,接口名是SayHello,所以对应的门路就是 /demo.hello.Greeter/SayHello

gRPC 协定规定 Content-Typeheader 的取值为 application/grpc 或者application/grpc+proto,应用 JSON 编码,能够设成application/grpc+json

gRPC的流式接口

gRPC能够源源不断收发音讯,有别于 HTTP/1.1 的一收一发模式

gRPC 持三种流式接口,定义的方法就是在参数前加上 stream 关键字,流类型蕴含如下

  • 申请流:在 RPC 发动之后一直发送新的申请音讯,场景有发推送或者短信
  • 响应流:在 RPC 发动之后一直接管新的响应音讯,场景有订阅音讯告诉
  • 双向流:在 RPC 发动之后同时收发音讯,场景有实时语音转字幕

对应 .proto 如下

service Greeter {rpc SayHello (HelloRequest) returns (HelloReply) {}
  rpc SayHello (stream HelloRequest) returns (HelloReply) {}
  rpc SayHello (HelloRequest) returns (stream HelloReply) {}
  rpc SayHello (stream HelloRequest) returns (stream HelloReply) {}}

为了实现流式传输,gRPC 引入Length-Prefixed Message,同一个 gRPC 申请的不同音讯共用 HTTP 头信息,给每个音讯独自加一个五字节的前缀来示意压缩和长度信息,第一个字节示意字节流是否被压缩,后四个字节存储数据长度

非流式 gRPC 申请格局

POST /demo.hello.Greeter/SayHello HTTP/1.1
Host: grpc.demo.com
Content-Type: application/grpc
Content-Length: 1234

<Length-Prefixed Message>

非流式 gRPC 返回格局

HTTP/1.1 200 OK
Content-Length: 5678
Content-Type: application/grpc

<Length-Prefixed Message>

非流式 gRPC 调用,跟一般的 HTTP 申请也没有太大区别,能够应用 HTTP/1.1 来承载 gRPC 流量

流式 gRPC 申请格局,如下,申请分为 header framedata frame,共计传输两个frame

HEADERS (flags = END_HEADERS) # header frame
:method = POST
:scheme = http
:path = /demo.hello.Greeter/SayHello
:authority = grpc.demo.com
content-type = application/grpc+proto

DATA (flags = END_STREAM) # data frame
<Length-Prefixed Message>

流式 gRPC 响应,共传输 3 个frame

HEADERS (flags = END_HEADERS) # header frame
:status = 200
content-type = application/grpc+proto

DATA # data frame
<Length-Prefixed Message>

HEADERS (flags = END_STREAM, END_HEADERS) # header frame
grpc-status = 0

流式 gRPC 应用HTTP/2,申请与响应的 headerdata 应用独立的 frame

gRPCrust 实际helloworld

依赖我的项目tonic

https://github.com/hyperium/tonic

创立一个我的项目hello

$ cargo new hello
$ cd new

先装置 protoc Protocol Buffers 编译器以及 Protocol Buffers 资源文件

Ubuntu

$ sudo apt update && sudo apt upgrade -y
$ sudo apt install -y protobuf-compiler libprotobuf-dev

定义一个 helloworld.proto 文件

$ mkdir proto
$ touch proto/helloworld.proto
syntax = "proto3";
package helloworld;

service Greeter {rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {string name = 1;}

message HelloReply {string message = 1;}

批改 Cargo.toml 新增如下

[dependencies]
# 用于从 proto2/proto3 文件生成 rust 代码
prost = "0.11"
tokio = {version = "1", features = ["full"] }
tonic = "0.9"

[build-dependencies]
# 用于在 build 阶段生成 gRPC 的客户端和服务端代码
tonic-build = "0.9"

在我的项目根门路下创立一个 build.rs 用于编译时生成代码

fn main() -> Result<(), Box<dyn std::error::Error>> {tonic_build::compile_protos("proto/helloworld.proto")?;
    Ok(())
}

编写服务端代码src/bin/server.rs

use tonic::{transport::Server, Request, Response, Status};

use hello_world::greeter_server::{Greeter, GreeterServer};
use hello_world::{HelloReply, HelloRequest};

pub mod hello_world {tonic::include_proto!("helloworld");
}

#[derive(Default)]
pub struct MyGreeter {}

#[tonic::async_trait]
impl Greeter for MyGreeter {
    async fn say_hello(
        &self,
        request: Request<HelloRequest>,
    ) -> Result<Response<HelloReply>, Status> {println!("Got a request from {:?}", request.remote_addr());

        let reply = hello_world::HelloReply {message: format!("Hello {}!", request.into_inner().name),
        };
        Ok(Response::new(reply))
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {let addr = "[::1]:50051".parse().unwrap();
    let greeter = MyGreeter::default();

    println!("GreeterServer listening on {}", addr);

    Server::builder()
        .add_service(GreeterServer::new(greeter))
        .serve(addr)
        .await?;

    Ok(())
}

客户端代码

use hello_world::greeter_client::GreeterClient;
use hello_world::HelloRequest;

pub mod hello_world {tonic::include_proto!("helloworld");
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {let mut client = GreeterClient::connect("http://[::1]:50051").await?;

    let request = tonic::Request::new(HelloRequest {name: "Tonic".into(),
    });

    let response = client.say_hello(request).await?;

    println!("RESPONSE={:?}", response);

    Ok(())
}

最初整个我的项目的构造如下

.
├── build.rs
├── Cargo.lock
├── Cargo.toml
├── proto
│   └── helloworld.proto
└── src
    ├── bin
    │   ├── client.rs
    │   └── server.rs
    └── main.rs

启动服务端

$ cargo run --bin server

新开终端,启动客户端

$ cargo run --bin client
RESPONSE=Response {metadata: MetadataMap { headers: {"content-type": "application/grpc", "date": "Thu, 24 Aug 2023 03:21:34 GMT", "grpc-status": "0"} }, message: HelloReply {message: "Hello Tonic!"}, extensions: Extensions }

查看服务端输入如下

$ cargo run --bin server
....
GreeterServer listening on [::1]:50051
Got a request from Some([::1]:39634)

浏览参考

了解 gRPC 协定

json从入门到实际

HTTP2 协定长文详解

tonic hello world readme

正文完
 0