Hello, gRPC

This post demonstrates how to print “Hello, World” using gRPC & gRPC-Web.

This post is inspired by the following works:

You can find the entire code in this repository:

Define the Service

Firstly, we define the protobuf Service and Messages like this in the api/api.proto file.

syntax = "proto3";
package api;
service Greeter {
rpc SayHello(HelloRequest) returns (HelloResponse) {}
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
string message = 1;
}

Compile the protobuf

Next, run the following command to compile the proto file. Follow the instruction here to install tools you need to compile it into the Go source code.

$ protoc -I api/ \
> -I${GOPATH}/src \
> --go_out=plugins=grpc:api \
> api/api.proto

NOTE: As of Feb 2019, if you’re using Go Modules, the generated code might raise a compile error. Try to use master branch of the protobuf library to work around.

$ go get github.com/golang/protobuf@master

Define the handler

Let’s define the handler to serve SayHello service. Save the following content as api/handler.go.

package api
import (
"context"
"fmt"
)
type Server struct{}func (s *Server) SayHello(ctx context.Context, in *HelloRequest) (*HelloResponse, error) {
return &HelloResponse{
Message: fmt.Sprintf("Hello, %s!", in.Name),
}, nil
}

This Server struct satisfies the GreeterServer interface defined in the generated file.

Implement server side

Save the following code as server/main.go.

package main
import (
"fmt"
"log"
"net"
"github.com/KentaKudo/hello-grpc/api"
"google.golang.org/grpc"
)
func main() {
// listen on port 9090
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", 9090))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// create a server
s := api.Server{}
grpcServer := grpc.NewServer()
// register Server has GreeterServer
api.RegisterGreeterServer(grpcServer, &s)
// start serving
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}

Implement client side

Client side would look like this. Save this as client/main.go.

package main
import (
"context"
"log"
"github.com/KentaKudo/hello-grpc/api"
"google.golang.org/grpc"
)
func main() {
// dial and connect
conn, err := grpc.Dial(":9090", grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
// create a client from the connection
c := api.NewGreeterClient(conn)
// call the SayHello RPC
req := &api.HelloRequest{Name: "World"}
response, err := c.SayHello(context.Background(), req)
if err != nil {
log.Fatalf("error when calling SayHello: %v", err)
}
log.Printf("response from server: %v", response.Message)
}

Build and run

Build all the source code and run both server and client.

$ go build -i -v -o bin/server github.com/KentaKudo/hello-grpc/server
$ go build -i -v -o bin/client github.com/KentaKudo/hello-grpc/client
$ bin/server &
$ bin/client
2019/02/19 19:13:26 response from server: Hello, World!

gRPC-Web

One of the hottest topics around gRPC is the web support. You can find the details and benefits we can take from it here.

Implement client JavaScript

Implement the js file as follows and save as client.js. Note the port number is 8080 not 9090 because we place a proxy on this port. I’ll cover it later.

const {HelloRequest, HelloResponse} = require('./api/api_pb.js')
const {GreeterClient} = require('./api/api_grpc_web_pb.js')
// create a client specifying host
var client = new GreeterClient('http://localhost:8080')
// create a request
var request = new HelloRequest();
request.setName('World')
// send a request and print
client.sayHello(request, {}, (err, response) => {
document.write(response.getMessage());
});

And we use the following package.json file.

{
"name": "hello-grpc",
"version": "0.1.0",
"description": "Hello, World in gRPC",
"devDependencies": {
"google-protobuf": "^3.6.1",
"grpc": "^1.15.0",
"grpc-web": "^1.0.0",
"webpack": "^4.16.5",
"webpack-cli": "^3.1.0"
}
}

Also, prepare the following index.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello gRPC</title>
<script src="./dist/main.js"></script>
</head>
<body></body>
</html>

Build client

Run the following command to compile proto into js files. See the details here to install js protobuf plugins.

$ protoc -I=. api/api.proto \
> --js_out=import_style=commonjs:. \
> --grpc-web_out=import_style=commonjs,mode=grpcwebtext:.

And compile client.js with webpack.

$ npm install
$ npx webpack client.js

Proxy

Because of the constraints of the browser APIs, gRPC-Web is a bit different from gRPC HTTP/2, and a translation proxy is needed to have client and server communicate each other. The official recommendation is to use Envoy. The details are explained here.

Grab the Envoy configuration file here and save as envoy.yaml.

For the convenience, I’m going to host it in Docker specified as the following envoy.Dockerfile.

FROM envoyproxy/envoy:latest
COPY ./envoy.yaml /etc/envoy/envoy.yaml
CMD /usr/local/bin/envoy -c /etc/envoy/envoy.yaml

Because Envoy communicates with the gRPC server, they need to be in the same network. To realise it, I’m going to move the server inside Docker with this server.Dockerfile.

ARG GO_VERSION=1.11FROM golang:${GO_VERSION}-alpine AS builder
RUN mkdir /user && \
echo 'nobody:x:65534:65534:nobody:/:' > /user/passwd && \
echo 'nobody:x:65534:' > /user/group
RUN apk add --no-cache git
WORKDIR /src
COPY ./go.mod ./go.sum ./
RUN go mod download
COPY ./ ./
RUN CGO_ENABLED=0 go build \
-installsuffix 'static' \
-o /app \
github.com/KentaKudo/hello-grpc/server
FROM scratch AS final
COPY --from=builder /user/group /user/passwd /etc/
COPY --from=builder /app /app
EXPOSE 9090
USER nobody:nobody
ENTRYPOINT [ "/app" ]

And host it along with Envoy using docker-compose.yaml.

version: "3"
services:
server:
build:
context: .
dockerfile: server.Dockerfile
ports:
- "9090:9090"
proxy:
build:
context: .
dockerfile: envoy.Dockerfile
depends_on:
- server
ports:
- "8080:8080"

Before docker-compose up, you need a small fix in envoy.yaml file as follows:

$ diff envoy.yaml envoy.yaml.org 
45c45
< hosts: [{ socket_address: { address: server, port_value: 9090 }}]
---
> hosts: [{ socket_address: { address: localhost, port_value: 9090 }}]

Finally, run the containers with $ docker-compose up.

Serve the client

Of course you can host the client in Docker as well, but for brevity I’m going to run it simply with the following command.

$ python -m SimpleHTTPServer 8081

Try access localhost:8081 to see if the gRPC communication successes.

--

--