RPC(Remote Procedure Call Protocol),是遠程過程調用的縮寫,通俗的說就是調用遠處的一個函數。與之相對應的是本地函數調用,咱們先來看一下本地函數調用。當咱們寫下以下代碼的時候:
規則golang
result := Add(1,2)
咱們知道,咱們傳入了1,2兩個參數,調用了本地代碼中的一個Add函數,獲得result這個返回值。這時參數,返回值,代碼段都在一個進程空間內,這是本地函數調用。shell
那有沒有辦法,咱們可以調用一個跨進程(因此叫"遠程",典型的事例,這個進程部署在另外一臺服務器上)的函數呢?json
這也是RPC主要實現的功能。服務器
咱們使用微服務化的一個好處就是,不限定服務的提供方使用什麼技術選型,可以實現公司跨團隊的技術解耦。網絡
這樣的話,若是沒有統一的服務框架,RPC框架,各個團隊的服務提供方就須要各自實現一套序列化、反序列化、網絡框架、鏈接池、收發線程、超時處理、狀態機等「業務以外」的重複技術勞動,形成總體的低效。因此,統一RPC框架把上述「業務以外」的技術勞動統一處理,是服務化首要解決的問題。框架
Go語言的RPC包的路徑爲net/rpc,也就是放在了net包目錄下面。所以咱們能夠猜想該RPC包是創建在net包基礎之上的。接着咱們嘗試基於rpc實現一個相似的例子。咱們先構造一個HelloService類型,其中的Hello方法用於實現打印功能:異步
type HelloService struct{} func(p *HelloService)Hello(request string,reply *string)error{ *reply = "hello:" + request return nil }
Hello方法方法必須知足Go語言的RPC規則:方法只能有兩個可序列化的參數,其中第二個參數是指針類型,而且返回一個error類型,同時必須是公開的方法。tcp
golang 中的類型好比:channel(通道)、complex(複數類型)、func(函數)均不能進行 序列化ide
而後就能夠將HelloService類型的對象註冊爲一個RPC服務:函數
func main(){ //rpc註冊服務 //註冊rpc服務,維護一個hash表,key值是服務名稱,value值是服務的地址 rpc.RegisterName("HelloService",new(HelloService)) //設置服務監聽 listener,err := net.Listen("tcp",":1234") if err != nil { panic(err) } //接受傳輸的數據 conn,err := listener.Accept() if err != nil { panic(err) } //rpc調用,並返回執行後的數據 //1.read,獲取服務名稱和方法名,獲取請求數據 //2.調用對應服務裏面的方法,獲取傳出數據 //3.write,把數據返回給client rpc.ServeConn(conn) }
其中rpc.Register函數調用會將對象類型中全部知足RPC規則的對象方法註冊爲RPC函數,全部註冊的方法會放在「HelloService」服務空間之下。而後咱們創建一個惟一的TCP連接,而且經過rpc.ServeConn函數在該TCP連接上爲對方提供RPC服務。
下面是客戶端請求HelloService服務的代碼:
func main(){ //用rpc鏈接 client,err := rpc.Dial("tcp","localhost:1234") if err != nil { panic(err) } var reply string //調用服務中的函數 err = client.Call("HelloService.Hello","world",&reply) if err != nil { panic(err) } fmt.Println("收到的數據爲,",reply) }
首選是經過rpc.Dial撥號RPC服務,而後經過client.Call調用具體的RPC方法。在調用client.Call時,第一個參數是用點號連接的RPC服務名字和方法名字,第二和第三個參數分別咱們定義RPC方法的兩個參數。
標準庫的RPC默認採用Go語言特有的gob編碼。所以,其它語言調用Go語言實現的RPC服務將比較困難。跨語言是互聯網時代RPC的一個首要條件,這裏咱們再來實現一個跨語言的RPC。得益於RPC的框架設計,Go語言的RPC其實也是很容易實現跨語言支持的。
這裏咱們將嘗試經過官方自帶的net/rpc/jsonrpc擴展實現一個跨語言RPC。
首先是基於json編碼從新實現RPC服務:
func main(){ //註冊rpc服務 rpc.RegisterName("HelloService",new(HelloService)) //設置監聽 listener,err := net.Listen("tcp",":1234") if err != nil { panic(err) } for{ //接收鏈接 conn,err := listener.Accept() if err != nil { panic(err) } //給當前鏈接提供針對json格式的rpc服務 go rpc.ServeCodec(jsonrpc.NewServerCodec(conn)) } }
代碼中最大的變化是用rpc.ServeCodec函數替代了rpc.ServeConn函數,傳入的參數是針對服務端的json編解碼器。
而後是實現json版本的客戶端:
func main(){ //簡歷tcp鏈接 conn,err := net.Dial("tcp","localhost:1234") if err !=nil{ panic(err) } //簡歷基於json編解碼的rpc服務 client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn)) var reply string //調用rpc服務方法 err = client.Call("HelloService.Hello"," world",&reply) if err != nil { panic(err) } fmt.Println("收到的數據爲:",reply) }
先手工調用net.Dial函數創建TCP連接,而後基於該連接創建針對客戶端的json編解碼器。
在確保客戶端能夠正常調用RPC服務的方法以後,咱們能夠用命令來查看一下客戶端發給服務端的究竟是什麼數據。這裏咱們使用 ==nc -l 1234== 這條命令 模擬服務器監聽1234端口接收的數據,而後從新運行客戶端,將會發現nc輸出瞭如下的信息:
{"method":"HelloService.Hello","params":["hello"],"id":0}
nc經常使用有兩種一種是鏈接到指定ip和端口
nc hostname port
另一種是監聽端口,等待鏈接
nc -l port
這是一個json編碼的數據,其中method部分對應要調用的rpc服務和方法組合成的名字,params部分的第一個元素爲參數,id是由調用端維護的一個惟一的調用編號。
請求的json數據對象在內部對應兩個結構體:客戶端是clientRequest,服務端是serverRequest。clientRequest和serverRequest結構體的內容基本是一致的:
type clientRequest struct { Method string `json:"method"` Params []interface{} `json:"params"` Id uint64 `json:"id"` } type serverRequest struct { Method string `json:"method"` Params *json.RawMessage `json:"params"` Id *json.RawMessage `json:"id"` }
瞭解了客戶端須要發送哪些數據以後,咱們能夠再來看看服務器接收到客戶端傳輸的數據以後會返回哪些數據,仍是用咱們的nc命令。操做以下:
echo -e '{"method":"HelloService.Hello","params":["hello"],"id":1}'| nc localhost 1234
返回的數據以下:
其中id對應輸入的id參數,result爲返回的結果,error部分在出問題時表示錯誤信息。對於順序調用來講,id不是必須的。可是Go語言的RPC框架支持異步調用,當返回結果的順序和調用的順序不一致時,能夠經過id來識別對應的調用。
返回的json數據也是對應內部的兩個結構體:客戶端是clientResponse,服務端是serverResponse。兩個結構體的內容一樣也是相似的:
type clientResponse struct { Id uint64 `json:"id"` Result *json.RawMessage `json:"result"` Error interface{} `json:"error"` } type serverResponse struct { Id *json.RawMessage `json:"id"` Result interface{} `json:"result"` Error interface{} `json:"error"` }
所以不管採用何種語言,只要遵循一樣的json結構,以一樣的流程就能夠和Go語言編寫的RPC服務進行通訊。這樣咱們就解用json簡單實現了跨語言的RPC。
可是通常在開發的時候除了用json作跨語言的RPC服務以外,如今不少公司還會選用protobuf作跨語言的RPC服務。那什麼是ProtoBuf呢?接下來咱們詳細瞭解一下。
上面的代碼服務名都是寫死的,不夠靈活(容易寫錯),這裏咱們對RPC的服務端和客戶端再次進行一次封裝,來屏蔽掉服務名,具體代碼以下
//抽離服務名稱 var serverName = "LoginService" //定義一個父類 type RPCDesign interface { Hello(string,*string)error } //實現工廠函數 func RegisterRPCServer(srv RPCDesign)error{ return rpc.RegisterName(serverName,srv) }
封裝以後的服務端實現以下:
type RpcServer struct{} //5 + 3i chan func complex func (this *RpcServer) Hello(req string, resp *string) error { *resp += req + "你好" return nil } func main() { //設置監聽 listener, err := net.Listen("tcp", ":8899") if err != nil { fmt.Println("設置監聽錯誤") return } defer listener.Close() fmt.Println("開始監聽....") for { //接收連接 conn, err := listener.Accept() if err != nil { fmt.Println("獲取鏈接失敗") return } defer conn.Close() fmt.Println(conn.RemoteAddr().String() + "鏈接成功") //rpc表 註冊rpc服務 if err = RegisterRPCServer(new(RpcServer)); err != nil { fmt.Println("註冊rpc服務失敗") return } //把rpc服務和套接字綁定 //rpc.ServeConn(conn) rpc.ServeCodec(jsonrpc.NewServerCodec(conn)) } }
type RPCClient struct { rpcClient *rpc.Client } func NewRpcClient(addr string)(RPCClient){ conn,err := net.Dial("tcp",addr) if err != nil { fmt.Println("連接服務器失敗") return RPCClient{} } defer conn.Close() //套接字和rpc服務綁定 client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn)) return RPCClient{rpcClient:client} } func (this*RPCClient)CallFunc(req string,resp*string)error{ return this.rpcClient.Call(serverName+".Hello",req,resp) }
封裝以後客戶端實現
func main() { //初始化對象 與服務名有關的內容徹底封裝起來了 client := NewRpcClient("127.0.0.1:8899") //調用成員函數 var temp string client.CallFunc("xiaoming",&temp) fmt.Println(temp) }