Part 1 of 3 – GO: API Dialing GRPC Endpoints –Setting Up The Development Container And Building The Services
The purpose of this article is to demonstrate how to setup a development environment that is containerized and has multiple microservices in a mono-repo that is all debugable with breakpoints at each point of the call chain from the HTTP Request, to the GRPC Request, the GRPC Response, and the HTTP Response.
Overview
In this overview we will be creating 3 microservices:
- An HTTP API
- A Strings Service
- A Numbers Service
We will be defining the Strings and Numbers service through Protobuff files and then generate server code. The HTTP API will implement the necessary client side code to communicate with these GRPC services.
We want to setup the Strings Service to convert an input string to a fully Upper-Case or fully Lower-Case string.
We want to setup the Numbers service to accept two numbers and then Add them or Subtract them.
We want the API to have the following HTTP endpoints and then communicate with the Strings or Numbers service as appropriate over GRPC and then return the GRPC response as the HTTP Response body:
- api/StringsService/MakeUpperCase
- api/StringsService/MakeLowerCase
- api/NumbersService/AddTwoNumbers
- api/NumbersService/SubtractTwoNumbers
We also want to be able to debug the entire operation from the HTTP Request, to the GRPC Request, to the Service Operations, to the GRPC Response, and the final HTTP Response.
Part 1: Setting Up The Development Container And Building The Services
Part 2: Running Each Service In Its Own Docker Container — Link
Part 3: Debug Each Service While It Is Running In Its Own Docker Container — Link
Part 1 – Setting Up The Development Container And Building The Services
To get started let’s create a development container in visual studio.
1 – Create a new folder on your computer. I will call mine “api_dialing_grpc”, and then open the folder in Visual Studio Code
2 – Do a CTRL+SHIFT+P to open the command pallet in Visual Studio Code and select the “Dev Containers: “Add Dev Container Configuration Files…” option
4 – Choose the “Go” configuration
6 – Select the “lts/*” Node.js version
7 – Check the boxes for Docker (Moby) support (Docker-in-Docker) and GitHub CLI
This should give your /.devcontainer/devcontainer.json file to have the following code
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/go
{
"name": "Go",
"build": {
"dockerfile": "Dockerfile",
"args": {
// Update the VARIANT arg to pick a version of Go: 1, 1.19, 1.18
// Append -bullseye or -buster to pin to an OS version.
// Use -bullseye variants on local arm64/Apple Silicon.
"VARIANT": "1.18",
// Options
"NODE_VERSION": "lts/*"
}
},
"runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ],
// Configure tool-specific properties.
"customizations": {
// Configure properties specific to VS Code.
"vscode": {
// Set *default* container specific settings.json values on container create.
"settings": {
"go.toolsManagement.checkForUpdates": "local",
"go.useLanguageServer": true,
"go.gopath": "/go"
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"golang.Go"
]
}
},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "go version",
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "vscode",
"features": {
"docker-in-docker": "latest",
"github-cli": "latest"
}
}
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/go/.devcontainer/base.Dockerfile
# [Choice] Go version (use -bullseye variants on local arm64/Apple Silicon): 1, 1.19, 1.18, 1-bullseye, 1.19-bullseye, 1.18-bullseye, 1-buster, 1.19-buster, 1.18-buster
ARG VARIANT="1.19-bullseye"
FROM mcr.microsoft.com/vscode/devcontainers/go:0-${VARIANT}
# [Choice] Node.js version: none, lts/*, 18, 16, 14
ARG NODE_VERSION="none"
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>
# [Optional] Uncomment the next lines to use go get to install anything else you need
# USER vscode
# RUN go get -x <your-dependency-or-tool>
# [Optional] Uncomment this line to install global node packages.
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1
Create a GO Workspace
Do a CTRL+SHIFT+` to bring up the command prompt. Execute the following command:
go work init
Create a new folder called “protos”
mkdir protos
go mod init github.com/woodman231/api_dialing_grpc/protos
Assuming that your command prompt is at /workspaces/api_dialing_grpc/protos, the commands will be:
cd ..
go work use ./protos
go 1.18
use ./protos
module github.com/woodman231/api_dialing_grpc/protos
go 1.18
Create PROTO Files for the Microservices
We are now ready to define the shape of our microservices.
In the “protos” folder create a new file called “string_service.proto”. Give it the following code:
syntax = "proto3";
package string_services;
option go_package = "github.com/woodman231/api_dialing_grpc/protos/stringspb";
message OperationRequest {
string input_string = 1;
}
message OperationResult {
string output_string = 1;
}
service StringService {
rpc MakeUpperCase(OperationRequest) returns (OperationResult);
rpc MakeLowerCase(OperationRequest) returns (OperationResult);
}
syntax = "proto3";
package number_services;
option go_package = "github.com/woodman231/api_dialing_grpc/protos/numberspb";
message OperationRequest {
int32 input_number_one = 1;
int32 input_number_two = 2;
}
message OperationResult {
int32 output_number = 1;
}
service NumberService {
rpc AddTwoNumbers(OperationRequest) returns (OperationResult);
rpc SubtractTwoNumbers(OperationRequest) returns (OperationResult);
}
Let’s go over these files. The string_service.proto file added an extra option to tell the proto compiler that the go_package for this will be github.com/woodman231/api_dialing_grpc/protos/stringspb. It then defines a message for an OperationRequest which will serve as the standard input model for our functions. It also creates an OperationResult message that will serve as the standard model for the output of our functions. Finally we define a service with two rpc functions: MakeUpperCase that will take in a parameter of OperationRequest and return an OperationResult; and a MakeLowerCase function that will take in an OperationRequest and return an OperationResult.
The number_service.proto file is very similar. We are telling the proto compiler what go package we expect to run. We have an OperationRequest that will serve as the standard message for the input parameter of our functions and an OperationResult message that will serve as the standard return of our functions. This time we are defining two numbers as properties for our OperationRequest. The service is then defined with two functions AddTwoNumbers and SubtractTwoNumbers. Again, each of them takes in an OperationRequest message and returns an OperationResult message.
Now that we have defined what our microservices are going to look like it is time to compile them to GO files that our Server code can use.
Before we can do that, we must install the proto compiler.
Install the PROTO Compiler into your Development Container
We will also need the proto compile plugins for go and gRPC. Update lines 11 through 18 in your /.devcontainer/Dockerfile to have the following code
# [Optional] Uncomment this section to install additional OS packages.
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends protobuf-compiler
# [Optional] Uncomment the next lines to use go get to install anything else you need
USER vscode
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
RUN go install google.golang.org/grpc/cmd/protoc-gen-go-gRPC@v1.2
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/go/.devcontainer/base.Dockerfile
# [Choice] Go version (use -bullseye variants on local arm64/Apple Silicon): 1, 1.19, 1.18, 1-bullseye, 1.19-bullseye, 1.18-bullseye, 1-buster, 1.19-buster, 1.18-buster
ARG VARIANT="1.19-bullseye"
FROM mcr.microsoft.com/vscode/devcontainers/go:0-${VARIANT}
# [Choice] Node.js version: none, lts/*, 18, 16, 14
ARG NODE_VERSION="none"
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
# [Optional] Uncomment this section to install additional OS packages.
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends protobuf-compiler
# [Optional] Uncomment the next lines to use go get to install anything else you need
USER vscode
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
RUN go install google.golang.org/grpc/cmd/protoc-gen-go-gRPC@v1.2
# [Optional] Uncomment this line to install global node packages.
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1
After your dev container has been rebuilt just type “protoc” in the terminal to confirm that the installation completed successfully.
Compile the PROTO Files to Files that GO understands
In the terminal change directories such that you are in the “/workspaces/api_dialing_grpc/protos” folder
Execute the following command in that folder
mkdir stringspb
protoc --go_out=./stringspb/ --go_opt=paths=source_relative --go-gRPC_out=./stringspb/ --go-gRPC_opt=paths=source_relative ./string_service.proto
We then executed the proto compiler and provided parameters to it
- go_out tells the proto compiler to output the service client side code to ./stringspb/ folder
- go_opt tells the proto compiler that any imported (which there aren’t any in this case) proto files are in the same folder as the proto file that we are compiling
- go_gRPC_out tells the proto compiler to output the server side code to ./stringspb/ folder
- go_gRPC_opt tells the proto compiler that any import (which there aren’t any in thise case) proto files are in the same folder as the proto file that we are compiling
This will create two new files
- /workspaces/api_dialing_grpc/protos/stringspb/string_service_grpc.pb.go
- /workspaces/api_dialing_grpc/protos/stringspb/string_service.pb.go
When you first open these files in Visual Studio code it will complain about being unable to use dependencies.
In a terminal that is at /workspaces/api_dialing_grpc/protos execute the following command
go get -d ./…
At this point things should start looking normal.
Let’s go ahead and create the proto files for our numbers_service as well and then continue.
Assuming that your command prompt is still at “/workspaces/api_dialing_grpc/protos” execute the following commands:
mkdir numberspb
protoc --go_out=./numberspb/ --go_opt=paths=source_relative --go-gRPC_out=./numberspb/ --go-gRPC_opt=paths=source_relative ./number_service.proto
- /workspaces/api_dialing_grpc/protos/numberspb/number_service_grpc.pb.go
- /workspaces/api_dialing_grpc/protos/numberspb/number_service.pb.go
Since you installed the dependencies earlier. Things should look just fine in your Visual Studio Code.
Create Build Tasks to compile the PROTO Files in the future
The commands to compile the proto buff files are long. Thankfully Visual Studio Code allows us to create Build Tasks. In the .vscode folder add a new file called tasks.json. Give it the following code
{
"version": "2.0.0",
"tasks": [
{
"label": "Compile Strings Service Protobuff",
"type": "shell",
"command": [
"protoc",
"--go_out=./stringspb/",
"--go_opt=paths=source_relative",
"--go-gRPC_out=./stringspb/",
"--go-gRPC_opt=paths=source_relative",
"string_service.proto"
],
"options": {
"cwd": "${workspaceFolder}/protos"
}
},
{
"label": "Compile Numbers Service Protobuff",
"type": "shell",
"command": [
"protoc",
"--go_out=./numberspb/",
"--go_opt=paths=source_relative",
"--go-gRPC_out=./numberspb/",
"--go-gRPC_opt=paths=source_relative",
"./number_service.proto"
],
"options": {
"cwd": "${workspaceFolder}/protos"
}
}
]
}
- /protos/numberspb/number_service_grpc.pb.go
- /protos/numberspb/number_service.pb.go
- /protos/stringspb/string_service_grpc.pb.go
- /protos/stringspb/string_service.pb.go
Then do a CTRL+SHIFT+P to bring up a command pallet and then select the option to “Tasks: Run Task” and then select the option to do the “Compile Strings Service Protobuff”, and then do it again to select the “Compile Numbers Service Protobuff” and the files should re-appear.
Build the Strings Service
In a command prompt at “/workspaces/api_dialing_grpc”, execute the following commands to create a new strings_service module and add it to the go workspace
mkdir strings_service
cd strings_service
go mod init github.com/woodman231/api_dialing_grpc/strings_service
cd ..
go work use ./strings_service
The go.work file should now look like this
go 1.18
use (
./protos
./strings_service
)
module github.com/woodman231/api_dialing_grpc/strings_service
go 1.18
replace github.com/woodman231/api_dialing_grpc/protos => ../protos
Create a “main.go” file in the “strings_service” folder. Give it the following code for now:
package main
import (
"flag"
"log"
pb "github.com/woodman231/api_dialing_grpc/protos/stringspb"
)
var (
port = flag.Int("port", 50051, "The server port")
)
// server is used to implment StringsServiceServer
type server struct {
pb.UnimplementedStringServiceServer
}
func main() {
flag.Parse()
log.Printf("Will listen at %v", *port)
}
We then set up a struct to implement the StringsServiceServer. We use a variable that will come in via a flag “–port” when launching this program to tell the service what port to listen to requests on. And for now we just say what port we will be listening on.
If you change directories in a terminal to /workspaces/api_dialing_grpc/strings_service and do a go run main.go you will see that it says what port it will listen at.
To determine what we need to have in our file we need to read the /protos/stringspb/string_service_grpc.pb.go file. You will see that there is a portion of the file that looks like this:
In our current version of go.main we imported this package as pb and so these will need to be defined with that.
Starting at line 19 of main.go add the following code:
// MakeUpperCase implments StringsServiceServer.MakeUpperCase
func (s *server) MakeUpperCase(context.Context, *pb.OperationRequest) (*pb.OperationResult, error) {
}
What we did here is add a MakeUpperCase function to our server struct that has the same signature defined in the interface of the protobuff file. Our signature includes the extra *pb… portion because we imported the package as pb. Complete this function by giving it the following code:
// MakeUpperCase implments StringsServiceServer.MakeUpperCase
func (s *server) MakeUpperCase(ctx context.Context, in *pb.OperationRequest) (*pb.OperationResult, error) {
requestedString := in.GetInputString()
log.Printf("Received: MakeUpperCase request for %v", requestedString)
return &pb.OperationResult{
OutputString: strings.ToUpper(requestedString),
}, nil
}
What we are doing in this function is setting the results of in.GetInputString to a variable. Logging the requestedString Variable, then setting the OutputString field of the OperationResult to an upper case version of the requested string and returning that object, and a nil error message.
Now add the MakeLowerCase function by adding the following code the main.go file:
// MakeLowerCase implements StringsSErviceServer.MakeLowerCase
func (s *server) MakeLowerCase(ctx context.Context, in *pb.OperationRequest) (*pb.OperationResult, error) {
requestedString := in.GetInputString()
log.Printf("Received: MakeLowerCase request for %v", requestedString)
return &pb.OperationResult{
OutputString: strings.ToLower(requestedString),
}, nil
}
func main() {
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := gRPC.NewServer()
pb.RegisterStringServiceServer(s, &server{})
log.Printf("server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Your entire main.go file should look like this:
package main
import (
"context"
"flag"
"fmt"
"log"
"net"
"strings"
"google.golang.org/grpc"
pb "github.com/woodman231/api_dialing_grpc/protos/stringspb"
)
var (
port = flag.Int("port", 50051, "The server port")
)
// server is used to implment StringsServiceServer
type server struct {
pb.UnimplementedStringServiceServer
}
// MakeUpperCase implments StringsServiceServer.MakeUpperCase
func (s *server) MakeUpperCase(ctx context.Context, in *pb.OperationRequest) (*pb.OperationResult, error) {
requestedString := in.GetInputString()
log.Printf("Received: MakeUpperCase request for %v", requestedString)
return &pb.OperationResult{
OutputString: strings.ToUpper(requestedString),
}, nil
}
// MakeLowerCase implements StringsServiceServer.MakeLowerCase
func (s *server) MakeLowerCase(ctx context.Context, in *pb.OperationRequest) (*pb.OperationResult, error) {
requestedString := in.GetInputString()
log.Printf("Received: MakeLowerCase request for %v", requestedString)
return &pb.OperationResult{
OutputString: strings.ToLower(requestedString),
}, nil
}
func main() {
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := gRPC.NewServer()
pb.RegisterStringServiceServer(s, &server{})
log.Printf("server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Test the Strings service
There are a couple of tools out there to test GRPC requests. I will use Postman in this demonstration.
In Postman create a new workspace called “API Dialing GRPC” (or anything else that you would like to call it) and make it a personal Workspace.
Click on the “Import Definition” button
Click on the “Choose Files” button
Browse on your computer to where you have the string_service.proto file
Click on “Collections” on the left
Click on the “New” button and not the “Create Collection” button
In the method drop down select the “strings_service” api, and then the MakeUpperCase method.
Your screen should look something like this.
go mod tidy
go run main.go
Return to Postman and click on Invoke.
Your Postman should now look like this:
Let’s save this Request in Postman to our strings_service collection by clicking on the “Save” button in Postman, and then clicking on “Create Collection” name the collection “strings_service”, and name the Request “MakeUpperCase”
In the area where the strings_service collection is hover over it and click the three dots and select Add Request and the gRPC Request option.
Not bad. Now let’s go ahead and turn off the Strings service and make the Numbers Service. In the Visual Studio Code Terminal press CTRL+C to stop the strings_service application.
Build the Numbers Service
This is very similar to how we built the strings_service so I will be giving a little bit of instructions and code, but not much of an explanation as last time
Create a new folder for /workspaces/api_dialing_grpc/numbers_service
Initialize the module
Include the module in the workspace
From the /workspaces/api_dialing_grpc command prompt
mkdir numbers_service
cd numbers_service
go mod init github.com/woodman231/api_dialing_grpc/numbers_service
cd ..
go work use ./numbers_service
module github.com/woodman231/api_dialing_grpc/numbers_service
go 1.18
replace github.com/woodman231/api_dialing_grpc/protos => ../protos
package main
import (
"context"
"flag"
"fmt"
"log"
"net"
"google.golang.org/grpc"
pb "github.com/woodman231/api_dialing_grpc/protos/numberspb"
)
var (
port = flag.Int("port", 50052, "The server port")
)
// server is used to implement NumbersServiceServer
type server struct {
pb.UnimplementedNumberServiceServer
}
// AddTwoNumbers implements NumbersServiceServer.AddTwoNumbers
func (s *server) AddTwoNumbers(ctx context.Context, in *pb.OperationRequest) (*pb.OperationResult, error) {
numberOne := in.GetInputNumberOne()
numberTwo := in.GetInputNumberTwo()
log.Printf("Received: AddTwoNumbers Request %v, %v", numberOne, numberTwo)
result := numberOne + numberTwo
return &pb.OperationResult{
OutputNumber: result,
}, nil
}
// SubtractTwoNumbers implments NumbersServiceServer.SubtractTwoNumbers
func (s *server) SubtractTwoNumbers(ctx context.Context, in *pb.OperationRequest) (*pb.OperationResult, error) {
numberOne := in.GetInputNumberOne()
numberTwo := in.GetInputNumberTwo()
log.Printf("Received: SubtractTwoNumbers Request %v, %v", numberOne, numberTwo)
result := numberOne - numberTwo
return &pb.OperationResult{
OutputNumber: result,
}, nil
}
func main() {
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := gRPC.NewServer()
pb.RegisterNumberServiceServer(s, &server{})
log.Printf("server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Test the Numbers Service
In a command prompt at /workspaces/api_dialing_grpc/numbers_service execute the following commands
go mod tidy
go run main.go
In your Postman and in the Workspace that you created earlier create a new API called numbers_service and import the .proto file from your computer for the numbers_service
Then create a new gRPC request
This time the url wil be gRPC://localhost:50052, and the API you will select is the numbers_service and the AddTwoNumbers method
Generate an example and Invoke it
Your Postman should look something like this.
Create a new request for the SubtractTwoNumbers method and save that request in to the numbers_service collection as well.
When your done testing it out return to Visual Studio Code and do a CTRL+C to stop the numbers_service
Test the Strings and Numbers Service at the same time
With a command prompt open in Visual Studio Code you are able to have multiple instances. Assuming that you have one running now via CTRL+SHIFT+`, you can click on the + button to add a new instance.
go run main.go
Your strings_service terminal should look something like this:
Build the API / HTTP Service
Starting with a command prompt at /workspaces/api_dialing_grpc execute the following commands:
mkdir api
cd api
go mod init github.com/woodman231/api_dialing_grpc/api
cd ..
go work use ./api
Your go.work file should now look like this:
go 1.18
use (
./api
./numbers_service
./protos
./strings_service
)
module github.com/woodman231/api_dialing_grpc/api
go 1.18
replace github.com/woodman231/api_dialing_grpc/protos => ../protos
Create a file in /api called main.go. Give it the following code:
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
stringspb "github.com/woodman231/api_dialing_grpc/protos/stringspb"
)
var (
strings_server_host = flag.String("strings_server_host", "localhost", "The host name for the strings_server service")
strings_server_port = flag.Int("strings_server_port", 50051, "The port for the strings_server service")
)
func main() {
flag.Parse()
stringsServerConnectionString := fmt.Sprintf("%v:%v", *strings_server_host, *strings_server_port)
log.Printf("stringsServerConnectionString: %v", stringsServerConnectionString)
// Connect to the Strings Service
stringsServiceConnection, err := gRPC.Dial(stringsServerConnectionString, gRPC.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect to strings gRPC server")
}
defer stringsServiceConnection.Close()
stringServiceClient := stringspb.NewStringServiceClient(stringsServiceConnection)
// Create Mux Server
muxServer := http.NewServeMux()
muxServer.HandleFunc("/api/hello", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World"))
})
muxServer.HandleFunc("/api/StringService/MakeUpperCase", func(w http.ResponseWriter, r *http.Request) {
// Create a variable for the String Service OperationRequest
var stringOperationRequest stringspb.OperationRequest
// Decode the HTTP Request Body and Store the decoded object in the stringOperationRequest variable
err := json.NewDecoder(r.Body).Decode(&stringOperationRequest)
// If we are not able to decode the JSON that was provided in the request body then let the person know
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// If the input_string property was not set or was empty, then let the requester know and do not make the GRPC Request
if stringOperationRequest.InputString == "" {
http.Error(w, "Missing input_string, or input_string is empty", http.StatusBadRequest)
return
}
ctx := context.Background()
// Call the MakeUpperCase method of the stringServiceClient over GRPC
gRPCResponse, err := stringServiceClient.MakeUpperCase(ctx, &stringOperationRequest)
// If we couldn't connect to the GRPC Service let the requester know
if err != nil {
log.Printf("Could not get response from GRPC")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Marshal the response to JSON to reply with in the Response Body
marshaledResponse, err := json.Marshal(gRPCResponse)
// If we couldn't marshal the response, then let the requester know
if err != nil {
log.Printf("Could not marshal response from GRPC")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Set the content-type of the response to application/json
// Write the marshaledResponse to the HTTP Response body
w.Header().Set("Content-Type", "application/json")
w.Write(marshaledResponse)
})
fmt.Println("Listening on port 8080")
http.ListenAndServe(":8080", muxServer)
}
In the beginning we set some parameters for the strings_server host name, and the strings_server port number. This means that we can execute this command with parameters like “–strings_server_host=localhost”
We read the flags and build a connection string to the String Service GRPC Server
We then connect to the GRPC Service and create a client from the StringsServiceConnection.
We then create a MuxServer which is the basic HTTP Handler that is built in to the GO Framework.
We add a Handler to the MuxServer for “/api/hello” as an endpoint that we can use to just be sure basic communications over HTTP work (not necessary for production)
We then add a Handler to the MuxSErver for “/api/StringService/MakeUpperCase”. This function will read the request body, and decode it in to a StringOperationsRequest struct (Remember we defined this as a Message type in the protobuff file and the protobuff compiler created a struct for us in the strings_service_pb.go file). We then validate the request body that we got, and if it is valid we send it along to the GRPC Client Method of MakeUpperCase which will return either a valid response or an error. If an error arises, then we return the error, otherwise we attempt to deserialize the response to text and write the response to the HTTP Response body after setting the Content-Type of the HTTP Response to “application/json”.
Test the API / HTTP Service
Test the API / HTTP Service
With what we have in place we should now be able to get responses at
- /api/hello
- /api/StringService/MakeUpperCase
These are case sensitive routes.
We will need to use the multiple terminals trick in Visual Studio code that we discussed earlier. Be sure to run “go run main.go” both in “/workspaces/api_dialing_grpc/api” and “/workspaces/api_dialing_grpc/string_service”.
Using Postman create a new HTTP Request to localhost:8080/api/hello using the GET method
If all is well, then you should get something like this:
Your Postman workspace should look something like this now:
Go to the body tab and select the raw radio button. Use the following body:
{
"input_string": "hElLo WoRlD"
}
Your Postman should look something like this:
Do the CTRL+C in each terminal when you are done testing.
Refactor the API / HTTP Service
Of course, we might be tempted to just make additional MuxServer HandleFunc’s for each Method in our service, but we do want our code to be as DRY (Don’t Repeat Yourself) as possible.
Where we run in to a little bit of a problem is that when we use the GRPC Client it does expect the input to be of a specific type when using any of the methods. Currently the arguments are the same for both of the String service methods and both of the Number service methods. What is the same though is that we are getting an HTTP Request in, calling the appropriate GRPC Client method and then writing the GRPC response to the HTTP Response body.
Furthermore, when the protobuff compiler created the files for us they created a special ServiceDesciption object. So we can use the information in this object for both services to build our routes.
The method that I came up with was to create an APIService struct that had 3 properties. One of the properties is for the StringServiceClient. Another property is for the NumberServiceCLient. The last property is for the GO / MUX Http Server.
While creating those services I assigned them a value to a new instance of this struct. I created a RegisterRoutes method for that struct so that it could take advantage of the three properties mentioned before. I also created a registerRoutesForService method which takes in a GRPC Service Description object. It takes the service name and methods and registers patterns for each method name. The handleRequest method takes basically the same code path as the single request route we discussed earlier; however, where things differ to actually call the method, they are wrapped in switch statements for the service being called and the method being called.
Modify the /api/main.go file as follows:
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"strings"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
numberspb "github.com/woodman231/api_dialing_grpc/protos/numberspb"
stringspb "github.com/woodman231/api_dialing_grpc/protos/stringspb"
)
var (
strings_server_host = flag.String("strings_server_host", "localhost", "The host name for the strings_server service")
strings_server_port = flag.Int("strings_server_port", 50051, "The port for the strings_server service")
numbers_server_host = flag.String("numbers_server_host", "localhost", "The host name for the numbers_server service")
numbers_server_port = flag.Int("numbers_server_port", 50052, "The port for the numbers_server port")
)
type APIService struct {
StringsServiceClient stringspb.StringServiceClient
NumbersServiceClient numberspb.NumberServiceClient
MuxServer *http.ServeMux
}
func main() {
flag.Parse()
var apiService APIService
stringsServerConnectionString := fmt.Sprintf("%v:%v", *strings_server_host, *strings_server_port)
numbersServerConnectionString := fmt.Sprintf("%v:%v", *numbers_server_host, *numbers_server_port)
log.Printf("stringsServerConnectionString: %v", stringsServerConnectionString)
log.Printf("numbersServerConnectionString: %v", numbersServerConnectionString)
// Connect to the Strings Service
stringsServiceConnection, err := gRPC.Dial(stringsServerConnectionString, gRPC.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect to strings gRPC server")
}
defer stringsServiceConnection.Close()
stringServiceClient := stringspb.NewStringServiceClient(stringsServiceConnection)
apiService.StringsServiceClient = stringServiceClient
// Connect to the Numbers Service
numbersServiceConnection, err := gRPC.Dial(numbersServerConnectionString, gRPC.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect to numbers gRPC server")
}
defer numbersServiceConnection.Close()
numbersServiceClient := numberspb.NewNumberServiceClient(numbersServiceConnection)
apiService.NumbersServiceClient = numbersServiceClient
// Create Mux Server
muxServer := http.NewServeMux()
apiService.MuxServer = muxServer
apiService.RegisterRoutes()
fmt.Println("Listening on port 8080")
http.ListenAndServe(":8080", muxServer)
}
func (a *APIService) RegisterRoutes() {
// Register a hello world route to be sure that the service is running
a.MuxServer.HandleFunc("/api/hello", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World"))
})
// Register routes for each service
a.registerRoutesForService(stringspb.StringService_ServiceDesc)
a.registerRoutesForService(numberspb.NumberService_ServiceDesc)
}
func (a *APIService) registerRoutesForService(serviceDesc gRPC.ServiceDesc) {
// Shorten the service name
shortServiceName := getShortServiceName(serviceDesc.ServiceName)
// For each method in the service description create a route for the API
for i := range serviceDesc.Methods {
methodName := serviceDesc.Methods[i].MethodName
pattern := fmt.Sprintf("/api/%v/%v", shortServiceName, methodName)
fmt.Printf("Registering: %v.%v @ %v\n", shortServiceName, methodName, pattern)
a.MuxServer.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
a.handleRequest(shortServiceName, methodName, w, r)
})
}
}
func getShortServiceName(currentServiceName string) string {
words := strings.Split(currentServiceName, ".")
wordCount := len(words)
if wordCount > 0 {
return words[wordCount-1]
}
return words[0]
}
func (a *APIService) handleRequest(serviceName string, methodName string, w http.ResponseWriter, r *http.Request) {
log.Printf("Received request for %v", r.URL.String())
// Set Up Some Variables to Hold our gRPCResponse, and possible gRPCError
var gRPCResponse interface{}
var gRPCErr error
switch serviceName {
case "StringService":
var stringOperationRequest stringspb.OperationRequest
// Convert the HTTP Request Body to a StringService OperationRequest
decodeErr := json.NewDecoder(r.Body).Decode(&stringOperationRequest)
// Validate the request
if decodeErr != nil {
http.Error(w, decodeErr.Error(), http.StatusBadRequest)
return
}
if stringOperationRequest.InputString == "" {
http.Error(w, "Missing input_string, or input_string is empty", http.StatusBadRequest)
return
}
// Use the decoded request body with the appropriate method that was passed in
switch methodName {
case "MakeUpperCase":
gRPCResponse, gRPCErr = a.StringsServiceClient.MakeUpperCase(context.Background(), &stringOperationRequest)
case "MakeLowerCase":
gRPCResponse, gRPCErr = a.StringsServiceClient.MakeLowerCase(context.Background(), &stringOperationRequest)
default:
http.Error(w, "Not Implemented", http.StatusInternalServerError)
return
}
case "NumberService":
var numberOperationRequest numberspb.OperationRequest
// Convert the HTTP Request Body to a NumbersSErvice OperationRequest
decodeErr := json.NewDecoder(r.Body).Decode(&numberOperationRequest)
// Validate the request
if decodeErr != nil {
http.Error(w, decodeErr.Error(), http.StatusBadRequest)
return
}
if numberOperationRequest.InputNumberOne == 0 && numberOperationRequest.InputNumberTwo == 0 {
http.Error(w, "Either input_number_one or input_number_two are missing or they are both 0", http.StatusBadRequest)
return
}
// Use the decoded request body with the appropriate method that was passed in
switch methodName {
case "AddTwoNumbers":
gRPCResponse, gRPCErr = a.NumbersServiceClient.AddTwoNumbers(context.Background(), &numberOperationRequest)
case "SubtractTwoNumbers":
gRPCResponse, gRPCErr = a.NumbersServiceClient.SubtractTwoNumbers(context.Background(), &numberOperationRequest)
default:
http.Error(w, "Not Implemented", http.StatusInternalServerError)
return
}
}
// Write the error from the GRPC Client if applicable
if gRPCErr != nil {
log.Print("Could not get response from GRPC")
http.Error(w, gRPCErr.Error(), http.StatusInternalServerError)
return
}
// Write the response from the GRPC Client if applicable
marshaledResponse, marshalErr := json.Marshal(gRPCResponse)
if marshalErr != nil {
log.Printf("Could not marshal response from GRPC")
http.Error(w, marshalErr.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(marshaledResponse)
}
Test the API / HTTP, String, and Number Services at the same time
Use the multiple terminal trick in Visual Studio code that we talked about earlier to have 3 terminals open. Execute “go run main.go” inside of each service (/workspaces/api_dialing_grpc/api, /workspaces/api_dialing_grpc/strings_service, /workspaces/api_dialing_grpc/numbers_service).
When they are all running use Postman to create a new HTTP Request to localhost:8080/api/StringService/MakeLowerCase, it will be a POST again, and use the same body that you used from the MakeUpperCase method.
Your Postman request should look like this:
{
"input_number_one": 8080,
"input_number_two": 5050
}
In Postman create another HTTP Request to localhost:8080/api/NumbersService/SubtractTwoNumbers. Give it the same body as you gave the AddTwoNumbers method. Feel free to use different numbers.
Your collection in Postman should look something like this
Conclusion
Thus far we have built our individual services and a proxy to accept HTTP requests and translate those to gRPC requests. Each service is running as it’s own microservice on our development environment host. Next up we will set up these microservices to run in their own Docker Container.
Part 1: Setting Up The Development Container And Building The Services
Part 2: Running Each Service In Its Own Docker Container — Link
Part 3: Debug Each Service While It Is Running In Its Own Docker Container — Link
About Intertech
Intertech is a Software Development Consulting Firm that provides single and multiple turnkey software development teams, available on your schedule and configured to achieve success as defined by your requirements independently or in co-development with your team. Intertech teams combine proven full-stack, DevOps, Agile-experienced lead consultants with Delivery Management, User Experience, Software Development, and QA experts in Business Process Automation (BPA), Microservices, Client- and Server-Side Web Frameworks of multiple technologies, Custom Portal and Dashboard development, Cloud Integration and Migration (Azure and AWS), and so much more. Each Intertech employee leads with the soft skills necessary to explain complex concepts to stakeholders and team members alike and makes your business more efficient, your data more valuable, and your team better. In addition, Intertech is a trusted partner of more than 4000 satisfied customers and has a 99.70% “would recommend” rating.