Golang在云原生时代是越来越火爆了, 而gRPC作为嫡系的RPC框架在微服务上应用也越来越多,那用Golang怎么用它实现远程调用呢?
-
确定你要使用的编程语言和开发工具。
-
安装GRPC库和其他所需的依赖库。
-
创建一个GRPC服务接口文件(.proto),定义你的服务所需的方法和请求/响应消息格式(按照proto的语法)。
-
使用GRPC提供的代码生成工具生成服务端和客户端的代码。
-
在服务端实现服务接口中定义的方法,并启动服务端。
-
在客户端中调用服务端的方法,并处理响应消息。
-
测试你的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中,服务端可以按照以下步骤操作:
- 首先,需要
go get安装etcd的gRPC client库,并在gRPC service端引入该库。 - 然后,在gRPC service启动时,创建一个etcd client实例,连接到etcd服务。
- 使用etcd client提供的接口,将gRPC service的ip和端口作为key-value存储到etcd中。
- 在gRPC service的主循环中,定期更新key-value来维护service的可用性。
- 当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地址。