golang RPC入門
1. RPC簡介
RPC是遠端系統呼叫的縮寫,通俗地講就是呼叫遠處的一個函式,可能是一個檔案內的不同函式,也可能是一個機器上另一個程序的函式,也可能是遠處機器上的函式。
RPC是分散式系統中不同節點之間的通訊方式,Go的標準庫也實現了一個簡單的RPC。
2. RPC簡單使用
首先構造一個HelloService型別,其中的Hello方法用於實現列印功能,Login實現簡單的使用者驗證
其中RPC方法必須滿足golang的RPC規則:
- 方法只能有兩個可序列化的引數,其中第二個引數是指標型別
- 返回一個error
- 必須是公開的方法,首字母大寫
type HelloService struct { conn net.Conn isLogin bool }// Hello: func (p *HelloService) Hello(request string, reply *string) error { if !p.isLogin { return fmt.Errorf("please login") } *reply = "hello:" + request + ",from" + p.conn.RemoteAddr().String() return nil } // Login: 提供使用者登入驗證 func (p *HelloService) Login(request string, reply *string) error { if request != "user:password" { return fmt.Errorf("auth failed") } log.Println("login ok") p.isLogin = true return nil }
然後我們可以將HelloService型別的物件註冊為一個RPC服務:
func main() { // 開啟監聽 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) } go func() { defer conn.Close() p := rpc.NewServer() // RegisterName呼叫會將物件型別中所有滿足RPC規則的物件方法註冊為RPC函式 // 所有註冊的方法會放在HelloService服務空間之下 p.Register(&HelloService{conn: conn}) // ServeConn函式在conn這個TCP連線上為對方提供RPC服務 p.ServeConn(conn) }() } }
下面是客戶端請求HelloService服務的程式碼:
func main() { // 撥號RPC服務 client, err := rpc.Dial("tcp", "localhost:1234") if err != nil { log.Fatal("Dail error:", err) } // 通過Call()呼叫RPC的具體方法 var reply string err = client.Call("HelloService.Login", "user:password", &reply) if err != nil { log.Fatal(err) } err = client.Call("HelloService.Hello", "client", &reply) if err != nil { log.Fatal(err) } fmt.Println(reply) }
我們在終端開啟server和client,看看會發生什麼:
go run server.go go run client.go ################## hello:client,from[::1]:56769
3. 跨語言的RPC
標準庫的RPC預設採用go語言特有的Gob編碼,因此從其他語言呼叫Go語言實現的RPC服務比較困難。
go語言的RPC框架有兩個比較有特色的設計:一個是RPC資料打包時可以通過外掛實現自定義的編碼和解碼;另一個是RPC建立在抽象的io.ReadWriteCloser介面之上,我們可以將RPC架設在不同的通訊協議之上。
我們利用net/rpc/jsonrpc實現一個跨語言的RPC。
func main() { rpc.RegisterName("HelloService", new(HelloService)) listener, err := net.Listen("tcp", ":1234") if err != nil { log.Fatal(err) } for { conn, err := listener.Accept() if err != nil { log.Fatal(err) } // 使用rpc.ServeCodec代替rpc.ServeConn函式 go rpc.ServeCodec(jsonrpc.NewServerCodec(conn)) } }
func main() { conn, err := net.Dial("tcp", "localhost:1234") if err != nil { log.Fatal(err) } client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn)) var reply string err = client.Call("HelloService.Hello", "hello", &reply) if err != nil { log.Fatal(err) } fmt.Println(reply) }
無論採用什麼樣的語言,只要遵循一致的json對映結構,以同樣的流程就可以實現和go語言編寫的RPC服務進行通訊
4. HTTP上的RPC服務
我們嘗試在HTTP協議上提供jsonrpc服務
新的RPC服務其實就是一個類似於REST規範的介面,接收請求並採用相應的處理流程:
type HelloService struct{} func (p *HelloService) Hello(request string, reply *string) error { *reply = "hello: " + request return nil } func main() { rpc.RegisterName("HelloService", new(HelloService)) http.HandleFunc("/jsonrpc", func(w http.ResponseWriter, r *http.Request) { var conn io.ReadWriteCloser = struct { io.Writer io.ReadCloser }{ ReadCloser: r.Body, Writer: w, } rpc.ServeRequest(jsonrpc.NewServerCodec(conn)) }) http.ListenAndServe(":1234", nil) }
RPC服務架設在/jsonrpc路徑,在處理函式中基於http.ResponseWriter和*http.Request型別的引數構造一個io.ReadWriteCloser型別的conn通道,然後基於conn構建針對伺服器端的json編碼解碼器,最後通過rpc.ServeRequest( )函式為每次請求處理一次RPC方法呼叫。
讓我們啟動RPC服務,並開啟postman,測試我們的RPC服務:
可以清楚的看到我們POST請求的body和response資訊,都是json格式的,其實這兩種格式基本都是固定寫法。
因為在內部都是使用類似的結構體來封裝的:
type clientRequest struct{ Method string `json:"method"` Params []interface{} `json:"params"` Id uint64 `json:"id"` }
type serverResponse struct{ Id *json.RawMessage `json:"id"` Result interface{} `json:"result"` Error interface{} `json:"error"` }