在这篇文章中,主要是跟你介绍一下gRPC
这个东西。git
而后,我会建立一个简单的练习项目,做为gRPC
的Hello World项目。github
在这个项目中,只有很简单的一个RPC函数,用于说明gRPC
的工做方式。golang
此外,我也会跟你分享一下我初次接触gRPC
所遇到的一些坑,主要是在protocol buffer
的proto-gen-go
插件上面。web
在这一节的内容中,我将简单的跟你介绍一下gRPC
这个东西。
RPC
的全称是Remote Procedure Call
,远程过程调用。这是一种协议,是用来屏蔽分布式计算中的各类调用细节,使得你能够像是本地调用同样直接调用一个远程的函数。
而gRPC
又是什么呢?用官方的话来讲:编程
A high-performance, open-source universal RPC frameworkapi
gRPC
是一个高性能的、开源的通用的RPC框架。服务器
在gRPC
中,咱们称调用方为client
,被调用方为server
。
跟其余的RPC
框架同样,gRPC
也是基于”服务定义“的思想。简单的来说,就是咱们经过某种方式来描述一个服务,这种描述方式是语言无关的。在这个”服务定义“的过程当中,咱们描述了咱们提供的服务服务名是什么,有哪些方法能够被调用,这些方法有什么样的入参,有什么样的回参。数据结构
也就是说,在定义好了这些服务、这些方法以后,gRPC
会屏蔽底层的细节,client
只须要直接调用定义好的方法,就能拿到预期的返回结果。对于server
端来讲,还须要实现咱们定义的方法。一样的,gRPC
也会帮咱们屏蔽底层的细节,咱们只须要实现所定义的方法的具体逻辑便可。框架
你能够发现,在上面的描述过程当中,所谓的”服务定义“,就跟定义接口的语义是很接近的。我更愿意理解为这是一种”约定“,双方约定好接口,而后server
实现这个接口,client
调用这个接口的代理对象。至于其余的细节,交给gRPC
。tcp
此外,gRPC
仍是语言无关的。你能够用C++做为服务端,使用Golang、Java等做为客户端。为了实现这一点,咱们在”定义服务“和在编码和解码的过程当中,应该是作到语言无关的。
下面放一张官网上面的图:
所以,gRPC
使用了Protocol Buffers
。
在这里我不会展开来说Protocol Buffers
这个东西,你能够把他当成一个代码生成工具以及序列化工具。这个工具能够把咱们定义的方法,转换成特定语言的代码。好比你定义了一种类型的参数,他会帮你转换成Golang
中的struct 结构体
,你定义的方法,他会帮你转换成func 函数
。此外,在发送请求和接受响应的时候,这个工具还会完成对应的编码和解码工做,将你即将发送的数据编码成gRPC
可以传输的形式,又或者将即将接收到的数据解码为编程语言可以理解的数据格式。
对gRPC
的简单介绍就到这里,下面的内容咱们直接开始实践。
在这一节中,可能不少内容会不那么的适用。
可是限于篇幅,我没有列举全部的安装方式。若是在安装的过程当中你遇到了问题,能够在网上搜索解决,也能够在文章末尾找到个人联系方式,咱们一块儿研究。
go get google.golang.org/grpc
这一步安装的是gRPC
的核心库,可是这一步是须要(特别的上网方式)的。因此若是在安装过程当中出错了,你能够科学一波,也能够找一找其余的安装方法。
在Mac OS中,直接用brew安装。
brew info protobuf
上一步安装的是protocol编译器。而上文中咱们提到了能够生成各类不一样语言的代码。所以,除了这个编译器,咱们还须要配合各个语言的代码生成工具。
对于Golang
来讲,称为protoc-gen-go
。
不过在这儿有个小小的坑,github.com/golang/protobuf/protoc-gen-go
和google.golang.org/protobuf/cmd/protoc-gen-go
是不一样的。
区别在于前者是旧版本,后者是google接管后的新版本,他们之间的API是不一样的,也就是说用于生成的命令,以及生成的文件都是不同的。
由于目前的gRPC-go
源码中的example用的是后者的生成方式,为了与时俱进,本文也采起最新的方式。
你须要安装两个库:
go install google.golang.org/protobuf/cmd/protoc-gen-go go install google.golang.org/grpc/cmd/protoc-gen-go-grpc
由于这些文件在安装grpc
的时候,已经下载下来了,所以使用install
命令就能够了,而不须要使用get
命令。
而后你看你的$GOPATH路径,应该有标1和2的两个文件:
至此,全部的准备工做已经完成。
在开始开发以前,先说说咱们的目标。
在这个grpc-practice
项目中,我但愿实现一个功能,客户端能够发送消息给服务端,服务端收到消息后,返回响应给客户端。
正如前面所说的,在开发server
与client
以前,咱们须要先定义服务。
所以,在这一节的内容中,我将向你介绍proto文件的编写。
在这以前,先让咱们看看整个项目的初始结构。
server
和client
咱们先无论,在这一节内容中咱们先编写`*.proto'文件。
在proto文件夹中建立message.proto
文件。
在文件的第一行,咱们写上:
syntax = "proto3";
这是在说明咱们使用的是proto3
语法。
而后咱们应该写上:
option go_package = ".;message";
这部分的内容是关于最后生成的go文件是处在哪一个目录哪一个包中,.
表明在当前目录生成,message
表明了生成的go文件
的包名是message
。
而后咱们须要定义一个服务,在这个服务中须要有一个方法,这个方法能够接受客户端的参数,再返回服务端的响应。
那么咱们能够这么写:
service MessageSender { rpc Send(MessageRequest) returns (MessageResponse) {} }
其实很容易能够看出,咱们定义了一个service,称为MessageSender
,这个服务中有一个rpc方法,名为Send
。这个方法会发送一个MessageRequest
,而后返回一个MessageResponse
。
让咱们在看看具体的MessageRequest
和MessageResponse
:
message MessageResponse { string responseSomething = 1; } message MessageRequest { string saySomething = 1; }
message
关键字,其实你能够理解为Golang
中的结构体。这里比较特别的是变量后面的“赋值”。注意,这里并非赋值,而是在定义这个变量在这个message中的位置。更具体的内容我应该会在源码分析部分讲到。
在编写完上面的内容后,在/grpc-practice/src/helloworld/proto
目录下执行以下命令:
protoc --go_out=. message.proto protoc --go-grpc_out=. message.proto
这两条命令会生成以下的两个文件:
在这两个文件中,包含了咱们定义方法的go语言实现,也包含了咱们定义的请求与相应的go语言实现。
简单来说,就是protoc-gen-go
已经把你定义的语言无关的message.proto
转换为了go语言的代码,以便server
和client
直接使用。
注意,到了这一部分你可能会有一些疑惑。
在网上的一些教程中,有这样的生成方式:
protoc --go_out=plugins=grpc:. helloworld.proto
这种生成方式,使用的就是github
版本的protoc-gen-go
,而目前这个项目已经由Google接管了。
而且,若是使用这种生成方式的话,并不会生成上图中的xxx_grpc.pb.go
与xxx.pb.go
两个文件,只会生成xxx.pb.go
这种文件。
此外,你也可能遇到这种错误:
protoc-gen-go-grpc: program not found or is not executable Please specify a program using absolute path or make sure the program is available in your PATH system variable --go-grpc_out: protoc-gen-go-grpc: Plugin failed with status code 1.
这是由于你没有安装protoc-gen-go-grpc
这个插件,这个问题在本文中应该不会出现。
你还可能会遇到这种问题:
--go_out: protoc-gen-go: plugins are not supported; use 'protoc --go-grpc_out=...' to generate gRPC
这是由于你安装的是更新版本的protoc-gen-go
,可是你却用了旧版本的生成命令。
可是这两种方法都是能够完成目标的,只不过api
不太同样。本文是基于Google版本的protoc
-gen-go进行示范。
至于其余更详细的资料,你能够在这里看到:https://github.com/protocolbuffers/protobuf-go/releases/tag/v1.20.0#v1.20-generated-code
咱们在server目录下面建立一个server.go
文件。
在main函数中加入以下的代码:
srv := grpc.NewServer() message.RegisterMessageSenderService(srv, &message.MessageSenderService{})
很容易能够看出,咱们在这一部分建立了一个Server,而后注册了咱们的Service。
在注册函数的第二个参数中,咱们传进去了一个MessageSenderService
实例。
来看看这个实例有什么样的结构:
type MessageSenderService struct { Send func(context.Context, *MessageRequest) (*MessageResponse, error) }
能够看出,这个实例里面有一个方法,这个方法就是咱们定义的send方法。也就是说,这一部分是须要咱们在Server
端实现这个send方法的。
所以咱们建立这么一个方法:
func handleSendMessage(ctx context.Context, req *message.MessageRequest) (*message.MessageResponse, error) { log.Println("receive message:", req.GetSaySomething()) resp := &message.MessageResponse{} resp.ResponseSomething = "roger that!" return resp, nil }
注意,“实现定义的方法”,并非说咱们须要建立一个同名的方法,而是说咱们须要建立一个有相同函数签名的方法。也就是说,须要有相同的入参,出参。
而后咱们将这个方法写进注册函数中,变成了这样:
message.RegisterMessageSenderService(srv, &message.MessageSenderService{ Send: handleSendMessage, })
至此,咱们已经成功的在server
端实现了咱们声明的方法了。
其实这个过程跟golang的web服务器是很像的,也是建立Handler,而后对端口进行监听。
那么到了这一步也同样。
listener, err := net.Listen("tcp", ":12345") if err != nil { log.Fatalf("failed to listen: %v", err) } err = srv.Serve(listener) if err != nil { log.Fatalf("failed to serve: %v", err) }
监听12345
端口的TCP链接,而后启动服务器。
至此,服务端开发完毕。
在客户端中,咱们应该先与server
端创建链接,而后才可以调用各类方法。
conn, err := grpc.Dial("127.0.0.1:12345", grpc.WithInsecure(), grpc.WithBlock()) if err != nil { log.Fatalf("did not connect: %v", err) } defer conn.Close()
以上代码,就是跟本地的12345
端口创建链接。
而后,按照定义,咱们调用server
端的方法,应该要像调用本地方法同样方便。
那么,咱们这么作:
client := message.NewMessageSenderClient(conn) resp, err := client.Send(context.Background(), &message.MessageRequest{SaySomething: "hello world!"}) if err != nil { log.Fatalf("could not greet: %v", err) }
很容易能够理解,咱们在本地建立了一个client,而后直接调用咱们以前定义好的Send方法,就能够实现咱们须要的逻辑了。
简单的来说,咱们在*.proto
文件中定义了方法,而后在server
端实现定义的rpc方法的具体逻辑,在client端调用这个方法。
对于其余的部分,由proto buffer
负责对Golang
中存储的数据结构与rpc
传输中的数据进行转换,grpc
负责封装全部的逻辑。
server
端和client
端都跑起来,你会看到这样的画面:
至此,成功Hello了个World。
首先,谢谢你能看到这里!
在这篇文章中,主要是跟你介绍一下hello world的写法,以及在say hello的过程当中可能遇到的一些坑。
我认为最大的坑是在于protoc-gen-go
这个插件这里,由于两种语法让我迷惑了好久。
若是在这期间,你还有一些问题没有解决,欢迎留言,或者直接公众号找到我,咱们一块儿研究。
若是在文章中有哪些错误,还请不吝指教,谢谢!
最后,再次感谢你能看到这里!
按照惯例,甩个公众号在这,无论有没有问题,都欢迎来找我玩~