Skip to content

Pluggable Protocol Layer

This document describes Bingo's pluggable protocol layer design, supporting any combination of HTTP/gRPC/WebSocket.

Design Goals

  1. Clean Architecture - Protocol layer decoupled from business layer
  2. Pluggable - Protocols are independent and configurable
  3. Unified Format - Consistent error responses and authentication across all protocols
  4. Development Efficiency - Write Biz layer once, reuse across multiple protocols

Architecture Overview

┌─────────────────────────────────────────────────────────────┐
│                    Protocol Layer (Pluggable)               │
│  ┌────────┐  ┌────────┐  ┌─────────┐  ┌────────┐           │
│  │  HTTP  │  │  gRPC  │  │ Gateway │  │   WS   │           │
│  │Handler │  │Handler │  │(Optional)│  │Handler │           │
│  └───┬────┘  └───┬────┘  └────┬────┘  └───┬────┘           │
└──────┼───────────┼────────────┼───────────┼────────────────┘
       └───────────┴─────┬──────┴───────────┘

┌─────────────────────────────────────────────────────────────┐
│                        Biz Layer                            │
│                (Protocol-agnostic business logic)           │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│                       Store Layer                           │
└─────────────────────────────────────────────────────────────┘

Directory Structure

internal/pkg/server/        # Common server infrastructure (reusable)
├── server.go               # Server interface, Runner
├── http.go                 # HTTP Server
├── grpc.go                 # gRPC Server
├── gateway.go              # gRPC-Gateway Server
├── websocket.go            # WebSocket Server
└── assembler.go            # Configuration-driven assembly

internal/apiserver/
├── biz/                    # Business logic (protocol-agnostic)
│   ├── biz.go              # Interface definitions
│   └── user/
│       └── user.go         # Implementation (see "Biz Layer Parameter Types" section)

├── handler/                # Protocol handlers (pluggable)
│   ├── http/               # Standalone HTTP (Gin)
│   ├── grpc/               # gRPC Handler
│   └── ws/                 # WebSocket (JSON-RPC 2.0)

└── store/                  # Data access

# WebSocket infrastructure provided by standalone library: github.com/bingo-project/websocket
# - Hub, Client, Router, Context and other core types
# - jsonrpc subpackage: JSON-RPC 2.0 message types
# - middleware subpackage: Built-in middleware

pkg/proto/                  # Proto definitions (data layer)
├── user/v1/
│   ├── user.proto          # Message definitions (shared by all modes)
│   └── user_service.proto  # Service definitions (for gRPC/Gateway)
└── common/
    └── error.proto         # Unified error type (optional)

Note: internal/pkg/server/ is common server infrastructure, reusable by all services (apiserver, admserver, etc.).

Configuration-Driven

yaml
# configs/apiserver.yaml
server:
  http:
    enabled: true
    addr: ":8080"
    mode: standalone    # standalone | gateway
  grpc:
    enabled: true
    addr: ":9090"
  websocket:
    enabled: true
    addr: ":8081"

Mode Options:

  • standalone: Standalone HTTP, calls Biz directly
  • gateway: gRPC-Gateway mode, proxies to gRPC (requires grpc.enabled = true)

Server Startup Logic

Server Interface

go
// internal/pkg/server/server.go

// Server pluggable server interface
type Server interface {
    Run(ctx context.Context) error      // Start (blocks until ctx cancelled)
    Shutdown(ctx context.Context) error // Graceful shutdown
    Name() string                       // Server name (for logging)
}

// Runner manages lifecycle of multiple servers
type Runner struct {
    servers []Server
}

func (r *Runner) Run(ctx context.Context) error    // Start concurrently, any failure triggers shutdown
func (r *Runner) Shutdown(ctx context.Context) error // Shutdown in reverse order

Assembler

go
// internal/pkg/server/assembler.go

// Option pattern configuration
func WithGinEngine(engine *gin.Engine) AssemblerOption      // HTTP
func WithGRPCServer(server *grpc.Server) AssemblerOption    // gRPC
func WithWebSocket(engine *gin.Engine, hub *ws.Hub) AssemblerOption // WebSocket

// Assemble based on configuration
func Assemble(cfg *config.Config, opts ...AssemblerOption) *Runner

Usage Example

go
// internal/apiserver/apiserver.go

