Original text: medium.com/@amsokol.co…
We have built a gPRC server and client in Part1. This chapter describes how to add HTTP/REST interface to the gRPC server to provide services. The full Part2 code is here
To add the HEEP/REST interface I’m going to use a very nice library grPC-Gateway. Here’s a great article that explains more about how GRPC-Gateway works.
Setp1: Add REST annotations to the API definition file
First we install grPC-Gateway and the Swagger document Generator plug-in
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
Copy the code
GRPC – gateway will be installed under the GOPATH/src/github.com/grpc-ecosystem/grpc-gateway file. We need to copy the third_party/googleapis/ Google file into our directory third_party/ Google and create protoc-gen-swagger/options folder in third_party folder
mkdir -p third_party\protoc-gen-swagger\options
Copy the code
Then the annotations in GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger/options. Proto and openapiv2. Proto file copy to me Third_party \protoc-gen-swagger/options in our project
Our current file directory is as follows:
Run the command
go get -u github.com/golang/protobuf/protoc-gen-go
Copy the code
Next, import the REST annotation file into API /proto/v1/todo-service.proto
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: "[email protected]";
};
};
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 todo
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}"}; }}Copy the code
You can see more Swagger annotations in proto here
Create API /swagger/v1 file
mkdir -p api\swagger\v1
Copy the code
Run the following command to update the contents of the third_party/protoc-gen. CMD file
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
Copy the code
Go to the go-grpc-HTTP-rest-microservice-tutorial file and run the following command
.\third_party\protoc-gen.cmd
Copy the code
It updates the PKG/API /v1/todo-service.pb.go file and creates two new files:
- PKG \ API \v1\todo-service.pb.gw.go — REST/HTTP skeleton generation
- API \ Swagger \v1\todo-service.swagger. Json — Swagger file generation
Our current project structure is as follows:
That’s how you add REST annotations to your API definition file
Step2: Create an HTTP startup gateway
Create the server.go file under PKG /protocol/ REST and the following
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(a) {
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()
}
Copy the code
In a real scenario you would need to configure HTPPS gateway, see here for an example
Next update the PKG/CMD /server.go file to enable the HTTP gateway
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(a) 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)
iferr ! =nil {
return fmt.Errorf("failed to open database: %v", err)
}
defer db.Close()
v1API := v1.NewToDoServiceServer(db)
// run HTTP gateway
go func(a) {
_ = rest.RunServer(ctx, cfg.GRPCPort, cfg.HTTPPort)
}()
return grpc.RunServer(ctx, v1API, cfg.GRPCPort)
}
Copy the code
What you need to understand is that the HTTP gateway is a wrapper around the gRPC service. My tests show 1-3 milliseconds of overhead.
The current directory structure is as follows:
Step3: create an HTTP/REST client
Create CMD /client-rest/main.go and the following, poke me
The current directory structure is as follows:
The final step to ensure that the HTTP/REST gateway works:
Enable the BUILD and run gRPC services of HTTP/REST gateways on terminals
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>
Copy the code
If you see
2018/09/15 21:08:21 starting HTTP/REST gateway...
2018/09/09 08:02:16 starting gRPC server...
Copy the code
This means that our service has started normally, then open another terminal build and run HTTP/REST client
cd cmd/client-rest
go build .
client-rest.exe -server=http://localhost:8080
Copy the code
If you see the output
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":"The title (the 2018-09-15 T18:10:05. 3600923 z)"."description":"Description (T18:2018-09-15 10:05. 3600923 z)"."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":"The title (the 2018-09-15 T18:10:05. 3600923 z) + updated"."description":"Description (T18:2018-09-15 10:05. 3600923 z) + updated"."reminder":"2018-09-15T18:10:05Z"}]
}
2018/09/15 21:10:05 Delete response: Code=200, Body={"api":"v1"."deleted":"1"}
Copy the code
Everything is working!
The last
That’s all Part2 is about, in this chapter we set up HTTP/REST services on the server side of gRPC, all the code can be seen here
Next in Part3 we will describe how to incorporate some middleware (log printing and tracing) into our services.
Thanks for watching! 🙏 🙏 🙏 🙏