Search code examples
gogrpcgrpc-go

Implementing a Go gRPC server with an active client connection


I want to proxy all RPC methods of an existing, established gRPC client connection on a gRPC Go server.

In theory, what I wanted would be something like

import (
    "fmt"
    "net"

    pb "google.golang.org/grpc/examples/helloworld/helloworld"

    "google.golang.org/grpc"
)

func server(port int, client pb.GreeterClient) {
    lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
    if err != nil {
        panic(err)
    }

    s := grpc.NewServer()
    // this fails:
    // cannot use client (variable of type helloworld.GreeterClient) as helloworld.GreeterServer
    // have SayHello(context.Context, *helloworld.HelloRequest, ...grpc.CallOption) (*helloworld.HelloReply, error)
    // want SayHello(context.Context, *helloworld.HelloRequest) (*helloworld.HelloReply, error)
    pb.RegisterGreeterServer(s, client)
    
    // other registrations specific to this server

    if err := s.Serve(lis); err != nil {
        panic(err)
    }
}

I could create a wrapper, encapsulating the client and mapping each request to the corresponding client method. But as there are many methods, and they might also vary over time, I'd prefer a solution without additional boilerplate, if possible.

Do you see any straightforward alternative to use the client as server implementation?


Solution

  • A gRPC client and server are fundamentally different, including the method signatures. Your best bet is to implement a server and set the client as its dependency. Assuming that the request and response types are identical:

    type ProxyGreeter struct {
        client pb.GreeterClient
    }
    
    func (p *ProxyGreeter) SayHello(ctx context.Context, req *helloworld.HelloRequest) (*helloworld.HelloReply, error) {
        return p.client.SayHello(ctx, req)
    }
    

    You should also remember to transform the server's incoming context into a client's outgoing context. That can be done with a middleware on the client. In a very simplified form:

    func UnaryCtxClientInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
        md, ok := metadata.FromIncomingContext(ctx)
        if !ok {
            // handle no incoming MD
        }
        outctx, err := metadata.NewOutgoingContext(ctx, md), nil
        // handle err
        return invoker(outctx, method, req, reply, cc, opts...)
    }
    

    And then use it when you create the client as:

    grpc.WithChainUnaryInterceptor(UnaryCtxClientInterceptor)
    

    Otherwise you can look into third-party libraries that provide gRPC reverse proxy.