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! 🙏 🙏 🙏 🙏