跳转至

使用golang进行rpc开发之三使用grpc进行开发

前两篇文章介绍了在golang中使用原生的rpc和使用json做为序列化工具进行rpc开发,但是它们都有着各自的缺点,原生的rpc不能跨语言,json在序列化和反序列化时效率又不高,所以本文我们再来学习一下grpc的使用,它完美的解决了它们的缺点

  1. 安装protoc

https://github.com/protocolbuffers/protobuf/releases 将下载的可执行文件放到对应系统的环境变量中,windows 中可以将protoc 目录添加到环境变量,mac 中可以将protoc 文件放到 /usr/local/bin 目录下,输入 protoc --version 来验证一下是否执行成功。

  1. 安装对应语言的插件, golang 中为

    go install github.com/golang/protobuf/protoc-gen-go@latest
    
    安装成功之后会在$GOPATH/bin/目录下生成一个protoc-gen-go 文件

  2. 编写 proto 文件

proto 文件与go 中的结构体一一对应,如果我们在go 中定义一个User的结构体

1
2
3
4
5
type User struct{
    Name string
    Age int
    Frends []string
}

那么我们可以在对应的proto文件中定义相应的message

1
2
3
4
5
6
7
8
syntax = "proto3";
option go_package = "./;user";

message userinfo{
    string name = 1;
    int32 age = 2;
    repeated string frends =3;
}

切换到proto文件所在的目录中,然后使用 protoc --go_out=./ *.proto来生成对应的文件,

这里几个注意的问题

  • proto 中定义message 时,我们即使用了小写字母,使用protoc 生成的go文件也会变成大写,如上面的userinfo 会变为Userinfo, name=>Name, age=>Age, frends=>Frends
  • 第一行 syntax = "proto3";为固定写法,指定proto的版本,目前基本上都是用的3版本。
  • 生成的文件以proto文件的名称加上.pb.go,如上面的proto文件为user.proto, 生成的go 文件名为user.pb.go, 包名为 go_package中指定的包名。

  • 生成的go 文件有什么用?

生成的go文件其实就是定义了一些结构体,与生成一些Geter方法,目前来看,还没有任何接口方法,那是因为我们并未在proto中定义 userinfo 需要实现的方法。

proto 的序列化与反序列化,通过 proto.Marshalproto.Unmarshal可以将proto文件生成的Userinfo 结构体进行序列化与反序列化

package main

import (
    "fmt"
    "server/proto/userinfo"

    "google.golang.org/protobuf/proto"
)

type User struct {
    Name   string
    Age    int
    Frends []string
}

func main() {
    var u = &userinfo.Userinfo{
        Name:   "yyx",
        Age:    18,
        Frends: []string{"aaa", "bbb"},
    }
    // 序列化
    data, err := proto.Marshal(u)
    if err != nil {
        fmt.Println(err.Error())
    } else {
        fmt.Println(data) // []byte 二进制数据
    }

    // 反序列化
    var user userinfo.Userinfo
    err = proto.Unmarshal(data, &user)
    if err != nil {
        fmt.Println(err.Error())
    } else {
        fmt.Println(&user)
    }
}

message 可嵌套 message

message userinfo{
    string name = 1;
    int32 age = 2;
    repeated string frends =3;
}

// 结构体嵌套, 某个message 可以嵌套别的结构体
message Oderinfo{
    int32 id =1;
    string address =2;
    userinfo userinfo=3;
}

定义rpc 服务

1
2
3
4
//  定义rpc 服务
service UserService{
    rpc GetUserInfosss(userid) returns (userinfo);
}

这时的编译命令和之前的不一样了,如果还是用之前的编译命令,则不会生成rpc 服务的接口, 需要添加上plugins 参数

