17/09/2020 - GO
In this example we are going to create gRPC unary middleware/interceptor for client and server Golang applications. The concept is same as creating middleware for HTTP router and HTTP server.
The example is simple. Client will use timer
interceptor so that we know how long it takes to get the response. It will then also use identity
interceptor to set its identity. This will be checked by server with its interceptor called verify
. Server also has a interceptor called log
which just logs something.
├── Makefile
├── client
│ └── main.go
├── go.mod
├── hello.pb.go
├── hello.proto
├── interceptor
│ ├── Identity.go
│ ├── log.go
│ ├── timer.go
│ └── verify.go
├── message
│ ├── client.go
│ └── server.go
└── server
└── main.go
Run make compile
to generate hello.pb.go
file.
.PHONY: compile
compile:
protoc --go_out=plugins=grpc:. --go_opt=paths=source_relative hello.proto
.PHONY: client
client:
go run --race client/main.go
.PHONY: server
server:
go run --race server/main.go
syntax = "proto3";
package message;
option go_package = "generated;generated";
message MessageRequest {
string text = 1;
}
message MessageResponse {
string text = 1;
}
service MessageService {
rpc SendMessage (MessageRequest) returns (MessageResponse) {}
}
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
"sport/interceptor"
"sport/message"
)
func main() {
log.Println("client")
opts := []grpc.DialOption{
grpc.WithInsecure(),
grpc.WithChainUnaryInterceptor(
interceptor.TimerUnaryClient,
interceptor.Identity{ID: "client-1"}.UnaryClient,
),
}
conn, err := grpc.Dial(":50051", opts...)
if err != nil {
log.Fatalln(err)
}
defer conn.Close()
messageClient := message.NewClient(conn, time.Second*10)
res, err := messageClient.SendMessage(context.Background(), "Hello")
log.Println("ERRR:", err)
log.Println("RESP:", res)
}
package interceptor
import (
"context"
"log"
"time"
"google.golang.org/grpc"
)
func TimerUnaryClient(
ctx context.Context,
method string,
req interface{},
reply interface{},
cc *grpc.ClientConn,
invoker grpc.UnaryInvoker,
opts ...grpc.CallOption,
) error {
start := time.Now()
log.Println("Timer: 1")
time.Sleep(time.Second)
err := invoker(ctx, method, req, reply, cc, opts...)
log.Println("Timer: 2")
time.Sleep(time.Second)
end := time.Since(start)
log.Printf("%s method call took %s\n", method, end)
return err
}
package interceptor
import (
"context"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
type Identity struct {
ID string
}
func (i Identity) UnaryClient(
ctx context.Context,
method string,
req interface{},
reply interface{},
cc *grpc.ClientConn,
invoker grpc.UnaryInvoker,
opts ...grpc.CallOption,
) error {
md := metadata.Pairs()
md.Set("client-id", i.ID)
ctx = metadata.NewOutgoingContext(ctx, md)
log.Println("Identity: 1")
time.Sleep(time.Second)
err := invoker(ctx, method, req, reply, cc, opts...)
log.Println("Identity: 2")
time.Sleep(time.Second)
return err
}
package message
import (
"context"
"time"
"google.golang.org/grpc"
generated "sport"
)
type Client struct {
messageClient generated.MessageServiceClient
timeout time.Duration
}
func NewClient(conn grpc.ClientConnInterface, timeout time.Duration) Client {
return Client{
messageClient: generated.NewMessageServiceClient(conn),
timeout: timeout,
}
}
func (c Client) SendMessage(ctx context.Context, message string) (*generated.MessageResponse, error) {
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(c.timeout))
defer cancel()
return c.messageClient.SendMessage(ctx, &generated.MessageRequest{Text: message})
}
package main
import (
"log"
"net"
"google.golang.org/grpc"
"sport/interceptor"
"sport/message"
generated "sport"
)
func main() {
log.Println("server")
listener, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalln(err)
}
opts := []grpc.ServerOption{
grpc.ChainUnaryInterceptor(
interceptor.VerifyUnaryServer,
interceptor.LogUnaryServer,
),
}
grpcServer := grpc.NewServer(opts...)
messageServer := message.NewServer()
generated.RegisterMessageServiceServer(grpcServer, messageServer)
log.Fatalln(grpcServer.Serve(listener))
}
package interceptor
import (
"context"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
func VerifyUnaryServer(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (resp interface{}, err error) {
log.Println("Verify: 1")
time.Sleep(time.Second)
md, ok := metadata.FromIncomingContext(ctx)
if !ok || len(md["client-id"]) == 0 {
return nil, status.Errorf(codes.InvalidArgument, "missing metadata")
}
if md["client-id"][0] != "client-1" {
return nil, status.Error(codes.PermissionDenied, "unexpected client")
}
res, err := handler(ctx, req)
log.Println("Verify: 2")
time.Sleep(time.Second)
return res, err
}
package interceptor
import (
"context"
"log"
"time"
"google.golang.org/grpc"
)
func LogUnaryServer(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (resp interface{}, err error) {
log.Println("Log: 1")
time.Sleep(time.Second)
log.Println("Logging")
res, err := handler(ctx, req)
log.Println("Log: 2")
time.Sleep(time.Second)
return res, err
}
package message
import (
"context"
generated "sport"
)
type Server struct {
generated.UnimplementedMessageServiceServer
}
func NewServer() Server {
return Server{}
}
func (s Server) SendMessage(ctx context.Context, req *generated.MessageRequest) (*generated.MessageResponse, error) {
return &generated.MessageResponse{Text: req.GetText()}, nil
}
First run make server
then run make client
command. The combination of both command output would look like below. This tells you how the interceptors are treated as in order.
CLIENT SERVER
2020/09/14 19:11:46 Timer: 1
2020/09/14 19:11:47 Identity: 1
2020/09/14 19:11:48 Verify: 1
2020/09/14 19:11:49 Log: 1
2020/09/14 19:11:50 Log: 2
2020/09/14 19:11:51 Verify: 2
2020/09/14 19:11:52 Identity: 2
2020/09/14 19:11:53 Timer: 2