gRPC是一个高性能、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计,带来诸如双向流、流控、头部压缩、单 TCP 连接上的多复用请求等特性,更省电和节省空间占用。
本文主要讲述如何快速搭建一个grpc服务,附带支持http接口以及swagger文档。

定义服务

示例使用项目名为grpc-demo,在 helloworld/simple.proto 文件内定义服务。

syntax = "proto3";

import "google/api/annotations.proto";

option go_package = "grpc-demo/helloworld";

package helloworld;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {
	option (google.api.http) ={
          post:"/v1/helloworld/sayHello"
          body:"*"
      };
}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

环境准备

没安装protobuf的需自行安装,mac下安装比较方便:brew install protobuf,也可下载源码后编译安装。

准备grpc工具:

go get -u google.golang.org/grpc
go get -u google.golang.org/protobuf/cmd/protoc-gen-go
go get -u google.golang.org/grpc/cmd/protoc-gen-go-grpc
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger

go install google.golang.org/protobuf/cmd/protoc-gen-go
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc
go install github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
go install github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger

转换HTTP接口需要依赖 googleapis 库 ,先把库准备好:
go get github.com/googleapis/googleapis
或者
git clone https://github.com/googleapis/googleapis.git $GOPATH/src/github.com/googleapis

然后找到googleapis库在本地存放的位置,可能是$GOPATH/src/github.com/googleapis/googleapis

google.golang.org 若无法下载的折中方法:

// 非go mod模式:
git clone https://github.com/grpc/grpc-go.git $GOPATH/src/google.golang.org/grpc

// go mod模式:
go mod edit -replace=google.golang.org/grpc=github.com/grpc/grpc-go@latest

自动生成代码

用 protobuf 编译器生成服务器和客户端代码:

protoc -I . \
    -I $GOPATH/src/github.com/googleapis/googleapis \
    --go_out . --go_opt paths=source_relative \
    --go-grpc_out . --go-grpc_opt paths=source_relative \
    --grpc-gateway_out . --grpc-gateway_opt logtostderr=true --grpc-gateway_opt paths=source_relative \
    --swagger_out . --swagger_opt logtostderr=true \
    simple.proto

注意 $GOPATH/bin 要提前加到PATH环境变量中去,否则工具会找不到。
这里已经一次性把GRPC结构代码、HTTP的Restful接口、Swagger格式文件都生成好了,后面会补充使用说明。

服务实现

使用 gRPC 的 Go API 为你的服务实现一个简单的客户端和服务器。

client.go

// Package main implements a client for Greeter service.
package main

import (
	"context"
	pb "grpc-demo/helloworld"
	"log"
	"os"
	"time"

	"google.golang.org/grpc"
)

const (
	address     = "localhost:50051"
	defaultName = "world"
)

func main() {
	// Set up a connection to the server.
	conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := pb.NewGreeterClient(conn)

	// Contact the server and print out its response.
	name := defaultName
	if len(os.Args) > 1 {
		name = os.Args[1]
	}
	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
	defer cancel()
	for i := 0; i < 10; i++ {
		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())
		time.Sleep(1 * time.Second)
	}
}

server.go

// Package main implements a server for Greeter service.
package main

import (
	"context"
	"grpc-demo/gateway"
	pb "grpc-demo/helloworld"
	"log"
	"net"

	"google.golang.org/grpc"
)

const (
	port = ":50051"
)

// server is used to implement helloworld.GreeterServer.
type server struct {
	pb.UnimplementedGreeterServer
}

// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	log.Printf("Received: %v", in.GetName())
	return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}