protoc  --go_out=plugins=grpc:./proto ./proto/*.proto

生成的pb.go 文件里默认生成了服务端与客户端的接口定义

1
2
3
4
5
6
7
type UserServiceClient interface {
    GetUserInfosss(ctx context.Context, in *Userid, opts ...grpc.CallOption) (*Userinfo, error)
}

type UserServiceServer interface {
    GetUserInfosss(context.Context, *Userid) (*Userinfo, error)
}
还有两个重要的方法, 后面创建rpc 服务提供了方便的调用
1
2
3
4
5
6
7
8
9
// 生成客户端实例
func NewUserServiceClient(cc grpc.ClientConnInterface) UserServiceClient {
    return &userServiceClient{cc}
}

// 注册服务端
func RegisterUserServiceServer(s *grpc.Server, srv UserServiceServer) {
    s.RegisterService(&_UserService_serviceDesc, srv)
}

官方greeter 实例的实现

proto 文件

syntax = "proto3";
option go_package = "./pb;";

service Greeter {
    rpc SayHello(HelloReq) returns (HelloRes);
}

message HelloReq{
    string name = 1;
}

message HelloRes{
    string message=1;
}

定义一个服务,里面只有一个rpc 方法

服务端

package main

import (
    "context"
    "fmt"
    "log"
    "net"
    "server/proto/pb"

    "google.golang.org/grpc"
)

type hello struct{}

// 定义 SayHello的实现
func (h hello) SayHello(ctx context.Context, in *pb.HelloReq) (*pb.HelloRes, error) {
    fmt.Println(in)
    res := &pb.HelloRes{
        Message: "Hello " + in.Name,
    }
    return res, nil
}

func main() {
    // 1. 初始化grpc 对象
    server := grpc.NewServer()
    // 2. 注册服务
    pb.RegisterGreeterServer(server, &hello{})
    // 3. 设置监听
    listener, err := net.Listen("tcp", "127.0.0.1:8080")
    if err != nil {
        log.Fatal(err.Error())
    }
    // 启动服务
    server.Serve(listener)
}

客户端

package main

import (
    "client/proto/pb"
    "context"
    "fmt"
    "log"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
)

func main() {
    // 与grpc 服务进行连接
    client, err := grpc.Dial("127.0.0.1:8080", grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        log.Fatal(err)
    }
    // 初始化client
    gclient := pb.NewGreeterClient(client)

    // 进行调用
    res, err := gclient.SayHello(context.Background(), &pb.HelloReq{
        Name: "杨彦星",
    })
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(res.Message)

}

思考几个问题

  1. 服务端可不可以有多个服务? 如上面的greeter, 如果还有一个order 服务,该怎样集成在一起? 客户端该怎么调用?
  2. 调用过程中如果tcp 断开了会如何? 或者说两次请求,是每次都创建新的tcp 连接吗?

服务端

package main

import (
    "context"
    "fmt"
    "log"
    "net"
    "server/proto/pb"

    "google.golang.org/grpc"
)

type hello struct{}

// 定SayHello的实现
func (h hello) SayHello(ctx context.Context, in *pb.HelloReq) (*pb.HelloRes, error) {
    fmt.Println(in)
    res := &pb.HelloRes{
        Message: "Hello " + in.Name,
    }
    return res, nil
}

type user struct{}

func (u user) GetUserInfosss(ctx context.Context, in *pb.Userid) (*pb.Userinfo, error) {
    fmt.Println(in)
    res := pb.Userinfo{
        Name:   "yangyanxing",
        Age:    in.Id,
        Frends: []string{"fjy", "zhangsan"},
    }
    return &res, nil
}

func main() {
    // 1. 初始化grpc 对象
    server := grpc.NewServer()
    // 2. 注册服务
    pb.RegisterGreeterServer(server, &hello{})
    pb.RegisterUserServiceServer(server, &user{})

    // 3. 设置监听
    listener, err := net.Listen("tcp", "127.0.0.1:8080")
    if err != nil {
        log.Fatal(err.Error())
    }
    // 启动服务
    server.Serve(listener)
}

客户端

package main

import (
    "client/proto/pb"
    "context"
    "fmt"
    "log"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
)

func main() {
    // 与grpc 服务进行连接
    client, err := grpc.Dial("127.0.0.1:8080", grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        log.Fatal(err)
    }
    // 初始化client
    gclient := pb.NewGreeterClient(client)
    uclient := pb.NewUserServiceClient(client)

    // 进行调用
    res, err := gclient.SayHello(context.Background(), &pb.HelloReq{
        Name: "杨彦星",
    })
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(res.Message)
    // 调用 user相关的rpc方法
    result, err := uclient.GetUserInfosss(context.Background(), &pb.Userid{Id: 1})
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%#v", result)
}