Go RPC

2022-05-13 17:42 更新

前面幾個小節(jié)我們介紹了如何基于Socket和HTTP來編寫網(wǎng)絡(luò)應(yīng)用,通過學習我們了解了Socket和HTTP采用的是類似"信息交換"模式,即客戶端發(fā)送一條信息到服務(wù)端,然后(一般來說)服務(wù)器端都會返回一定的信息以表示響應(yīng)??蛻舳撕头?wù)端之間約定了交互信息的格式,以便雙方都能夠解析交互所產(chǎn)生的信息。但是很多獨立的應(yīng)用并沒有采用這種模式,而是采用類似常規(guī)的函數(shù)調(diào)用的方式來完成想要的功能。

RPC就是想實現(xiàn)函數(shù)調(diào)用模式的網(wǎng)絡(luò)化??蛻舳司拖裾{(diào)用本地函數(shù)一樣,然后客戶端把這些參數(shù)打包之后通過網(wǎng)絡(luò)傳遞到服務(wù)端,服務(wù)端解包到處理過程中執(zhí)行,然后執(zhí)行的結(jié)果反饋給客戶端。

RPC(Remote Procedure Call Protocol)——遠程過程調(diào)用協(xié)議,是一種通過網(wǎng)絡(luò)從遠程計算機程序上請求服務(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工作原理


運行時,一次客戶機對服務(wù)器的RPC調(diào)用,其內(nèi)部操作大致有如下十步:

  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ù)

Go RPC

Go標準包中已經(jīng)提供了對RPC的支持,而且支持三個級別的RPC:TCP、HTTP、JSONRPC。但Go的RPC包是獨一無二的RPC,它和傳統(tǒng)的RPC系統(tǒng)不同,它只支持Go開發(fā)的服務(wù)器與客戶端之間的交互,因為在內(nèi)部,它們采用了Gob來編碼。

Go RPC的函數(shù)只有符合下面的條件才能被遠程訪問,不然會被忽略,詳細的要求如下:

  • 函數(shù)必須是導出的(首字母大寫)
  • 必須有兩個導出類型的參數(shù),
  • 第一個參數(shù)是接收的參數(shù),第二個參數(shù)是返回給客戶端的參數(shù),第二個參數(shù)必須是指針類型的
  • 函數(shù)還要有一個返回值error

舉個例子,正確的RPC函數(shù)格式如下:

func (t *T) MethodName(argType T1, replyType *T2) error

T、T1和T2類型必須能被encoding/gob包編解碼。

任何的RPC都需要通過網(wǎng)絡(luò)來傳遞數(shù)據(jù),Go RPC可以利用HTTP和TCP來傳遞數(shù)據(jù),利用HTTP的好處是可以直接復用net/http里面的一些函數(shù)。詳細的例子請看下面的實現(xiàn)

HTTP RPC

http的服務(wù)端代碼實現(xiàn)如下:

package main

import (
	"errors"
	"fmt"
	"net/http"
	"net/rpc"
)

type Args struct {
	A, B int
}

type Quotient struct {
	Quo, Rem int
}

type Arith int

func (t *Arith) Multiply(args *Args, reply *int) error {
	*reply = args.A * args.B
	return 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.B
	quo.Rem = args.A % args.B
	return nil
}

func main() {

	arith := new(Arith)
	rpc.Register(arith)
	rpc.HandleHTTP()

	err := http.ListenAndServe(":1234", nil)
	if err != nil {
		fmt.Println(err.Error())
	}
}

通過上面的例子可以看到,我們注冊了一個Arith的RPC服務(wù),然后通過?rpc.HandleHTTP?函數(shù)把該服務(wù)注冊到了HTTP協(xié)議上,然后我們就可以利用http的方式來傳遞數(shù)據(jù)了。

請看下面的客戶端代碼:

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)
	}
	// Synchronous call
	args := Args{17, 8}
	var reply int
	err = 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 Quotient
	err = client.Call("Arith.Divide", args, &quot)
	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ù)端開啟,然后開啟客戶端,輸入代碼,就會輸出如下信息:

