Skip to content

gRPC ও Protocol Buffers

High-performance service-to-service communication — service-রা যখন একে অপরের সাথে কথা বলে, gRPC তখন REST যা হতে চায় তা-ই।

গল্পে বুঝি

সিনার কোম্পানির দুটো অফিস — ঢাকায় হেড অফিস, চট্টগ্রামে ব্রাঞ্চ। আগে দুই অফিস একে অপরকে লম্বা চিঠি লিখত — “অমুক গ্রাহকের নাম, ঠিকানা, অর্ডার নম্বর, তারিখ…” — পুরো কাহিনি প্রতিবার নতুন করে লেখা। কেউ একটা শব্দ এদিক-ওদিক লিখলে অন্য অফিস ভুল বুঝত, আর মোটা খামে ডাকখরচও বেশি পড়ত।

তাই খোয়ারিজমি একটা বুদ্ধি বের করল। সে একজন ছাপাখানার মালিককে দিয়ে দুই অফিসের জন্য একদম হুবহু একই রকম ছাপানো ফর্ম বানাল — যেখানে ঘর নম্বর ১ মানে গ্রাহকের নাম, ঘর ২ মানে অর্ডার নম্বর, ঘর ৩ মানে তারিখ। এখন হেড অফিস আর ব্রাঞ্চ, দুই জায়গাতেই এক তাড়া খালি ফর্ম। চিঠি লেখার দরকার নেই — ফাতিমা শুধু ঘরগুলোতে ছোট ছোট সংখ্যা বসিয়ে একটা এক টুকরো স্লিপ পাঠায়। ওপারে যেহেতু হুবহু একই ফর্ম, ফাতিমার সহকর্মী চোখ বন্ধ করেই বলে দিতে পারে ঘর ২-এ যা আছে সেটাই অর্ডার নম্বর। কোনো ভুল বোঝাবুঝি নেই, স্লিপ ছোট, পাঠানোও দ্রুত।

এই ছাপানো ফর্মটাই হলো gRPC-র .proto contract — client আর server দুই পক্ষ একই schema-তে একমত। ছাপাখানার মালিক, যে দুই পক্ষের জন্য মিলিয়ে খালি ফর্ম বানিয়ে দেয়, সে-ই protoc code generation: এক .proto থেকে দুই দিকের মিলে-যাওয়া কোড তৈরি করে দেয়। আর ঘরে-ভরা সেই ছোট্ট স্লিপ হলো compact binary message — লম্বা JSON চিঠির বদলে কেবল দরকারি মানটুকু, তাই হালকা ও দ্রুত। বাস্তবে এভাবেই এক backend service আরেক service-কে service-to-service ডাকে: দুই পক্ষ আগে থেকেই ফর্ম চেনে বলে ব্যাখ্যার দরকার হয় না, শুধু ভরা-স্লিপ যায়-আসে। Google, Netflix-এর মতো কোম্পানি তাদের ভেতরের হাজারো microservice-এর কথাবার্তা ঠিক এভাবেই gRPC দিয়ে চালায়।

gRPC কেন?

browser-to-server communication-এর জন্য REST দারুণ। কিন্তু আপনার backend-এর ভেতরে service-to-service communication-এর জন্য gRPC প্রায়ই ভালো পছন্দ:

FeatureREST/JSONgRPC/Protobuf
SerializationJSON (text, ~10x বড়)Protobuf (binary, compact)
SchemaOpenAPI (optional, প্রায়ই পুরনো).proto file (required, সবসময় হালনাগাদ)
Code generationOptionalBuilt-in (Go, Java, Python, ইত্যাদি)
StreamingWebSocket (আলাদা protocol)Built-in bidirectional streaming
Performanceভালোদুর্দান্ত (2-10x দ্রুত serialization)
Browser supportNativegRPC-Web proxy দরকার

বাস্তব জীবনের উপমা

REST অনেকটা চিঠি পাঠানোর মতো — প্রতিটা চিঠির একটা ঠিকানা (URL), একটা format (JSON) লাগে, আর ডাকঘর (HTTP) সেটা পৌঁছে দেয়। gRPC অনেকটা ফোন কলের মতো — আপনি একটা connection স্থাপন করেন, দুই পক্ষ একটা shared language (protobuf) বলে, আর communication তাৎক্ষণিক ও bidirectional।

Protobuf দিয়ে একটা Service সংজ্ঞায়িত করা

Protocol Buffers (protobuf) আপনার API schema সংজ্ঞায়িত করে:

// proto/user/v1/user.proto
syntax = "proto3";

package user.v1;

option go_package = "github.com/yourname/myapp/gen/user/v1;userv1";

// Messages — your data types
message User {
  int32 id = 1;
  string email = 2;
  string name = 3;
  string role = 4;
  google.protobuf.Timestamp created_at = 5;
}

message CreateUserRequest {
  string email = 1;
  string name = 2;
  string password = 3;
}

message CreateUserResponse {
  User user = 1;
}

message GetUserRequest {
  int32 id = 1;
}

message GetUserResponse {
  User user = 1;
}

message ListUsersRequest {
  int32 page = 1;
  int32 page_size = 2;
}

message ListUsersResponse {
  repeated User users = 1;
  int32 total_count = 2;
}

// Service — your API contract
service UserService {
  rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
  rpc GetUser(GetUserRequest) returns (GetUserResponse);
  rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
}
# Install protoc compiler and Go plugins
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

# Generate Go code
protoc --go_out=. --go-grpc_out=. proto/user/v1/user.proto

Server Implement করা

package main

import (
    "context"
    "log"
    "net"

    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"

    userv1 "github.com/yourname/myapp/gen/user/v1"
)

