协定介绍

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的独特长处

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

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.1Host: grpc.demo.comContent-Type: application/grpcContent-Length: 1234<Length-Prefixed Message>

非流式gRPC返回格局

HTTP/1.1 200 OKContent-Length: 5678Content-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.comcontent-type = application/grpc+protoDATA (flags = END_STREAM) # data frame<Length-Prefixed Message>

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

HEADERS (flags = END_HEADERS) # header frame:status = 200content-type = application/grpc+protoDATA # data frame<Length-Prefixed Message>HEADERS (flags = END_STREAM, END_HEADERS) # header framegrpc-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 clientRESPONSE=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]:50051Got a request from Some([::1]:39634)

浏览参考

了解 gRPC 协定

json从入门到实际

HTTP2 协定长文详解

tonic hello world readme