I'm currently working on oof.gg, a scalable Mobile Gaming platform that allows developers to build scalable multiplayer Web and Mobile games without the stress of building infra. To do this, we'll be building various services and clients integrating with the previously built oof.gg-protobufs.

oof-gg
oof-gg has 5 repositories available. Follow their code on GitHub.

Goals

Build a scalable starting architecture that can provide authentication, verification, APIs, and streaming events for game sessions for oof.gg.

  • Leverage envoy for serving up gRPC micro-service instances.
  • Use PostgreSQL for persisting games, users, etc...
  • Redis for queues, game sessions, shared states, key-value caches, player presence, etc...
  • Kafka for logging, scoring, match history, etc...
  • Reuse components located in pkg across all micro-services.
  • Use docker compose to lift local environment.

Project Setup

To start, we'll implement a mono-repo for the gRPC services that will serve up the platform. The services will be developed using Go.

pkg/
  auth/
    auth.go
    auth_test.go
  config/
    config.go
  db/
    db.go
    models.go
  middleware/
    auth_interceptor.go
    stream_interceptor.go
  redis/
    redis.go
  utils/
    utils.go
services/
  auth-service/
    cmd/main.go
    server/server.go
    Dockerfile
  game-service/
    cmd/main.go
    server/server.go
    Dockerfile
docker-compose.yaml
envoy.yaml
go.mod
go.sum

Local Environment

To work locally, I found it best to just set up a docker-compose.yaml with the services, set up a network, and configure Envoy to serve up the gRPC services.

Below is an example docker-compose.yaml that was used to run the project locally. The Dockerfiles in each service are what you would expect that would be required to set up a Go project for Docker (with the exception that we run the build from the root directory).

version: '3.8'

services:
  auth-service:
    build:
      context: .
      dockerfile: services/auth-service/Dockerfile
    environment:
      - CONFIG_PATH=/app/
    ports:
      - "50051:50051"
    networks:
      - oof-network
  game-service:
    build:
      context: .
      dockerfile: services/game-service/Dockerfile
    environment:
      - CONFIG_PATH=/app/
    ports:
      - "50052:50052"
    networks:
      - oof-network

  envoy:
    image: envoyproxy/envoy:v1.18.3
    volumes:
      - ./envoy.yaml:/etc/envoy/envoy.yaml
    ports:
      - "8080:8080"
      - "9901:9901"
    depends_on:
      - auth-service
      - game-service
      - redis
      - postgres
    networks:
      - oof-network
  postgres:
    image: postgres:latest
    ports:
      - "5432:5432"
    networks:
      - oof-network
  redis:
    image: redis:latest
    ports:
      - "6379:6379"
    networks:
      - oof-network

networks:
  oof-network:
    name: oof-network
    driver: bridge

docker-compose.yaml

For Envoy, we will work on a production configuration later – for now, we'll use compose to serve up locally accessible gRPC services.

Setting up a basic gRPC service

As a reminder, we'll be starting with a simple gRPC service that implements the protobuf structures provided by the oof.gg protocol buffers.

package server

import (
  "context"
  "log"
  "net"
  "oof-gg/pkg/config"
  
  "github.com/oof-gg/oof-protobufs/generated/go/v1/api/auth"
  "google.golang.org/grpc"
)

type AuthService struct {
  authpb.UnimplementedAuthServiceServer
  Auth auth.AuthInterface
  DB *gorm.DB
}

func (s *AuthService) Register(ctx context.Context, req *authpb.RegisterRequest) (*authpb.RegisterResponse) {
  // implement Register logic
}

func (s *AuthService) Login(ctx context.Context, req *authpb.LoginRequest) (*authpb.LoginResponse) {
  // implement Login logic
}

func (s *AuthService) RefreshToken(ctx context.Context, req *authpb.RefreshTokenRequest) (*authpb.RefreshTokenResponse) {
  // implement Refresh Token logic
}

func (s *AuthService) ValidateToken(ctx context.Context, req *authpb.ValidateTokenRequest) (*authpb.ValidateTokenResponse) {
  // implement Validate Token logic
}

func Start(cfg *config.Config) error {
  lis, err := net.Listen("tcp", ":50051")
  if err != nil {
    return err
  }
  s := grpc.NewServer()
  auth.RegisterAuthServiceServer(s, &AuthService{})
  log.Printf("Server listening at %v", lis.Addr())

  // Register reflection service on gRPC server
  if cfg.EnableReflection {
      reflection.Register(s)
  }
  return s.Serve(lis)
}

server/server.go

The service reflects the gRPC service definitions in the protocol buffers defined below:

/// Service definition for authentication
service AuthService {
    /// User login RPC to generate an access token
    rpc Login(LoginRequest) returns (LoginResponse);

    /// User registration RPC to create a new user
    rpc Register(RegisterRequest) returns (RegisterResponse);

    /// RPC to validate an existing token
    rpc ValidateToken(ValidateTokenRequest) returns (ValidateTokenResponse);

    /// RPC to refresh an access token using a refresh token
    rpc RefreshToken(RefreshTokenRequest) returns (RefreshTokenResponse);
}

Then we set up main.go to serve up the basic authentication service.

package main

import (
  "log"
  auth "oof-gg/pkg/auth"
  "oof-gg/pkg/config"
  "oof-gg/pkg/db"
  "oof-gg/services/auth-service/server"
)

func main() {
  cfg, err := config.LoadConfig()
  log.Default().Println("Loading config")
  if err != nil {
      log.Fatalf("Failed to load config: %v", err)
  }
  
  // Initialize the database
  db.InitDB(cfg.Database.DSN)
  
  // Close the database connection when the main function exitss
  defer func() {
      if err := db.CloseDB(); err != nil {
          log.Fatalf("Failed to close database: %v", err)
      } else {
          log.Println("Database closed")
      }
  }()
  
  // Bootstrap the database with some initial data
  if err := db.Bootstrap(); err != nil {
      log.Fatalf("Failed to bootstrap database: %v", err)
  } else {
      log.Println("Database bootstrapped")
  }
  
  // Initialize the auth pkg
  instance, err := auth.NewAuth(cfg)
  if err != nil {
      log.Fatalf("Failed to initialize auth service: %v", err)
  } else {
      log.Println("Auth service initialized")
  }
  
  // Start the server
  go func() {
      log.Default().Println("Starting server")
      authSvc := &server.AuthService{
          Auth: instance,
          DB:   db.GetDB(),
      }
  
      if err := server.Start(authSvc, cfg); err != nil {
          log.Fatalf("Failed to start server: %v", err)
      } else {
          log.Printf("Server started successfully")
      }
  }()
  
  // Block the main from exiting
  select {}
}

cmd/main.go

What is not shown here are database and auth pkg files which have basic initializations and base methods for those packages. From there, you can go ahead and initialize the service.

Initializing & Testing

go run main/cmd.go

Once the your service is initializing, gRPC Reflection can be configured for easy and quick testing services locally to simplify development. Tools like evans or Postman are useful to test the gRPC Service Reflection once they're serving requests.

ktr0731/evans
💡
Note: It is generally NOT recommended to keep gRPC Reflection on for Production environments, so bind reflection to a configuration as shown in the example above.

Up Next

We'll be building out the game services to set up game streams, queues, sessions, broadcasts, and logging using gRPC with Redis and Kafka.

Building gRPC micro-services with Go for oof.gg - Part 1

I'm currently working on oof.gg, a scalable Mobile Gaming platform that allows developers to build scalable multiplayer Web and Mobile games without the stress of building infra.