簡介
RPC是在分布式計算,遠程過程調用(英語:Remote Procedure Call,縮寫為 RPC)是一個計算機通信協議。在互聯網時代,RPC已經和IPC一樣成為一個不可或缺的基礎構建。RPC是進程之間的通信方式(inter-process communication,IPC)不同的進程有不停的地址空間。
如果client和server在同一台機器上,盡管物理地址空間是相同的,但是虛擬地址空間不同。
如果他們在不同的主機上,物理地址空間不停,。RPC的實現的技術各不相同,也一定不兼容。
一個正常的RPC過程可以分為下面幾步:
- client調用client stub,這是一個過程調用
- client stub將參數打包成一個消息,然后發送這個消息,打包過程叫做marshalling
- client所在的系統將消息發送給server
- server的系統將收到的包傳給server stub
- server stub解包得到參數。解包也被稱作unmarshalling
- 最后server stub調用服務過程,返回結果按照相反的步驟傳給client
RPC只是描繪了 Client 與 Server 之間的點對點調用流程,包括 stub、通信、RPC 消息解析等部分,在實際應用中,還需要考慮服務的高可用、負載均衡等問題,所以產品級的 RPC 框架除了點對點的 RPC 協議的具體實現外,還應包括服務的發現與注銷、提供服務的多台 Server 的負載均衡、服務的高可用等更多的功能。目前的 RPC 框架大致有兩種不同的側重方向,一種偏重於服務治理,另一種偏重於跨語言調用。
服務治理型的 RPC 框架有 Dubbo、DubboX、Motan 等,這類的 RPC 框架的特點是功能豐富,提供高性能的遠程調用以及服務發現及治理功能,適用於大型服務的微服務化拆分以及管理,對於特定語言(Java)的項目可以十分友好的透明化接入。但缺點是語言耦合度較高,跨語言支持難度較大。
跨語言調用型的 RPC 框架有 Thrift、gRPC、Hessian、Hprose 等,這一類的 RPC 框架重點關注於服務的跨語言調用,能夠支持大部分的語言進行語言無關的調用,非常適合於為不同語言提供通用遠程服務的場景。但這類框架沒有服務發現相關機制,實際使用時一般需要代理層進行請求轉發和負載均衡策略控制。
下面是一個基於HTTP的 JSON的 RPC:
HelloWorld
GO語言標准庫net/rpc也提供了一個簡單的RPC實現
分別建立兩個項目client、server
server.go
package main
import (
"fmt"
"net"
"net/rpc"
)
type HelloService struct {}
// Hello的邏輯 就是將對方發送的消息前面添加一個Hello 然后返還給對方
// 由於是一個rpc服務, 因此參數上面還是有約束:
// 第一個參數是請求
// 第二個參數是響應
// 可以類比Http handler
func (p *HelloService) Hello(request string, reply *string) error {
*reply = "hello:" + request
return nil
}
func main() {
// 把對象注冊成一個rpc的 receiver
// 其中rpc.Register函數調用會將對象類型中所有滿足RPC規則的對象方法注冊為RPC函數,
// 所有注冊的方法會放在“HelloService”服務空間之下
rpc.RegisterName("HelloService", new(HelloService))
// 然后建立一個唯一的TCP鏈接,
listener, err := net.Listen("tcp", ":1234")
if err != nil {
log.Fatal("ListenTCP error:", err)
}
// 通過rpc.ServeConn函數在該TCP鏈接上為對方提供RPC服務。
// 沒Accept一個請求,就創建一個goroutie進行處理
for {
conn, err := listener.Accept()
if err != nil {
log.Fatal("Accept error:", err)
}
// 前面都是Tcp的知識, 到這個RPC就接管了,因此 你可以認為 rpc 封裝消息到函數調用的這個邏輯,
// 提升了工作效率, 邏輯比較簡潔
go rpc.ServeConn(conn)
}
}
client.go
func main() {
// 首先是通過rpc.Dial撥號RPC服務, 建立連接
client, err := rpc.Dial("tcp", "localhost:1234")
if err != nil {
log.Fatal("dialing:", err)
}
// 然后通過client.Call調用具體的RPC方法
// 在調用client.Call時:
// 第一個參數是用點號鏈接的RPC服務名字和方法名字,
// 第二個參數是 請求參數
// 第三個是請求響應, 必須是一個指針, 有底層rpc服務幫你賦值
var reply string
err = client.Call("HelloService.Hello", "hello", &reply)
if err != nil {
log.Fatal(err)
}
fmt.Println(reply)
}
測試:
啟動服務端:
go run server.go
啟動客戶端:
go run client.go
>>>>>>>>>>>>>>>>>
輸出結果:
hello wrold!
rpc服務最多的優點就是可以像使用本地函數一樣使用 遠程服務上的函數, 因此有幾個關鍵點:
- 遠程連接: 類似於pkg
- 函數名稱: 要表用的函數名稱
- 函數參數: 這個需要符合RPC服務的調用簽名, 及第一個參數是請求,第二個參數是響應
- 函數返回: rpc函數的返回是 連接異常信息, 真正的業務Response不能作為返回值
基於接口的RPC服務
// Call invokes the named function, waits for it to complete, and returns its error status.
func (client *Client) Call(serviceMethod string, args interface{}, reply interface{}) error {
call := <-client.Go(serviceMethod, args, reply, make(chan *Call, 1)).Done
return call.Error
}
上面是client call 方法, 里面3個參數2個interface{}, 你再使用的時候 可能真不知道要傳入什么, 這就好像你寫了一個HTTP的服務, 沒有接口文檔, 容易調用錯誤
為了避免這種情況, 可以對客戶端進行一次封裝, 使用接口當作文檔, 明確參數類型
定義hello service的接口
package service
const HelloServiceName = "HelloService"
type HelloService interface {
Hello(request string, reply *string) error
}
約束服務端:
// 通過接口約束HelloService服務
var _ service.HelloService = (*HelloService)(nil)
封裝客戶端, 讓其滿足HelloService接口約束
// 約束客戶端
var _ service.HelloService = (*HelloServiceClient)(nil)
type HelloServiceClient struct {
*rpc.Client
}
func DialHelloService(network, address string) (*HelloServiceClient, error) {
c, err := rpc.Dial(network, address)
if err != nil {
return nil, err
}
return &HelloServiceClient{Client: c}, nil
}
func (p *HelloServiceClient) Hello(request string, reply *string) error {
return p.Client.Call(service.HelloServiceName+".Hello", request, reply)
}
基於接口約束后的客戶端使用就要容易很多了
func main() {
client, err := DialHelloService("tcp", "localhost:1234")
if err != nil {
log.Fatal("dialing:", err)
}
var reply string
err = client.Hello("hello", &reply)
if err != nil {
log.Fatal(err)
}
fmt.Println(reply)
}
Gob編碼
標准庫的RPC默認采用Go語言特有的gob編碼, 標准庫gob是golang提供的“私有”的編解碼方式,它的效率會比json,xml等更高,特別適合在Go語言程序間傳遞數據
// ServeConn runs the server on a single connection.
// ServeConn blocks, serving the connection until the client hangs up.
// The caller typically invokes ServeConn in a go statement.
// ServeConn uses the gob wire format (see package gob) on the
// connection. To use an alternate codec, use ServeCodec.
// See NewClient's comment for information about concurrent access.
func (server *Server) ServeConn(conn io.ReadWriteCloser) {
buf := bufio.NewWriter(conn)
srv := &gobServerCodec{
rwc: conn,
dec: gob.NewDecoder(conn),
enc: gob.NewEncoder(buf),
encBuf: buf,
}
server.ServeCodec(srv)
}
gob的使用很簡單, 和使用base64編碼理念一樣, 有 Encoder和Decoder
func GobEncode(val interface{}) ([]byte, error) {
buf := bytes.NewBuffer([]byte{})
encoder := gob.NewEncoder(buf)
if err := encoder.Encode(val); err != nil {
return []byte{}, err
}
return buf.Bytes(), nil
}
func GobDecode(data []byte, value interface{}) error {
reader := bytes.NewReader(data)
decoder := gob.NewDecoder(reader)
return decoder.Decode(value)
}
測試用例:
func TestGobCode(t *testing.T) {
t1 := &TestStruct{"name", "value"}
resp, err := service.GobEncode(t1)
fmt.Println(resp, err)
t2 := &TestStruct{}
service.GobDecode(resp, t2)
fmt.Println(t2, err)
}
Json ON TCP
gob是golang提供的“私有”的編解碼方式,因此從其它語言調用Go語言實現的RPC服務將比較困難
因此可以選用所有語言都支持的比較好的一些編碼:
- MessagePack: 高效的二進制序列化格式。它允許在多種語言(如JSON)之間交換數據。但它更快更小
- JSON: 文本編碼
- XML:文本編碼
- Protobuf 二進制編碼
Go語言的RPC框架有兩個比較有特色的設計:
- RPC數據打包時可以通過插件實現自定義的編碼和解碼;
- RPC建立在抽象的io.ReadWriteCloser接口之上的,可以將RPC架設在不同的通訊協議之上。
嘗試通過官方自帶的net/rpc/jsonrpc擴展實現一個跨語言的RPC。
server
func main() {
rpc.RegisterName("HelloService", new(HelloService))
listener, err := net.Listen("tcp", ":1234")
if err != nil {
log.Fatal("ListenTCP error:", err)
}
for {
conn, err := listener.Accept()
if err != nil {
log.Fatal("Accept error:", err)
}
// 代碼中最大的變化是用rpc.ServeCodec函數替代了rpc.ServeConn函數,
// 傳入的參數是針對服務端的json編解碼器
go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
}
}
客戶端
func DialHelloService(network, address string) (*HelloServiceClient, error) {
// 建立鏈接
conn, err := net.Dial("tcp", "localhost:1234")
if err != nil {
log.Fatal("net.Dial:", err)
}
// 采用Json編解碼的客戶端
c := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
return &HelloServiceClient{Client: c}, nil
}
驗證功能是否正常,由於沒有合適的tcp工具, 比如nc, 可以下來自己驗證
echo -e '{"method":"HelloService.Hello","params":["hello"],"id":1}' | nc localhost 1234
{"id":1,"result":"hello:hello","error":null}
Json ON HTTP
Go語言內在的RPC框架已經支持在Http協議上提供RPC服務, 為了支持跨語言,編碼依然使用Json
新的RPC服務其實是一個類似REST規范的接口,接收請求並采用相應處理流程
首先依然要解決JSON編解碼的問題,需要將HTTP接口的Handler參數傳遞給jsonrpc, 因此需要滿足jsonrpc接口, 因此需要提前構建也給conn io.ReadWriteCloser, writer現成的 reader就是request的body, 直接內嵌就可以
func NewRPCReadWriteCloserFromHTTP(w http.ResponseWriter, r *http.Request) *RPCReadWriteCloser {
return &RPCReadWriteCloser{w, r.Body}
}
type RPCReadWriteCloser struct {
io.Writer
io.ReadCloser
}
服務端:
func main() {
rpc.RegisterName("HelloService", new(HelloService))
// RPC的服務架設在“/jsonrpc”路徑,
// 在處理函數中基於http.ResponseWriter和http.Request類型的參數構造一個io.ReadWriteCloser類型的conn通道。
// 然后基於conn構建針對服務端的json編碼解碼器。
// 最后通過rpc.ServeRequest函數為每次請求處理一次RPC方法調用
http.HandleFunc("/jsonrpc", func(w http.ResponseWriter, r *http.Request) {
conn := NewRPCReadWriteCloserFromHTTP(w, r)
rpc.ServeRequest(jsonrpc.NewServerCodec(conn))
})
http.ListenAndServe(":1234", nil)
}
這種用法常見於你的rpc服務需要暴露多種協議的時候, 其他時候還是老老實實寫Restful API
參考:什么是RPC