type userServer struct {
    userv1.UnimplementedUserServiceServer  // Forward compatibility
    repo UserRepository
}

func (s *userServer) CreateUser(ctx context.Context, req *userv1.CreateUserRequest) (*userv1.CreateUserResponse, error) {
    // Validate input
    if req.Email == "" {
        return nil, status.Error(codes.InvalidArgument, "email is required")
    }
    if req.Name == "" {
        return nil, status.Error(codes.InvalidArgument, "name is required")
    }

    // Create user
    user, err := s.repo.Create(ctx, req.Email, req.Name, req.Password)
    if err != nil {
        return nil, status.Errorf(codes.Internal, "failed to create user: %v", err)
    }

    return &userv1.CreateUserResponse{
        User: toProtoUser(user),
    }, nil
}

func (s *userServer) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error) {
    user, err := s.repo.GetByID(ctx, int(req.Id))
    if err != nil {
        if errors.Is(err, ErrNotFound) {
            return nil, status.Errorf(codes.NotFound, "user %d not found", req.Id)
        }
        return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
    }

    return &userv1.GetUserResponse{
        User: toProtoUser(user),
    }, nil
}

func (s *userServer) ListUsers(ctx context.Context, req *userv1.ListUsersRequest) (*userv1.ListUsersResponse, error) {
    pageSize := int(req.PageSize)
    if pageSize == 0 {
        pageSize = 20
    }
    offset := int(req.Page-1) * pageSize

    users, total, err := s.repo.List(ctx, pageSize, offset)
    if err != nil {
        return nil, status.Errorf(codes.Internal, "failed to list users: %v", err)
    }

    protoUsers := make([]*userv1.User, len(users))
    for i, u := range users {
        protoUsers[i] = toProtoUser(u)
    }

    return &userv1.ListUsersResponse{
        Users:      protoUsers,
        TotalCount: int32(total),
    }, nil
}

func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }

    grpcServer := grpc.NewServer()
    userv1.RegisterUserServiceServer(grpcServer, &userServer{repo: repo})

    log.Println("gRPC server starting on :50051")
    if err := grpcServer.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

gRPC Client

generate করা কোড আপনাকে একটা type-safe client দেয়:

func main() {
    conn, err := grpc.NewClient("localhost:50051",
        grpc.WithTransportCredentials(insecure.NewCredentials()),
    )
    if err != nil {
        log.Fatalf("failed to connect: %v", err)
    }
    defer conn.Close()

    client := userv1.NewUserServiceClient(conn)

    // Create user
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    resp, err := client.CreateUser(ctx, &userv1.CreateUserRequest{
        Email:    "fatima@example.com",
        Name:     "Fatima",
        Password: "secret123",
    })
    if err != nil {
        // gRPC errors have status codes
        st, ok := status.FromError(err)
        if ok {
            log.Printf("gRPC error: code=%s, message=%s", st.Code(), st.Message())
        }
        return
    }

    fmt.Printf("Created user: %+v\n", resp.User)
}

gRPC Streaming

gRPC-র অন্যতম সেরা ফিচার — built-in streaming:

service AnalyticsService {
  // Server streaming — server sends multiple responses
  rpc WatchMetrics(WatchMetricsRequest) returns (stream MetricUpdate);

  // Client streaming — client sends multiple requests
  rpc UploadLogs(stream LogEntry) returns (UploadLogsResponse);

  // Bidirectional streaming — both sides stream
  rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}
// Server streaming implementation
func (s *analyticsServer) WatchMetrics(req *pb.WatchMetricsRequest, stream pb.AnalyticsService_WatchMetricsServer) error {
    ticker := time.NewTicker(time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-ticker.C:
            metric := collectMetric(req.MetricName)
            if err := stream.Send(&pb.MetricUpdate{
                Name:      metric.Name,
                Value:     metric.Value,
                Timestamp: timestamppb.Now(),
            }); err != nil {
                return err
            }
        case <-stream.Context().Done():
            return nil  // Client disconnected
        }
    }
}

gRPC Interceptor (Middleware)

// Unary interceptor (for single request/response RPCs)
func loggingInterceptor(
    ctx context.Context,
    req any,
    info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler,
) (any, error) {
    start := time.Now()
    resp, err := handler(ctx, req)
    slog.Info("gRPC request",
        "method", info.FullMethod,
        "duration_ms", time.Since(start).Milliseconds(),
        "error", err,
    )
    return resp, err
}

// Apply interceptors
grpcServer := grpc.NewServer(
    grpc.UnaryInterceptor(loggingInterceptor),
)

internal service-to-service communication-এর জন্য gRPC ব্যবহার করুন আর external/browser-facing API-র জন্য REST। অনেক কোম্পানি দুটোই চালায় — একটা REST gateway যা ভেতরে gRPC-তে translate করে। grpc-gateway-র মতো tool protobuf সংজ্ঞা থেকে এই translation স্বয়ংক্রিয় করে।

মূল শিক্ষা

  1. Protobuf contract সংজ্ঞায়িত করে — .proto file হলো আপনার API-র একমাত্র সত্যের উৎস
  2. Code generation যেকোনো ভাষায় type-safe client আর server বানায়
  3. gRPC status code HTTP status code-এর জায়গা নেয় — codes.NotFound, codes.InvalidArgument
  4. Streaming built-in — WebSocket ছাড়াই server, client, আর bidirectional streaming
  5. Interceptor হলো gRPC middleware — logging, auth, metrics HTTP middleware-এর মতোই কাজ করে
  6. internal service-এর জন্য ব্যবহার করুন, external API-র জন্য REST — দুটোরই জন্য grpc-gateway-র সাথে মেলান