這章的內容需要安裝好外掛和protoc,建議閱讀我的上一篇 grpc使用篇
個人網站:linzyblog.netlify.app/
示例程式碼已經上傳到github:點選跳轉
gRPC官方文件:點選跳轉
gRPC 有兩種型別的請求模型:
- 一元 - 直接的請求響應對映在
HTTP/2
請求響應之上。- 簡單來說一元就是一個簡單的 RPC,其中客戶端使用存根向伺服器傳送請求並等待響應返回,就像正常的函式呼叫一樣。
rpc SayHi(Request) returns (Response);
- 流式傳輸——多個請求和響應透過長壽命
HTTP/2
流進行交換,可以是單向或雙向的。- 其中許多程式可以透過
HTTP/2 的多路複用能力
(透過單個 TCP 連線一起傳送多個響應或接收多個請求)在單個請求中發生。 Server-side streaming RPC
—— 客戶端向伺服器傳送單個請求並接收回資料序列流(讀回一系列訊息)。客戶端從返回的流中讀取,直到沒有更多訊息為止。Client-side streaming RPC
—— 客戶端向伺服器傳送資料序列流(寫入一系列訊息),一旦客戶端完成了訊息的寫入,它會等待伺服器讀取所有訊息並返回其響應結果。Bidirectional streaming RPC
—— 它是雙向流式傳輸,客戶端和伺服器使用讀寫流傳送一系列訊息。兩個流獨立執行;因此,因此客戶端和伺服器可以按照他們喜歡的任何順序讀取和寫入。保留每個流中訊息的順序。例如,伺服器可以在寫入響應之前等待接收所有客戶端訊息,或者它可以交替讀取訊息然後寫入訊息,或其他一些讀取和寫入的組合。
- 其中許多程式可以透過
Server-side streaming RPC:伺服器端流式 RPC
Client-side streaming RPC:客戶端流式 RPC
Bidirectional streaming RPC:雙向流式 RPCstream
可以透過將關鍵字放在請求型別之前來指定流式處理方法。
gRPC 是基於HTTP/2開發的,該協議於 2015 年釋出,以克服 HTTP/1.1 的限制。在相容 HTTP/1.1 的同時,我們來了解一下HTTP/2 帶來了許多高階功能,例如:
- 二進位制分幀層 —— 與 HTTP/1.1 不同,HTTP/2 請求/響應分為小訊息並以二進位制格式分幀,使訊息傳輸高效。
透過二進位制幀,HTTP/2 協議使請求/響應多路複用成為可能,而不會阻塞網路資源。
- 流式傳輸 —— 客戶端可以請求並且伺服器可以同時響應的
雙向全雙工流式傳輸
。 流控制
—— HTTP/2 中使用流控制機制,可以對用於緩衝動態訊息的記憶體進行詳細控制。- 標頭壓縮 —— HTTP/2 中的所有內容,包括標頭,都在傳送前進行編碼,顯著提高了整體效能。使用
HPACK
壓縮方式,HTTP/2 只共享與之前的 HTTP 頭包不同的值。 - 處理 —— 使用 HTTP/2,gRPC 支援同步和非同步處理,可用於執行不同型別的互動和流式 RPC。
HTTP/2 的所有這些特性使 gRPC 能夠使用更少的資源,從而減少在雲中執行的應用程式和服務之間的響應時間,並延長執行移動裝置的客戶端的電池壽命。
1、為什麼我們要用流式傳輸,簡單的一元RPC不行麼?
流式為什麼要存在呢?我們在使用一元請求的時候可能會遇到以下問題:
- 資料包過大會造成的瞬時壓力。
- 接收資料包時,需要所有資料包都接受成功且正確後,才能夠回撥響應,進行業務處理(無法客戶端邊傳送,服務端邊處理)
而流式傳輸卻可以:
HTTP2 透過長期 TCP 連線多路複用流,因此新請求沒有 TCP 連線開銷。HTTP2 成幀允許在單個 TCP 資料包中傳送多個 gRPC 訊息。
對於長期連線,流式請求應該在每條訊息的基礎上具有最佳效能。
一元請求需要為每個請求建立一個新的 HTTP2 流,包括透過網路傳送的附加標頭幀。一旦建立,透過流式請求傳送的每條新訊息只需要透過連線傳送訊息的資料幀。
2、目錄結構
go-grpc-example
├── client
│ └──hello_client
│ │ └── client.go
│ └── stream_client
│ └── client.go
├── proto
│ └──hello
│ │ └── hello.proto
│ └──stream
│ │ └── stream.proto
├── server
│ └──hello_server
│ │ └── server.go
│ └──stream_server
│ │ └── server.go
├── Makefile
增加 stream_server、stream_client
存放服務端和客戶端檔案,proto/stream/stream.proto
用於編寫 IDL
3、編寫IDL
在 proto/stream 資料夾下的 stream.proto 檔案中,寫入如下內容:
syntax = "proto3";
option go_package="./proto/stream;stream";
package proto;
service StreamService {
//List:伺服器端流式 RPC
rpc List(StreamRequest) returns (stream StreamResponse) {};
//Record:客戶端流式 RPC
rpc Record(stream StreamRequest) returns (StreamResponse) {};
//Route:雙向流式 RPC
rpc Route(stream StreamRequest) returns (stream StreamResponse) {};
}
message StreamPoint {
string name = 1;
int32 value = 2;
}
message StreamRequest {
StreamPoint pt = 1;
}
message StreamResponse {
StreamPoint pt = 1;
}
注意關鍵字 stream,宣告其為一個流方法。這裡共涉及三個方法,對應關係為
- List:伺服器端流式 RPC
- Record:客戶端流式 RPC
- Route:雙向流式 RPC
4、Makefile
這是我拖了很久的關於Makefile的用法,感覺Makefile更適合在專案使用中穿插講解一下。
有一篇很不錯的Makefile文件:點選跳轉
作用:Makefile 用於幫助決定大型程式的哪些部分需要重新編譯。
這裡我們用make gen
指令代替proto外掛從我們的.proto 服務定義中生成 gRPC 客戶端和伺服器介面。
在Makefile檔案中寫入:
gen:
protoc --go_out=. --go-grpc_out=. ./proto/stream/*.proto
用make gen
指令生成Go程式碼:
➜ make gen
protoc --go_out=. --go-grpc_out=. ./proto/stream/*.proto
注意使用Makefile生成的時候,要注意
.proto
檔案 go_package 指定生成的位置。
5、寫出基礎模板和空定義
我們先把基礎的模板和空定義寫出來在進行完善,不太懂的看我上一篇文章
1)server.go
type StreamService struct {
pb.UnimplementedStreamServiceServer
}
const PORT = "8888"
func main() {
server := grpc.NewServer() //建立 gRPC Server 物件
pb.RegisterStreamServiceServer(server, &StreamService{})
lis, err := net.Listen("tcp", ":"+PORT)
if err != nil {
log.Fatalf("net.Listen err: %v", err)
}
server.Serve(lis)
}
//服務端流式RPC,Server是Stream,Client為普通RPC請求
//客戶端傳送一次普通的RPC請求,服務端透過流式響應多次傳送資料集
func (s *StreamService) List(r *pb.StreamRequest, stream pb.StreamService_ListServer) error {
return nil
}
//客戶端流式RPC,單向流
//客戶端透過流式多次傳送RPC請求給服務端,服務端傳送一次普通的RPC請求給客戶端
func (s *StreamService) Record(stream pb.StreamService_RecordServer) error {
return nil
}
//雙向流,由客戶端發起流式的RPC方法請求,服務端以同樣的流式RPC方法響應請求
//首個請求一定是client發起,具體互動方法(誰先誰後,一次發多少,響應多少,什麼時候關閉)根據程式編寫方式來確定(可以結合協程)
func (s *StreamService) Route(stream pb.StreamService_RouteServer) error {
return nil
}
2)client.go
const PORT = "8888"
func main() {
conn, err := grpc.Dial(":"+PORT, grpc.WithInsecure())
if err != nil {
log.Fatalf("grpc.Dial err: %v", err)
}
defer conn.Close()
client := pb.NewStreamServiceClient(conn)
err = printLists(client, &pb.StreamRequest{Pt: &pb.StreamPoint{Name: "gRPC Stream Client: List", Value: 1234}})
if err != nil {
log.Fatalf("printLists.err: %v", err)
}
err = printRecord(client, &pb.StreamRequest{Pt: &pb.StreamPoint{Name: "gRPC Stream Client: Record", Value: 9999}})
if err != nil {
log.Fatalf("printRecord.err: %v", err)
}
err = printRoute(client, &pb.StreamRequest{Pt: &pb.StreamPoint{Name: "gRPC Stream Client: Route", Value: 1111}})
if err != nil {
log.Fatalf("printRoute.err: %v", err)
}
}
func printLists(client pb.StreamServiceClient, r *pb.StreamRequest) error {
return nil
}
func printRecord(client pb.StreamServiceClient, r *pb.StreamRequest) error {
return nil
}
func printRoute(client pb.StreamServiceClient, r *pb.StreamRequest) error {
return nil
}
6、Server-side streaming RPC:伺服器端流式 RPC
服務端流式RPC,Server是Stream,Client為普通RPC請求,客戶端傳送一次普通的RPC請求,服務端透過流式響應多次傳送資料集。
1)server
/*
1. 建立連線 獲取client
2. 透過 client 獲取stream
3. for迴圈中透過stream.Recv()依次獲取服務端推送的訊息
4. err==io.EOF則表示服務端關閉stream了
*/
func (s *StreamService) List(r *pb.StreamRequest, stream pb.StreamService_ListServer) error {
// 具體返回多少個response根據業務邏輯調整
for n := 0; n <= 6; n++ {
// 透過 send 方法不斷推送資料
err := stream.Send(&pb.StreamResponse{
Pt: &pb.StreamPoint{
Name: r.Pt.Name,
Value: r.Pt.Value + int32(n),
},
})
if err != nil {
return err
}
time.Sleep(time.Second)
}
// 返回nil表示已經完成響應
return nil
}
在 Server,主要留意 stream.Send 方法。它看上去能傳送 N 次?有沒有大小限制?
type StreamService_ListServer interface {
Send(*StreamResponse) error
grpc.ServerStream
}
func (x *streamServiceListServer) Send(m *StreamResponse) error {
return x.ServerStream.SendMsg(m)
}
透過閱讀原始碼,可得知是 protoc 在生成時,根據定義生成了各式各樣符合標準的介面方法。最終再統一排程內部的 SendMsg 方法,該方法涉及以下過程:
- 訊息體(物件)序列化
- 壓縮序列化後的訊息體
- 對正在傳輸的訊息體增加 5 個位元組的 header
- 判斷壓縮+序列化後的訊息體總位元組長度是否大於預設的 maxSendMessageSize(預設值為 math.MaxInt32),若超出則提示錯誤
- 寫入給流的資料集
2)client
/*
1. 建立連線 獲取client
2. 透過 client 獲取stream
3. for迴圈中透過stream.Recv()依次獲取服務端推送的訊息
4. err==io.EOF則表示服務端關閉stream了
*/
func printLists(client pb.StreamServiceClient, r *pb.StreamRequest) error {
// 呼叫獲取stream
stream, err := client.List(context.Background(), r)
if err != nil {
return err
}
// for迴圈獲取服務端推送的訊息
for {
// 透過 Recv() 不斷獲取服務端send()推送的訊息
resp, err := stream.Recv()
// err==io.EOF則表示服務端關閉stream了
if err == io.EOF {
break
}
if err != nil {
return err
}
log.Printf("resp: pj.name: %s, pt.value: %d", resp.Pt.Name, resp.Pt.Value)
}
return nil
}
在 Client,主要留意 stream.Recv() 方法。什麼情況下 io.EOF ?什麼情況下存在錯誤資訊呢?
type StreamService_ListClient interface {
Recv() (*StreamResponse, error)
grpc.ClientStream
}
func (x *streamServiceListClient) Recv() (*StreamResponse, error) {
m := new(StreamResponse)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
RecvMsg 會從流中讀取完整的 gRPC 訊息體,另外透過閱讀原始碼可得知:
(1)RecvMsg 是阻塞等待的
(2)RecvMsg 當流成功/結束(呼叫了 Close)時,會返回 io.EOF
(3)RecvMsg 當流出現任何錯誤時,流會被中止,錯誤資訊會包含 RPC 錯誤碼。而在 RecvMsg 中可能出現如下錯誤:
# 啟動服務端
$ go run server.go
API server listening at: 127.0.0.1:55149
# 啟動客戶端
$ go run client.go
API server listening at: 127.0.0.1:55158
2022/11/03 09:35:03 resp: pj.name: gRPC Stream Client: List, pt.value: 1234
2022/11/03 09:35:04 resp: pj.name: gRPC Stream Client: List, pt.value: 1235
2022/11/03 09:35:05 resp: pj.name: gRPC Stream Client: List, pt.value: 1236
2022/11/03 09:35:06 resp: pj.name: gRPC Stream Client: List, pt.value: 1237
2022/11/03 09:35:07 resp: pj.name: gRPC Stream Client: List, pt.value: 1238
2022/11/03 09:35:08 resp: pj.name: gRPC Stream Client: List, pt.value: 1239
2022/11/03 09:35:09 resp: pj.name: gRPC Stream Client: List, pt.value: 1240
伺服器流式 RPC 類似於一元 RPC,除了伺服器返回訊息流以響應客戶端的請求。傳送所有訊息後,伺服器的狀態詳細資訊(狀態程式碼和可選狀態訊息)和可選尾隨後設資料將傳送到客戶端。這樣就完成了伺服器端的處理。客戶端在擁有伺服器的所有訊息後完成。
7、Client-side streaming RPC:客戶端流式 RPC
客戶端透過流式多次傳送RPC請求給服務端,服務端傳送一次響應給客戶端。
1)server
/*
1. for迴圈中透過stream.Recv()不斷接收client傳來的資料
2. err == io.EOF表示客戶端已經傳送完畢關閉連線了,此時在等待服務端處理完並返回訊息
3. stream.SendAndClose() 傳送訊息並關閉連線(雖然在客戶端流裡伺服器這邊並不需要關閉 但是方法還是叫的這個名字,內部也只會呼叫Send())
*/
func (s *StreamService) Record(stream pb.StreamService_RecordServer) error {
// for迴圈接收客戶端傳送的訊息
for {
// 透過 Recv() 不斷獲取客戶端 send()推送的訊息
r, err := stream.Recv()
// err == io.EOF表示已經獲取全部資料
if err == io.EOF {
// SendAndClose 返回並關閉連線
// 在客戶端傳送完畢後服務端即可返回響應
return stream.SendAndClose(&pb.StreamResponse{Pt: &pb.StreamPoint{Name: "gRPC Stream Server: Record", Value: 1}})
}
if err != nil {
return err
}
log.Printf("stream.Recv pt.name: %s, pt.value: %d", r.Pt.Name, r.Pt.Value)
time.Sleep(time.Second)
}
return nil
}
stream.SendAndClose
:我們對每一個 Recv 都進行了處理,當發現 io.EOF (流關閉) 後,需要將最終的響應結果傳送給客戶端,同時關閉正在另外一側等待的 Recv
2)client
/*
1. 建立連線並獲取client
2. 獲取 stream 並透過 Send 方法不斷推送資料到服務端
3. 傳送完成後透過stream.CloseAndRecv() 關閉stream並接收服務端返回結果
*/
func printRecord(client pb.StreamServiceClient, r *pb.StreamRequest) error {
// 獲取 stream
stream, err := client.Record(context.Background())
if err != nil {
return err
}
for i := 0; i <= 6; i++ {
// 透過 Send 方法不斷推送資料到服務端
err := stream.Send(r)
if err != nil {
return err
}
}
// 傳送完成後透過stream.CloseAndRecv() 關閉stream並接收服務端返回結果
// (服務端則根據err==io.EOF來判斷client是否關閉stream)
resp, err := stream.CloseAndRecv()
if err != nil {
return err
}
log.Printf("resp: pj.name: %s, pt.value: %d", resp.Pt.Name, resp.Pt.Value)
return nil
}
stream.CloseAndRecv 和 stream.SendAndClose 是配套使用的流方法
3)啟動 & 請求
# 啟動服務端
$ go run server.go
API server listening at: 127.0.0.1:57789
2022/11/03 11:59:31 stream.Recv pt.name: gRPC Stream Client: Record, pt.value: 9999
2022/11/03 11:59:32 stream.Recv pt.name: gRPC Stream Client: Record, pt.value: 9999
2022/11/03 11:59:33 stream.Recv pt.name: gRPC Stream Client: Record, pt.value: 9999
2022/11/03 11:59:34 stream.Recv pt.name: gRPC Stream Client: Record, pt.value: 9999
2022/11/03 11:59:35 stream.Recv pt.name: gRPC Stream Client: Record, pt.value: 9999
2022/11/03 11:59:36 stream.Recv pt.name: gRPC Stream Client: Record, pt.value: 9999
2022/11/03 11:59:37 stream.Recv pt.name: gRPC Stream Client: Record, pt.value: 9999
# 啟動客戶端
$ go run client.go
API server listening at: 127.0.0.1:57793
2022/11/03 11:59:38 resp: pj.name: gRPC Stream Server: Record, pt.value: 1
8、Bidirectional streaming RPC:雙向流式 RPC
雙向流,由客戶端發起流式的RPC方法請求,服務端以同樣的流式RPC方法響應請求 首個請求一定是client發起,具體互動方法(誰先誰後,一次發多少,響應多少,什麼時候關閉)根據程式編寫方式來確定(可以結合協程)。
1)server
一般是使用兩個 Goroutine,一個接收資料,一個推送資料。最後透過 return nil 表示已經完成響應。
/*
// 1. 建立連線 獲取client
// 2. 透過client呼叫方法獲取stream
// 3. 開兩個goroutine(使用 chan 傳遞資料) 分別用於Recv()和Send()
// 3.1 一直Recv()到err==io.EOF(即客戶端關閉stream)
// 3.2 Send()則自己控制什麼時候Close 服務端stream沒有close 只要跳出迴圈就算close了。 具體見https://github.com/grpc/grpc-go/issues/444
*/
func (s *StreamService) Route(stream pb.StreamService_RouteServer) error {
var (
wg sync.WaitGroup //任務編排
msgCh = make(chan *pb.StreamPoint)
)
wg.Add(1)
go func() {
n := 0
defer wg.Done()
for v := range msgCh {
err := stream.Send(&pb.StreamResponse{
Pt: &pb.StreamPoint{
Name: v.GetName(),
Value: int32(n),
},
})
if err != nil {
fmt.Println("Send error :", err)
continue
}
n++
}
}()
wg.Add(1)
go func() {
defer wg.Done()
for {
r, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("recv error :%v", err)
}
log.Printf("stream.Recv pt.name: %s, pt.value: %d", r.Pt.Name, r.Pt.Value)
msgCh <- &pb.StreamPoint{
Name: "gRPC Stream Server: Route",
}
}
close(msgCh)
}()
wg.Wait() //等待任務結束
return nil
}
2)client
和服務端類似,不過客戶端推送結束後需要主動呼叫 stream.CloseSend() 函式來關閉Stream。
/*
1. 建立連線 獲取client
2. 透過client獲取stream
3. 開兩個goroutine 分別用於Recv()和Send()
3.1 一直Recv()到err==io.EOF(即服務端關閉stream)
3.2 Send()則由自己控制
4. 傳送完畢呼叫 stream.CloseSend()關閉stream 必須呼叫關閉 否則Server會一直嘗試接收資料 一直報錯...
*/
func printRoute(client pb.StreamServiceClient, r *pb.StreamRequest) error {
var wg sync.WaitGroup
// 呼叫方法獲取stream
stream, err := client.Route(context.Background())
if err != nil {
return err
}
// 開兩個goroutine 分別用於Recv()和Send()
wg.Add(1)
go func() {
defer wg.Done()
for {
resp, err := stream.Recv()
if err == io.EOF {
fmt.Println("Server Closed")
break
}
if err != nil {
continue
}
log.Printf("resp: pj.name: %s, pt.value: %d", resp.Pt.Name, resp.Pt.Value)
}
}()
wg.Add(1)
go func() {
defer wg.Done()
for n := 0; n <= 6; n++ {
err := stream.Send(r)
if err != nil {
log.Printf("send error:%v\n", err)
}
time.Sleep(time.Second)
}
// 傳送完畢關閉stream
err = stream.CloseSend()
if err != nil {
log.Printf("Send error:%v\n", err)
return
}
}()
wg.Wait()
return nil
}
3)啟動 & 請求
# 啟動服務端
$ go run server.go
API server listening at: 127.0.0.1:55108
2022/11/03 12:29:35 stream.Recv pt.name: gRPC Stream Client: Route, pt.value: 1111
2022/11/03 12:29:36 stream.Recv pt.name: gRPC Stream Client: Route, pt.value: 1111
2022/11/03 12:29:37 stream.Recv pt.name: gRPC Stream Client: Route, pt.value: 1111
2022/11/03 12:29:38 stream.Recv pt.name: gRPC Stream Client: Route, pt.value: 1111
2022/11/03 12:29:39 stream.Recv pt.name: gRPC Stream Client: Route, pt.value: 1111
2022/11/03 12:29:40 stream.Recv pt.name: gRPC Stream Client: Route, pt.value: 1111
2022/11/03 12:29:41 stream.Recv pt.name: gRPC Stream Client: Route, pt.value: 1111
# 啟動客戶端
$ go run client.go
API server listening at: 127.0.0.1:55113
2022/11/03 12:29:35 resp: pj.name: gRPC Stream Server: Route, pt.value: 0
2022/11/03 12:29:36 resp: pj.name: gRPC Stream Server: Route, pt.value: 1
2022/11/03 12:29:37 resp: pj.name: gRPC Stream Server: Route, pt.value: 2
2022/11/03 12:29:38 resp: pj.name: gRPC Stream Server: Route, pt.value: 3
2022/11/03 12:29:39 resp: pj.name: gRPC Stream Server: Route, pt.value: 4
2022/11/03 12:29:40 resp: pj.name: gRPC Stream Server: Route, pt.value: 5
2022/11/03 12:29:41 resp: pj.name: gRPC Stream Server: Route, pt.value: 6
Server Closed
客戶端或者服務端都有對應的 推送或者 接收物件,我們只要 不斷迴圈 Recv()
或者 Send()
就能接收或者推送了!
gRPC Stream 和 goroutine 配合簡直完美。透過 Stream 我們可以更加靈活的實現自己的業務。如 訂閱,大資料傳輸等。
Client傳送完成後需要手動呼叫Close()或者CloseSend()方法關閉stream,Server端則return nil就會自動 Close。
1)ServerStream
- 服務端處理完成後return nil代表響應完成
- 客戶端透過 err == io.EOF判斷服務端是否響應完成
2)ClientStream
- 客戶端傳送完畢透過
CloseAndRecv
關閉stream 並接收服務端響應 - 服務端透過
err == io.EOF
判斷客戶端是否傳送完畢,完畢後使用SendAndClose關閉 stream並返回響應。
3)BidirectionalStream
- 客戶端服務端都透過stream向對方推送資料
- 客戶端推送完成後透過CloseSend關閉流,透過
err == io.EOF
判斷服務端是否響應完成 - 服務端透過
err == io.EOF
判斷客戶端是否響應完成,透過return nil表示已經完成響應
透過err == io.EOF
來判定是否把對方推送的資料全部獲取到了。
客戶端透過CloseAndRecv或者CloseSend關閉 Stream,服務端則透過SendAndClose或者直接 return nil來返回響應。
參考文章:
www.lixueduan.com/posts/grpc/03-st...
本作品採用《CC 協議》,轉載必須註明作者和本文連結