【Go语言】RPC 使用指南(初学者版)

发布于:2025-05-01 ⋅ 阅读:(29) ⋅ 点赞:(0)

RPC(Remote Procedure Call,远程过程调用)是一种计算机通信协议,允许程序调用另一台计算机上的子程序,就像调用本地程序一样。Go 语言内置了 RPC 支持,下面我会详细介绍如何使用。

1

一、基本概念

在 Go 中,RPC 主要通过 net/rpc 包实现,它使用 Gob 编码进行数据传输。Go 还提供了 net/rpc/jsonrpc 包,支持 JSON 编码的 RPC。

二、最简单的 RPC 示例

1. 定义服务

首先需要定义一个服务类型及其方法:

package main

import (
	"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.B
	return 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.B
	quo.Rem = args.A % args.B
	return 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 main

import (
	"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 int
	err = 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 int
	err = 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 int
	err = 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 main

import (
	"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 main

import (
	"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)
}

六、选择建议

  1. 标准库 RPC:简单、轻量,适合内部服务通信
  2. JSON-RPC:需要跨语言通信时使用
  3. gRPC:现代、高性能、支持多种语言,适合生产环境

七、常见问题

  1. 方法不满足要求:确保方法签名符合要求(两个参数,第二个是指针,返回 error)
  2. 连接问题:检查服务器是否启动,端口是否正确

在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
    }
  
    // 对于普通RPC
    client := 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 int
  
    done := 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
}

最佳实践

  1. 同时设置连接超时和调用超时:连接超时和调用超时针对不同阶段的问题
  2. 合理设置超时时间:根据网络环境和业务需求设置合适的超时时间
  3. 超时后清理资源:确保超时后关闭连接或取消操作
  4. 记录超时日志:记录超时事件以便分析和优化

完整示例

package main

import (
    "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 int
  
    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)
    }
}

type Args struct {
    A, B int
}

通过以上方法,你可以有效地控制RPC客户端的超时行为,提高系统的健壮性和可靠性。


网站公告

今日签到

点亮在社区的每一天
去签到