Golang: Run multiple services on one port

Ever faced the problem of having multiple ports in an application, one for each service? In this post, I’m going to brief about how to run multiple services via the same listener port.

At Dgraph , we used to have one port to serve HTTP requests, one for gRPC and one more for internal communication among the servers. But now we just use one port for all the outside facing services and one for internal server communications.

Cmux is a connection multiplexing library for Go. It allows you to differentiate services based on the payload. Hence, you can serve HTTP, HTTPS, gRPC, etc on the same port. For complete information on the protocols supported, refer to their godoc.

Let us jump into the three simple steps with some code sample and get this working in a jiffy.

First setup the different services as you would usually do. In our case we setup a gRPC service and an HTTP handler function.

// Setup gRPC server.
type grpcServer struct{}

func (s *grpcServer) Query(ctx context.Context,
  req *graph.Request) (*graph.Response, error) {
  .
  .
  .
}

// Handler function for http/https queries.
func queryHandler(w http.ResponseWriter, r *http.Request) {
  addCorsHeaders(w)
  .
  .
  .

}

Second, write separate functions to start each service using a net.Listener object as if it is the only service using that listener. Later we’ll multiplex a single TCP listener into multiple listeners.

// Wrapper functions to start serving different services.

func serveGRPC(l net.Listener) {
  s := grpc.NewServer(grpc.CustomCodec(&query.Codec{}))
  graph.RegisterDgraphServer(s, &grpcServer{})
  if err := s.Serve(l); err != nil {
    log.Fatalf("While serving gRpc request: %v", err)
  }
}

func serveHTTP(l net.Listener) {
  if err := http.Serve(l, nil); err != nil {
    log.Fatalf("While serving http request: %v", err)
  }
}

Third, create a listener object and multiplex it using a cmux matcher. It’ll read the header bytes of exchanges and figure out which service to trigger by giving us a new sub-listener (We just call it that, though it’s actually just net.Listener) for every match. We then call the services that we wrote earlier with these corresponding sub-listeners. Look at the following code sample to get a better hang of the above-mentioned steps.

func setupServer() {
  go worker.RunServer(*workerPort) // For internal communication.

  // Create a listener at the desired port.
  l, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
  if err != nil {
    log.Fatal(err)
  }

  // Create a cmux object.
  tcpm := cmux.New(l)

  // Declare the match for different services required.
  httpl := tcpm.Match(cmux.HTTP1Fast())
  grpcl := tcpm.MatchWithWriters(
    cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc"))
  http2 := tcpm.Match(cmux.HTTP2())

  // Link the endpoint to the handler function.
  http.HandleFunc("/query", queryHandler)

  // Initialize the servers by passing in the custom listeners (sub-listeners).
  go serveGRPC(grpcl)
  go serveHTTP(httpl)
  go serveHTTP(http2)

  // Close the listener when done.
  go func() {
    <-closeCh
    // Stops listening further but already accepted connections are not closed.
    l.Close()
  }()

  log.Println("grpc server started.")
  log.Println("http server started.")
  log.Println("Server listening on port", *port)

  // Start cmux serving.
  if err := tcpm.Serve(); !strings.Contains(err.Error(),
    "use of closed network connection") {
    log.Fatal(err)
  }
}

So, there we have it. A single port to cater to many services that you might be using.

Hope you had fun with this post and learnt something new. Thanks for reading and do let us know your thoughts and how it works out for you.