写点什么

【Go 微服务】开发 gRPC 总共分三步

作者:王中阳Go
  • 2022-10-14
    北京
  • 本文字数:5347 字

    阅读完需:约 1 分钟

【Go微服务】开发gRPC总共分三步

前言

上一篇文章我们介绍了 ProtoBuf 的使用,不了解 ProtoBuf 的同学建议先读这篇文章:# 一文带你玩转ProtoBuf,会用 protobuf 是学习 gRPC 的基础。


之前我也有写过 RPC 相关的文章:# Go RPC入门指南:RPC的使用边界在哪里?如何实现跨语言调用?,详细介绍了 RPC 是什么,使用边界在哪里?并且用 Go 和 php 举例,实现了跨语言调用。不了解 RPC 的同学建议先读这篇文章补补课。


上面提到的这些基础知识,不是本文的重点。


所以建议小伙伴们先读上面两篇,再读这篇,体验更好哦。


这篇文章将重点介绍在微服务中 gRPC 的使用:

开发流程

在微服务分布式架构中开发 gRPC 其实非常简单,不要畏难畏烦,没有什么心智负担的。


开发 gRPC 的流程和宋丹丹把大象装冰箱是一样的:


  1. 把冰箱门打开

  2. 把大象装进去

  3. 把冰箱门关上


开发 gRPC 的流程;


  1. 写 proto 文件定义服务和消息

  2. 使用 protoc 工具生成代码

  3. 编写业务逻辑代码提供服务


就是这么简单。


下面我仍然以 Go 语言举例,其他语言的实现思路也是一样的。

入门实践

为了让大家更好理解,我参考 gRPC 官方文档,写了一个 helloword 示例。


下图是使用 Go 实现 gRPC 开发的目录结构图,先让大家有个整体的认识:



欢迎大家按照我的步骤进行复刻实践:


看文章是学不会编程的,但是一边看文章一边敲代码可以!

1. 写 proto 文件定义服务和消息

service Greeter {} 是我们定义的服务


rpc SayHello (HelloRequest) returns (HelloReply) {} 是在服务中定义的方法


protoc 工具集,会根据我们定义的服务、方法、和消息生成指定语言的代码。


syntax = "proto3";
option go_package = "./;hello";
package hello;
service Greeter {rpc SayHello (HelloRequest) returns (HelloReply) {}}
message HelloRequest {string name = 1;}
message HelloReply {string message = 1;}
复制代码


如果小伙伴们看上面代码有不懂的地方,那就是 protobuf 基础不牢了,请看这篇:一文带你玩转protobuf,回顾一下知识点。

2. 使用 protoc 工具生成代码

切换到 proto 文件所在目录下


cd protos/helloword/
复制代码


生成 Go 代码


protoc --go_out=. helloworld.proto
复制代码


小技巧之同步依赖:当你生成 Go 代码后,发现生成的文件飘红报错,不要紧张,多数情况是因为依赖不存在导致的。


执行下面的命令,同步依赖就可以了:


go mod tidy
复制代码


3. 编写业务逻辑代码 提供服务

下面是今天的重点,我们用 Go 实现业务逻辑的编写,注意看:


在微服务架构开发 gRPC 时,一定有两个端:服务端和客户端。


我们的习惯是,在搞定 protobuf 之后,先写服务端逻辑,暴露端口,提供服务;再写客户端逻辑,连接服务,发送请求,处理响应。


小提示:PHP 和 Objective-C 只能实现 gRPC 中的客户端,不能实现服务端。

3.1 编写服务端业务逻辑

编写服务端非常简单,我们只需要实现在 proto 中定义的 rpc 方法。


小技巧:在我们实际开发中,我们导入 protos 服务的时候,默认是一个比较长的名字,建议结合自己项目,改成比较短又容易理解的名字。


package greeter_server
import "context"
//导入我们在protos文件中定义的服务import pb "juejin/rpc/protos/helloworld"
//定义一个结构体,作用是实现helloworld中的GreeterServertype server struct{}
// SayHello implements helloworld.GreeterServerfunc (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { return &pb.HelloReply{Message: "Hello " + in.Name}, nil}
复制代码


以上就完成了服务端的业务逻辑编写:


  1. 用我们在 proto 中定义的消息,构建并填充了一个我们在接口定义的 HelloReply 应答对象。

  2. HelloReply 对象返回给客户端。


到这里业务功能是实现了,但是服务端的业务如何让客户端调用呢?


下面我们继续编写:暴露端口,提供服务

3.2 暴露端口,提供服务

踩坑分享:我在编码的过程中使用了错误的 gRPC 依赖,浪费了不少时间。应该用下面这个依赖包:


go get google.golang.org/grpc
复制代码


注意:下面的代码是在 3.1 的基础上添加的,并不是另外创建一个新的 Go 文件。


关键代码注释已经在代码段中写清楚了,建议大家参考步骤,手敲一遍。


