花5分钟写个 grpc 微服务架构吧

背景:当前微服务架构在开发中越来越常见,其目的在于将各个模块进行解耦,实现各个模块之间快速迭代。在 golang 项目中,最流行的微服务框架当属谷歌旗下的 grpc 框架。回想起我学 grpc的 时候, 虽说不难,代码量不大, 但还是遇到了很多坑的, 如果照着网上的教程来写代码大概率是跑不通的。 特此写一篇小白也能看懂的,最简单的,带你手把手写的基于 grpc 微服务架构项目。

安装 grpc , protoc 工具 和 protobuf

在命令行中输入以下三行命令:

go get google.golang.org/grpc
go get -u github.com/golang/protobuf/proto
go get -u github.com/golang/protobuf/protoc-gen-go
复制代码

光有这几个还是不够的,还得要去下载 protobuf 安装包 下载完成后解压,将 bin 目录下的 protoc 可执行文件复制到 go安装目录下的 bin 目录,比如我的是 C:\Users\Chester_Zhang\go\bin 。要不然等等命令行无法识别 protoc 命令。

接下来确保环境变量配置正确, 如果环境变量没配置好, 可能后续的 protoc 命令无法识别。 确保go安装目录下的 bin 目录位于环境变量 中。比如我是默认安装go的,那么C:\Users\Chester_Zhang\go\bin 应该位于环境变量中。

如果 go get 过程中遇到了网络问题,可以更改 go proxy 为goproxy.cn,direct 。

正式开始吧

创建目录和工程

首先来看一看目录结构,目录结构也有很多坑。新建一个 grpc 目录,grpc 下面创建client, proto, server 三个目录

--grpc
    --client
    --proto
    --server

复制代码

然后进入 grpc 目录 命令行输入

go mod init grpc
go mod tidy
复制代码

这样就在 grpc 下面创建了一个 go 工程。

写写 protobuf

protobuf 是一种数据格式,和 json 类似,但是传输效率更高。在rpc中,一般使用protobuf格式的数据,就好比restful中使用json。

在 proto/chat 目录下创建chat.proto文件

syntax = "proto3";  

package proto;

option go_package="/";

// 定义 message
message ChatMessage {
  string body = 1;
}

// 定义 service
service ChatService {
  rpc SayHello(ChatMessage ) returns (ChatMessage ) {}
}
复制代码

当然你也可以取别的名字,但一定要以 .proto 后缀结尾。 这里有一个坑, 必须要写 go_package, 否则 等等生成 .go 文件时会报错。 go_package 这里我只写了一个斜杠, 大家也可以试试写其他的看看会发生什么。

写完 protobuf 之后就可以编译了。还是在 grpc 目录下进入命令行输入:

protoc --go_out=plugins=grpc:proto  --proto_path= proto/chat.proto

复制代码

这样就会在 proto 目录下生成一个 chat.pb.go 文件。先不看这个 chat.pb.go 文件里面有什么, 先来看看 上面这条命令的含义。

protoc 是命令, 最重要的有两个参数: go_out 和 proto_path。 proto_path 指定了 .proto 文件在哪里。 go_out 指定了 生成的 .pb.go 文件在哪里以及用什么插件生成, 这里我们用了 grpc 插件生成,生成目录还是在 proto 目录下。 至于为什么 要用 grpc 插件生成, 我也不知道哈,可能这个比较好?hhh。

回头来看 那个 chat.pb.go 文件,看看里面都有些什么,我只截取部分精华部分, 因为别的部分我也看不太懂了hhh

// 定义 message
type ChatMessage struct {
   state         protoimpl.MessageState
   sizeCache     protoimpl.SizeCache
   unknownFields protoimpl.UnknownFields

   Body string `protobuf:"bytes,1,opt,name=body,proto3" json:"body,omitempty"`
}

 
func NewChatServiceClient(cc grpc.ClientConnInterface) ChatServiceClient {
   return &chatServiceClient{cc}
}
 
 
// ChatServiceServer is the server API for ChatService service.
type ChatServiceServer interface {
   SayHello(context.Context, *ChatMessage) (*ChatMessage, error)
}
 
 
func (*UnimplementedChatServiceServer) SayHello(context.Context, *ChatMessage) (*ChatMessage, error) {
   return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
}
 
 
func RegisterChatServiceServer(s *grpc.Server, srv ChatServiceServer) {
   s.RegisterService(&_ChatService_serviceDesc, srv)
}
 

复制代码

来看看这里面都有啥

  • ChatMessage 这个结构体,刚刚在 chat.proto 文件中的 message 类型会被转换成 一个 go 的结构体。

  • NewChatServiceClient 返回一个 ChatService 的 client。

  • ChatServiceServer 这个接口,刚刚 chat.proto 文件中定义了一个 service叫 ChatService, 里面还有个 SayHello 方法,也被翻译成了这个接口。

  • 我们等等还要取实现一下这个 SayHello 方法,如果不实现的话,就会调用 UnimplementedChatServiceServer 里面的 SayHello 方法。

