前言
筆者最近因為必須要串接一個 GRPC 的 service 要做使用,發現公司許多同仁也跟我一樣需要這個 service,所以每個人都得在自己的 module 裡面都要寫一段 code 去 connect 這個 GRPC service。許多人寫相同的 code 去 connection 相同的 service 其實並不好,不但效率不高而且倘若這個 service 的 proto 改變了,那其實每個人都得要修改。
因此,這邊便想跟讀者 share 一下,筆者如何將一個 GRPC service 包裝起來,統稱叫做 shared library
,提供外部的其他 module 可以簡單快速地使用,各自 module 不但不用寫相同的 code,而且維護上也更為方便,只需更改此 shared library
就可以了。
GRPC
以下是筆者想要包裝起來的 GRPC service 範例,因有公司的 usage 所以筆者不會列出所有細節,而是列出相似範例來做說明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
syntax = "proto3";
service systemMessage {
rpc getInfoMsg (getRequest) returns (getResponse) {}
rpc getWarningMsg (getRequest) returns (getResponse) {}
rpc addInfoMsg (addRequest) returns (addResponse) {}
rpc addWarningMsg (addRequest) returns (addResponse) {}
...
}
...
大概說明一下這個 service 流程以及作用。這個 GRPC service 是一個 message center,各自 module 可以將想要送的 message (一般的 information 或是 warning 類別的) 送到此 GRPC service,有另外一端的 consumer 會取得所有 messages 並呈現到 UI。
PS. 有關 Request 及 Response 牽扯到實作部分,筆者就不列實際的 struct 出來了,讀者可以自行補齊,像是 Request 裡面就會有 title、message 等等的欄位。
Wrap the GRPC service
接著,我們就可以開始來包裝此 service。整體的 code 就如以下:
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package sysmsg
import (
"context"
"crypto/sha256"
"fmt"
"log"
"os"
"os/signal"
spec "pb/systemmessage"
"sync"
"syscall"
"time"
"google.golang.org/grpc"
)
//SystemMsg define
type SystemMsg struct {
Conn *grpc.ClientConn
Client spec.SystemMessageClient
Module string
Mutex sync.Mutex
}
//connecToSystemWarnAgent is to connect SystemMessage agent
func connecToSystemWarnAgent(server string) (*SystemMsg, error) {
conn, err := grpc.Dial(
server,
grpc.WithInsecure(),
)
if err != nil {
return &SystemMsg{
Conn: nil,
Client: nil,
}, err
}
return &SystemMsg{
Conn: conn,
Client: spec.NewSystemWarningClient(conn),
}, nil
}
//NewSystemMessageSender is to generate a sender for system messages, input for your own module name,
//default server will be "systemmessage:9876" by using docker network to connect systemmessage. If you are host mode,
//you can assign gateway IP instead. Like: NewSystemMessageSender("your_module_name","gateway_ip:9876")
func NewSystemMessageSender(module string, server ...string) *SystemMsg {
var c = make(chan os.Signal, 1)
sysServer := "systemwarning:9876"
if len(server) != 0 {
sysServer = server[0]
}
systemCli, err := connecToSystemWarnAgent(sysServer)
if err != nil {
log.Printf("Can not connect to systemwarning module, err: %v\n", err)
}
systemCli.Module = module
signal.Notify(c, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
go func() {
for range c {
log.Printf("Close systemwarning connection instance\n")
systemCli.Close()
}
}()
return systemCli
}
//Close systemwarning connection
func (s *SystemMsg) Close() {
if s.Conn != nil {
s.Conn.Close()
}
}
//Info is to send messages to systemmessage, assign title and message in parameter field
func (s *SystemMsg) Info(title string, message string) {
s.Mutex.Lock()
defer s.Mutex.Unlock()
if s.Conn == nil {
return
}
if s.Client != nil {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
_, err := s.Client.PostSysInfo(ctx, &spec.PostSysInfoRequest{
Module: s.Module,
Title: title,
Message: message,
})
//if send message failed, print the error msg on console
if err != nil {
log.Printf("Send system message failed, err: %s\n", err)
}
}
}
這邊就一部分一部分來說明。
generate grpc proto
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import (
"context"
"crypto/sha256"
"fmt"
"log"
"os"
"os/signal"
spec "pb/systemmessage"
"sync"
"syscall"
"time"
"google.golang.org/grpc"
)
這邊的 spec 就是剛剛上面 systemmessage 的 pb.go 檔,因此這邊要做 Import。還不太了解 GRPC 怎麼 build 出 pb.go 檔的人可以來學習一下官網。
define wrapper and how to use it
接著,我們來定義一下 wrapper。SystemMsg
會有需要儲存 GRPC connection 以及 client 的物件。筆者這邊多訂一個 Module 欄位,方便後面 Implementation 的 function,就不用每次都要填 Module 名稱,代表這個物件送 Message 就都是使用這個 Module 名稱去帶到 Request 裡面。Mutex 其實很重要,當你有許多地方會使用到這個 wrapper 的時候就要用 Mutex 去做 Lock 的動作,這個概念套用到很多實作都是一樣喔。
1
2
3
4
5
6
7
//SystemMsg define
type SystemMsg struct {
Conn *grpc.ClientConn
Client spec.SystemMessageClient
Module string
Mutex sync.Mutex
}
再來就是連接的部分,把 connection 及 client 產生出來並且放進 wrapper 中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//connecToSystemWarnAgent is to connect SystemMessage agent
func connecToSystemWarnAgent(server string) (*SystemMsg, error) {
conn, err := grpc.Dial(
server,
grpc.WithInsecure(),
)
if err != nil {
return &SystemMsg{
Conn: nil,
Client: nil,
}, err
}
return &SystemMsg{
Conn: conn,
Client: spec.NewSystemMessageClient(conn),
}, nil
}
generate the wrapper
接著,Implement 產生 wrapper 的 function。這個 function 就是使用者會直接用到的,包含使用者 module 名稱,message center server 的位置在哪裡。因為筆者公司是用 docker 包起來的系統架構,所以 comment 是在說明有關如何 connect message center 的事項。底下的 Close 則是用關閉 connection。讀者們在撰寫一般的 GRPC 的時候也需注意這部分唷,沒有 close 的話常常會造成系統 FD Leak 導致 crash。
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
//NewSystemMessageSender is to generate a sender for system messages, input for your own module name,
//default server will be "systemmessage:9876" by using docker network to connect systemmessage. If you are host mode,
//you can assign gateway IP instead. Like: NewSystemMessageSender("your_module_name","gateway_ip:9876")
func NewSystemMessageSender(module string, server ...string) *SystemMsg {
var c = make(chan os.Signal, 1)
sysServer := "systemwarning:9876"
if len(server) != 0 {
sysServer = server[0]
}
systemCli, err := connecToSystemWarnAgent(sysServer)
if err != nil {
log.Printf("Can not connect to systemwarning module, err: %v\n", err)
}
systemCli.Module = module
signal.Notify(c, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
go func() {
for range c {
log.Printf("Close systemwarning connection instance\n")
systemCli.Close()
}
}()
return systemCli
}
//Close systemwarning connection
func (s *SystemMsg) Close() {
if s.Conn != nil {
s.Conn.Close()
}
}
send the message
最後,就可以撰寫送 message 的 function。前面有提到先用 Mutex Lock 這個 section 防止造成 race condition。接著利用 client 將 message 送出去囉。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//Info is to send messages to systemmessage, assign title and message in parameter field
func (s *SystemMsg) Info(title string, message string) {
s.Mutex.Lock()
defer s.Mutex.Unlock()
if s.Conn == nil {
return
}
if s.Client != nil {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
_, err := s.Client.PostSysInfo(ctx, &spec.PostSysInfoRequest{
Module: s.Module,
Title: title,
Message: message,
})
//if send message failed, print the error msg on console
if err != nil {
log.Printf("Send system message failed, err: %s\n", err)
}
}
}
How to use it
到這邊為止就大功告成啦! 接著如何來使用這個 shared library
呢 ? 筆者這邊是使用 go module 來管理套件。所以要使用 shared library
,要在 go.mod
裡面指定到你的 shared library
,假設你將上面的 code 放在一個 folder sharedlib
,並且把它放在跟你的 project 同一層:
1
2
ls
sharedlib/ your_module/
go.mod
1
2
3
4
5
6
7
8
9
10
module your_module
go 1.12
require (
...
sharedlib
)
replace sharedlib => ../sharedlib
接著下
1
go mod vendor
便可以把你剛剛撰寫的 sharedlib 在你 module 裡使用囉! 以剛剛我們上面的 message center 為例子,就可以在自己的 module 裡使用像是:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import (
...
"sharedlib/sysmsg"
...
)
//SysMsgSender is message sender for module testing
var SysMsgSender = sysmsg.NewSystemMessageSender("testing")
...
SysMsgSender.Info("test sender", fmt.Sprintf("Testing message will be sent!"))
...
在你的 module 中,你可以只產生一個 SysMsgSender
,重複使用它即可。若是想使用多個 wrapper 在不同的 package 中去送 message 當然也是可以的。
總結
這樣一來,每個人都只需要使用這一個 shared library
,就可以達到連接這個 GRPC service 並且不需要寫重複相關的 code,不但更簡潔一點而且維護上只需要 maintain 此 shared library
就可以了! 此種做法可以套用到任何 GRPC service 上面,像是很常見的 Log center 也都是可以運用這個機制。但是,(人生就是很多但是XD),很多同仁使用的時候有遇到一些問題,後來發現是筆者的 testing 做得不夠啊! Testing 很重要,之前的前輩也是一直耳提面命一定都要寫 testing。因此,筆者後面一篇就會提到,該如何針對這個機制去做 Unit testing
!
Contact me
有任何疑問或是建議歡迎聯絡 Jimmy!
Gmail: jimmyw86878@gmail.com
-
Previous
Rancher with K8S cluster 之旅 (4) - OneTouchDeployRancher -
Next
Golang - Unit testing for GRPC function and Golang build proxy