RPC(Remote Procedure Call,遠程過程調用)是一種計算機通信協議,允許程序調用另一臺計算機上的子程序,就像調用本地程序一樣。Go 語言內置了 RPC 支持,下面我會詳細介紹如何使用。
一、基本概念
在 Go 中,RPC 主要通過 net/rpc
包實現,它使用 Gob 編碼進行數據傳輸。Go 還提供了 net/rpc/jsonrpc
包,支持 JSON 編碼的 RPC。
二、最簡單的 RPC 示例
1. 定義服務
首先需要定義一個服務類型及其方法:
package mainimport ("errors""log""net""net/rpc"
)// 定義服務結構體
type Arith struct{}// 定義服務方法
// 注意:方法必須滿足以下條件:
// 1. 方法是導出的(首字母大寫)
// 2. 有兩個參數,都是導出類型或內建類型
// 3. 第二個參數是指針
// 4. 返回 error 類型
func (t *Arith) Multiply(args *Args, reply *int) error {*reply = args.A * args.Breturn nil
}func (t *Arith) Divide(args *Args, quo *Quotient) error {if args.B == 0 {return errors.New("divide by zero")}quo.Quo = args.A / args.Bquo.Rem = args.A % args.Breturn nil
}// 定義參數結構體
type Args struct {A, B int
}// 定義返回結構體
type Quotient struct {Quo, Rem int
}
2. 啟動 RPC 服務器
func main() {// 創建服務實例arith := new(Arith)// 注冊服務rpc.Register(arith)// 注冊服務到HTTP處理器(可選)// rpc.HandleHTTP()// 監聽TCP連接l, err := net.Listen("tcp", ":1234")if err != nil {log.Fatal("listen error:", err)}// 開始接受連接for {conn, err := l.Accept()if err != nil {log.Fatal("accept error:", err)}// 為每個連接創建goroutine處理go rpc.ServeConn(conn)}// 如果使用HTTP,可以這樣啟動:// http.ListenAndServe(":1234", nil)
}
3. 創建 RPC 客戶端
package mainimport ("log""net/rpc"
)// 定義參數結構體
type Args struct {A, B int
}// 定義返回結構體
type Quotient struct {Quo, Rem int
}func main() {// 連接RPC服務器client, err := rpc.Dial("tcp", "localhost:1234")if err != nil {log.Fatal("dialing:", err)}// 同步調用args := &Args{7, 8}var reply interr = client.Call("Arith.Multiply", args, &reply)if err != nil {log.Fatal("arith error:", err)}log.Printf("Arith: %d*%d=%d", args.A, args.B, reply)// 異步調用quotient := new(Quotient)divCall := client.Go("Arith.Divide", args, quotient, nil)replyCall := <-divCall.Done // 等待完成if replyCall.Error != nil {log.Fatal("arith error:", replyCall.Error)}log.Printf("Arith: %d/%d=%d...%d", args.A, args.B, quotient.Quo, quotient.Rem)
}
三、JSON-RPC 示例
如果你想使用 JSON 編碼而不是 Gob 編碼:
服務器端
func main() {arith := new(Arith)rpc.Register(arith)l, err := net.Listen("tcp", ":1234")if err != nil {log.Fatal("listen error:", err)}for {conn, err := l.Accept()if err != nil {log.Fatal("accept error:", err)}// 使用JSON編碼go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))}
}
客戶端
func main() {conn, err := net.Dial("tcp", "localhost:1234")if err != nil {log.Fatal("dial error:", err)}client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))args := &Args{7, 8}var reply interr = client.Call("Arith.Multiply", args, &reply)if err != nil {log.Fatal("arith error:", err)}log.Printf("Arith: %d*%d=%d", args.A, args.B, reply)
}
四、HTTP 上的 RPC
服務器端
func main() {arith := new(Arith)rpc.Register(arith)rpc.HandleHTTP()err := http.ListenAndServe(":1234", nil)if err != nil {log.Fatal("listen error:", err)}
}
客戶端
func main() {client, err := rpc.DialHTTP("tcp", "localhost:1234")if err != nil {log.Fatal("dialing:", err)}args := &Args{7, 8}var reply interr = client.Call("Arith.Multiply", args, &reply)if err != nil {log.Fatal("arith error:", err)}log.Printf("Arith: %d*%d=%d", args.A, args.B, reply)
}
五、更現代的 gRPC
Go 的標準 RPC 包功能有限,Google 開發的 gRPC 是更現代的 RPC 框架:
1. 安裝 gRPC
go get -u google.golang.org/grpc
go get -u github.com/golang/protobuf/protoc-gen-go
2. 定義 proto 文件
創建 hello.proto
:
syntax = "proto3";package hello;service Greeter {rpc SayHello (HelloRequest) returns (HelloReply) {}
}message HelloRequest {string name = 1;
}message HelloReply {string message = 1;
}
3. 生成代碼
protoc --go_out=plugins=grpc:. hello.proto
4. 實現服務端
package mainimport ("context""log""net""google.golang.org/grpc"pb "path/to/your/package" // 替換為你的包路徑
)type server struct{}func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}func main() {lis, err := net.Listen("tcp", ":50051")if err != nil {log.Fatalf("failed to listen: %v", err)}s := grpc.NewServer()pb.RegisterGreeterServer(s, &server{})if err := s.Serve(lis); err != nil {log.Fatalf("failed to serve: %v", err)}
}
5. 實現客戶端
package mainimport ("context""log""os""time""google.golang.org/grpc"pb "path/to/your/package" // 替換為你的包路徑
)func main() {conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())if err != nil {log.Fatalf("did not connect: %v", err)}defer conn.Close()c := pb.NewGreeterClient(conn)name := "world"if len(os.Args) > 1 {name = os.Args[1]}ctx, cancel := context.WithTimeout(context.Background(), time.Second)defer cancel()r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})if err != nil {log.Fatalf("could not greet: %v", err)}log.Printf("Greeting: %s", r.Message)
}
六、選擇建議
- 標準庫 RPC:簡單、輕量,適合內部服務通信
- JSON-RPC:需要跨語言通信時使用
- gRPC:現代、高性能、支持多種語言,適合生產環境
七、常見問題
- 方法不滿足要求:確保方法簽名符合要求(兩個參數,第二個是指針,返回 error)
- 連接問題:檢查服務器是否啟動,端口是否正確
在Go RPC客戶端中設置超時時間
在Go語言的net/rpc
包中,客戶端默認沒有直接提供設置超時時間的接口,但可以通過以下幾種方式實現超時控制:
1. 使用net.DialTimeout
創建連接
在創建RPC客戶端連接時,可以使用net.DialTimeout
代替net.Dial
來設置連接超時:
func createClientWithTimeout() (*rpc.Client, error) {// 設置連接超時時間為5秒conn, err := net.DialTimeout("tcp", "localhost:1234", 5*time.Second)if err != nil {return nil, err}// 對于普通RPCclient := rpc.NewClient(conn)// 對于JSON-RPC// client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))return client, nil
}
2. 使用context
實現調用超時
對于RPC調用本身的超時控制,可以使用context
包:
func callWithTimeout(client *rpc.Client) {ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)defer cancel()args := &Args{7, 8}var reply int// 使用channel來接收結果ch := make(chan error, 1)go func() {ch <- client.Call("Arith.Multiply", args, &reply)}()select {case <-ctx.Done():fmt.Println("RPC調用超時:", ctx.Err())// 這里可以添加清理邏輯case err := <-ch:if err != nil {fmt.Println("RPC調用錯誤:", err)return}fmt.Printf("結果: %d\n", reply)}
}
3. 使用time.After
實現超時
如果不使用context,也可以使用time.After
實現類似的超時控制:
func callWithTimeoutAlt(client *rpc.Client) {args := &Args{7, 8}var reply intdone := make(chan error, 1)go func() {done <- client.Call("Arith.Multiply", args, &reply)}()select {case <-time.After(3 * time.Second):fmt.Println("RPC調用超時")case err := <-done:if err != nil {fmt.Println("RPC調用錯誤:", err)return}fmt.Printf("結果: %d\n", reply)}
}
4. 對于HTTP RPC的超時設置
如果使用HTTP作為傳輸協議,可以設置http.Client
的超時:
func createHTTPClientWithTimeout() (*rpc.Client, error) {// 創建自定義HTTP客戶端并設置超時httpClient := &http.Client{Timeout: 5 * time.Second,}// 使用自定義HTTP客戶端創建RPC連接client, err := rpc.DialHTTPWithClient("tcp", "localhost:1234", httpClient)if err != nil {return nil, err}return client, nil
}
最佳實踐
- 同時設置連接超時和調用超時:連接超時和調用超時針對不同階段的問題
- 合理設置超時時間:根據網絡環境和業務需求設置合適的超時時間
- 超時后清理資源:確保超時后關閉連接或取消操作
- 記錄超時日志:記錄超時事件以便分析和優化
完整示例
package mainimport ("context""fmt""net""net/rpc""time"
)func main() {// 創建帶超時的客戶端client, err := createClientWithTimeout()if err != nil {fmt.Println("創建客戶端失敗:", err)return}defer client.Close()// 帶超時的RPC調用callWithTimeout(client)
}func createClientWithTimeout() (*rpc.Client, error) {// 5秒連接超時conn, err := net.DialTimeout("tcp", "localhost:1234", 5*time.Second)if err != nil {return nil, err}return rpc.NewClient(conn), nil
}func callWithTimeout(client *rpc.Client) {ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)defer cancel()args := &Args{7, 8}var reply intch := make(chan error, 1)go func() {ch <- client.Call("Arith.Multiply", args, &reply)}()select {case <-ctx.Done():fmt.Println("RPC調用超時:", ctx.Err())case err := <-ch:if err != nil {fmt.Println("RPC調用錯誤:", err)return}fmt.Printf("結果: %d\n", reply)}
}type Args struct {A, B int
}
通過以上方法,你可以有效地控制RPC客戶端的超時行為,提高系統的健壯性和可靠性。