grpc
- grpc
- grpc-go
grpc 無縫整合了 protobuf
protobuf
- 習慣用 Json、XML 資料儲存格式的你們,相信大多都沒聽過 Protocol Buffer。
- Protocol Buffer 其實是 Google 出品的一種輕量 & 高效的結構化資料儲存格式,效能比 Json、XML 真的強!太!多!
- protobuf 經歷了 protobuf2 和 protobuf3,pb3 比 pb2 簡化了很多,目前主流的版本是 pb3。
protoc 的下載
下載 protoc 和go get github.com/golang/protobuf/protoc-gen-go
syntax = "proto3";
option go_package = "."; //這名不寫現在編譯不了
message HelloRequest {
string name = 1; //1 是編號不是值
}
protoc --proto_path=. \
--go_out=. \
--go-grpc_out=. \
./helloworld.proto
會在目錄中產生一個helloworld.pb.go的檔案
壓縮比的對比
package main
import (
"RpcLearn/protobuf"
"encoding/json"
"fmt"
"github.com/golang/protobuf/proto"
)
// TIP <p>To run your code, right-click the code and select <b>Run</b>.</p> <p>Alternatively, click
// the <icon src="AllIcons.Actions.Execute"/> icon in the gutter and select the <b>Run</b> menu item from here.</p>
type Hello struct {
Name string `json:"name"`
}
func main() {
//TIP <p>Press <shortcut actionId="ShowIntentionActions"/> when your caret is at the underlined text
// to see how GoLand suggests fixing the warning.</p><p>Alternatively, if available, click the lightbulb to view possible fixes.</p>
//s := "gopher"
//fmt.Printf("Hello and welcome, %s!\n", s)
//
//for i := 1; i <= 5; i++ {
// //TIP <p>To start your debugging session, right-click your code in the editor and select the Debug option.</p> <p>We have set one <icon src="AllIcons.Debugger.Db_set_breakpoint"/> breakpoint
// // for you, but you can always add more by pressing <shortcut actionId="ToggleLineBreakpoint"/>.</p>
// fmt.Println("i =", 100/i)
//}
jsonStruct := Hello{
Name: "gopher",
}
jsonReq, _ := json.Marshal(jsonStruct)
fmt.Println(string(jsonReq))
fmt.Println(len(jsonReq))
req := __.HelloRequest{
Name: "gopher",
}
rsp, _ := proto.Marshal(&req)
fmt.Println(rsp)
fmt.Println(len(rsp))
}
差不多兩倍的差異
Go 語言中使用 Protobuf 的變化說明
| 教學內容 | 目前實際 |
|---|---|
以前只有一個 .pb.go 檔案 |
現在產生兩個檔案:一個 .pb.go(資料結構),一個 _grpc.pb.go(gRPC 介面) |
| 外掛程式不分離 | 外掛程式已拆分為 protoc-gen-go 和 protoc-gen-go-grpc,需要分別安裝和呼叫 |
📌 目前行為說明
從 Go 的 Protobuf 外掛程式 v1.20+ 開始,gRPC 支援被拆分為獨立的外掛程式 protoc-gen-go-grpc,用於產生服務介面相關程式碼。這種方式更模組化,職責更清晰。
因此,執行以下指令:
protoc --proto_path=. \
--go_out=. \
--go-grpc_out=. \
./your_file.proto
將會產生:
- your_file.pb.go: 包含訊息結構(如 Request、Response)
- your_file_grpc.pb.go: 包含服務介面定義(如 YourServiceServer)
建議保留並同步這兩個檔案。
簡單梳理一下 grpc 開啟的流程
- 建立存根檔案及方法定義,然後編譯
// proto/helloworld.proto
syntax = "proto3";
option go_package = ".;proto";
service Greeter{
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
- server 端實作定義方法的呼叫,注意 server 實作(鴨子型別)
package main
import (
"RpcLearn/grpc_test/proto"
"context"
"google.golang.org/grpc"
"log"
"net"
)
type Server struct {
proto.UnimplementedGreeterServer // 👈 重點:嵌入這個型別
}
func (s *Server) SayHello(ctx context.Context, req *proto.HelloRequest) (*proto.HelloReply, error) {
reply := &proto.HelloReply{
Message: "Hello " + req.Name,
}
return reply, nil
}
func main() {
g := grpc.NewServer() // 建立一個新的gRPC伺服器
proto.RegisterGreeterServer(g, &Server{}) // 註冊服務
listener, err := net.Listen("tcp", ":9090")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
err = g.Serve(listener)
if err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
- 用戶端呼叫實作
package main
import (
"RpcLearn/grpc_test/proto"
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
// Create a new gRPC server
//conn, err := grpc.Dial("localhost:9090", grpc.WithInsecure())
// 使用安全連線
//creds := credentials.NewTLS(&tls.Config{InsecureSkipVerify: true})
conn, err := grpc.NewClient("localhost:9090",
//grpc.WithInsecure()
grpc.WithTransportCredentials(insecure.NewCredentials()), // ✅ 新方式
)
if err != nil {
panic(err)
}
defer conn.Close()
c := proto.NewGreeterClient(conn)
r, err := c.SayHello(context.Background(), &proto.HelloRequest{Name: "world"})
if err != nil {
panic(err)
}
fmt.Println(r.Message)
}
grpc 的 stream 方式(串流模式)
- 簡單模式(simple RPC)
- 伺服器端的資料流
- 用戶端的資料流
- 雙向的
集中學習 grpc
| Protobuf 型別 | Go 型別 |
|---|---|
| double | float64 |
| float | float32 |
| int32 | int32 |
| int64 | int64 |
| uint32 | uint32 |
| uint64 | uint64 |
| sint32 | int32 |
| sint64 | int64 |
| fixed32 | uint32 |
| fixed64 | uint64 |
預設值
預設值說明
在 Protobuf 中,不同欄位型別有以下預設值:
- 字串型別(string):預設值是一個空字串。
- 位元組型別(bytes):預設值是一個空的位元組序列。
- 布林型別(bool):預設值是
false。 - 數值型別(數字型別):預設值為
0。 - 列舉型別:預設值是第一個定義的列舉值,且必須是
0。 - 訊息型別(message):預設值是未設定(即
null)。
注意事項
- 對於純量訊息欄位:一旦訊息被解析後,無法區分欄位是否被設定為預設值(例如布林值是否為
false)還是欄位根本未被設定。 - 布林值的使用建議:避免定義布林值的預設值為
false,因為可能會導致觸發無意的行為。 - 純量訊息的序列化:如果一個純量訊息欄位被設定為預設值,該值不會被序列化傳輸。
可以透過參考 Generated Code Guide 來更詳細地瞭解 Protobuf 預設值的處理方式。
列舉型別的定義
在需要為訊息型別的某個欄位指定一組「預定義值序列」時,可以使用列舉型別。每個列舉值都對應一個整數值。
範例
下面是一個列舉的定義範例:
message SearchRequest {
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 1;
}
- 伺服器端資料流(取得即時資料)
- 用戶端資料流(上報資料)
- 雙向資料流
syntax = "proto3";
option go_package = ".;proto";
service Greeter {
rpc GetStream (StreamReqData) returns (stream StreamResData); // 伺服器端串流模式
rpc PutStream (stream StreamReqData) returns ( StreamResData); // 用戶端串流模式
rpc AllStream (stream StreamReqData) returns (stream StreamResData); // 雙向串流模式
}
message StreamReqData {
string data = 1;
}
message StreamResData {
string message = 1;
}
編譯產生 Go 的 protobuf
protoc --proto_path=. \
--go_out=. \
--go-grpc_out=. \
./helloworld.proto
伺服器端程式碼
package main
import (
"RpcLearn/stream_grpc_test/proto"
"fmt"
"google.golang.org/grpc"
"log"
"net"
"sync"
"time"
)
const PORT = ":50052"
type server struct {
proto.UnimplementedGreeterServer // 👈 重點:嵌入這個型別
}
func (s server) GetStream(data *proto.StreamReqData, res proto.Greeter_GetStreamServer) error {
//TODO implement me
//panic("implement me")
i := 0
for {
i++
_ = res.Send(&proto.StreamResData{
Message: fmt.Sprintf("%v", time.Now().Unix()),
})
time.Sleep(time.Second)
if i >= 10 {
break
}
}
return nil
}
func (s server) PutStream(cliStr proto.Greeter_PutStreamServer) error {
//TODO implement me
//panic("implement me")//
for {
if a, err := cliStr.Recv(); err != nil {
fmt.Println(err)
break
} else {
fmt.Println(a)
}
}
return nil
}
func (s server) AllStream(allStr proto.Greeter_AllStreamServer) error {
//TODO implement me
//panic("implement me")
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
for {
if a, err := allStr.Recv(); err != nil {
fmt.Println(err)
break
} else {
fmt.Println("用戶端傳送的訊息:", a)
}
}
}()
go func() {
defer wg.Done()
for {
allStr.Send(&proto.StreamResData{
Message: fmt.Sprintf("我是伺服器端資料:%v\n", time.Now().Unix()),
})
time.Sleep(time.Second)
}
}()
wg.Wait()
return nil
}
func (s *server) SomeRPC(ctx context.Context, in *pb.SomeRequest) (*pb.SomeResponse, error) {
md, ok := metadata.FromIncomingContext(ctx)
if (!ok) {
// handle missing metadata
}
// do something with metadata
return &pb.SomeResponse{}, nil
}
func main() {
// 建立一個新的gRPC伺服器
g := grpc.NewServer()
proto.RegisterGreeterServer(g, &server{}) // 註冊服務
listener, err := net.Listen("tcp", PORT)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
err = g.Serve(listener)
if err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
用戶端程式碼
package main
import (
"RpcLearn/stream_grpc_test/proto"
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"sync"
"time"
)
func main() {
// Create a new gRPC server
conn, err := grpc.("localhost:50052",
//grpc.WithInsecure()
grpc.WithTransportCredentials(insecure.NewCredentials()), // ✅ 新方式
)
if err != nil {
panic(err)
}
defer conn.Close()
c := proto.NewGreeterClient(conn)
//res, _ := c.GetStream(context.Background(), &proto.StreamReqData{Data: "慕課網"})
//for {
// r, err := res.Recv()
// if err != nil {
// break
// }
// fmt.Println(r)
//}
//// 用戶端串流模式
//putS, _ := c.PutStream(context.Background())
//i := 0
//for {
// i++
// putS.Send(&proto.StreamReqData{Data: fmt.Sprintf("慕課網%v", i)})
// time.Sleep(time.Second)
// if i >= 10 {
// break
// }
//}
//雙向串流模式
allStr, _ := c.AllStream(context.Background())
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
for {
if a, err := allStr.Recv(); err != nil {
fmt.Println(err)
break
} else {
fmt.Println("我是用戶端:%v", a)
}
}
}()
go func() {
defer wg.Done()
i := 0
for {
i++
allStr.Send(&proto.StreamReqData{Data: fmt.Sprintf("雙向的慕課網:%v", i)})
time.Sleep(time.Second)
if i >= 10 {
break
}
}
}()
wg.Wait()
}
Protobuf 型別與 Go 型別對應關係(含編碼建議)
.proto Type |
描述說明 | Go Type |
|---|---|---|
double |
浮點型別 | float64 |
float |
浮點型別 | float32 |
int32 |
使用變長編碼,對負值效率低;如有可能為負數,建議用 sint32 替代 |
int32 |
int64 |
使用變長編碼 | int64 |
uint32 |
使用變長編碼 | uint32 |
uint64 |
使用變長編碼 | uint64 |
sint32 |
使用變長編碼,對負值更高效(比 uint32 更好) |
int32 |
sint64 |
使用變長編碼,對負值更高效 | int64 |
fixed32 |
始終佔 4 位元組,適用於總是比 2²²⁸ 大的值,比 uint32 更高效 |
uint32 |
fixed64 |
始終佔 8 位元組,適用於總是比 2²⁵⁶ 大的值,比 uint64 更高效 |
uint64 |
sfixed32 |
始終佔 4 位元組 | int32 |
sfixed64 |
始終佔 8 位元組 | int64 |
bool |
布林值 | bool |
string |
必須是 UTF-8 或 ASCII 編碼的文字 | string |
bytes |
可包含任意順序的位元組資料 | []byte |
option go_package
編譯產生目錄的指定
如何在一個 proto 中引入其他 proto
import "base.proto"
import "google/protobuf/empty.proto"
巢狀的 message
列舉型別
enum Gender{
MALE = 0;
FEMALE = 1;
}
map 型別 map<string,string> mp = 4
如何使用時間戳
message HelloRequest {
string name = 1; // 姓名 相當於文件
string url = 2;
Gender g = 3;
map<string, string> mp = 4;
google.protobuf.Timestamp addTime = 5;
}
grpc 的更多功能
gRPC 讓我們可以像本地呼叫一樣實現遠端呼叫,對於每一次的 RPC 呼叫中,都可能會有一些有用的資料,而這些資料就可以透過 metadata 來傳遞。metadata 是以 key-value 的形式儲存資料的,其中 key 是 string 型別,而 value 是[]string,即一個字串陣列型別。metadata 使得 client 和 server 能夠為對方提供關於本次呼叫的一些資訊,就像一次 http 請求的 RequestHeader 和 ResponseHeader 一樣。http 中 header 的生命週期是一 http 請求,那麼 metadata 的生命週期就是一次 RPC 呼叫。
metadata
不是直接侵入到業務程式碼中的
// 新建
type MD mp[string][]string
// 建立
// 第一種方式
md := metadata.New(map[string]string{"key1": "val1", "key2": "val2"})
// 第二種方式 key 不區分大小寫,會被統一轉成小寫。
md := metadata.Pairs(
"key1", "val1",
"key1", "val1-2", // "key1" will have map value []string{"val1", "val1-2"}
"key2", "val2",
)
傳送 metadata
md := metadata.Pairs("key", "val")
// 新建一個有 metadata 的 context
ctx := metadata.NewOutgoingContext(context.Background(), md)
// 單向 RPC
response, err := client.SomeRPC(ctx, someRequest)
接收 metadata
func (s *server) SomeRPC(ctx context.Context, in *pb.SomeRequest) (*pb.SomeResponse, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
// handle missing metadata
}
// do something with metadata
return &pb.SomeResponse{}, nil
}
grpc 的攔截器
伺服器端和用戶端的攔截器
package main
import (
"RpcLearn/grpc_test/proto"
"context"
"google.golang.org/grpc"
"log"
"net"
)
type Server struct {
proto.UnimplementedGreeterServer // 👈 重點:嵌入這個型別
}
func (s *Server) SayHello(ctx context.Context, req *proto.HelloRequest) (*proto.HelloReply, error) {
reply := &proto.HelloReply{
Message: "Hello " + req.Name,
}
return reply, nil
}
func main() {
interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
log.Println("Request:接收到一個新的請求", req)
resp, err = handler(ctx, req)
if err != nil {
log.Println("Error:", err)
}
log.Println("Response:處理這個新請求完成", resp)
return resp, err
}
opt := grpc.UnaryInterceptor(interceptor)
g := grpc.NewServer(opt) // 建立一個新的gRPC伺服器
proto.RegisterGreeterServer(g, &Server{}) // 註冊服務
listener, err := net.Listen("tcp", ":9090")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
err = g.Serve(listener)
if err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
package main
import (
"RpcLearn/grpc_test/proto"
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"time"
)
func main() {
// Create a new gRPC server
//conn, err := grpc.Dial("localhost:9090", grpc.WithInsecure())
// 使用安全連線
//creds := credentials.NewTLS(&tls.Config{InsecureSkipVerify: true})
// 添加攔截器
clientInterceptor := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
start := time.Now()
err := invoker(ctx, method, req, reply, cc, opts...)
fmt.Println("耗時", time.Since(start), err)
return err
}
opt := grpc.WithUnaryInterceptor(clientInterceptor)
conn, err := grpc.NewClient("localhost:9090",
//grpc.WithInsecure()
grpc.WithTransportCredentials(insecure.NewCredentials()), // ✅ 新方式
opt,
)
if err != nil {
panic(err)
}
defer conn.Close()
c := proto.NewGreeterClient(conn)
r, err := c.SayHello(context.Background(), &proto.HelloRequest{Name: "world"})
if err != nil {
panic(err)
}
fmt.Println(r.Message)
}