go之grpc与protoc的结合

1,993 阅读7分钟

我们还是从rpc开始了解,RPC(Remote Procedure Call: 远程过程调用)是一个计算机通信协议,该协议允许运行于一台计算机的程序调用另一个地址空间(通常为一个开放网络的一台计算机)的子程序,而程序员就像调用本地程序一样,无需额外地为这个交互作用编程(无需关注细节)。我们先看俩张图片在大脑中建立一个模糊的映像

image.png

image.png

其实这么说可能还是没有完全的概念,不用担心,我们一步一步的来,一边编码,一边总结,慢慢的学习这个协议的操作使用,我们写一个客户端和服务端的案例,然后再结合图片一起来看


// server.go
package main

import (
	"fmt"
	"log"
	"net"
	"net/rpc"
)



type World struct {
}
// rpc 方法的签名,函数名必须是可导出(首字母大写),第一个字母是入参,第二个字母是出参,出参必须是一个指针,签名必须返回一个error,如果error返回的非空,传出的参数就不会赋值
func (w *World) HelloWorld(in string, out *string) error {
	*out = "hello" + in
	return nil
}
func main() {

        // 我们先注册服务,第一个参数没太多要求,一般按照公司的要求来命名,后面的这个参数就是定义了rpc类型的一个方法
	rpc.RegisterName("hello", new(World))
        // 这步就是获取连接
	l, err := net.Listen("tcp", "127.0.0.1:8088")

	if err != nil {
		log.Fatal("listen err")
	}
	defer l.Close()

	c, err := l.Accept()
	if err != nil {
		log.Fatal("accept err")
	}
	defer c.Close()
	// 启动服务,记得先启动服务
	rpc.ServeConn(c)
}

// client.go
package main

import (
	"fmt"
	"log"
	"net/rpc"
)

func main() {
        // 获取连接,相当于拿到了一个操作的句柄
	c, err := rpc.Dial("tcp", "127.0.0.1:8088")
	if err != nil {
		log.Fatal("dial err")
	}
	var str string
        // 这个地方就是远程的调用,第一个参数:hello.Helloworld中,hello是指服务的名称,HelloWorld是指服务提供的方法,
        // 第二个参数是入参,根据需要参入你的参数,第三个指针就是传出的参数,你调用一个方法,总得是有点什么目的的吧,所以这个参数就是你想拿到的那个数据
        // 还有就是需要去获取返回值error,看调用过程是否发生了不可预知的错误
	err = c.Call("hello.HelloWorld", "libai", &str)
	if err != nil {
		log.Fatal("call err")

	}
        // 拿到返回值后todo
	fmt.Println(str)

}

// 如果没有发生意外的时候会输出hellolibai然后服务端结束程序

这时候是不是对这个远程过程调用有点理解了,这里可以把实现了方法的进程称之为服务端,把去调用服务端的进程称之为客户端,俩个进程之间的相互调动,平时我们都是在一个进程内写一些接口,然后直接处理了,但是现在我们变成了三部分,可以理解为前端是一部分,客户端是一部分,前端去调用客户端,服务端又是一部分,因为客户端要去调用服务端,整个过程连贯起来就是前端不变,但是客户端要能接收前端的请求(gin框架很适合,小巧、灵活、快捷,gin框架接收参数后调用客户端的这些接口,然后客户端就会去调用服务端),服务端收到客户端的请求然后处理响应给客户端,客户端再根据当前服务端的返回处理后返回给前端,这一般就是一个完整的http+rpc请求过程




protoc

这部分大家需要对proto文件进行操作,这个语法相对比较简单写一个简单的proto文件,做一个简单的入门,现在使用的一般都是遵循proto3的语法规则,以“;”分号结尾表示一句代码的结束,文件是以.proto为后缀,详细的语法下来大家可以找一些文档学习一下,可以看一下这个文档,比较详细:proto博客园学习笔记

// hello.proto
// syntax 标识文件遵循什么样的协议
syntax="proto3";

// package 标识使用包名,如果文件使用go_package选项将按照go_package选项命名包名,如果未使用则使用package关键字命名包名
package hello

// 命名存储的包名
option go_package=“hello”

// service rpc returns 都是关键字
// req 是入参,resp是出参
// 经过protoc编译器解析后会生成go的rpc服务
service Hello1 {
    rpc SayHello(Req)returns(Resp);
}


// message是关键字
// 1是标识符号,每个类中都是唯一的,不能重复
// 这个会生成go中Req的机构提
message Req {
   string Name = 1;
}

message Resp {
    string Name = 1;
}

// 文件中还可以定义数组、map、枚举、嵌套的message,总之很灵活,这个我们就不展开讲了,总而言之,言而总之,必须要亲自去试验一下,不然容易忘记


