繼上一篇的TIL:gRPC 1. 初介紹,我們開始來製作第一個類型的gRPC:Unary,如下圖看到的,他是一個如過去HTTP 1.1模式的溝通方式,Client發送一個請求,然後Server端再把結果回覆給Client:
關於gRPC的開發,不管是哪一種類型,Server與Client的溝通介面都是從proto buffer文件的定義開始,所以Client只要拿到.proto文件,然後根據自己熟悉的開發語言找到套件進行Compile,就可以開始開發Client端的程式碼了。
所以,我們先從定義.proto文件開始,下面示範Client傳遞一個名稱,然後Server端回傳該名字對應的ID,下圖為這個範例的資料夾結構。
Step 1. 定義.proto文件
撰寫.proto文件時,可以分為幾個區塊
第一個是文件格式定義,其中
- syntax: proto buffer版本,因為目前最新為第三版,故可以直接給予proto3
- package: 表這份文件所屬的package,這邊我們用與檔名相同
- go_package: 表在用Golang語言開發時引入的package名稱,我們用pb作為名稱來與其他package區分。
1
2
3
4
5syntax = "proto3";
package lookup;
option go_package = "pb";
接下來,就開始撰寫邏輯的部分,我們可以分為兩個大部分
- message: 定義溝通的參數介面,這邊要注意到
- 變數間必須給予順序編號
- 變數的類型包含double, float, int32, int64, bool, string, bytes等,與各語言對應的關係可以參考:https://developers.google.com/protocol-buffers/docs/proto3#scalar
- service: 定義溝通的function介面,這邊這邊要注意到
- 可以透過service名稱來區隔不同的service type,例如:OrderService、CheckOutService與Shipservice就可以分開來定義
- 每一個service內的function name需以rpc開頭,並且將input與output介面定義上去,之後會用到串流方式傳輸資料,就可以在參數前加上stream關鍵字
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17message LookingUp {
string first_name = 1;
string last_name = 2;
}
message LookUpRequest {
LookingUp lookingUp = 1;
}
message LookUpResponse {
int32 result = 1;
}
service LookUpService{
//unary
rpc LookUp(LookUpRequest) returns (LookUpResponse) {};
}- message: 定義溝通的參數介面,這邊要注意到
Step 2. 將.proto文件做Compile
安裝protobuf的工具,並且記得將該路徑加入到$GOPATH/bin
1
go get -u github.com/golang/protobuf/protoc-gen-go
然後切換到專案的資料夾下後,輸入下面的command,如果沒有發生錯誤,就可以在
/pb
中發現多了一個檔案,其副檔名.pb.go
。1
protoc Lookupservice/Unary/pb/lookup.proto --go_out=plugins=grpc:.
Step 3. 開發Server端程式
首先,先建立一個模擬的data table,讓client呼叫時可以達到查詢的效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23type MyDataTable struct {
ID int32
Name string
}
var (
recordTable []MyDataTable
)
func InitData() {
recordTable = make([]MyDataTable, 0)
recordTable = append(recordTable, MyDataTable{
ID: 13579,
Name: "WeiZhe",
})
recordTable = append(recordTable, MyDataTable{
ID: 24680,
Name: "Ray",
})
}
我們先來定義一個structure,他會包含與Client溝通的function
我們先到剛剛產生的
lookup.pb.go
檔案找尋到下面這段,這代表LookUpServiceServer這個interface中有一個function等待我們去implement,我們把LookUp(context.Context, *LookUpRequest) (*LookUpResponse, error)
複製起來1
2
3
4type LookUpServiceServer interface {
//unary
LookUp(context.Context, *LookUpRequest) (*LookUpResponse, error)
}回到server.go,先宣告一個structure
- 然後,將剛剛從
lookup.pb.go
複製的function附屬於剛剛宣告的structure,來讓我們來implement他- 一開始貼上會出現錯誤,這是因為function中的名稱是附屬於
lookup.pb.go
下,故記得補上pb.
即可。
- 一開始貼上會出現錯誤,這是因為function中的名稱是附屬於
- implement的邏輯即是for loop查詢
recordTable
這個slice,然後比對如果有與傳進來的req.LookingUp.FirstName
相同,則return查到的ID,否則回傳error message說明找不到。- 傳進來的參數可以透過
*pb.LookUpRequest
來查詢 - 回傳的
*pb.LookUpResponse
是一個structure,所以我們需要將結果組裝起來成相同類型:記得回傳的需要是一個位址類型。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31type serviceSvr struct{}
func (s *serviceSvr) LookUp(ctx context.Context, req *pb.LookUpRequest) (response *pb.LookUpResponse, err error) {
var (
firstName string
lookUpResp *pb.LookUpResponse
content MyDataTable
result int32
)
log.Printf("LookUp function invoked by %v", req)
log.Println(recordTable)
firstName = req.LookingUp.FirstName
result = -1
for _, content = range recordTable {
log.Println(content.Name, "/", firstName)
if content.Name == firstName {
result = content.ID
break
}
}
if result == -1 {
return nil, errors.New("failed to find ID, please check first name again")
}
lookUpResp = &pb.LookUpResponse{
Result: result,
}
return lookUpResp, nil
}
- 傳進來的參數可以透過
- 定義完service中function的邏輯後,我們來初始化一個gRPC的Server
- 先建立一個listener,並且用tcp監聽著port: 50051
1
2
3
4
5if listener, err = net.Listen("tcp", ":50051"); err != nil {
log.Fatalf("failed to create a listener: %v", err)
return
}
- 首先引入gRPC package:
google.golang.org/grpc
,接著呼叫NewServer
就能成功初始一個gRPC Server - 接著,引入
/pb
資料夾後,就可以參考到剛剛透過protoc產生的RegisterLookUpServiceServer
function,在這邊把我們剛剛初始成功的gRPC:grpcSvr
與上一個步驟產生的serviceSvr這個綁有LookUp
function的structure:&serviceSvr{}
綁上去。 - 最後,我們將gRPC server開始launch起來,就開始正式服務了。
1
2
3
4
5
6
7
8grpcSvr = grpc.NewServer()
pb.RegisterLookUpServiceServer(grpcSvr, &serviceSvr{})
if err = grpcSvr.Serve(listener); err != nil {
log.Fatalf("failed to serve gRPC: %v", err)
return
}
Step 4. 開發Client端程式
接著,我們可以開始來開發client的程式,相較於server端,client端的開發較為簡單
- 首先,一樣引入
google.golang.org/grpc
,然後使用Dial
function來與server進行溝通
- 第一個參數填入server端的位址
- 第二個參數填入
Dial
function的選項,通常都是grpc.With
開頭,這邊因為我們只是基本功能實現,所以就不用到SSL驗證,故填入grpc.WithInsecure()
:當然如果你有多個功i能要打開,你可以看到後面是一個interface,只要把要選項附加上去即可。 - 記得養成好習慣,一但打開connection,就要加上defer來將這個connection做close。
然後,引入
github.com/weizhe0422/gRPC/LookupService/Unary/pb
,就可以參考到NewLookUpServiceClient
function,並且把我們剛剛初始的connection傳進去,並得到一個gRPC Client的變數。1
2
3
4
5if conn, err = grpc.Dial(":50051", grpc.WithInsecure()); err != nil {
log.Fatalf("failed to coneect to gRPC server: %v", err)
}
defer conn.Close()
client = pb.NewLookUpServiceClient(conn)client端的最後一步就是開始呼叫server端的function並得到回應
- 我們先把剛剛gRPC Client的變數傳到一個function來實作
- 其中function實作中
- 把request的資料組成
pb.LookUpRequest
類型 - 使用Client變數中protoBuf定義的function name,並傳入context與剛剛組成的
pb.LookUpRequest
類型資料 - 接著就可以拿到該function回傳的response結果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23doUnary(client, firstName, lastName)
func doUnary(client pb.LookUpServiceClient, firstName, lastName string){
var(
lookUpReq *pb.LookUpRequest
lookUpResp *pb.LookUpResponse
err error
)
lookUpReq = &pb.LookUpRequest{
LookingUp: &pb.LookingUp{
FirstName:firstName,
LastName:lastName,
},
}
if lookUpResp, err = client.LookUp(context.Background(),lookUpReq); err!=nil{
log.Fatalf("failed to send LookUp: %v", err)
}
fmt.Println("LookUp result: ", lookUpResp.Result)
}
- 把request的資料組成
Step 5. 測試連接
- 切換到server端與client端的資料夾,並且各自將其run起來,就可以看到Client端成功從server查詢到first_name於data table中的編號。
至此已經完成第一個gRPC類型的開發,後面三個類型都是這個類型的些微變化就可以實現成功,所以如果能把這些步驟做到熟悉,後面的範例就能夠更快上手囉! 我們下個章節見啦!