協議介紹
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的共同優點
- 可讀性好,結構清晰
- 分層存儲(層次嵌套)
- 都可作為
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.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?;
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協議
使用wireshark抓包如下
有以下幾點需要注意
- 可以看到共有一次
TCP三次握手以及一次揮手斷開 TCP連接建立成功之後,會發送一個Magic幀,之後緊跟着SETTINGS幀(幀類型 = 0x4遞影響端點通信方式的配置參數,例如設置對端行為的首選項和約束)- 每個
gRPC包裏面會有多個stream
閲讀參考
理解 gRPC 協議
json從入門到實踐
HTTP2 協議長文詳解
tonic hello world readme
HTTP2幀定義