func Run(cfg *config.Config) error {
    // 1. Initialize dependencies
    bizInstance := biz.NewBiz(store.S)

    // 2. HTTP
    httpEngine := gin.New()
    router.MapHTTPRouters(httpEngine, bizInstance)

    // 3. gRPC
    grpcSrv := grpc.NewServer()
    router.MapGRPCRouters(grpcSrv, bizInstance)

    // 4. WebSocket
    hub := ws.NewHub()
    wsRouter := ws.NewRouter()
    router.RegisterWSHandlers(wsRouter)
    wsEngine := gin.New()
    wsEngine.GET("/ws", wshandler.ServeWS(hub, wsRouter))

    // 5. Assemble and run
    runner := server.Assemble(cfg,
        server.WithGinEngine(httpEngine),
        server.WithGRPCServer(grpcSrv),
        server.WithWebSocket(wsEngine, hub),
    )

    ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
    defer stop()

    return runner.Run(ctx)
}

Design Principles:

  • Dependency Injection: Handlers created by caller, Server only manages lifecycle
  • Single Responsibility: internal/pkg/server has no business code dependencies
  • Reusable: All services (apiserver, admserver, etc.) share the same infrastructure

Biz Layer Parameter Types

The Biz layer method parameters and return values support two approaches:

Approach 1: Go Struct (used in this project)

go
// pkg/api/apiserver/v1/auth.go
type LoginRequest struct {
    Username string `json:"username" binding:"required"`
    Password string `json:"password" binding:"required"`
}

// internal/apiserver/biz/auth/auth.go
func (b *authBiz) Login(ctx context.Context, req *v1.LoginRequest) (*v1.LoginResponse, error)

Characteristics:

  • Uses binding tags for request validation, natively supported by Gin
  • Supports native Go types (time.Time, *string for optional fields, etc.)
  • gRPC Handler requires Go Struct ↔ Proto Message conversion

Approach 2: Proto Message

go
// Generated from proto file
// internal/apiserver/biz/auth/auth.go
func (b *authBiz) Login(ctx context.Context, req *pb.LoginRequest) (*pb.LoginResponse, error)

Characteristics:

  • Proto file serves as single source of truth for type definitions
  • gRPC Handler requires no type conversion
  • HTTP Handler requires JSON ↔ Proto conversion
  • Validation logic requires protoc-gen-validate or manual implementation

Handler Implementation

gRPC Handler

go
type UserHandler struct {
    pb.UnimplementedUserServiceServer
    biz biz.IBiz
}

func (h *UserHandler) Login(ctx context.Context, req *pb.LoginRequest) (*pb.LoginResponse, error) {
    return h.biz.User().Login(ctx, req)
}

HTTP Handler (Standalone Mode)

go
type UserHandler struct {
    biz biz.IBiz
}

func (h *UserHandler) Login(c *gin.Context) {
    var req pb.LoginRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        core.Response(c, nil, errno.ErrInvalidArgument.WithMessage(err.Error()))
        return
    }

    resp, err := h.biz.User().Login(c.Request.Context(), &req)
    core.Response(c, resp, err)
}

WebSocket Handler

WebSocket uses JSON-RPC 2.0 specification and middleware architecture. See WebSocket Design and Implementation.

go
// internal/apiserver/router/ws.go

func RegisterWSHandlers(router *ws.Router) {
    h := wshandler.NewHandler(store.S)

    // Global middleware
    router.Use(middleware.Recovery, middleware.RequestID, middleware.Logger)

    // Public methods
    public := router.Group()
    public.Handle("auth.login", h.Login, middleware.LoginStateUpdater)

    // Authenticated methods
    private := router.Group(middleware.Auth)
    private.Handle("auth.user-info", h.UserInfo)
}

Supported Combinations

CombinationConfiguration
HTTP onlyhttp.enabled=true
gRPC onlygrpc.enabled=true
WS onlywebsocket.enabled=true
HTTP + gRPChttp.enabled=true, grpc.enabled=true
gRPC-Gatewayhttp.enabled=true, http.mode=gateway, grpc.enabled=true
HTTP + WShttp.enabled=true, websocket.enabled=true
AllAll enabled

Adding New API

Protocol ModeSteps to Add an API
gRPC onlyModify proto -> Implement Biz method -> Done
HTTP onlyModify proto -> Implement Biz method -> Add route -> Done
gRPC-GatewayModify proto (add http annotations) -> Implement Biz method -> Done
HTTP + gRPCModify proto -> Implement Biz method -> Add HTTP route -> Done
With WebSocketAbove + Register one line

Proto Generation Strategy

ModeGenerated ContentMakefile Command
HTTP only*.pb.gomake gen.proto.msg
gRPC only*.pb.go + *_grpc.pb.gomake gen.proto.grpc
gRPC-Gateway*.pb.go + *_grpc.pb.go + *.pb.gw.gomake gen.proto.gateway

Next Step: Learn about WebSocket Design and Implementation for details on JSON-RPC 2.0 protocol and middleware architecture.

Released under the Apache 2.0 License.