RPC(Remote procedure Call)是分散式系統中不同節點之間廣泛使用的通訊方式,是網路時代的基礎技術。 Go的標準函式庫在net/rpc套件下提供了RPC的簡單實作。本文旨在透過引導您使用 net/rpc 套件實作一個簡單的 RPC 介面來幫助您了解 RPC。
本文首刊於Medium MPP計畫。如果您是 Medium 用戶,請在 Medium 上關注我。非常感謝。
要使net/rpc中的函數能夠被遠端調用,必須滿足以下五個條件:
- 方法的型別已匯出。
- 方法已匯出。
- 此方法有兩個參數,兩者都是導出(或內建)類型。
- 此方法的第二個參數是一個指標。
- 該方法的回傳類型為錯誤。
換句話說,函數簽章必須是:
func (t *T) MethodName(argType T1, replyType *T2) error
基於這五個條件,我們可以建構一個簡單的RPC介面:
type HelloService struct{} func (p *HelloService) Hello(request string, reply *string) error { log.Println("HelloService Hello") *reply = "hello:" + request return nil }
接下來,您可以將HelloService類型的物件註冊為RPC服務:
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) } go rpc.ServeConn(conn) } }
客戶端實作如下:
func main() { conn, err := net.Dial("tcp", ":1234") if err != nil { log.Fatal("net.Dial:", err) } client := rpc.NewClient(conn) var reply string err = client.Call("HelloService.Hello", "hello", &reply) if err != nil { log.Fatal(err) } fmt.Println(reply) }
首先,客戶端使用 rpc.Dial 撥打 RPC 服務,然後透過 client.Call() 呼叫特定的 RPC 方法。第一個參數是RPC服務名稱和方法名稱用點組合起來,第二個是輸入,第三個是回傳值,是一個指標。這個例子展示了使用 RPC 是多麼容易。
在伺服器和客戶端程式碼中,我們都需要記住 RPC 服務名稱 HelloService 和方法名稱 Hello。這很容易導致開發過程中出現錯誤,因此我們可以透過抽象化公共部分來稍微包裝程式碼。完整程式碼如下:
// server.go const ServerName = "HelloService" type HelloServiceInterface = interface { Hello(request string, reply *string) error } func RegisterHelloService(srv HelloServiceInterface) error { return rpc.RegisterName(ServerName, srv) } type HelloService struct{} func (p *HelloService) Hello(request string, reply *string) error { log.Println("HelloService Hello") *reply = "hello:" + request return nil } func main() { _ = RegisterHelloService(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) } go rpc.ServeConn(conn) } }
// client.go type HelloServiceClient struct { *rpc.Client } var _ HelloServiceInterface = (*HelloServiceClient)(nil) const ServerName = "HelloService" func DialHelloService(network, address string) (*HelloServiceClient, error) { conn, err := net.Dial(network, address) client := rpc.NewClient(conn) if err != nil { return nil, err } return &HelloServiceClient{Client: client}, nil } func (p *HelloServiceClient) Hello(request string, reply *string) error { return p.Client.Call(ServerName+".Hello", request, reply) } func main() { client, err := DialHelloService("tcp", "localhost:1234") if err != nil { log.Fatal("net.Dial:", err) } var reply string err = client.Hello("hello", &reply) if err != nil { log.Fatal(err) } fmt.Println(reply) }
是不是很眼熟?
預設情況下,Go 的標準 RPC 函式庫使用 Go 專有的 Gob 編碼。但是,在其之上實現其他編碼(例如 Protobuf 或 JSON)非常簡單。標準函式庫已經支援jsonrpc編碼,我們可以透過對服務端和客戶端程式碼進行小改動來實作JSON編碼。
// server.go 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) } go rpc.ServeCodec(jsonrpc.NewServerCodec(conn)) //go rpc.ServeConn(conn) } } //client.go func DialHelloService(network, address string) (*HelloServiceClient, error) { conn, err := net.Dial(network, address) //client := rpc.NewClient(conn) client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn)) if err != nil { return nil, err } return &HelloServiceClient{Client: client}, nil }
JSON請求資料物件內部對應兩種結構:在客戶端,是clientRequest,在伺服器端,是serverRequest。 clientRequest 和 serverRequest 結構體的內容本質上是相同的:
type clientRequest struct { Method string `json:"method"` Params [1]any `json:"params"` Id uint64 `json:"id"` } type serverRequest struct { Method string `json:"method"` Params *json.RawMessage `json:"params"` Id *json.RawMessage `json:"id"` }
這裡的Method表示由serviceName和Method組成的服務名稱。 Params第一個元素是參數,Id是呼叫者維護的唯一呼叫號,用來區分並發場景下的請求。
我們可以使用 nc 來模擬伺服器,然後運行客戶端程式碼來查看 JSON 編碼的客戶端向伺服器發送了哪些資訊:
nc -l 1234
nc 指令接收以下資料:
{"method":"HelloService.Hello","params":["hello"],"id":0}
這與serverRequest一致。
我們也可以運行伺服器程式碼並使用 nc 發送請求:
echo -e '{"method":"HelloService.Hello","params":["Hello"],"Id":1}' | nc localhost 1234 --- {"id":1,"result":"hello:Hello","error":null}
本文介紹了Go標準庫中的rpc包,強調了它的簡單性和強大的性能。許多第三方 rpc 函式庫都是建構在 rpc 套件之上的。本文是 RPC 研究系列的第一部分。在下一篇文章中,我們將把protobuf與RPC結合起來,最終實現我們自己的RPC框架。
以上是RPC Action EP在Go中實作一個簡單的RPC接口的詳細內容。更多資訊請關注PHP中文網其他相關文章!