RPC(Remote Procedure Call,远程过程调用)是一种计算机通信协议,允许程序调用另一台计算机上的子程序,就像调用本地程序一样。Go 语言内置了 RPC 支持,下面我会详细介绍如何使用。
一、基本概念
在 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)
}
六、选择建议
- 标准库 RPC:简单、轻量,适合内部服务通信
- JSON-RPC:需要跨语言通信时使用
- gRPC:现代、高性能、支持多种语言,适合生产环境
七、常见问题
- 方法不满足要求:确保方法签名符合要求(两个参数,第二个是指针,返回 error)
- 连接问题:检查服务器是否启动,端口是否正确