原文:medium.com/@amsokol.co…mysql
在Part1中咱们已经构建了一个gPRC的服务端以及客户端,本章介绍如何在gRPC服务端中加入HTTP/REST接口提供服务。完整的part2代码戳这里git
为了加入HEEP/REST接口我打算用一个很是好的库grpc-gateway。下面有一篇很棒的文章来介绍更多关于grpc-gateway的工做原理。github
首先咱们安装grpc-gateway以及swagger文档生成器插件golang
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
复制代码
grpc-gateway会被安装在GOPATH/src/github.com/grpc-ecosystem/grpc-gateway文件下。咱们须要将里面的third_party/googleapis/google文件拷贝到咱们目录的third_party/google下,而且建立protoc-gen-swagger/options文件夹在third_party文件夹内sql
mkdir -p third_party\protoc-gen-swagger\options
复制代码
而后将GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger/options中的annotations.proto和openapiv2.proto文件复制到咱们项目中的third_party\protoc-gen-swagger/options下json
当前咱们的文件目录以下图: api
运行命令bash
go get -u github.com/golang/protobuf/protoc-gen-go
复制代码
接下来将REST的注解文件引入到api/proto/v1/todo-service.proto中app
syntax = "proto3";
package v1;
import "google/protobuf/timestamp.proto";
import "google/api/annotations.proto";
import "protoc-gen-swagger/options/annotations.proto";
option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = {
info: {
title: "ToDo service";
version: "1.0";
contact: {
name: "go-grpc-http-rest-microservice-tutorial project";
url: "https://github.com/amsokol/go-grpc-http-rest-microservice-tutorial";
email: "medium@amsokol.com";
};
};
schemes: HTTP;
consumes: "application/json";
produces: "application/json";
responses: {
key: "404";
value: {
description: "Returned when the resource does not exist.";
schema: {
json_schema: {
type: STRING;
}
}
}
}
};
// Task we have to do
message ToDo {
// Unique integer identifier of the todo task
int64 id = 1;
// Title of the task
string title = 2;
// Detail description of the todo task
string description = 3;
// Date and time to remind the todo task
google.protobuf.Timestamp reminder = 4;
}
// Request data to create new todo task
message CreateRequest{
// API versioning: it is my best practice to specify version explicitly
string api = 1;
// Task entity to add
ToDo toDo = 2;
}
// Contains data of created todo task
message CreateResponse{
// API versioning: it is my best practice to specify version explicitly
string api = 1;
// ID of created task
int64 id = 2;
}
// Request data to read todo task
message ReadRequest{
// API versioning: it is my best practice to specify version explicitly
string api = 1;
// Unique integer identifier of the todo task
int64 id = 2;
}
// Contains todo task data specified in by ID request
message ReadResponse{
// API versioning: it is my best practice to specify version explicitly
string api = 1;
// Task entity read by ID
ToDo toDo = 2;
}
// Request data to update todo task
message UpdateRequest{
// API versioning: it is my best practice to specify version explicitly
string api = 1;
// Task entity to update
ToDo toDo = 2;
}
// Contains status of update operation
message UpdateResponse{
// API versioning: it is my best practice to specify version explicitly
string api = 1;
// Contains number of entities have beed updated
// Equals 1 in case of succesfull update
int64 updated = 2;
}
// Request data to delete todo task
message DeleteRequest{
// API versioning: it is my best practice to specify version explicitly
string api = 1;
// Unique integer identifier of the todo task to delete
int64 id = 2;
}
// Contains status of delete operation
message DeleteResponse{
// API versioning: it is my best practice to specify version explicitly
string api = 1;
// Contains number of entities have beed deleted
// Equals 1 in case of succesfull delete
int64 deleted = 2;
}
// Request data to read all todo task
message ReadAllRequest{
// API versioning: it is my best practice to specify version explicitly
string api = 1;
}
// Contains list of all todo tasks
message ReadAllResponse{
// API versioning: it is my best practice to specify version explicitly
string api = 1;
// List of all todo tasks
repeated ToDo toDos = 2;
}
// Service to manage list of todo tasks
service ToDoService {
// Read all todo tasks
rpc ReadAll(ReadAllRequest) returns (ReadAllResponse){
option (google.api.http) = {
get: "/v1/todo/all"
};
}
// Create new todo task
rpc Create(CreateRequest) returns (CreateResponse){
option (google.api.http) = {
post: "/v1/todo"
body: "*"
};
}
// Read todo task
rpc Read(ReadRequest) returns (ReadResponse){
option (google.api.http) = {
get: "/v1/todo/{id}"
};
}
// Update todo task
rpc Update(UpdateRequest) returns (UpdateResponse){
option (google.api.http) = {
put: "/v1/todo/{toDo.id}"
body: "*"
additional_bindings {
patch: "/v1/todo/{toDo.id}"
body: "*"
}
};
}
// Delete todo task
rpc Delete(DeleteRequest) returns (DeleteResponse){
option (google.api.http) = {
delete: "/v1/todo/{id}"
};
}
}
复制代码
你能够点这里查看更多的Swagger注解文件在proto文件内的用法tcp
建立api/swagger/v1文件
mkdir -p api\swagger\v1
复制代码
经过如下命令更新third_party/protoc-gen.cmd文件的内容
protoc --proto_path=api/proto/v1 --proto_path=third_party --go_out=plugins=grpc:pkg/api/v1 todo-service.proto
protoc --proto_path=api/proto/v1 --proto_path=third_party --grpc-gateway_out=logtostderr=true:pkg/api/v1 todo-service.proto
protoc --proto_path=api/proto/v1 --proto_path=third_party --swagger_out=logtostderr=true:api/swagger/v1 todo-service.proto
复制代码
进入go-grpc-http-rest-microservice-tutorial文件运行如下命令
.\third_party\protoc-gen.cmd
复制代码
它会更新pkg/api/v1/todo-service.pb.go文件以及建立两个新的文件:
当前咱们的项目结构以下:
以上就是将REST注解加入API定义文件的步骤
建立server.go文件在pkg/protocol/rest下以及如下内容
package rest
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"time"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"google.golang.org/grpc"
"github.com/amsokol/go-grpc-http-rest-microservice-tutorial/pkg/api/v1"
)
// RunServer runs HTTP/REST gateway
func RunServer(ctx context.Context, grpcPort, httpPort string) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
mux := runtime.NewServeMux()
opts := []grpc.DialOption{grpc.WithInsecure()}
if err := v1.RegisterToDoServiceHandlerFromEndpoint(ctx, mux, "localhost:"+grpcPort, opts); err != nil {
log.Fatalf("failed to start HTTP gateway: %v", err)
}
srv := &http.Server{
Addr: ":" + httpPort,
Handler: mux,
}
// graceful shutdown
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
for range c {
// sig is a ^C, handle it
}
_, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
_ = srv.Shutdown(ctx)
}()
log.Println("starting HTTP/REST gateway...")
return srv.ListenAndServe()
}
复制代码
真实场景你须要配置HTPPS网关,例子参考这里
接下来更新pkg/cmd/server.go文件去开启HTTP网关
package cmd
import (
"context"
"database/sql"
"flag"
"fmt"
// mysql driver
_ "github.com/go-sql-driver/mysql"
"github.com/amsokol/go-grpc-http-rest-microservice-tutorial/pkg/protocol/grpc"
"github.com/amsokol/go-grpc-http-rest-microservice-tutorial/pkg/protocol/rest"
"github.com/amsokol/go-grpc-http-rest-microservice-tutorial/pkg/service/v1"
)
// Config is configuration for Server
type Config struct {
// gRPC server start parameters section
// GRPCPort is TCP port to listen by gRPC server
GRPCPort string
// HTTP/REST gateway start parameters section
// HTTPPort is TCP port to listen by HTTP/REST gateway
HTTPPort string
// DB Datastore parameters section
// DatastoreDBHost is host of database
DatastoreDBHost string
// DatastoreDBUser is username to connect to database
DatastoreDBUser string
// DatastoreDBPassword password to connect to database
DatastoreDBPassword string
// DatastoreDBSchema is schema of database
DatastoreDBSchema string
}
// RunServer runs gRPC server and HTTP gateway
func RunServer() error {
ctx := context.Background()
// get configuration
var cfg Config
flag.StringVar(&cfg.GRPCPort, "grpc-port", "", "gRPC port to bind")
flag.StringVar(&cfg.HTTPPort, "http-port", "", "HTTP port to bind")
flag.StringVar(&cfg.DatastoreDBHost, "db-host", "", "Database host")
flag.StringVar(&cfg.DatastoreDBUser, "db-user", "", "Database user")
flag.StringVar(&cfg.DatastoreDBPassword, "db-password", "", "Database password")
flag.StringVar(&cfg.DatastoreDBSchema, "db-schema", "", "Database schema")
flag.Parse()
if len(cfg.GRPCPort) == 0 {
return fmt.Errorf("invalid TCP port for gRPC server: '%s'", cfg.GRPCPort)
}
if len(cfg.HTTPPort) == 0 {
return fmt.Errorf("invalid TCP port for HTTP gateway: '%s'", cfg.HTTPPort)
}
// add MySQL driver specific parameter to parse date/time
// Drop it for another database
param := "parseTime=true"
dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?%s",
cfg.DatastoreDBUser,
cfg.DatastoreDBPassword,
cfg.DatastoreDBHost,
cfg.DatastoreDBSchema,
param)
db, err := sql.Open("mysql", dsn)
if err != nil {
return fmt.Errorf("failed to open database: %v", err)
}
defer db.Close()
v1API := v1.NewToDoServiceServer(db)
// run HTTP gateway
go func() {
_ = rest.RunServer(ctx, cfg.GRPCPort, cfg.HTTPPort)
}()
return grpc.RunServer(ctx, v1API, cfg.GRPCPort)
}
复制代码
你须要清楚的一点是HTTP网关是对gRPC服务的一个封装。个人测试显示会增长1-3毫秒的开销。
当前目录结构以下:
建立cmd/client-rest/main.go文件以及如下内容,戳我
当前目录结构以下:
最后一步来确保HTTP/REST网关能正常工做:
开启终端build和run HTTP/REST网关的gRPC服务
cd cmd/server
go build .
server.exe -grpc-port=9090 -http-port=8080 -db-host=<HOST>:3306 -db-user=<USER> -db-password=<PASSWORD> -db-schema=<SCHEMA>
复制代码
若是看到
2018/09/15 21:08:21 starting HTTP/REST gateway...
2018/09/09 08:02:16 starting gRPC server...
复制代码
意味这咱们的服务已经正常启动了,这时打开另外一个终端build以及run HTTP/REST客户端
cd cmd/client-rest
go build .
client-rest.exe -server=http://localhost:8080
复制代码
若是看到输出
2018/09/15 21:10:05 Create response: Code=200, Body={"api":"v1","id":"24"}
2018/09/15 21:10:05 Read response: Code=200, Body={"api":"v1","toDo":{"id":"24","title":"title (2018-09-15T18:10:05.3600923Z)","description":"description (2018-09-15T18:10:05.3600923Z)","reminder":"2018-09-15T18:10:05Z"}}
2018/09/15 21:10:05 Update response: Code=200, Body={"api":"v1","updated":"1"}
2018/09/15 21:10:05 ReadAll response: Code=200, Body={"api":"v1","toDos":[{"id":"24","title":"title (2018-09-15T18:10:05.3600923Z) + updated","description":"description (2018-09-15T18:10:05.3600923Z) + updated","reminder":"2018-09-15T18:10:05Z"}]
}
2018/09/15 21:10:05 Delete response: Code=200, Body={"api":"v1","deleted":"1"}
复制代码
全部东西都生效了!
这就是Part2全部的介绍,在本章咱们在gRPC的服务端上创建了HTTP/REST服务,全部的代码能够查看此处
接下来Part3咱们将介绍如何在咱们的服务当中加入一些中间件(日志打印与跟踪)
感谢收看!🙏🙏🙏🙏