$ ./http_c localhost
Arith: 17*8=136
Arith: 17/8=2 remainder 1

通過上面的調(diào)用可以看到參數(shù)和返回值是我們定義的struct類型,在服務(wù)端我們把它們當做調(diào)用函數(shù)的參數(shù)的類型,在客戶端作為client.Call的第2,3兩個參數(shù)的類型??蛻舳俗钪匾木褪沁@個Call函數(shù),它有3個參數(shù),第1個要調(diào)用的函數(shù)的名字,第2個是要傳遞的參數(shù),第3個要返回的參數(shù)(注意是指針類型),通過上面的代碼例子我們可以發(fā)現(xiàn),使用Go的RPC實現(xiàn)相當?shù)暮唵?,方便?br>

TCP RPC

上面我們實現(xiàn)了基于HTTP協(xié)議的RPC,接下來我們要實現(xiàn)基于TCP協(xié)議的RPC,服務(wù)端的實現(xiàn)代碼如下所示:

package main

import (
	"errors"
	"fmt"
	"net"
	"net/rpc"
	"os"
)

type Args struct {
	A, B int
}

type Quotient struct {
	Quo, Rem int
}

type Arith int

func (t *Arith) Multiply(args *Args, reply *int) error {
	*reply = args.A * args.B
	return 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.B
	quo.Rem = args.A % args.B
	return 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)
	}
}

上面這個代碼和http的服務(wù)器相比,不同在于:在此處我們采用了TCP協(xié)議,然后需要自己控制連接,當有客戶端連接上來后,我們需要把這個連接交給rpc來處理。

如果你留心了,你會發(fā)現(xiàn)這它是一個阻塞型的單用戶的程序,如果想要實現(xiàn)多并發(fā),那么可以使用goroutine來實現(xiàn),我們前面在socket小節(jié)的時候已經(jīng)介紹過如何處理goroutine。 下面展現(xiàn)了TCP實現(xiàn)的RPC客戶端:

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:port")
		os.Exit(1)
	}
	service := os.Args[1]

	client, err := rpc.Dial("tcp", service)
	if err != nil {
		log.Fatal("dialing:", err)
	}
	// Synchronous call
	args := Args{17, 8}
	var reply int
	err = 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 Quotient
	err = client.Call("Arith.Divide", args, &quot)
	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)

}

這個客戶端代碼和http的客戶端代碼對比,唯一的區(qū)別一個是DialHTTP,一個是Dial(tcp),其他處理一模一樣。

JSON RPC

JSON RPC是數(shù)據(jù)編碼采用了JSON,而不是gob編碼,其他和上面介紹的RPC概念一模一樣,下面我們來演示一下,如何使用Go提供的json-rpc標準包,請看服務(wù)端代碼的實現(xiàn):

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 Arith int

func (t *Arith) Multiply(args *Args, reply *int) error {
	*reply = args.A * args.B
	return 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.B
	quo.Rem = args.A % args.B
	return 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é)議實現(xiàn)的,目前它還不支持HTTP方式。

請看客戶端的實現(xiàn)代碼:

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:port")
		log.Fatal(1)
	}
	service := os.Args[1]

	client, err := jsonrpc.Dial("tcp", service)
	if err != nil {
		log.Fatal("dialing:", err)
	}
	// Synchronous call
	args := Args{17, 8}
	var reply int
	err = 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 Quotient
	err = client.Call("Arith.Divide", args, &quot)
	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已經(jīng)提供了對RPC的良好支持,通過上面HTTP、TCP、JSON RPC的實現(xiàn),我們就可以很方便的開發(fā)很多分布式的Web應(yīng)用,我想作為讀者的你已經(jīng)領(lǐng)會到這一點。但遺憾的是目前Go尚未提供對SOAP RPC的支持,欣慰的是現(xiàn)在已經(jīng)有第三方的開源實現(xiàn)了。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號