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

时间:2024-10-05 07:24:47

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

安装 grpc , protoc 工具 和 protobuf

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

  1. go get /grpc
  2. go get -u /golang/protobuf/proto
  3. go get -u /golang/protobuf/protoc-gen-go
  4. 复制代码

光有这几个还是不够的,还得要去下载 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 为,direct 。

正式开始吧

创建目录和工程

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

  1. --grpc
  2. --client
  3. --proto
  4. --server
  5. 复制代码

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

  1. go mod init grpc
  2. go mod tidy
  3. 复制代码

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

写写 protobuf

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

在 proto/chat 目录下创建文件

  1. syntax = "proto3";
  2. package proto;
  3. option go_package="/";
  4. // 定义 message
  5. message ChatMessage {
  6. string body = 1;
  7. }
  8. // 定义 service
  9. service ChatService {
  10. rpc SayHello(ChatMessage ) returns (ChatMessage ) {}
  11. }
  12. 复制代码

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

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

  1. protoc --go_out=plugins=grpc:proto --proto_path= proto/
  2. 复制代码

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

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

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

  1. // 定义 message
  2. type ChatMessage struct {
  3. state
  4. sizeCache
  5. unknownFields
  6. Body string `protobuf:"bytes,1,opt,name=body,proto3" json:"body,omitempty"`
  7. }
  8. func NewChatServiceClient(cc ) ChatServiceClient {
  9. return &chatServiceClient{cc}
  10. }
  11. // ChatServiceServer is the server API for ChatService service.
  12. type ChatServiceServer interface {
  13. SayHello(, *ChatMessage) (*ChatMessage, error)
  14. }
  15. func (*UnimplementedChatServiceServer) SayHello(, *ChatMessage) (*ChatMessage, error) {
  16. return nil, status.Errorf(, "method SayHello not implemented")
  17. }
  18. func RegisterChatServiceServer(s *, srv ChatServiceServer) {
  19. (&_ChatService_serviceDesc, srv)
  20. }
  21. 复制代码

来看看这里面都有啥

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

  • NewChatServiceClient 返回一个 ChatService 的 client。

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

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

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

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

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

接着在 proto 目录下新建 文件

  1. package __
  2. import (
  3. "fmt"
  4. "/x/net/context"
  5. )
  6. type Server struct {
  7. }
  8. func (s *Server) SayHello(ctx , in *ChatMessage ) (*ChatMessage, error) {
  9. ("Receive message body from client: %s", )
  10. return &ChatMessage{Body: "Hello From the Server!"}, nil
  11. }
  12. 复制代码

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

写个 server 并注册

在 server 目录下创建

  1. package main
  2. import (
  3. proto "grpc/proto"
  4. "/grpc"
  5. "log"
  6. "net"
  7. )
  8. func main() {
  9. // 监听8000 端口, 返回一个 listener 和 error
  10. lis, err := ("tcp", ":8000")
  11. if err != nil {
  12. ("Fail to listen: %v", err)
  13. }
  14. s:= {}
  15. grpcServer := ()
  16. //注册一个server
  17. (grpcServer,&s)
  18. // server 开始监听
  19. if err := (lis); err != nil {
  20. ("Fail to serve: %v", err)
  21. }
  22. }
  23. 复制代码

写个 Client 客户端

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

  1. package main
  2. import (
  3. proto "grpc/proto"
  4. "context"
  5. "/grpc"
  6. "log"
  7. )
  8. func main() {
  9. //获得一个 Client 连接
  10. var conn *
  11. conn, err := (":8000", ())
  12. if err != nil {
  13. ("did not connect: %s", err)
  14. }
  15. defer ()
  16. // 获得一个 ChatService 的 client
  17. c := (conn)
  18. // grpc 调用远程的 SayHello
  19. response, err := ((), &{Body: "Hello From Client!"})
  20. if err != nil {
  21. ("Error when calling SayHello: %s", err)
  22. }
  23. ("Response from server: %s", )
  24. }
  25. 复制代码

运行起来

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

  1. go run server/server.go
  2. go run client/client.go
  3. 复制代码

在 server 端 命令行会输出

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

在 client 端 命令行会输出

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

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

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

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

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

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

看一看 rpc 原理

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

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

  1. s:= {}
  2. grpcServer := ()
  3. //注册一个server
  4. (grpcServer,&s)
  5. 复制代码

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

  1. c := (conn)
  2. 复制代码

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