golang實(shí)現(xiàn)RPC的幾種方式
什么是RPC
遠(yuǎn)程過程調(diào)用(Remote Procedure Call,縮寫為 RPC)是一個計(jì)算機(jī)通信協(xié)議。該協(xié)議允許運(yùn)行于一臺計(jì)算機(jī)的程序調(diào)用另一臺計(jì)算機(jī)的子程序,而程序員無需額外地為這個交互作用編程。如果涉及的軟件采用面向?qū)ο缶幊蹋敲催h(yuǎn)程過程調(diào)用亦可稱作遠(yuǎn)程調(diào)用或遠(yuǎn)程方法調(diào)用。
用通俗易懂的語言描述就是:RPC允許跨機(jī)器、跨語言調(diào)用計(jì)算機(jī)程序方法。打個比方,我用go語言寫了個獲取用戶信息的方法getUserInfo,并把go程序部署在阿里云服務(wù)器上面,現(xiàn)在我有一個部署在騰訊云上面的php項(xiàng)目,需要調(diào)用golang的getUserInfo方法獲取用戶信息,php跨機(jī)器調(diào)用go方法的過程就是RPC調(diào)用。
golang中如何實(shí)現(xiàn)RPC
在golang中實(shí)現(xiàn)RPC非常簡單,有封裝好的官方庫和一些第三方庫提供支持。Go RPC可以利用tcp或http來傳遞數(shù)據(jù),可以對要傳遞的數(shù)據(jù)使用多種類型的編解碼方式。golang官方的net/rpc庫使用encoding/gob進(jìn)行編解碼,支持tcp或http數(shù)據(jù)傳輸方式,由于其他語言不支持gob編解碼方式,所以使用net/rpc庫實(shí)現(xiàn)的RPC方法沒辦法進(jìn)行跨語言調(diào)用。
golang官方還提供了net/rpc/jsonrpc庫實(shí)現(xiàn)RPC方法,JSON RPC采用JSON進(jìn)行數(shù)據(jù)編解碼,因而支持跨語言調(diào)用。但目前的jsonrpc庫是基于tcp協(xié)議實(shí)現(xiàn)的,暫時不支持使用http進(jìn)行數(shù)據(jù)傳輸。
除了golang官方提供的rpc庫,還有許多第三方庫為在golang中實(shí)現(xiàn)RPC提供支持,大部分第三方rpc庫的實(shí)現(xiàn)都是使用protobuf進(jìn)行數(shù)據(jù)編解碼,根據(jù)protobuf聲明文件自動生成rpc方法定義與服務(wù)注冊代碼,在golang中可以很方便的進(jìn)行rpc服務(wù)調(diào)用。
net/rpc庫
下面的例子演示一下如何使用golang官方的net/rpc庫實(shí)現(xiàn)RPC方法,使用http作為RPC的載體,通過net/http包監(jiān)聽客戶端連接請求。
$GOPATH/src/test/rpc/rpc_server.go
package?main
import?(
????"errors"
????"fmt"
????"log"
????"net"
????"net/http"
????"net/rpc"
????"os"
)
//?算數(shù)運(yùn)算結(jié)構(gòu)體
type?Arith?struct?{
}
//?算數(shù)運(yùn)算請求結(jié)構(gòu)體
type?ArithRequest?struct?{
????A?int
????B?int
}
//?算數(shù)運(yùn)算響應(yīng)結(jié)構(gòu)體
type?ArithResponse?struct?{
????Pro?int?//?乘積
????Quo?int?//?商
????Rem?int?//?余數(shù)
}
//?乘法運(yùn)算方法
func?(this?*Arith)?Multiply(req?ArithRequest,?res?*ArithResponse)?error?{
????res.Pro?=?req.A?*?req.B
????return?nil
}
//?除法運(yùn)算方法
func?(this?*Arith)?Divide(req?ArithRequest,?res?*ArithResponse)?error?{
????if?req.B?==?0?{
????????return?errors.New("divide?by?zero")
????}
????res.Quo?=?req.A?/?req.B
????res.Rem?=?req.A?%?req.B
????return?nil
}
func?main()?{
????rpc.Register(new(Arith))?//?注冊rpc服務(wù)
????rpc.HandleHTTP()?????????//?采用http協(xié)議作為rpc載體
????lis,?err?:=?net.Listen("tcp",?"127.0.0.1:8095")
????if?err?!=?nil?{
????????log.Fatalln("fatal?error:?",?err)
????}
????fmt.Fprintf(os.Stdout,?"%s",?"start?connection")
????http.Serve(lis,?nil)
}上述服務(wù)端程序運(yùn)行后,將會監(jiān)聽本地的8095端口,我們可以實(shí)現(xiàn)一個客戶端程序,連接服務(wù)端并實(shí)現(xiàn)RPC方法調(diào)用。
$GOPATH/src/test/rpc/rpc_client.go
package?main
import?(
????"fmt"
????"log"
????"net/rpc"
)
//?算數(shù)運(yùn)算請求結(jié)構(gòu)體
type?ArithRequest?struct?{
????A?int
????B?int
}
//?算數(shù)運(yùn)算響應(yīng)結(jié)構(gòu)體
type?ArithResponse?struct?{
????Pro?int?//?乘積
????Quo?int?//?商
????Rem?int?//?余數(shù)
}
func?main()?{
????conn,?err?:=?rpc.DialHTTP("tcp",?"127.0.0.1:8095")
????if?err?!=?nil?{
????????log.Fatalln("dailing?error:?",?err)
????}
????req?:=?ArithRequest{9,?2}
????var?res?ArithResponse
????err?=?conn.Call("Arith.Multiply",?req,?&res)?//?乘法運(yùn)算
????if?err?!=?nil?{
????????log.Fatalln("arith?error:?",?err)
????}
????fmt.Printf("%d?*?%d?=?%d\n",?req.A,?req.B,?res.Pro)
????err?=?conn.Call("Arith.Divide",?req,?&res)
????if?err?!=?nil?{
????????log.Fatalln("arith?error:?",?err)
????}
????fmt.Printf("%d?/?%d,?quo?is?%d,?rem?is?%d\n",?req.A,?req.B,?res.Quo,?res.Rem)
}net/rpc/jsonrpc庫
上面的例子我們演示了使用net/rpc實(shí)現(xiàn)RPC的過程,但是沒辦法在其他語言中調(diào)用上面例子實(shí)現(xiàn)的RPC方法。所以接下來的例子我們演示一下使用net/rpc/jsonrpc庫實(shí)現(xiàn)RPC方法,此方式實(shí)現(xiàn)的RPC方法支持跨語言調(diào)用。
$GOPATH/src/test/rpc/jsonrpc_server.go
package?main
import?(
????"errors"
????"fmt"
????"log"
????"net"
????"net/rpc"
????"net/rpc/jsonrpc"
????"os"
)
//?算數(shù)運(yùn)算結(jié)構(gòu)體
type?Arith?struct?{
}
//?算數(shù)運(yùn)算請求結(jié)構(gòu)體
type?ArithRequest?struct?{
????A?int
????B?int
}
//?算數(shù)運(yùn)算響應(yīng)結(jié)構(gòu)體
type?ArithResponse?struct?{
????Pro?int?//?乘積
????Quo?int?//?商
????Rem?int?//?余數(shù)
}
//?乘法運(yùn)算方法
func?(this?*Arith)?Multiply(req?ArithRequest,?res?*ArithResponse)?error?{
????res.Pro?=?req.A?*?req.B
????return?nil
}
//?除法運(yùn)算方法
func?(this?*Arith)?Divide(req?ArithRequest,?res?*ArithResponse)?error?{
????if?req.B?==?0?{
????????return?errors.New("divide?by?zero")
????}
????res.Quo?=?req.A?/?req.B
????res.Rem?=?req.A?%?req.B
????return?nil
}
func?main()?{
????rpc.Register(new(Arith))?//?注冊rpc服務(wù)
????lis,?err?:=?net.Listen("tcp",?"127.0.0.1:8096")
????if?err?!=?nil?{
????????log.Fatalln("fatal?error:?",?err)
????}
????fmt.Fprintf(os.Stdout,?"%s",?"start?connection")
????for?{
????????conn,?err?:=?lis.Accept()?//?接收客戶端連接請求
????????if?err?!=?nil?{
????????????continue
????????}
????????go?func(conn?net.Conn)?{?//?并發(fā)處理客戶端請求
????????????fmt.Fprintf(os.Stdout,?"%s",?"new?client?in?coming\n")
????????????jsonrpc.ServeConn(conn)
????????}(conn)
????}
}上述服務(wù)端程序啟動后,將會監(jiān)聽本地的8096端口,并處理客戶端的tcp連接請求。我們可以用golang實(shí)現(xiàn)一個客戶端程序連接上述服務(wù)端并進(jìn)行RPC調(diào)用。
$GOPATH/src/test/rpc/jsonrpc_client.go
package?main
import?(
????"fmt"
????"log"
????"net/rpc/jsonrpc"
)
//?算數(shù)運(yùn)算請求結(jié)構(gòu)體
type?ArithRequest?struct?{
????A?int
????B?int
}
//?算數(shù)運(yùn)算響應(yīng)結(jié)構(gòu)體
type?ArithResponse?struct?{
????Pro?int?//?乘積
????Quo?int?//?商
????Rem?int?//?余數(shù)
}
func?main()?{
????conn,?err?:=?jsonrpc.Dial("tcp",?"127.0.0.1:8096")
????if?err?!=?nil?{
????????log.Fatalln("dailing?error:?",?err)
????}
????req?:=?ArithRequest{9,?2}
????var?res?ArithResponse
????err?=?conn.Call("Arith.Multiply",?req,?&res)?//?乘法運(yùn)算
????if?err?!=?nil?{
????????log.Fatalln("arith?error:?",?err)
????}
????fmt.Printf("%d?*?%d?=?%d\n",?req.A,?req.B,?res.Pro)
????err?=?conn.Call("Arith.Divide",?req,?&res)
????if?err?!=?nil?{
????????log.Fatalln("arith?error:?",?err)
????}
????fmt.Printf("%d?/?%d,?quo?is?%d,?rem?is?%d\n",?req.A,?req.B,?res.Quo,?res.Rem)
}protorpc庫
為了實(shí)現(xiàn)跨語言調(diào)用,在golang中實(shí)現(xiàn)RPC方法的時候我們應(yīng)該選擇一種跨語言的數(shù)據(jù)編解碼方式,比如JSON,上述的jsonrpc可以滿足此要求,但是也存在一些缺點(diǎn),比如不支持http傳輸,數(shù)據(jù)編解碼性能不高等。于是呢,一些第三方rpc庫都選擇采用protobuf進(jìn)行數(shù)據(jù)編解碼,并提供一些服務(wù)注冊代碼自動生成功能。下面的例子我們使用protobuf來定義RPC方法及其請求響應(yīng)參數(shù),并使用第三方的protorpc庫來生成RPC服務(wù)注冊代碼。