package main
import ( "context" "flag" "fmt" "google.golang.org/gRPC" "log" "net")
//导入我们在protos文件中定义的服务import pb "juejin/rpc/protos/helloworld"
//定义一个结构体,作用是实现helloworld中的GreeterServertype server struct { pb.UnimplementedGreeterServer}
// SayHello implements helloworld.GreeterServerfunc (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { return &pb.HelloReply{Message: "Hello " + in.Name}, nil}
//定义端口号 支持启动的时候输入端口号var ( port = flag.Int("port", 50051, "The server port"))
func main() { //解析输入的端口号 默认50051 flag.Parse() //tcp协议监听指定端口号 lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) if err != nil { log.Fatalf("failed to listen: %v", err) } //实例化gRPC服务 s := gRPC.NewServer() //服务注册 pb.RegisterGreeterServer(s, &server{}) log.Printf("server listening at %v", lis.Addr()) //启动服务 if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) }}
复制代码


启动成功,普天同庆:



到这里我们就完成了 gRPC 服务端的编写:我们实现了将 Greeter 服务绑定到一个端口,我们启动这个服务时,服务端已准备好从 Greeter 服务的客户端接收请求了。


我们接下来再编写客户端:

3.3 编写客户端逻辑代码

客户端的 gRPC 更简单!


我们将用 protoc 生成的代码写一个简单的客户端程序,来访问我们在创建的 Greeter 服务端。


小技巧:在 gRPC Go 我们使用一个特殊的 Dial() 方法来创建频道,实现和服务端的连接。


关键代码已添加注释,编写客户端逻辑代码,强烈建议大家和我一起手敲一遍。


“编程要有工匠精神,做的多了手感就出来了。”


package main
import ( "context" "flag" "google.golang.org/gRPC" //这个依赖不要搞错 "google.golang.org/gRPC/credentials/insecure" pb "juejin/rpc/protos/helloworld" "log" "time")
//默认数据 也支持在控制台自定义const ( defaultName = "world")
//监听地址和传入的namevar ( addr = flag.String("addr", "localhost:50051", "the address to connect to") name = flag.String("name", defaultName, "Name to greet"))
func main() { flag.Parse() //通过gRPC.Dial()方法建立服务连接 conn, err := gRPC.Dial(*addr, gRPC.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatalf("did not connect: %v", err) } //连接要记得关闭 defer func(conn *gRPC.ClientConn) { err := conn.Close() if err != nil {
} }(conn) //实例化客户端连接 c := pb.NewGreeterClient(conn)
//设置请求上下文,因为是网络请求,我们需要设置超时时间 ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() //客户端调用在proto中定义的SayHello()rpc方法,发起请求,接收服务端响应 r, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name}) if err != nil { log.Fatalf("could not greet: %v", err) } log.Printf("Greeting: %s", r.GetMessage())}
复制代码


到这里我们就已经完成了服务端和客户端业务逻辑的编写,下面就是见证奇迹的时刻了:

3.4 调用 gRPC 两端互通

如何实现两端的消息互通?


  1. 我们之前已经打开了一个终端,启动了服务端的服务。

  2. 我们再打开一个新的终端,运行客户端,看下服务端是否给我们返回了数据:



和我们预想中的结果一样:


服务端给我们返回了“Hello world”,其中 Hello 是服务端设置的,world 是客户端传给服务端的参数,服务端进行拼接之后给客户端返回了。


至此,一个经典的 gRPC 通信示例就搞定了!

扩展:自定义输入

没用过 go flag 自定义输入的小伙伴重点看一下,这部分是为你写的:


客户端和服务端代码中的 flag.Parse 的作用是:支持我们在终端控制台自定义输入参数,如果没有输入的话,使用程序中设置的默认参数,比如客户端的 name,在代码中是这么定义的:


name = flag.String("name", "world", "Name to greet")
复制代码


我们在终端输入如下命令:


go run main.go --name 王中阳
复制代码


效果是这样的:



好了,咱们再接着聊进阶的内容:


gRPC 另外一个特点就是和语言无关,我们可以使用不同的语言定义客户端和服务端。


下面咱们再进阶实战一下,用 gRPC 实现跨语言的调用。

进阶实战:跨语言调用

入门实战我给出了详细的示例代码,甚至连目录结构都分享给大家了,相信大家只要按照步骤复刻,一定也能运行成功。


关于进阶实战的跨语言调用:服务端不重复编写了,我们仍然使用上面用 Go 编写的服务端。


客户端我将用我熟悉的 PHP 语言来编写,实现两端的 rpc 通信。


建议大家回顾一下“大象装冰箱”的步骤,用自己擅长的语言开发客户端,像我一样实现 gRPC 的跨语言调用。

1. 编写 proto 文件

和入门实战是一样的

2. 根据 proto 文件生成代码

和入门实战思路一样,区别指定生成代码语言不一样:


protoc-gen-php -i . -o . ./helloworld.proto
复制代码

3. 编写业务逻辑代码

3.1 先写服务端

服务端使用 Go 实现的服务端,不进行编写。


确定服务端是开启状态:



再次提醒一下:


PHP 和 Objective-C 只能实现 gRPC 中的客户端,不能实现服务端。

3.2 再写客户端

我用 PHP 实现客户端的编写,你擅长什么语言呢?有没有踩到坑,欢迎大家在评论区讨论。


<?php//命名空间namespace Helloworld;
//定义PHP客户端class GreeterClient extends \gRPC\BaseStub{
//定义构造方法 public function __construct($hostname, $opts, $channel = null) { parent::__construct($hostname, $opts, $channel); }
/** * 实现proto文件中定义的SayHello()方法 * Sends a greeting * @param \Helloworld\HelloRequest $argument input argument * @param array $metadata metadata * @param array $options call options * @return \gRPC\UnaryCall */ public function SayHello(\Helloworld\HelloRequest $argument, $metadata = [], $options = []) { return $this->_simpleRequest('/helloworld.Greeter/SayHello', $argument, ['\Helloworld\HelloReply', 'decode'], $metadata, $options); }
}
复制代码

