共计 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)
进行文件传输
xml
和 json
的独特长处
- 可读性好,构造清晰
- 分层存储(档次嵌套)
- 都可作为
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 frame
和data 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
,申请与响应的 header
和 data
应用独立的 frame
gRPC
的 rust
实际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