func main() {
	lis, err := net.Listen("tcp", port)
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
	s := grpc.NewServer()
	pb.RegisterGreeterServer(s, &server{})
	log.Printf("GRPC server listening at %v", lis.Addr())
	go func() {
		listener, err := net.Listen("tcp", "127.0.0.1:8000")
		if err != nil {
			log.Fatalf("net.Listen err: %v", err)
		}
		//使用gateway把grpcServer转成httpServer
		httpServer := gateway.ProvideHTTP("127.0.0.1:8000", s)
		if err = httpServer.Serve(listener); err != nil {
			log.Fatal("HTTP ListenAndServe: ", err)
		}
	}()
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

gateway/gateway.go

package gateway

import (
	"context"
	"log"
	"net/http"
	"strings"

	pb "grpc-demo/helloworld"

	"github.com/grpc-ecosystem/grpc-gateway/runtime"
	"golang.org/x/net/http2"
	"golang.org/x/net/http2/h2c"
	"google.golang.org/grpc"
)

// ProvideHTTP 把gRPC服务转成HTTP服务,让gRPC同时支持HTTP
func ProvideHTTP(endpoint string, grpcServer *grpc.Server) *http.Server {
	ctx := context.Background()
	dopts := []grpc.DialOption{grpc.WithInsecure()}
	//新建gwmux,它是grpc-gateway的请求复用器。它将http请求与模式匹配,并调用相应的处理程序。
	gwmux := runtime.NewServeMux()
	//将服务的http处理程序注册到gwmux。处理程序通过endpoint转发请求到grpc端点
	err := pb.RegisterGreeterHandlerFromEndpoint(ctx, gwmux, endpoint, dopts)
	if err != nil {
		log.Fatalf("Register Endpoint err: %v", err)
	}
	//新建mux,它是http的请求复用器
	mux := http.NewServeMux()
	//注册gwmux
	mux.Handle("/", gwmux)
	// 设置静态目录
	fsh := http.FileServer(http.Dir("swagger"))
	mux.Handle("/swagger/", http.StripPrefix("/swagger/", fsh))
	log.Println(endpoint + " HTTP.Listing ...")
	return &http.Server{
		Addr:    endpoint,
		Handler: grpcHandlerFunc(grpcServer, mux),
	}
}

// grpcHandlerFunc 根据不同的请求重定向到指定的Handler处理
func grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler {
	return h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
			grpcServer.ServeHTTP(w, r)
		} else {
			otherHandler.ServeHTTP(w, r)
		}
	}), &http2.Server{})
}

扩展HTTP接口

上面示例中已包含此功能,注意查看server.go的如下代码段,增加了8000作为http的服务端口

    go func() {
		listener, err := net.Listen("tcp", "127.0.0.1:8000")
		if err != nil {
			log.Fatalf("net.Listen err: %v", err)
		}
		//使用gateway把grpcServer转成httpServer
		httpServer := gateway.ProvideHTTP("127.0.0.1:8000", s)
		if err = httpServer.Serve(listener); err != nil {
			log.Fatal("HTTP ListenAndServe: ", err)
		}
	}()

扩展swagger文档

  1. 下载swagger-ui
    git clone https://github.com/swagger-api/swagger-ui,把dist目录下的所有文件拷贝我们项目的swagger/目录下。

  2. http设置静态目录
    注意gateway/gateway.go如下代码段

    fsh := http.FileServer(http.Dir("swagger"))
	mux.Handle("/swagger/", http.StripPrefix("/swagger/", fsh))
  1. 将自动生成的simple.swagger.json覆盖到swagger/swagger.json文件
  2. 修改swagger/index.html文件,url换成 url: "swagger.json"

编译运行

编译客户端: go build -o c client.go
编译服务端: go build -o s server.go

启动服务端: ./s
启动客户端: ./c

运行效果

# ./s
2021/07/17 13:27:38 GRPC server listening at [::]:50051
2021/07/17 13:27:38 127.0.0.1:8000 HTTP.Listing ...
2021/07/17 13:30:58 Received: world
2021/07/17 13:30:59 Received: world
2021/07/17 13:31:00 Received: world
2021/07/17 13:31:01 Received: world
2021/07/17 13:31:02 Received: world
2021/07/17 13:31:03 Received: world
2021/07/17 13:31:04 Received: world
2021/07/17 13:31:05 Received: world
2021/07/17 13:31:06 Received: world
2021/07/17 13:31:07 Received: world
# ./c
2021/07/17 13:30:58 Greeting: Hello world
2021/07/17 13:30:59 Greeting: Hello world
2021/07/17 13:31:00 Greeting: Hello world
2021/07/17 13:31:01 Greeting: Hello world
2021/07/17 13:31:02 Greeting: Hello world
2021/07/17 13:31:03 Greeting: Hello world
2021/07/17 13:31:04 Greeting: Hello world
2021/07/17 13:31:05 Greeting: Hello world
2021/07/17 13:31:06 Greeting: Hello world
2021/07/17 13:31:07 Greeting: Hello world

浏览器访问: http://127.0.0.1:8000/swagger/

至此,你已经学会如何快速搭建一个grpc服务了,赶快动手试试吧。
还有,别忘了补习下 proto3 的配置语法喔。

关于我

name: yison.li
blog: http://yyeer.com
github: https://github.com/yisonli