3.3 启动服务,进行调用

编写 PHP 脚本文件:


连接50051端口(Go 实现的 gRPC 服务端对外暴露的端口)


<?phprequire dirname(__FILE__).'/vendor/autoload.php';
function greet($hostname, $name){ $client = new Helloworld\GreeterClient($hostname, [ 'credentials' => gRPC\ChannelCredentials::createInsecure(), ]); $request = new Helloworld\HelloRequest(); $request->setName($name); list($response, $status) = $client->SayHello($request)->wait(); if ($status->code !== gRPC\STATUS_OK) { echo "ERROR: " . $status->code . ", " . $status->details . PHP_EOL; exit(1); } echo $response->getMessage() . PHP_EOL;}
$name = !empty($argv[1]) ? $argv[1] : 'world';$hostname = !empty($argv[2]) ? $argv[2] : 'localhost:50051';greet($hostname, $name);
复制代码


通过终端,启动 PHP 客户端:



我们发现,PHP 的客户端通过 gRPC 成功的连接了 Go 服务端提供的 50051 服务,并成功调用了 SayHello()方法,获得了返回值:Hello world

实操技巧

纸上得来终觉浅,绝知此事要躬行。


强烈建议大家动手实操,使用自己熟悉的语言完成 gRPC 跨语言调用,可以参考:gRPC 各种语言教程详解这篇技术博客更适合小白入门 gRPC 的开发,有个整体的理解和概念。


进阶知识点安利大家看官方文档进行实践:


gRPC 官方文档中文版


gRPC 官方示例GitHub

本文总结

通过这篇文章我们已经掌握了 gRPC 相关的知识点,可以独立用 Go 实现客户端和服务端的编写,并且通过服务注册对外提供服务,实现可客户端和服务端的 gRPC 通信。


为了验证 gRPC 支持跨语言调用的特性,在进阶实战中又使用 PHP 开发了客户端,实现了 PHP 客户端和 Go 服务端的远程跨语言调用。


养成良好的编程习惯有助于减少奇奇怪怪的问题,强烈建议大家严格按照“大象装冰箱”的顺序进行 gRPC 的开发:


1. 写 proto 文件定义服务和消息


2. 使用 protoc 工具生成代码


3. 编写业务逻辑代码提供服务


关注我,下一篇带大家玩转 Go 微服务。


最后:万事起于忽微,量变引起质变,相信坚持的力量。

关于专栏

近期会更新一系列 Go 实战进阶的文章,欢迎大家关注我的签约专栏# Go语言进阶实战


这是近期会更新文章的知识脉络图,感兴趣的小伙伴可以关注一波,欢迎日常催更。


已完成

《一文玩转ProtoBuf》


《开发gRPC总共分三步》


《Go WEB进阶实战:基于GoFrame搭建的电商前后台API系统》


小伙伴们还想看哪些内容,欢迎在评论区留言。

一起学习,升级打怪

我们搞了一个对学 Go 真正有帮助的群,欢迎加入:


公众号:程序员升级打怪之旅


微信号:wangzhongyang1993

发布于: 2022-10-14阅读数: 50
用户头像

王中阳Go

关注

公众号:程序员升级打怪之旅 2022-10-09 加入

微信:wangzhongyang1993

评论 (1 条评论)

发布
用户头像
开发gRPC总共分三步,用“把大象装冰箱”的思路规范开发gRPC的流程:1. 写proto文件定义服务和消息 2. 使用protoc工具生成代码 3. 编写业务逻辑代码提供服务。
2022-10-14 13:55 · 北京
回复
没有更多了
【Go微服务】开发gRPC总共分三步_微服务_王中阳Go_InfoQ写作社区