周武 發表於 2020-9-24 21:08:00

gRPC-go 入门(1):Hello World

<h2 id="摘要">摘要</h2>
<p>在这篇文章中,主要是跟你介绍一下<code>gRPC</code>这个东西。</p>
<p>然后,我会创建一个简单的练习项目,作为<code>gRPC</code>的Hello World项目。</p>
<p>在这个项目中,只有很简单的一个RPC函数,用于说明<code>gRPC</code>的工作方式。</p>
<p>此外,我也会跟你分享一下我初次接触<code>gRPC</code>所遇到的一些坑,主要是在<code>protocol buffer</code>的<code>proto-gen-go</code>插件上面。</p>
<h2 id="1-简单介绍">1. 简单介绍</h2>
<p>在这一节的内容中,我将简单的跟你介绍一下<code>gRPC</code>这个东西。<br>
<code>RPC</code>的全称是<code>Remote Procedure Call</code>,远程过程调用。这是一种协议,是用来屏蔽分布式计算中的各种调用细节,使得你可以像是本地调用一样直接调用一个远程的函数。<br>
而<code>gRPC</code>又是什么呢?用官方的话来说:</p>
<blockquote>
<p>A high-performance, open-source universal RPC framework</p>
</blockquote>
<p><strong><code>gRPC</code>是一个高性能的、开源的通用的RPC框架。</strong></p>
<p>在<code>gRPC</code>中,我们称调用方为<code>client</code>,被调用方为<code>server</code>。<br>
跟其他的<code>RPC</code>框架一样,<code>gRPC</code>也是基于”服务定义“的思想。简单的来讲,就是我们通过某种方式来描述一个服务,这种描述方式是语言无关的。在这个”服务定义“的过程中,我们描述了我们提供的服务服务名是什么,有哪些方法可以被调用,这些方法有什么样的入参,有什么样的回参。</p>
<p>也就是说,在定义好了这些服务、这些方法之后,<code>gRPC</code>会屏蔽底层的细节,<code>client</code>只需要直接调用定义好的方法,就能拿到预期的返回结果。对于<code>server</code>端来说,还需要实现我们定义的方法。同样的,<code>gRPC</code>也会帮我们屏蔽底层的细节,我们只需要实现所定义的方法的具体逻辑即可。</p>
<p>你可以发现,在上面的描述过程中,所谓的”服务定义“,就跟定义接口的语义是很接近的。我更愿意理解为这是一种”约定“,双方约定好接口,然后<code>server</code>实现这个接口,<code>client</code>调用这个接口的代理对象。至于其他的细节,交给<code>gRPC</code>。</p>
<p>此外,<code>gRPC</code>还是语言无关的。你可以用C++作为服务端,使用Golang、Java等作为客户端。为了实现这一点,我们在”定义服务“和在编码和解码的过程中,应该是做到<strong>语言无关的</strong>。</p>
<p>下面放一张官网上面的图:</p>
<p><img src="https://img2020.cnblogs.com/blog/1998080/202009/1998080-20200924161632078-309009747.jpg"></p>
<p>因此,<code>gRPC</code>使用了<code>Protocol Buffers</code>。</p>
<p>在这里我不会展开来讲<code>Protocol Buffers</code>这个东西,你可以把他当成一个代码生成工具以及序列化工具。这个工具可以把我们定义的方法,转换成特定语言的代码。比如你定义了一种类型的参数,他会帮你转换成<code>Golang</code>中的<code>struct 结构体</code>,你定义的方法,他会帮你转换成<code>func 函数</code>。此外,在发送请求和接受响应的时候,这个工具还会完成对应的编码和解码工作,将你即将发送的数据编码成<code>gRPC</code>能够传输的形式,又或者将即将接收到的数据解码为编程语言能够理解的数据格式。</p>
<p>对<code>gRPC</code>的简单介绍就到这里,下面的内容我们直接开始实践。</p>
<h2 id="2-环境配置">2. 环境配置</h2>
<p>在这一节中,可能很多内容会不那么的适用。</p>
<p>但是限于篇幅,我没有列举所有的安装方式。如果在安装的过程中你遇到了问题,可以在网上搜索解决,也可以在文章末尾找到我的联系方式,我们一起研究。</p>
<h3 id="21-grpc">2.1 gRPC</h3>
<pre><code>go get google.golang.org/grpc
</code></pre>
<p>这一步安装的是<code>gRPC</code>的核心库,但是这一步是需要(特别的上网方式)的。所以如果在安装过程中出错了,你可以科学一波,也可以找一找其他的安装方法。</p>
<h3 id="22-protocol-buffers">2.2 protocol buffers</h3>
<p>在Mac OS中,直接用brew安装。</p>
<pre><code>brew info protobuf
</code></pre>
<h3 id="23-protoc-gen-go">2.3 protoc-gen-go</h3>
<p>上一步安装的是protocol编译器。而上文中我们提到了可以生成各种不同语言的代码。因此,除了这个编译器,我们还需要配合各个语言的代码生成工具。</p>
<p>对于<code>Golang</code>来说,称为<code>protoc-gen-go</code>。</p>
<p>不过在这儿有个小小的坑,<code>github.com/golang/protobuf/protoc-gen-go</code>和<code>google.golang.org/protobuf/cmd/protoc-gen-go</code>是不同的。</p>
<p>区别在于前者是旧版本,后者是google接管后的新版本,他们之间的API是不同的,也就是说用于生成的命令,以及生成的文件都是不一样的。</p>
<p>因为目前的<code>gRPC-go</code>源码中的example用的是后者的生成方式,为了与时俱进,本文也采取最新的方式。</p>
<p>你需要安装两个库:</p>
<pre><code>go install google.golang.org/protobuf/cmd/protoc-gen-go
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc
</code></pre>
<p>因为这些文件在安装<code>grpc</code>的时候,已经下载下来了,因此使用<code>install</code>命令就可以了,而不需要使用<code>get</code>命令。</p>
<p>然后你看你的$GOPATH路径,应该有标1和2的两个文件:</p>
<p><img src="https://img2020.cnblogs.com/blog/1998080/202009/1998080-20200924161817553-1294594791.jpg"></p>
<p>至此,所有的准备工作已经完成。</p>
<h2 id="3-proto文件创建">3. proto文件创建</h2>
<p>在开始开发之前,先说说我们的目标。</p>
<p>在这个<code>grpc-practice</code>项目中,我希望实现一个功能,客户端可以发送消息给服务端,服务端收到消息后,返回响应给客户端。</p>
<p>正如前面所说的,在开发<code>server</code>与<code>client</code>之前,我们需要先定义服务。</p>
<p>因此,在这一节的内容中,我将向你介绍proto文件的编写。</p>
<h3 id="31-项目结构">3.1 项目结构</h3>
<p>在这之前,先让我们看看整个项目的初始结构。<br>
<img src="https://img2020.cnblogs.com/blog/1998080/202009/1998080-20200924161834930-1252892090.jpg"></p>
<p><code>server</code>和<code>client</code>我们先不管,在这一节内容中我们先编写`*.proto'文件。</p>
<p>在proto文件夹中创建<code>message.proto</code>文件。</p>
<p>在文件的第一行,我们写上:</p>
<pre><code>syntax = "proto3";
</code></pre>
<p>这是在说明我们使用的是<code>proto3</code>语法。</p>
<p>然后我们应该写上:</p>
<pre><code>option go_package = ".;message";
</code></pre>
<p>这部分的内容是关于最后生成的go文件是处在哪个目录哪个包中,<code>.</code>代表在当前目录生成,<code>message</code>代表了生成的<code>go文件</code>的包名是<code>message</code>。</p>
<p>然后我们需要定义一个服务,在这个服务中需要有一个方法,这个方法可以接受客户端的参数,再返回服务端的响应。</p>
<p>那么我们可以这么写:</p>
<pre><code>service MessageSender {
rpc Send(MessageRequest) returns (MessageResponse) {}
}
</code></pre>
<p>其实很容易可以看出,我们定义了一个service,称为<code>MessageSender</code>,这个服务中有一个rpc方法,名为<code>Send</code>。这个方法会发送一个<code>MessageRequest</code>,然后返回一个<code>MessageResponse</code>。</p>
<p>让我们在看看具体的<code>MessageRequest</code>和<code>MessageResponse</code>:</p>
<pre><code>message MessageResponse {
string responseSomething = 1;
}

message MessageRequest {
string saySomething = 1;
}
</code></pre>
<p><code>message</code>关键字,其实你可以理解为<code>Golang</code>中的结构体。这里比较特别的是变量后面的“赋值”。注意,这里并不是赋值,而是在定义这个变量在这个message中的位置。更具体的内容我应该会在源码分析部分讲到。</p>
<p>在编写完上面的内容后,在<code>/grpc-practice/src/helloworld/proto</code>目录下执行如下命令:</p>
<pre><code>protoc --go_out=. message.proto
protoc --go-grpc_out=. message.proto
</code></pre>
<p>这两条命令会生成如下的两个文件:</p>
<p><img src="https://img2020.cnblogs.com/blog/1998080/202009/1998080-20200924161855325-187405311.jpg"></p>
<p>在这两个文件中,包含了我们定义方法的go语言实现,也包含了我们定义的请求与相应的go语言实现。</p>
<p>简单来讲,就是<code>protoc-gen-go</code>已经把你定义的语言无关的<code>message.proto</code>转换为了go语言的代码,以便<code>server</code>和<code>client</code>直接使用。</p>
<p><strong>注意,到了这一部分你可能会有一些疑惑。</strong></p>
<p>在网上的一些教程中,有这样的生成方式:</p>
<pre><code>protoc --go_out=plugins=grpc:. helloworld.proto
</code></pre>
<p>这种生成方式,使用的就是<code>github</code>版本的<code>protoc-gen-go</code>,而目前这个项目已经由Google接管了。</p>
<p>并且,如果使用这种生成方式的话,并不会生成上图中的<code>xxx_grpc.pb.go</code>与<code>xxx.pb.go</code>两个文件,只会生成<code>xxx.pb.go</code>这种文件。</p>
<p>此外,你也可能遇到这种错误:</p>
<pre><code>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.
</code></pre>
<p>这是因为你没有安装<code>protoc-gen-go-grpc</code>这个插件,这个问题在本文中应该不会出现。</p>
<p>你还可能会遇到这种问题:</p>
<pre><code>--go_out: protoc-gen-go: plugins are not supported; use 'protoc --go-grpc_out=...' to generate gRPC
</code></pre>
<p>这是因为你安装的是更新版本的<code>protoc-gen-go</code>,但是你却用了旧版本的生成命令。</p>
<p>但是这两种方法都是可以完成目标的,只不过<code>api</code>不太一样。本文是基于Google版本的<code>protoc</code>-gen-go进行示范。</p>
<p>至于其他更详细的资料,你可以在这里看到:https://github.com/protocolbuffers/protobuf-go/releases/tag/v1.20.0#v1.20-generated-code</p>
<h2 id="4-服务端">4. 服务端</h2>
<h3 id="41-注册">4.1 注册</h3>
<p>我们在server目录下面创建一个<code>server.go</code>文件。</p>
<p>在main函数中加入如下的代码:</p>
<pre><code>srv := grpc.NewServer()
message.RegisterMessageSenderService(srv, &amp;message.MessageSenderService{})
</code></pre>
<p>很容易可以看出,我们在这一部分创建了一个Server,然后注册了我们的Service。</p>
<p>在注册函数的第二个参数中,我们传进去了一个<code>MessageSenderService</code>实例。</p>
<p>来看看这个实例有什么样的结构:</p>
<pre><code>type MessageSenderService struct {
        Send func(context.Context, *MessageRequest) (*MessageResponse, error)
}
</code></pre>
<p>可以看出,这个实例里面有一个方法,这个方法就是我们定义的send方法。也就是说,这一部分是需要我们在<code>Server</code>端实现这个send方法的。</p>
<p>因此我们创建这么一个方法:</p>
<pre><code>func handleSendMessage(ctx context.Context, req *message.MessageRequest) (*message.MessageResponse, error) {
        log.Println("receive message:", req.GetSaySomething())
        resp := &amp;message.MessageResponse{}
        resp.ResponseSomething = "roger that!"
        return resp, nil
}
</code></pre>
<p>注意,“实现定义的方法”,并不是说我们需要创建一个同名的方法,而是说我们需要创建一个有相同函数签名的方法。也就是说,需要有相同的入参,出参。</p>
<p>然后我们将这个方法写进注册函数中,变成了这样:</p>
<pre><code>message.RegisterMessageSenderService(srv, &amp;message.MessageSenderService{
                Send: handleSendMessage,
        })
</code></pre>
<p>至此,我们已经成功的在<code>server</code>端实现了我们声明的方法了。</p>
<h3 id="42-监听">4.2 监听</h3>
<p>其实这个过程跟golang的web服务器是很像的,也是创建Handler,然后对端口进行监听。</p>
<p>那么到了这一步也一样。</p>
<pre><code>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)
}
</code></pre>
<p>监听<code>12345</code>端口的TCP连接,然后启动服务器。</p>
<p>至此,服务端开发完毕。</p>
<h2 id="5-客户端">5. 客户端</h2>
<p>在客户端中,我们应该先与<code>server</code>端建立连接,然后才能够调用各种方法。</p>
<pre><code>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()
</code></pre>
<p>以上代码,就是跟本地的<code>12345</code>端口建立连接。</p>
<p>然后,按照定义,我们调用<code>server</code>端的方法,应该要像调用本地方法一样方便。</p>
<p>那么,我们这么做:</p>
<pre><code>client := message.NewMessageSenderClient(conn)
resp, err := client.Send(context.Background(), &amp;message.MessageRequest{SaySomething: "hello world!"})
if err != nil {
   log.Fatalf("could not greet: %v", err)
}
</code></pre>
<p>很容易可以理解,我们在本地创建了一个client,然后直接调用我们之前定义好的Send方法,就可以实现我们需要的逻辑了。</p>
<p>简单的来讲,我们在<code>*.proto</code>文件中定义了方法,然后在<code>server</code>端实现定义的rpc方法的具体逻辑,在client端调用这个方法。</p>
<p>对于其他的部分,由<code>proto buffer</code>负责对<code>Golang</code>中存储的数据结构与<code>rpc</code>传输中的数据进行转换,<code>grpc</code>负责封装所有的逻辑。</p>
<p><code>server</code>端和<code>client</code>端都跑起来,你会看到这样的画面:<br>
<img src="https://img2020.cnblogs.com/blog/1998080/202009/1998080-20200924162448389-804698371.jpg"></p>
<p><img src="https://img2020.cnblogs.com/blog/1998080/202009/1998080-20200924162500188-1337762987.jpg"></p>
<p>至此,成功Hello了个World。</p>
<h2 id="写在最后">写在最后</h2>
<p>首先,谢谢你能看到这里!</p>
<p>在这篇文章中,主要是跟你介绍一下hello world的写法,以及在say hello的过程中可能遇到的一些坑。</p>
<p>我认为最大的坑是在于<code>protoc-gen-go</code>这个插件这里,因为两种语法让我迷惑了很久。</p>
<p>如果在这期间,你还有一些问题没有解决,欢迎留言,或者直接公众号找到我,我们一起研究。</p>
<p>如果在文章中有哪些错误,还请不吝指教,谢谢!</p>
<p>最后,再次感谢你能看到这里!</p>
<p>按照惯例,甩个公众号在这,不管有没有问题,都欢迎来找我玩~<br>
<img src="https://img2020.cnblogs.com/blog/1998080/202009/1998080-20200924162007452-1782731082.jpg"></p><br><br>
来源:https://www.cnblogs.com/hongjijun/p/13724738.html
頁: [1]
查看完整版本: gRPC-go 入门(1):Hello World