go 語言中 RPC

golang_real.jpg

微服務(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)用流程

屏幕快照 2019-04-24 下午8.21.15.png

我們通過上面圖來看东揣,這個流程比較清晰药薯,也不難理解。

  1. 調(diào)用客戶端句柄救斑,執(zhí)行傳送參數(shù)
  2. 調(diào)用本地系統(tǒng)內(nèi)核發(fā)送網(wǎng)絡(luò)消息
  3. 消息傳送至遠程機器
  4. 服務(wù)器句柄得到消息并取得參數(shù)
  5. 執(zhí)行遠程過程
  6. 執(zhí)行過程將結(jié)果返回給服務(wù)器句柄
  7. 服務(wù)器句柄返回結(jié)果童本,調(diào)用遠程系統(tǒng)內(nèi)核
  8. 消息傳回本地主機
  9. 客戶句柄由內(nèi)核接收消息
  10. 客戶接收句柄返回的數(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ù)
    1. 第一個參數(shù)接收的參數(shù)
    2. 第二個參數(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, &quot)
    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, &quot)
    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)
}
grpc_square_reverse_4x.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末懈贺,一起剝皮案震驚了整個濱河市经窖,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌梭灿,老刑警劉巖画侣,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異堡妒,居然都是意外死亡配乱,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來搬泥,“玉大人桑寨,你說我怎么就攤上這事》揲荩” “怎么了尉尾?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長燥透。 經(jīng)常有香客問我沙咏,道長,這世上最難降的妖魔是什么班套? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任肢藐,我火速辦了婚禮,結(jié)果婚禮上吱韭,老公的妹妹穿的比我還像新娘吆豹。我一直安慰自己,他們只是感情好理盆,可當我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布痘煤。 她就那樣靜靜地躺著,像睡著了一般熏挎。 火紅的嫁衣襯著肌膚如雪速勇。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天坎拐,我揣著相機與錄音,去河邊找鬼养匈。 笑死哼勇,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的呕乎。 我是一名探鬼主播积担,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼猬仁!你這毒婦竟也來了帝璧?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤湿刽,失蹤者是張志新(化名)和其女友劉穎的烁,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體诈闺,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡渴庆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片襟雷。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡刃滓,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出耸弄,到底是詐尸還是另有隱情咧虎,我是刑警寧澤,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布计呈,位于F島的核電站砰诵,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏震叮。R本人自食惡果不足惜胧砰,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望苇瓣。 院中可真熱鬧尉间,春花似錦、人聲如沸击罪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽媳禁。三九已至眠副,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間竣稽,已是汗流浹背囱怕。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留毫别,地道東北人娃弓。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像岛宦,于是被迫代替她去往敵國和親台丛。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內(nèi)容