func (*UnimplementedChatServiceServer) SayHello(context.Context, *ChatMessage) (*ChatMessage, error) { return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented") }
复制代码

最后的 RegisterChatServiceServer 是用了注册一个service的,只有被注册以后才能使用这个service。

说了半天,还没有正式进入主菜。我们的 service 里面的SayHello 方法还没有实现呀。

接着在 proto 目录下新建 chat.go 文件

package __

import (
   "fmt"
   "golang.org/x/net/context"
)

type Server struct {
}

func (s *Server) SayHello(ctx context.Context, in *ChatMessage ) (*ChatMessage, error) {
   log.Printf("Receive message body from client: %s", in.Body)
   return &ChatMessage{Body: "Hello From the Server!"}, nil
}
复制代码

这里写了个 结构体,包含了 SayHello 方法, 结构体的名字你可以取其他的,但是这个 SayHello 方法可就不能改了,因为在 .proto 文件里面已经写好了,等等要关联上。

写个 server 并注册

在 server 目录下创建 server.go

package main

import (
   proto "grpc/proto"
   "google.golang.org/grpc"
   "log"
   "net"
)

func main() {

   // 监听8000 端口, 返回一个 listener 和 error
   lis, err := net.Listen("tcp", ":8000")
   if err != nil {
      log.Fatalf("Fail to listen: %v", err)
   }

   s:= proto.Server{}

   grpcServer := grpc.NewServer()

   //注册一个server
   proto.RegisterChatServiceServer(grpcServer,&s)

   // server 开始监听
   if err := grpcServer.Serve(lis); err != nil {
      log.Fatalf("Fail to serve: %v", err)
   }


}
复制代码

写个 Client 客户端

服务端写完了,写个 Client 客户端 来调用 远程的 SayHello 方法吧。 在 client 目录下创建 client.go 文件。

package main

import (
   proto "grpc/proto"
   "context"
   "google.golang.org/grpc"
   "log"
)

func main() {

   //获得一个 Client 连接
   var conn *grpc.ClientConn
   conn, err := grpc.Dial(":8000", grpc.WithInsecure())
   if err != nil {
      log.Fatalf("did not connect: %s", err)
   }
   defer conn.Close()

   // 获得一个 ChatService 的 client
   c := proto.NewChatServiceClient(conn)

   // grpc 调用远程的 SayHello
   response, err := c.SayHello(context.Background(), &proto.ChatMessage{Body: "Hello From Client!"})
   if err != nil {
      log.Fatalf("Error when calling SayHello: %s", err)
   }
   log.Printf("Response from server: %s", response.Body)

}
复制代码

运行起来

在grpc目录下开两个命令行,依次输入下面两条命令

go run server/server.go
go run client/client.go
复制代码

在 server 端 命令行会输出

2022/08/23 16:43:05 Receive message body from client: Hello From Client!
复制代码

在 client 端 命令行会输出

2022/08/23 16:43:05 Receive message body from client: Hello From Client!
复制代码

闲聊一下 分布式,微服务, rpc 与 restful

如果你去面试,面试官可能会问,为什么要用微服务呢? 微服务和分布式架构有啥不一样呢。首先从目的上来说,分布式和微服务都是一个目的,将各个模块分开来开发,分开来部署,因为单独的机器内存有限,CPU处理资源有限,总不能一直加大内存,加大算力的。而微服务的精髓在于一个 "微" 字, 所谓 "微" 就是各个模块之间的粒度足够的小。 就好比 我们上面写的 一个最简单 微服务项目,单独一个 SayHello 函数也可以领出来做一个 服务端。

看到这里你可能会问 明明有 resful了,为什么还要 rpc 呢? 这个问题问得非常好。 虽然 restful 和 rpc 都是基于 请求-响应的模型,但 restful 和 rpc 的使用场景和意义还是有很大不同的。 restful 一般用于自己这个项目去访问外部的资源, 你发一个请求, 外部资源返回一个响应。而 rpc 则一般用于一个项目之间内部模块的相互调用,强调的是像在本地调用一个函数一样访问另一个模块上的函数。比如来看刚刚我们上面写的 client 端代码有这么一行:

response, err := c.SayHello(context.Background(), &proto.ChatMessage{Body: "Hello From Client!"})
复制代码

看到没有, c.SayHello 这是不是相当于之间调用服务端的 SayHello 方法,这和 restful是不一样的, restful 只关注我往接口发请求,强调的是 发送 , 而rpc 强调的是好比服务端的函数在我手里一样,我直接拿来用, 这就是核心的区别。

看一看 rpc 原理

回头来看一下 rpc 的原理,我画了一幅图

函数A调用函数B的过程,可以用从1到10这个过程来表示。其中的序列化和反序列化就是 protobuf 格式的数据与 客户端服务端语言的数据类型转换的过程。而函数映射指的是 functionB 在在 rpc-server 中注册好,就好比 server端写过的这3行代码:

s:= proto.Server{}

grpcServer := grpc.NewServer()

//注册一个server
proto.RegisterChatServiceServer(grpcServer,&s)
复制代码

同样的在 客户端也有这么1行代码,其目的在于从 rpc client 中获得服务端的 clint。

c := proto.NewChatServiceClient(conn)
复制代码

通过 rpc-clinet 调用 functionA, 从直观来看,就好比在本地调用一样。

 

相关文章

学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习...
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面...
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生...
Can’t connect to local MySQL server through socket \'/v...
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 ...
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服...