文件有了,我们应该如何使用呢,下面我们就要使用protoc编译器。安装的方法有很多,一般下载压缩好的二进制文件就好:google/protobuf,这个在github上托管,还可以在codeChina去下载,也可以源码安装

  1. protoc --proto_path=$GOPATH/src --proto_path=. --go_out=. ./*.proto
    a. 上面的句编译语句中,--proto_path用于表示要编译的proto文件所依赖的其他proto文件的查找位置,可以使用-I来替代。如果没有指定则从当前目录中查找。

    b. --go_out有两层含义,一层是输出的是go语言对应的文件;一层是指定生成的go文件的存放位置

    c. --go_out=plugins=grpc:helloworld,这里使用了grpc插件。如果proto文件想在rpc中使用,可以在proto中定义接口如下:

    service Hello1 {

    rpc SayHello(Req)returns(Resp);

    }

    helloworld表示生成的文件存放地址。

  2. protoc --go_out=plugins=grpc:. --go_opt=paths=source_relative ./update.proto

    a. --go_opt表示生成go文件时候的目录选项,如上面写时表示生成的文件与proto在同一目录。

  3. import、go_package、package

    a. package主要是用于避免命名冲突的,不同的项目(project)需要指定不同的package。

    b. import,如果proto文件需要使用在其他proto文件中已经定义的结构,可以使用import引入。

    c. option go_package = "hellopb"; go_packge有两层意思,一层是表明如果要引用这个proto生成的文件的时候import后面的路径;一层是如果不指定--go_opt(默认值),生成的go文件存放的路径。

    d. 需要注意的是package和go_package的含义。在官方给的文档中,package和go_package的最后一个单词不一样

grpc

现在了解了编译器的使用我们现在就编译一下这个文件,但是我们编译前开启goproxy安装个库:

go get google.golang.org/grpc

继续安装不过在这儿有个小小的坑,虽然都是protoc-gen-go工具,但是在后期google已经接管,所以我们尽量还是按照goole的工具来

go get github.com/golang/protobuf/protoc-gen-go go get google.golang.org/protobuf/cmd/protoc-gen-go

然后使用下面的命令,这样会生成俩个文件

protoc --go_out=. hello.proto
protoc --go-grpc_out=. hello.proto 可能还有一部分的人看网上的操作是这样的 protoc --go_out=plugins=grpc:. hello.proto

这种生成方式,使用的就是github版本的protoc-gen-go,而目前这个项目已经由Google接管了。如果使用这种生成方式的话,并不会生成上图中的xxx_grpc.pb.goxxx.pb.go两个文件,只会生成xxx.pb.go这种文件。而且还有可能出错,提示使用上面的那种方式.

现在看代码,我在一个proto文件夹下新建hello.proto文件,然后让生成的文件放到pb中,这样就可以在server和client中都可以调用,生成的源码原则上我们不做任何的修改,只是使用,其实也很简单,就是把我们实现的部分现在通过编译生成

.
├── client
│   └── client.go
├── go.mod
├── go.sum
├── proto
│   ├── hello.proto
│   └── pb
│       └── hello.pb.go
└── server
    └── server.go

server.go

package main

import (
	"context"
	"day5/proto/pb"
	"log"
	"net"

	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"
)

type T struct {
}

// 这部分就是proto文件编译后生成的签名格式,我们只需要按照自己的需要实现一下,是不是就像实现一个接口一样
func (t *T) SayHello(ctx context.Context, in *pb.Req) (*pb.Resp, error) {
	return &pb.Resp{Name: "hello" + in.Name}, nil

}

func main() {
	l, err := net.Listen("tcp", ":8000")
	if err != nil {
		log.Fatal("listen err")
	}
         
	s := grpc.NewServer()
        // 这个也是编译器生成的方法,看源码的时候就会发现只是层层代码的封装
	pb.RegisterHello1Server(s, new(T))

	reflection.Register(s)

	err2 := s.Serve(l)
	if err2 != nil {
		log.Fatal("serve err")
	}

}

image.png

client.go

package main

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

	"google.golang.org/grpc"
)

func main() {
	c, err := grpc.Dial(":8000", grpc.WithInsecure())
	if err != nil {
		log.Fatal(err)
	}

	n := pb.NewHello1Client(c)
	r, err2 := n.SayHello(context.Background(), &pb.Req{Name: "libai"})

	if err2 != nil {
		log.Fatal(err2)
	}

	fmt.Println(r)
}

image.png

这个proto生成的源码文件我们知道这些地方基本就可以了,其实在后面的go-micro也基本上就是这样,下面我们一步一步再来学习go-micro 框架




完结