前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >和大象装冰箱一样:开发gRPC总共分三步 【掘金签约文章】

和大象装冰箱一样:开发gRPC总共分三步 【掘金签约文章】

作者头像
王中阳Go
发布2022-10-26 15:13:37
3950
发布2022-10-26 15:13:37
举报
文章被收录于专栏:Go语言学习专栏

前言

上一篇文章我们介绍了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工具集,会根据我们定义的服务、方法、和消息生成指定语言的代码。

代码语言:javascript
复制
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文件所在目录下

代码语言:javascript
复制
cd protos/helloword/

生成Go代码

代码语言:javascript
复制
protoc --go_out=. helloworld.proto

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

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

代码语言:javascript
复制
go mod tidy

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

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

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

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

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

3.1 编写服务端业务逻辑

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

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

代码语言:javascript
复制
package greeter_server

import "context"

//导入我们在protos文件中定义的服务
import pb "juejin/rpc/protos/helloworld"

//定义一个结构体,作用是实现helloworld中的GreeterServer
type server struct{}

// SayHello implements helloworld.GreeterServer
func (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依赖,浪费了不少时间。应该用下面这个依赖包:

代码语言:javascript
复制
go get google.golang.org/grpc

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

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

代码语言:javascript
复制
package main

import (
   "context"
   "flag"
   "fmt"
   "google.golang.org/gRPC"
   "log"
   "net"
)

//导入我们在protos文件中定义的服务
import pb "juejin/rpc/protos/helloworld"

//定义一个结构体,作用是实现helloworld中的GreeterServer
type server struct {
   pb.UnimplementedGreeterServer
}

// SayHello implements helloworld.GreeterServer
func (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() 方法来创建频道,实现和服务端的连接。

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

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

代码语言:javascript
复制
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"
)

//监听地址和传入的name
var (
   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,在代码中是这么定义的:

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

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

代码语言:javascript
复制
go run main.go --name 王中阳

效果是这样的:

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

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

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

进阶实战:跨语言调用

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

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

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

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

1. 编写proto文件

和入门实战是一样的

2. 根据proto文件生成代码

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

代码语言:javascript
复制
protoc-gen-php -i . -o . ./helloworld.proto

3. 编写业务逻辑代码

3.1 先写服务端

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

确定服务端是开启状态:

再次提醒一下:

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

3.2 再写客户端

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

代码语言:javascript
复制
<?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服务端对外暴露的端口)

代码语言:javascript
复制
<?php
require 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. 编写业务逻辑代码提供服务

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-10-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员升级打怪之旅 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 开发流程
  • 入门实践
    • 1. 写proto文件定义服务和消息
      • 2. 使用protoc工具生成代码
        • 3. 编写业务逻辑代码 提供服务
          • 3.1 编写服务端业务逻辑
          • 3.2 暴露端口,提供服务
          • 3.3 编写客户端逻辑代码
          • 3.4 调用gRPC 两端互通
          • 扩展:自定义输入
      • 进阶实战:跨语言调用
        • 1. 编写proto文件
          • 2. 根据proto文件生成代码
            • 3. 编写业务逻辑代码
              • 3.1 先写服务端
              • 3.2 再写客户端
              • 3.3 启动服务,进行调用
          • 实操技巧
          • 本文总结
          相关产品与服务
          微服务引擎 TSE
          微服务引擎(Tencent Cloud Service Engine)提供开箱即用的云上全场景微服务解决方案。支持开源增强的云原生注册配置中心(Zookeeper、Nacos 和 Apollo),北极星网格(腾讯自研并开源的 PolarisMesh)、云原生 API 网关(Kong)以及微服务应用托管的弹性微服务平台。微服务引擎完全兼容开源版本的使用方式,在功能、可用性和可运维性等多个方面进行增强。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档