快速入门gRPC(适合有Golang基础的人)

257 阅读6分钟

Golang在云原生时代是越来越火爆了, 而gRPC作为嫡系的RPC框架在微服务上应用也越来越多,那用Golang怎么用它实现远程调用呢?

  1. 确定你要使用的编程语言和开发工具。

  2. 安装GRPC库和其他所需的依赖库。

  3. 创建一个GRPC服务接口文件(.proto),定义你的服务所需的方法和请求/响应消息格式(按照proto的语法)。

  4. 使用GRPC提供的代码生成工具生成服务端和客户端的代码。

  5. 在服务端实现服务接口中定义的方法,并启动服务端。

  6. 在客户端中调用服务端的方法,并处理响应消息。

  7. 测试你的GRPC程序是否能正常工作。

怎么创建GRPC服务的接口文件呢

创建GRPC服务接口文件需要使用protobuf。

例如,你可以创建一个名为service.proto的文件,内容如下:

syntax = "proto3";

option go_package = "./pb";
package pb;

service MyService {
    rpc MyMethod(MyRequest) returns (MyResponse) {}
}

message MyRequest {
    string message = 1;
}

message MyResponse {
    string response = 1;
}

这个文件定义了一个名为MyService的GRPC服务,它包含一个名为MyMethod的方法,请求消息类型为MyRequest,响应消息类型为MyResponse

然后,你可以使用GRPC提供的代码生成工具生成服务端和客户端的代码(服务端和客户端要分别生成)。

例如,如果你使用的是Golang,可以使用以下命令生成代码:

protoc --go_out=./pb --go_opt=paths=source_relative --go-grpc_out=./pb --go-grpc_opt=paths=source_relative service.proto

这样,在./pb文件下会生成service_grpc.pb.go 和service.pb.go

生成完golang代码后, 怎么实现服务端的功能

在生成的文件中找到服务端代码。

例如,如果你的服务接口文件名为service.proto,生成的文件名为service.pb.go,那么你可以在这个文件中找到我们需要的服务端代码。

// MyServiceServer is the server API for MyService service.
// All implementations must embed UnimplementedMyServiceServer
// for forward compatibility
type MyServiceServer interface {
	MyMethod(context.Context, *MyRequest) (*MyResponse, error)
	mustEmbedUnimplementedMyServiceServer()
}

// UnimplementedMyServiceServer must be embedded to have forward compatible implementations.
type UnimplementedMyServiceServer struct {
}

func (UnimplementedMyServiceServer) MyMethod(context.Context, *MyRequest) (*MyResponse, error) {
	return nil, status.Errorf(codes.Unimplemented, "method MyMethod not implemented")
}
func (UnimplementedMyServiceServer) mustEmbedUnimplementedMyServiceServer() {}

下面是一个简单的服务端实现例子:

// 服务端代码service_main.go
package main

import (
    "context"
    "log"
    "net"

    "xxxx/pb"
    // xxxx是你创建项目时go mod init xxxx的名字
    "google.golang.org/grpc"
)

type MyServiceServer struct{
    // 必须嵌入pb.UnimplementedMyServiceServer以保持向前兼容, 具体名称参考service.pb.go里的代码
    // 如果没有会报错提示有个接口方法你没有实现
    pb.UnimplementedMyServiceServer
}

func (s *MyServiceServer) MyMethod(ctx context.Context, req *pb.MyRequest) (*pb.MyResponse, error) {
    log.Printf("Received request: %v", req.Message)
    return &pb.MyResponse{Response: "Hello " + req.Message}, nil
}

func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("Failed to listen: %v", err)
    }

    s := grpc.NewServer()
    pb.RegisterMyServiceServer(s, &MyServiceServer{})

    if err := s.Serve(lis); err != nil {
        log.Fatalf("Failed to serve: %v", err)
    }
}

这个例子实现了一个GRPC服务端,并实现了MyMethod方法,它接收一个MyRequest类型的请求消息,并返回一个MyResponse类型的响应消息。

客户端怎么实现

在生成的文件中找到客户端代码。(没错,客户端和服务端用同一个proto也用同样的protoc生成的代码文件)

例如,如果你的服务接口文件名为service.proto,生成的文件名为service.pb.go,那么你可以在这个文件中找到我们需要的客户端代码。

type MyServiceClient interface {
	MyMethod(ctx context.Context, in *MyRequest, opts ...grpc.CallOption) (*MyResponse, error)
}

type myServiceClient struct {
	cc grpc.ClientConnInterface
}

func NewMyServiceClient(cc grpc.ClientConnInterface) MyServiceClient {
	return &myServiceClient{cc}
}

func (c *myServiceClient) MyMethod(ctx context.Context, in *MyRequest, opts ...grpc.CallOption) (*MyResponse, error) {
	out := new(MyResponse)
	err := c.cc.Invoke(ctx, "/pb.MyService/MyMethod", in, out, opts...)
	if err != nil {
		return nil, err
	}
	return out, nil
}

下面是一个简单的客户端实现例子:

// 客户端代码client_main.go
package main

import (
    "context"
    "log"

    "xxxx/pb"
    // xxxx是你创建项目时go mod init xxxx的名字
    "google.golang.org/grpc"
)