首先,需要安裝protobuf及protoc可執(zhí)行命令,可以參考此篇文章:protobuf快速上手指南
然后,我們編寫一個proto文件,定義要實(shí)現(xiàn)的RPC方法及其相關(guān)參數(shù)。
$GOPATH/src/test/rpc/pb/arith.proto
syntax?=?"proto3";package?pb;??//?算術(shù)運(yùn)算請求結(jié)構(gòu)
message?ArithRequest?{????int32?a?=?1;????int32?b?=?2;}//?算術(shù)運(yùn)算響應(yīng)結(jié)構(gòu)
message?ArithResponse?{????
????int32?pro?=?1;??//?乘積
????int32?quo?=?2;??//?商
????int32?rem?=?3;??//?余數(shù)
}//?rpc方法
service?ArithService?{????
rpc?multiply?(ArithRequest)?returns?(ArithResponse);??//?乘法運(yùn)算方法
rpc?divide?(ArithRequest)?returns?(ArithResponse);???//?除法運(yùn)算方法
}接下來我們需要根據(jù)上述定義的arith.proto文件生成RPC服務(wù)代碼。要先安裝protorpc庫:go get github.com/chai2010/protorpc?然后使用protoc工具生成代碼:protoc --go_out=plugin=protorpc=. arith.proto?執(zhí)行protoc命令后,在與arith.proto文件同級的目錄下生成了一個arith.pb.go文件,里面包含了RPC方法定義和服務(wù)注冊的代碼。
基于生成的arith.pb.go代碼我們來實(shí)現(xiàn)一個rpc服務(wù)端
$GOPATH/src/test/rpc/protorpc_server.go
package?main
import?(
????"errors"
????"test/rpc/pb"
)
//?算術(shù)運(yùn)算結(jié)構(gòu)體
type?Arith?struct?{
}
//?乘法運(yùn)算方法
func?(this?*Arith)?Multiply(req?*pb.ArithRequest,?res?*pb.ArithResponse)?error?{
????res.Pro?=?req.GetA()?*?req.GetB()
????return?nil
}
//?除法運(yùn)算方法
func?(this?*Arith)?Divide(req?*pb.ArithRequest,?res?*pb.ArithResponse)?error?{
????if?req.GetB()?==?0?{
????????return?errors.New("divide?by?zero")
????}
????res.Quo?=?req.GetA()?/?req.GetB()
????res.Rem?=?req.GetA()?%?req.GetB()
????return?nil
}
func?main()?{
????pb.ListenAndServeArithService("tcp",?"127.0.0.1:8097",?new(Arith))
}
運(yùn)行上述程序,將會監(jiān)聽本地的8097端口并接收客戶端的tcp連接。
基于ariti.pb.go再來實(shí)現(xiàn)一個客戶端程序。
$GOPATH/src/test/protorpc_client.go
package?main
import?(
????"fmt"
????"log"
????"test/rpc/pb"
)
func?main()?{
????conn,?err?:=?pb.DialArithService("tcp",?"127.0.0.1:8097")
????if?err?!=?nil?{
????????log.Fatalln("dailing?error:?",?err)
????}
????defer?conn.Close()
????req?:=?&pb.ArithRequest{9,?2}
????res,?err?:=?conn.Multiply(req)
????if?err?!=?nil?{
????????log.Fatalln("arith?error:?",?err)
????}
????fmt.Printf("%d?*?%d?=?%d\n",?req.GetA(),?req.GetB(),?res.GetPro())
????res,?err?=?conn.Divide(req)
????if?err?!=?nil?{
????????log.Fatalln("arith?error?",?err)
????}
????fmt.Printf("%d?/?%d,?quo?is?%d,?rem?is?%d\n",?req.A,?req.B,?res.Quo,?res.Rem)
}如何跨語言調(diào)用golang的RPC方法
上面的三個例子,我們分別使用net/rpc、net/rpc/jsonrpc、protorpc實(shí)現(xiàn)了golang中的RPC服務(wù)端,并給出了對應(yīng)的golang客戶端RPC調(diào)用示例,因?yàn)镴SON和protobuf是支持多語言的,所以使用jsonrpc和protorpc實(shí)現(xiàn)的RPC方法我們是可以在其他語言中進(jìn)行調(diào)用的。下面給出一個php客戶端程序,通過socket連接調(diào)用jsonrpc實(shí)現(xiàn)的服務(wù)端RPC方法。
$PHPROOT/jsonrpc.php
class?JsonRPC?{
????private?$conn;
????function?__construct($host,?$port)?{
????????$this->conn?=?fsockopen($host,?$port,?$errno,?$errstr,?3);
????????if?(!$this->conn)?{
????????????return?false;
????????}
????}
????public?function?Call($method,?$params)?{
????????if?(!$this->conn)?{
????????????return?false;
????????}
????????$err?=?fwrite($this->conn,?json_encode(array(
????????????????'method'?=>?$method,
????????????????'params'?=>?array($params),
????????????????'id'?????=>?0,
????????????))."\n");
????????if?($err?===?false)?{
????????????return?false;
????????}
????????stream_set_timeout($this->conn,?0,?3000);
????????$line?=?fgets($this->conn);
????????if?($line?===?false)?{
????????????return?NULL;
????????}
????????return?json_decode($line,true);
????}
}
$client?=?new?JsonRPC("127.0.0.1",?8096);
$args?=?array('A'=>9,?'B'=>2);
$r?=?$client->Call("Arith.Multiply",?$args);
printf("%d?*?%d?=?%d\n",?$args['A'],?$args['B'],?$r['result']['Pro']);
$r?=?$client->Call("Arith.Divide",?array('A'=>9,?'B'=>2));
printf("%d?/?%d,?Quo?is?%d,?Rem?is?%d\n",?$args['A'],?$args['B'],?$r['result']['Quo'],?$r['result']['Rem']);其他RPC庫
除了上面提到的三種在golang實(shí)現(xiàn)RPC的方式外,還有一些其他的rpc庫提供了類似的功能,比較出名的有g(shù)oogle開源的grpc,但是grpc的初次安裝比較麻煩,這里就不做進(jìn)一步介紹了,有興趣的可以自己了解。
