协议介绍
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
非流式gRPC
返回格式
HTTP/1.1 200 OK
Content-Length: 5678
Content-Type: application/grpc
非流式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
流式gRPC
响应,共传输3个frame
HEADERS (flags = END_HEADERS) # header frame
:status = 200
content-type = application/grpc+proto
DATA # data frame
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> {
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,
) -> Result, 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> {
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> {
let mut client = GreeterClient::connect("http://[::1]:50051").await?;
for i in 0..3 {
let request = tonic::Request::new(HelloRequest {
name: format!("Tonic {i}"),
});
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
Finished dev [unoptimized + debuginfo] target(s) in 0.06s
Running `target/debug/client`
RESPONSE=Response { metadata: MetadataMap { headers: {"content-type": "application/grpc", "date": "Thu, 24 Aug 2023 06:54:10 GMT", "grpc-status": "0"} }, message: HelloReply { message: "Hello Tonic 0!" }, extensions: Extensions }
RESPONSE=Response { metadata: MetadataMap { headers: {"content-type": "application/grpc", "date": "Thu, 24 Aug 2023 06:54:10 GMT", "grpc-status": "0"} }, message: HelloReply { message: "Hello Tonic 1!" }, extensions: Extensions }
RESPONSE=Response { metadata: MetadataMap { headers: {"content-type": "application/grpc", "date": "Thu, 24 Aug 2023 06:54:10 GMT", "grpc-status": "0"} }, message: HelloReply { message: "Hello Tonic 2!" }, extensions: Extensions }
查看服务端输出如下
$ cargo run --bin server
....
Finished dev [unoptimized + debuginfo] target(s) in 0.06s
Running `target/debug/server`
GreeterServer listening on [::1]:50051
Got a request from Some([::1]:54820)
Got a request from Some([::1]:54820)
Got a request from Some([::1]:54820)
配置wireshark
的proto buffers
加载.proto
文件路径
配置分析(A)
的解码为(decode as)
,配置解析TCP
的50051
端口为HTTP2
协议
有以下几点需要注意
- 可以看到共有一次
TCP
三次握手以及一次挥手断开 TCP
连接建立成功之后,会发送一个Magic
帧,之后紧跟着SETTINGS
帧(帧类型 = 0x4
递影响端点通信方式的配置参数,例如设置对端行为的首选项和约束)- 每个
gRPC
包里面会有多个stream