func main() {
    conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
    if err != nil {
        log.Fatalf("Failed to connect: %v", err)
    }
    defer conn.Close()

    client := pb.NewMyServiceClient(conn)

    response, err := client.MyMethod(context.Background(), &pb.MyRequest{Message: "world"})
    if err != nil {
        log.Fatalf("Failed to call method: %v", err)
    }

    log.Printf("Got response: %v", response.Response)
}

这个例子实现了一个GRPC客户端,并调用了服务端的MyMethod方法,并处理响应消息。

以上客户端和服务端例子是使用明文传输, 但是,强烈建议不要禁用安全传输,因为这样会使你的网络通信暴露在安全风险之下。

让我们试验一下: 一定要先启动服务端 go run service_main.go 再运行客户端 go run client_main.go 结果如下:

服务端:

2022/12/12 19:07:28 Received request: world

客户端:

2022/12/12 19:07:28 Got response: Hello world

那我要是想启用安全传输呢

如果你想启用安全传输(TLS/SSL),你需要在客户端和服务端都使用安全传输选项。

在客户端,你可以使用以下代码连接到服务端:

creds, _ := credentials.NewClientTLSFromFile(certFile, "")
conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(creds))

其中,creds是一个实现了安全传输协议的凭证,例如grpc.WithTLS()grpc.WithCredentialsBundle()

在服务端,你可以使用以下代码启动服务:

creds, _ := credentials.NewServerTLSFromFile(certFile, keyFile)
s := grpc.NewServer(grpc.Creds(creds))

其中,creds是一个实现了安全传输协议的凭证,例如grpc.Creds()grpc.CredsBundle()

这样,客户端和服务端就都启用了安全传输。

如果服务端的IP是通过Etcd服务发现获取的,客户端该怎么改呢

(以下前提都是先在本地环境安装好etcd并已经运行,默认ip是localhost:2379)

如果想要gRPC service端注册ip到etcd中,服务端可以按照以下步骤操作:

  1. 首先,需要go get安装etcd的gRPC client库,并在gRPC service端引入该库。
  2. 然后,在gRPC service启动时,创建一个etcd client实例,连接到etcd服务。
  3. 使用etcd client提供的接口,将gRPC service的ip和端口作为key-value存储到etcd中。
  4. 在gRPC service的主循环中,定期更新key-value来维护service的可用性。
  5. 当gRPC service退出时,使用etcd client提供的接口删除对应的key-value。 由于具体实现比较复杂可以参考这个博客 www.cnblogs.com/ricklz/p/15…

这里我们的服务端的IP地址是通过Etcd服务发现获取的,那么客户端就需要先连接到Etcd服务器,并从中获取服务端的IP地址。

例如,你可以在客户端中使用以下代码实现这个功能:

// 连接到Etcd服务器
client, err := clientv3.New(clientv3.Config{
    Endpoints: []string{"localhost:2379"},
})
if err != nil {
    log.Fatalf("Failed to connect to etcd server: %v", err)
}
defer client.Close()

// 获取服务端的IP地址
response, err := client.Get(context.Background(), "/mypackage/MyService/IP", clientv3.WithPrefix())
if err != nil {
    log.Fatalf("Failed to get service IP: %v", err)
}

// 解析服务端的IP地址
var ip string
for _, kv := range response.Kvs {
    ip = string(kv.Value)
    break
}

//

下面是客户端的完整代码,它实现了从Etcd服务器获取服务端的IP地址,然后连接到服务端,并调用服务端的方法:

package main

import (
    "context"
    "log"

    pb "mypackage"
    "google.golang.org/grpc"
    "go.etcd.io/etcd/clientv3"
)

func main() {
    // 连接到Etcd服务器
    etcdClient, err := clientv3.New(clientv3.Config{
            Endpoints: []string{"localhost:2379"},
    })
    if err != nil {
            log.Fatalf("Failed to connect to etcd server: %v", err)
    }
    defer etcdClient.Close()

    // 获取服务端的IP地址
    etcdResponse, err := etcdClient.Get(context.Background(), "/mypackage/MyService/IP", clientv3.WithPrefix())
    if err != nil {
            log.Fatalf("Failed to get service IP: %v", err)
    }

    // 解析服务端的IP地址
    var ip string
    for _, kv := range etcdResponse.Kvs {
            ip = string(kv.Value)
            break
    }

    // 连接到服务端
    conn, err := grpc.Dial(ip, grpc.WithInsecure())
    if err != nil {
        log.Fatalf("Failed to connect to server: %v", err)
    }
    defer conn.Close()

    // 创建客户端
    client := pb.NewMyServiceClient(conn)

    // 调用服务端的方法
    response, err := client.MyMethod(context.Background(), &pb.MyRequest{Message: "world"})
    if err != nil {
        log.Fatalf("Failed to call method: %v", err)
    }

    log.Printf("Got response: %v", response.Response)
}

代码里的"/mypackage/MyService/IP"是什么意思

"/mypackage/MyService/IP"是Etcd存储的一个键。

Etcd是一个分布式键值存储系统(类似Redis),可以将服务端的信息存储在其中。

这个键表示的是一个服务端的信息,例如IP地址。

例如,你可以将服务端的IP地址存储在Etcd中,并使用"/mypackage/MyService/IP"作为键,这样,客户端就可以通过这个键来获取服务端的IP地址。