手把手教你写一个 go gRPC 的入门教程

这篇文章将手把手教你写一个 go 语言的 gRPC 服务以及客户端教材,不管你是一个新手,还是一个有一点经验的求学者,我想这篇文章都让你耳目一新,它在官方教材基础上加上了一些具有实战意义的用法。虽然在实战的项目中,我们并不一定会用纯粹的 gRPC 服务(比如生产环境中使用的多的 go-micro 框架,它是一套完整的框架,可集成 gRPC 服务),但是对于将来理解,灵活的使用 go-micro 框架,那么了解一点 gRPC 是很有必要的。
 

gRPC 简介


gRPC 是一个高性能、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计。目前提供 C、Java 和 Go 语言版本,分别是:grpc, grpc-java, grpc-go. 其中 C 版本支持 C, C++, Node.js, Python, Ruby, Objective-C, PHP 和 C# 支持。
 
gRPC 基于 HTTP/2 标准设计,带来诸如双向流、流控、头部压缩、单 TCP 连接上的多复用请求等特。这些特性使得其在移动设备上表现更好,更省电和节省空间占用。它默认使用 protocol buffers 作为接口定义语言,来描述服务接口和有效载荷消息结构。 如果您不熟悉 protocol buffers,可以阅读——Protocol Buffer 中文入门使用文档

步骤


  1. 安装 Protocol Buffers v3(protoc 可执行文件),它还需要一个 protoc 插件 protoc-gen-go
  2. 写 .proto 文件,生成 go 源码
  3. 安装 gRPC,这里安装的仅仅是一个 github 的仓库而已
  4. 写一个服务端
  5. 写一个测试的客户端
 

Protocol Buffers


下载 protoc,去到下载页面找到对应包
https://github.com/protocolbuffers/protobuf/releases
windows:下载 protoc-3.10.0-win64.zip
linux:protoc-3.10.0-linux-x86_64.zip
解压可以得到一个 protoc 文件,你可以把它放到你的 GOPATH 路径的 bin 目录下面
比如我本机:C:\golang\bin\protoc.exe,我本地配置的 GOPATH 是 C:\golang,然后再将 C:\golang\bin 配置到环境变量
 
还需要下载安装 protoc 插件,执行如下命令,它会在你的 GOPATH 路径的 bin 目录下生成一个 protoc-gen-go 可执行文件,C:\golang\bin\protoc-gen-go.exe
go get -u github.com/golang/protobuf/protoc-gen-go
 

写 .proto 文件


messages.proto
syntax = "proto3";

package messages;
option go_package = "github.com/iissy/gogrpc/messages";

message HelloRequest {
    string name = 1;
}

message HelloReply {
    string message = 1;
}
这里与官方例子不同就是将消息体独立到一个 .proto,包名是messages,下面 import 它的 helloworld.proto 文件需要用到,同时这里用到 option go_package,表示其他包要使用这里的message,就引用这个包名,相当于 go_package 是重写代替 package。比如 helloworld.proto 导入了这个文件,见 helloworld.proto 生成的 go 文件 helloworld.pb.go,源码地址:https://github.com/iissy/gogrpc/blob/master/helloworld/helloworld.pb.go
 
helloworld.proto
syntax = "proto3";

import "github.com/iissy/gogrpc/messages/messages.proto";
 
package helloworld;

service Greeter {
  rpc SayHello (messages.HelloRequest) returns (messages.HelloReply) {}
}
 这里使用 import 导入了一个 message.proto ,它即包含意思(文件前面的路径是文件目录,F:\github.com\iissy\gogrpc\messages\messages.proto),这样可以把一个很大的 .proto 文件拆封成多个,分布到不同包下面。对于构建一个大项目,微服务群,中台很有用。明白了这个使用方法,就可以任意摆布这些 .proto,加上被 import 包的包名即可,比如 messages.HelloRequest
 
看 helloworld.pb.go 文件的 import,里面包含 messages,它就是第一个 messages.proto 对应生成的包,后面会介绍生成
 

生成 go 源码


编写一个 *.bat 文件
@rem Generate the Go code for .proto files

setlocal

@rem enter this directory
cd /d %~dp0

set TOOLS_PATH=C:\golang\bin
%TOOLS_PATH%\protoc -I../../../ -I=. --go_out=../../../ ./messages/messages.proto
%TOOLS_PATH%\protoc -I../../../ -I=. --go_out=plugins=grpc:. ./helloworld/helloworld.proto

endlocal
如果你将 protoc 所在目录像我上面介绍那样配置到环境变量,就可以直接执行 protoc 命令,无需指定它的目录
另:两行 protoc 执行语句稍有不同,第一行没有 plugins=grpc:,这是因为 messages.proto 文件并无包含任何 RPC 接口,可以不需要 grpc 插件
这是可以看见在两个 .proto 所在目录下分别生成了一个同名的 *.pb.go 文件,如何生成到对应的目录,请参考:Protocol Buffer 中文入门使用文档
 

服务 


package main

import (
   "log"
   "net"

   "github.com/iissy/gogrpc/helloworld"
   "github.com/iissy/gogrpc/messages"
   "golang.org/x/net/context"
   "google.golang.org/grpc"
   "google.golang.org/grpc/reflection"
)

const (
   port = ":50052"
)

type server struct{}

func (s *server) SayHello(ctx context.Context, in *messages.HelloRequest) (*messages.HelloReply, error) {
   return &messages.HelloReply{Message: "Hello " + in.Name}, nil
}

func main() {
   lis, err := net.Listen("tcp", port)
   if err != nil {
      log.Fatalf("failed to listen: %v", err)
   }

   s := grpc.NewServer()
   helloworld.RegisterGreeterServer(s, &server{})
   reflection.Register(s)
   if err := s.Serve(lis); err != nil {
      log.Fatalf("failed to serve: %v", err)
   }
}
 

客服端


package main

import (
   "log"
   "os"

   "github.com/iissy/gogrpc/helloworld"
   "github.com/iissy/gogrpc/messages"

   "golang.org/x/net/context"
   "google.golang.org/grpc"
)

const (
   address     = "0.0.0.0:50052"
   defaultName = "world"
)

func main() {
   conn, err := grpc.Dial(address, grpc.WithInsecure())
   if err != nil {
      log.Fatalf("did not connect: %v", err)
   }
   defer conn.Close()
   c := helloworld.NewGreeterClient(conn)

   name := defaultName
   if len(os.Args) > 1 {
      name = os.Args[1]
   }
   r, err := c.SayHello(context.Background(), &messages.HelloRequest{Name: name})
   if err != nil {
      log.Fatalf("could not greet: %v", err)
   }
   log.Printf("Greeting: %s", r.Message)
}
 

运行


启动服务
F:\github.com\iissy\gogrpc> go run main.go 
客户端测试
F:\github.com\iissy\gogrpc\client> go run main.go
2020/02/27 11:58:48 Greeting: Hello world
Posted by 何敏 on 2020-02-28 00:53:29