go中的RPC
RPC(Remote Procedure Call Protocol)——遠(yuǎn)程過程調(diào)用協(xié)議,是一種通過網(wǎng)絡(luò)從遠(yuǎn)程計(jì)算機(jī)程序上請(qǐng)求服務(wù),而不需要了解底層網(wǎng)絡(luò)技術(shù)的協(xié)議。它假定某些傳輸協(xié)議的存在,如TCP或UDP,以便為通信程序之間攜帶信息數(shù)據(jù)。通過它可以使函數(shù)調(diào)用模式網(wǎng)絡(luò)化。在OSI網(wǎng)絡(luò)通信模型中,RPC跨越了傳輸層和應(yīng)用層。RPC使得開發(fā)包括網(wǎng)絡(luò)分布式多程序在內(nèi)的應(yīng)用程序更加容易。
RPC工作原理
?
運(yùn)行時(shí),一次客戶機(jī)對(duì)服務(wù)器的RPC調(diào)用,其內(nèi)部操作大致有如下十步:
- 1.調(diào)用客戶端句柄;執(zhí)行傳送參數(shù)
- 2.調(diào)用本地系統(tǒng)內(nèi)核發(fā)送網(wǎng)絡(luò)消息
- 3.消息傳送到遠(yuǎn)程主機(jī)
- 4.服務(wù)器句柄得到消息并取得參數(shù)
- 5.執(zhí)行遠(yuǎn)程過程
- 6.執(zhí)行的過程將結(jié)果返回服務(wù)器句柄
- 7.服務(wù)器句柄返回結(jié)果,調(diào)用遠(yuǎn)程系統(tǒng)內(nèi)核
- 8.消息傳回本地主機(jī)
- 9.客戶句柄由內(nèi)核接收消息
- 10.客戶接收句柄返回的數(shù)據(jù)
Go RPC
Go標(biāo)準(zhǔn)包中已經(jīng)提供了對(duì)RPC的支持,而且支持三個(gè)級(jí)別的RPC:TCP、HTTP、JSONRPC。但Go的RPC包是獨(dú)一無(wú)二的RPC,它和傳統(tǒng)的RPC系統(tǒng)不同,它只支持Go開發(fā)的服務(wù)器與客戶端之間的交互,因?yàn)樵趦?nèi)部,它們采用了Gob來(lái)編碼。
Go RPC的函數(shù)只有符合下面的條件才能被遠(yuǎn)程訪問,不然會(huì)被忽略,詳細(xì)的要求如下:
- 函數(shù)必須是導(dǎo)出的(首字母大寫)
- 必須有兩個(gè)導(dǎo)出類型的參數(shù),
- 第一個(gè)參數(shù)是接收的參數(shù),第二個(gè)參數(shù)是返回給客戶端的參數(shù),第二個(gè)參數(shù)必須是指針類型的
- 函數(shù)還要有一個(gè)返回值error
舉個(gè)例子,正確的RPC函數(shù)格式如下:
func (t *T) MethodName(argType T1, replyType *T2) errorT、T1和T2類型必須能被encoding/gob包編解碼。
任何的RPC都需要通過網(wǎng)絡(luò)來(lái)傳遞數(shù)據(jù),Go RPC可以利用HTTP和TCP來(lái)傳遞數(shù)據(jù),利用HTTP的好處是可以直接復(fù)用net/http里面的一些函數(shù)。詳細(xì)的例子請(qǐng)看下面的實(shí)現(xiàn)
HTTP RPC
http的服務(wù)端代碼實(shí)現(xiàn)如下:
package mainimport ("errors""fmt""net/http""net/rpc" )type Args struct {A, B int }type Quotient struct {Quo, Rem int }type Arith intfunc (t *Arith) Multiply(args *Args, reply *int) error {*reply = args.A * args.Breturn nil }func (t *Arith) Divide(args *Args, quo *Quotient) error {if args.B == 0 {return errors.New("divide by zero")}quo.Quo = args.A / args.Bquo.Rem = args.A % args.Breturn nil }func main() {arith := new(Arith)rpc.Register(arith)rpc.HandleHTTP()err := http.ListenAndServe(":1234", nil)if err != nil {fmt.Println(err.Error())} }通過上面的例子可以看到,我們注冊(cè)了一個(gè)Arith的RPC服務(wù),然后通過rpc.HandleHTTP函數(shù)把該服務(wù)注冊(cè)到了HTTP協(xié)議上,然后我們就可以利用http的方式來(lái)傳遞數(shù)據(jù)了。
請(qǐng)看下面的客戶端代碼:
package mainimport ("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)}// Synchronous callargs := Args{17, 8}var reply interr = client.Call("Arith.Multiply", args, &reply)if err != nil {log.Fatal("arith error:", err)}fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)var quot Quotienterr = client.Call("Arith.Divide", args, ")if err != nil {log.Fatal("arith error:", err)}fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)}我們把上面的服務(wù)端和客戶端的代碼分別編譯,然后先把服務(wù)端開啟,然后開啟客戶端,輸入代碼,就會(huì)輸出如下信息:
$ ./http_c localhost Arith: 17*8=136 Arith: 17/8=2 remainder 1通過上面的調(diào)用可以看到參數(shù)和返回值是我們定義的struct類型,在服務(wù)端我們把它們當(dāng)做調(diào)用函數(shù)的參數(shù)的類型,在客戶端作為client.Call的第2,3兩個(gè)參數(shù)的類型。客戶端最重要的就是這個(gè)Call函數(shù),它有3個(gè)參數(shù),第1個(gè)要調(diào)用的函數(shù)的名字,第2個(gè)是要傳遞的參數(shù),第3個(gè)要返回的參數(shù)(注意是指針類型),通過上面的代碼例子我們可以發(fā)現(xiàn),使用Go的RPC實(shí)現(xiàn)相當(dāng)?shù)暮?jiǎn)單,方便。
TCP RPC
上面我們實(shí)現(xiàn)了基于HTTP協(xié)議的RPC,接下來(lái)我們要實(shí)現(xiàn)基于TCP協(xié)議的RPC,服務(wù)端的實(shí)現(xiàn)代碼如下所示:
package mainimport ("errors""fmt""net""net/rpc""os" )type Args struct {A, B int }type Quotient struct {Quo, Rem int }type Arith intfunc (t *Arith) Multiply(args *Args, reply *int) error {*reply = args.A * args.Breturn nil }func (t *Arith) Divide(args *Args, quo *Quotient) error {if args.B == 0 {return errors.New("divide by zero")}quo.Quo = args.A / args.Bquo.Rem = args.A % args.Breturn nil }func main() {arith := new(Arith)rpc.Register(arith)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}rpc.ServeConn(conn)}}func checkError(err error) {if err != nil {fmt.Println("Fatal error ", err.Error())os.Exit(1)} }上面這個(gè)代碼和http的服務(wù)器相比,不同在于:在此處我們采用了TCP協(xié)議,然后需要自己控制連接,當(dāng)有客戶端連接上來(lái)后,我們需要把這個(gè)連接交給rpc來(lái)處理。
如果你留心了,你會(huì)發(fā)現(xiàn)這它是一個(gè)阻塞型的單用戶的程序,如果想要實(shí)現(xiàn)多并發(fā),那么可以使用goroutine來(lái)實(shí)現(xiàn),我們前面在socket小節(jié)的時(shí)候已經(jīng)介紹過如何處理goroutine。 下面展現(xiàn)了TCP實(shí)現(xiàn)的RPC客戶端:
package mainimport ("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:port")os.Exit(1)}service := os.Args[1]client, err := rpc.Dial("tcp", service)if err != nil {log.Fatal("dialing:", err)}// Synchronous callargs := Args{17, 8}var reply interr = client.Call("Arith.Multiply", args, &reply)if err != nil {log.Fatal("arith error:", err)}fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)var quot Quotienterr = client.Call("Arith.Divide", args, ")if err != nil {log.Fatal("arith error:", err)}fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)}這個(gè)客戶端代碼和http的客戶端代碼對(duì)比,唯一的區(qū)別一個(gè)是DialHTTP,一個(gè)是Dial(tcp),其他處理一模一樣。
JSON RPC
JSON RPC是數(shù)據(jù)編碼采用了JSON,而不是gob編碼,其他和上面介紹的RPC概念一模一樣,下面我們來(lái)演示一下,如何使用Go提供的json-rpc標(biāo)準(zhǔn)包,請(qǐng)看服務(wù)端代碼的實(shí)現(xiàn):
package mainimport ("errors""fmt""net""net/rpc""net/rpc/jsonrpc""os" )type Args struct {A, B int }type Quotient struct {Quo, Rem int }type Arith intfunc (t *Arith) Multiply(args *Args, reply *int) error {*reply = args.A * args.Breturn nil }func (t *Arith) Divide(args *Args, quo *Quotient) error {if args.B == 0 {return errors.New("divide by zero")}quo.Quo = args.A / args.Bquo.Rem = args.A % args.Breturn nil }func main() {arith := new(Arith)rpc.Register(arith)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())os.Exit(1)} }通過示例我們可以看出 json-rpc是基于TCP協(xié)議實(shí)現(xiàn)的,目前它還不支持HTTP方式。
請(qǐng)看客戶端的實(shí)現(xiàn)代碼:
package mainimport ("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:port")log.Fatal(1)}service := os.Args[1]client, err := jsonrpc.Dial("tcp", service)if err != nil {log.Fatal("dialing:", err)}// Synchronous callargs := Args{17, 8}var reply interr = client.Call("Arith.Multiply", args, &reply)if err != nil {log.Fatal("arith error:", err)}fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)var quot Quotienterr = client.Call("Arith.Divide", args, ")if err != nil {log.Fatal("arith error:", err)}fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)}總結(jié)
- 上一篇: go中的REST
- 下一篇: go预防CSRF攻击