微服務(wù)已經(jīng) hot 了一段時間浸剩,自己作為 web 開發(fā)人員當然也不由自主想研究研究微服務(wù),不過微服務(wù)的整個知識體系過于龐大澈歉,要掌握的概念和技術(shù)太多工腋,一時有點吃不消姨丈。個人為了生計又沒有大塊時間去搞畅卓。不過還是難舍微服務(wù),最近學習了 go 語言蟋恬,想一塊一塊地吃掉微服務(wù)翁潘,先從 go 和容器入手。
我們知道微服務(wù)之間是需要相互調(diào)用和通訊的歼争,一般會采用 RPC 來實現(xiàn)不同語言不用服務(wù)間的調(diào)用和通訊拜马。那么我就先從 RPC 入手來學習微服務(wù)。
RPC 框架的特點
所謂的特點就是他能夠滿足那些需求沐绒,RPC 框架要實現(xiàn)以上目的需要滿足以下需求
序列化(GOB)語言單一
上下文管理(超時控制)
攔截器(鑒權(quán)俩莽、統(tǒng)計和限流)
跨語言
服務(wù)注冊
Call ID:由于客戶端和服務(wù)端運行在不同進程,為了讓客戶端和服務(wù)端都了解調(diào)用了哪個函數(shù)洒沦,需要在兩端維護一個函數(shù)到Call ID 的映射表豹绪,客戶端根據(jù)表獲取函數(shù)的 Call ID,發(fā)起請求申眼,服務(wù)端根據(jù) Call ID 來執(zhí)行對應(yīng)的函數(shù),返回值給客戶端蝉衣。
序列化和反序列化:在本地調(diào)用時候函數(shù)是從棧中獲取參數(shù)運行函數(shù)括尸。而遠程調(diào)用時候,如果需要在不同語言間相互調(diào)用函數(shù)病毡,需要將參數(shù)進行序列化然后以字節(jié)流方式傳遞給服務(wù)端濒翻,服務(wù)端在反序列化來得到參數(shù)
網(wǎng)絡(luò)傳輸:客戶端和服務(wù)端間的調(diào)用往往是通過網(wǎng)絡(luò)完成。只要能傳遞數(shù)據(jù)就行啦膜,與協(xié)議無關(guān)有送,可以使用 TCP 或 UDP。gRcp 使用的 HTTP2 僧家。
什么是 RPC
RPC是指遠程過程調(diào)用雀摘,也就是說兩臺服務(wù)器A,B八拱,一個應(yīng)用部署在A服務(wù)器上阵赠,想要調(diào)用B服務(wù)器上應(yīng)用提供的函數(shù)/方法,由于不在一個內(nèi)存空間肌稻,不能直接調(diào)用清蚀,需要通過網(wǎng)絡(luò)來表達調(diào)用的語義和傳達調(diào)用的數(shù)據(jù)。
為什么需要 RPC 呢
因為 RPC 是分布式系統(tǒng)中不同節(jié)點間流行的通訊方式爹谭,在互聯(lián)網(wǎng)時代枷邪,RPC 和 IPC 一樣成為不可或缺的基礎(chǔ)構(gòu)建。在 Go 語言的標準庫也提供了簡單的 RPC 的實現(xiàn)诺凡。
RPC 在服務(wù)間調(diào)用流程
我們通過上面圖來看东揣,這個流程比較清晰药薯,也不難理解。
- 調(diào)用客戶端句柄救斑,執(zhí)行傳送參數(shù)
- 調(diào)用本地系統(tǒng)內(nèi)核發(fā)送網(wǎng)絡(luò)消息
- 消息傳送至遠程機器
- 服務(wù)器句柄得到消息并取得參數(shù)
- 執(zhí)行遠程過程
- 執(zhí)行過程將結(jié)果返回給服務(wù)器句柄
- 服務(wù)器句柄返回結(jié)果童本,調(diào)用遠程系統(tǒng)內(nèi)核
- 消息傳回本地主機
- 客戶句柄由內(nèi)核接收消息
- 客戶接收句柄返回的數(shù)據(jù)
有了上面理論基礎(chǔ),我們基于理論來實現(xiàn)脸候。
type RPCService struct{}
創(chuàng)建一個RPCService
服務(wù)穷娱,隨后將其進行注冊
func (s *RPCService) Hello(request string, reply *string) error{
*reply = "Hello " + request
return nil
}
- 函數(shù)必須是外部可以訪問函數(shù),函數(shù)名需要首字母大寫
- 函數(shù)需要有兩個參數(shù)
- 第一個參數(shù)接收的參數(shù)
- 第二個參數(shù)是返回給客戶端的參數(shù)运沦,而且需要是指針類型
- 函數(shù)還需要有一個 error 返回值
rpc.RegisterName("RPCService",new(RPCService))
注冊rpc服務(wù)
listener, err := net.Listen("tcp",":1234")
創(chuàng)建 tcp 服務(wù)端口號為 1234 用于 rpc 服務(wù)泵额。
conn, err := listener.Accept()
if err != nil{
log.Fatal("Accept error:", err)
}
rpc.ServeConn(conn)
服務(wù)端完整代碼
package main
import(
// "fmt"
"log"
"net"
"net/rpc"
)
type RPCService struct{}
func (s *RPCService) Hello(request string, reply *string) error{
*reply = "Hello " + request
return nil
}
func main() {
rpc.RegisterName("RPCService",new(RPCService))
listener, err := net.Listen("tcp",":1234")
if err != nil{
log.Fatal("ListenTCP error:",err)
}
conn, err := listener.Accept()
if err != nil{
log.Fatal("Accept error:", err)
}
rpc.ServeConn(conn)
}
客戶端代碼
client, err := rpc.Dial("tcp","localhost:1234")
客戶端調(diào)用 RPC 服務(wù),然后通過client.Call調(diào)用具體的RPC方法携添。
err = client.Call("RPCService.Hello","World",&reply)
在調(diào)用client.Call時嫁盲,第一個參數(shù)是用點號鏈接的RPC服務(wù)名字和方法名字,第二和第三個參數(shù)分別我們定義RPC方法的兩個參數(shù)烈掠。
package main
import(
"fmt"
"log"
"net/rpc"
)
func main() {
client, err := rpc.Dial("tcp","localhost:1234")
if err != nil{
log.Fatal("dialing:", err)
}
var reply string
err = client.Call("RPCService.Hello","World",&reply)
if err != nil{
log.Fatal("call Hello method of RPCService:",err)
}
fmt.Println(reply)
}
我們先后啟動服務(wù)端和客戶端就可以看到下面效果
Hello World
下面更加貼近實際來寫一個基于 HTTP 的 RPC 服務(wù)羞秤,服務(wù)提供兩個數(shù)四則運算。
rpcService := new(RPCService)
rpc.Register(rpcService)
rpc.HandleHTTP()
這里的注冊方式略有不同左敌,但是大同小異相信大家一看就懂瘾蛋。
服務(wù)端完整代碼
package main
import(
"errors"
"fmt"
"net/http"
"net/rpc"
)
type Args struct{
A, B int
}
type Quotient struct{
Quo, Rem int
}
type RPCService int
func (t *RPCService) Add(args *Args, reply *int) error{
*reply = args.A - args.B
return nil
}
func (t *RPCService) Multiply(args *Args, reply *int) error{
*reply = args.A * args.B
return nil
}
func (t *RPCService) Divide(args *Args, quo *Quotient) error{
if args.B == 0{
return errors.New("divide by zero")
}
quo.Quo = args.A / args.B
quo.Rem = args.A % args.B
return nil
}
func main() {
rpcService := new(RPCService)
rpc.Register(rpcService)
rpc.HandleHTTP()
err := http.ListenAndServe(":1234",nil)
if err != nil{
fmt.Println(err.Error())
}
}
package main
import(
"fmt"
"log"
"net/rpc"
"os"
)
type Args struct{
A, B int
}
type Quotient struct{
Quo, Rem int
}
func main() {
if len(os.Args) != 2{
fmt.Println("Usage: ", os.Args[0],"server")
os.Exit(1)
}
serverAddress := os.Args[1]
client, err := rpc.DialHTTP("tcp",serverAddress + ":1234")
if err != nil {
log.Fatal("dialing: ", err)
}
args := Args{17, 8}
var reply int
err = client.Call("RPCService.Add",args, &reply)
if err != nil{
log.Fatal("RPCService error: ", err)
}
fmt.Printf("RPCService: %d + %d = %d\n", args.A, args.B, &reply)
var quot Quotient
err = client.Call("RPCService.Divide",args, ")
if err != nil{
log.Fatal("RPCService error: ",err)
}
fmt.Printf("RPCService: %d/%d=%d remainder %d\n",args.A, args.B, quot.Quo,quot.Rem)
}
RPCService: 17 + 8 = 824634312296
RPCService: 17/8=2 remainder 1
其實在實際開發(fā)中我們還需要對其進行改造,例如讓 rpc 請求可以獲得一個 context 對象矫限,其中包含用戶信息等哺哼,然后可以對 rpc 進行超時處理。
JSONRPC
Go語言內(nèi)置的 RPC 框架已經(jīng)支持在 Http 協(xié)議上提供 RPC 服務(wù)叼风。但是 Http 服務(wù)內(nèi)置采用了 GOB 協(xié)議取董。編碼不是 JSON 編碼,不方便其他語言調(diào)用无宿。不過 go 提供 JsonRPC 的 RPC 服務(wù)支持茵汰,我們來看一看怎么用代碼實現(xiàn)。
服務(wù)端代碼
package main
import(
"errors"
"fmt"
"net"
"net/rpc"
"net/rpc/jsonrpc"
// "os"
)
type Args struct{
A, B int
}
type Quotient struct{
Quo, Rem int
}
type RPCService int
func (t *RPCService) Multiply(args *Args, reply *int) error{
*reply = args.A * args.B
return nil
}
func (t *RPCService) Divide(args *Args, quo *Quotient) error{
if args.B == 0{
return errors.New("divide by zero")
}
quo.Quo = args.A / args.B
quo.Rem = args.A % args.B
return nil
}
func main() {
rpcService := new(RPCService)
rpc.Register(rpcService)
tcpAddr, err := net.ResolveTCPAddr("tcp",":1234")
checkError(err)
listener, err := net.ListenTCP("tcp",tcpAddr)
checkError(err)
for{
conn, err := listener.Accept()
if err != nil{
continue
}
jsonrpc.ServeConn(conn)
}
}
func checkError(err error){
if err != nil{
fmt.Println("Fatal error ", err.Error())
}
}
客戶端代碼
package main
import(
"fmt"
"log"
"net/rpc/jsonrpc"
"os"
)
type Args struct{
A, B int
}
type Quotient struct{
Quo, Rem int
}
func main() {
if len(os.Args) != 2{
fmt.Println("Usage: ", os.Args[0],"server")
log.Fatal(1)
}
serverAddress := os.Args[1]
client, err := jsonrpc.Dial("tcp",serverAddress + ":1234")
if err != nil {
log.Fatal("dialing: ", err)
}
args := Args{17, 8}
var quot Quotient
err = client.Call("RPCService.Divide",args, ")
if err != nil{
log.Fatal("RPCService error: ",err)
}
fmt.Printf("RPCService: %d/%d=%d remainder %d\n",args.A, args.B, quot.Quo,quot.Rem)
}