A detailed backend reference

gRPC, memanggil fungsi jarak jauh seperti fungsi lokal.

Versi Sederhana

Penelusuran dari prinsip dasar tentang framework RPC yang menggerakkan internal Google, Netflix, Uber, dan kebanyakan armada microservice modern: dari kontrak (Protocol Buffers) dan format wire biner, lewat transport HTTP/2 dan empat bentuk streaming, sampai deadline, interceptor, mTLS, dan ketahanan produksi. Ditulis bukan hanya menyebut apa fungsi tiap bagian, tapi kenapa ia ada dan bagaimana ia bekerja di dalamnya. Ada implementasi Go dan Python.

RPC over HTTP/2 Protocol Buffers Go 1.22+ / Python 3.11+ 21 sections
Part I / Foundations
01

Apa Sebenarnya gRPC Itu

gRPC adalah framework untuk memanggil fungsi yang hidup di mesin lain seolah-olah fungsi lokal di proses Anda sendiri. Namanya adalah kepanjangan dari gRPC Remote Procedure Call (akronim rekursif; huruf "g"-nya diberi backronym kata yang berbeda di tiap rilis sekadar untuk seru). Frasa yang penting yang tengah, Remote Procedure Call. Anda menulis user, err := client.GetUser(ctx, &pb.GetUserRequest{Id: 42}) dan ia tampak seperti panggilan method biasa, tapi di bawah kap argumennya diserialkan, dikirim melewati jaringan ke server, dieksekusi di sana, dan nilai kembaliannya dikirim kembali, semua tersembunyi di balik satu baris itu.

Tiga hal benar tentang gRPC dan Anda perlu ketiganya di kepala sekaligus. Ia (1) framework RPC kontrak-pertama: Anda mendeskripsikan API di file .proto dan kode dibuat darinya; (2) men-serialize data dengan Protocol Buffers, format biner ringkas, alih-alih teks seperti JSON; dan (3) mengangkut byte itu lewat HTTP/2, yang memberinya multiplexing dan streaming dua arah sungguhan. Semua hal lain di manual ini adalah konsekuensi dari tiga fakta itu.

REST hands you a building with addressable rooms (resources) and a fixed set of things you may do to each room (GET, POST, ...). gRPC hands you a remote control with labelled buttons (methods), you don't think about rooms, you press GetUser or SendPayment and the work happens elsewhere. One models nouns; the other models verbs.
Where gRPC sits
gRPC is three layers stacked: your generated API, Protobuf, and HTTP/2
YOU WORK HERE Your service, generated stubs & servers client.GetUser(ctx, req) / func (s) GetUser(ctx, req) / the methods you call & implement Protocol Buffers, the contract + binary serialization messages -> compact bytes / sec 04-05 HTTP/2, multiplexed, bidirectional framed transport streams carry the bytes / enables all 4 call types / sec 06 TCP + TLS, the reliable, encrypted pipe
The top layer is the only part you hand-write. gRPC generates the glue that turns your method calls into Protobuf bytes flowing over HTTP/2 streams.

"Procedure call" adalah seluruh gagasannya

Dalam kode biasa, memanggil fungsi adalah pipa yang tak terlihat: Anda meneruskan argumen, CPU melompat ke fungsi, ia mengembalikan nilai. RPC mengajukan pertanyaan sederhana, bagaimana jika fungsi itu hidup di komputer berbeda? Impian RPC (yang berakar di 1980-an) adalah membuat jaringan menghilang, sehingga sistem terdistribusi terasa seperti satu program. gRPC adalah realisasi modern kelas produksi dari impian itu: Anda tidak pernah menulis kode socket, tidak pernah mem-parse respons manual, tidak pernah membangun URL. Anda memanggil method bertipe dan menangani hasil bertipe atau error bertipe.

The one-sentence definition

gRPC = panggilan method jarak jauh yang bertipe, didefinisikan kontrak Protobuf, diserialkan sebagai biner ringkas, diangkut lewat stream HTTP/2: jadi klien dalam bahasa apa pun bisa memanggil server bahasa lain seolah lokal.

02

Kenapa gRPC Ada

gRPC bukan pengganti REST di mana-mana, ia dibangun untuk memecahkan rasa sakit spesifik yang muncul saat banyak layanan (sering dalam bahasa berbeda) terus-menerus bicara satu sama lain, volume tinggi, di dalam satu sistem. Google membangunnya (open source 2015, berevolusi dari sistem internal bernama Stubby) justru karena REST-over-JSON terlalu lambat, terlalu longgar, dan terlalu terbatas untuk lalu lintas antar-layanan di skala mereka. Lima masalah konkret menggerakkan desainnya.

Masalah dengan REST/JSON Yang gRPC lakukan sebagai gantinya
JSON besar & lambat. Teks, dengan nama field berulang di tiap objek; parsingnya berat CPU. Biner Protobuf: field diwakili nomor bukan nama, dikemas varint. Sering 3-10x lebih kecil dan jauh lebih cepat di-(de)serialisasi.
Tanpa kontrak yang dipaksakan. Bentuk payload JSON hidup di dokumen (atau kepala seseorang); penyimpangan menyebabkan kerusakan runtime. File .proto adalah kontraknya. Klien & server membuat kode dari sumber yang sama, ketidakcocokan gagal saat kompilasi.
Streaming lemah. REST HTTP/1.1 polos request->response; streaming butuh tempelan (SSE, polling, WebSocket mentah). Empat jenis panggilan termasuk streaming dua arah penuh, natif di framework lewat HTTP/2.
Gesekan poliglot. Tiap bahasa menulis klien & serialisasi sendiri-sendiri, tidak konsisten. Satu .proto membuat klien/server idiomatis untuk Go, Python, Java, C++, Rust, dan lainnya.
Overhead koneksi HTTP/1.1. Satu request per koneksi (atau head-of-line blocking), handshake berulang. HTTP/2 memultipleks banyak panggilan serentak di atas satu koneksi berumur panjang.

Cakupan yang jujur

Kekuatan-kekuatan itu ditujukan untuk komunikasi internal, machine-to-machine: microservice, backend-ke-backend, mobile-ke-backend tempat Anda mengendalikan kedua ujung. gRPC lebih lemah daripada REST untuk API publik yang menghadap browser (browser tidak bisa bicara gRPC mentah, lihat bagian 19) dan untuk endpoint yang bisa di-debug manusia, ramah cache, dan kompatibel luas. Perbandingan lengkapnya bagian 18; untuk sekarang, pegang kerangkanya: gRPC adalah sistem saraf internal; REST adalah pintu depan publik.

Where you'll meet it

Service mesh (Istio, Linkerd), mesin API Kubernetes sendiri, etcd, CockroachDB, xDS Envoy, dan grafik panggilan internal kebanyakan platform fintech/konsumen besar. Jika Anda pernah mengirim pembayaran atau streaming video, hampir pasti gRPC membawa salah satu hop request itu antar layanan backend.

03

Model Mental RPC

Sebelum sintaks apa pun, internalisasi mesin yang membuat panggilan jarak jauh tampak lokal, karena setiap konsep gRPC belakangan hanyalah bagian bernama dari gambar ini. Panggilan fungsi lokal dan jarak jauh berbeda tepat di satu tempat: antara "panggil" dan "eksekusi", argumennya harus menyeberangi jaringan. RPC menyisipkan dua bagian kode hasil generasi, stub di klien dan skeleton/handler di server, untuk menyembunyikan penyeberangan itu.

The anatomy of a remote call
Stub and skeleton hide the network between "call" and "execute"
CLIENT PROCESS your code client.GetUser(req) STUB (generated) serialize args -> bytes send over HTTP/2 SERVER PROCESS real method runs query DB, compute... SKELETON (generated) deserialize bytes -> args dispatch to handler network, Protobuf over HTTP/2 request bytes -> <- response bytes travel the reverse path, deserialized back into a typed return value
"Marshalling" = turning a language object into bytes; "unmarshalling" = the reverse. The stub marshals the request and unmarshals the reply; the skeleton does the mirror image. You write neither.

Peringatan abstraksi yang bocor

RPC mencoba membuat jaringan tak terlihat, tapi jaringan tidak pernah benar-benar tak terlihat, dan berpura-pura sebaliknya adalah jebakan RPC klasik. Panggilan lokal tidak bisa timeout, hilang, atau tertukar urutan; panggilan jarak jauh bisa ketiganya. Itulah kenapa gRPC memberi tool kelas satu untuk hal yang tidak pernah dibutuhkan panggilan lokal: deadline (bagian 13), status code untuk kegagalan parsial (bagian 14), retry (bagian 17), dan pembatalan. Perlakukan setiap panggilan jarak jauh sebagai "panggilan lokal yang bisa gagal dengan cara-cara bentuk jaringan", dan Anda akan merancang sistem yang tangguh alih-alih rapuh.

Don't pretend the wire isn't there

Kesalahan tunggal terbesar dengan RPC adalah menulis panggilan jarak jauh seolah gratis dan tidak pernah gagal, tanpa timeout, tanpa penanganan error untuk UNAVAILABLE, tanpa memikirkan latensi dalam loop. Abstraksinya kenyamanan, bukan jaminan. Selalu teruskan context/deadline dan selalu tangani error-nya.

Part II / The Two Pillars
04

Protocol Buffers: Kontraknya

Protocol Buffers ("protobuf") adalah dua hal yang memakai satu nama: IDL (Interface Definition Language) untuk mendeskripsikan message dan service Anda, dan format serialisasi biner untuk meng-encode-nya di kabel. Bagian ini paruh IDL-nya, file .proto yang menjadi sumber kebenaran tunggal yang dipakai kedua sisi membuat kode. Dapatkan kontraknya benar dan klien serta server secara harfiah tidak bisa berselisih soal bentuk data.

File .proto mendefinisikan message (struktur data) dan service (koleksi method RPC). Ini contoh lengkap dan realistis, service pengguna, dengan anotasi untuk setiap aturan yang penting:

user.proto
syntax = "proto3";              // always declare the syntax; proto3 is current

package user.v1;                // namespace + a versioning convention (v1, v2...)

option go_package = "example.com/gen/userv1;userv1";  // where generated Go lands

// A message is a typed record. Each FIELD has a type, a name, and a NUMBER.
message User {
  string id          = 1;       // the "= 1" is the FIELD NUMBER, not a value
  string email       = 2;
  string full_name   = 3;
  Role   role        = 4;       // a nested enum (declared below)
  repeated string tags = 5;     // "repeated" = a list/array of strings
  int64  created_at  = 6;       // unix seconds; proto3 has no native date
}

// An enum is a fixed set of named values. The first MUST be 0 (the default).
enum Role {
  ROLE_UNSPECIFIED = 0;         // 0 is the implicit default, reserve it
  ROLE_MEMBER      = 1;
  ROLE_ADMIN       = 2;
}

message GetUserRequest  { string id = 1; }
message GetUserResponse { User user = 1; }   // messages nest inside messages

message CreateUserRequest {
  string email     = 1;
  string full_name = 2;
  optional string phone = 3;    // "optional" tracks presence: set vs unset vs ""
}

// A SERVICE is a set of methods. Each takes one message and returns one message.
service UserService {
  rpc GetUser    (GetUserRequest)    returns (GetUserResponse);
  rpc CreateUser (CreateUserRequest) returns (User);
}

Aturan yang benar-benar menggigit

  • Nomor field adalah identitas sebenarnya, bukan nama. Di kabel, email dikirim sebagai field 2, tidak pernah sebagai string "email" (inilah kenapa protobuf ringkas, bagian 05). Namanya untuk kode Anda; nomornya kontraknya.
  • Jangan pernah mengubah atau memakai ulang nomor field. Ini aturan kardinal evolusi skema. Mengganti nama field aman (nama tidak ada di kabel); mengubah nomor atau tipenya diam-diam merusak data bagi peer mana pun yang masih memakai definisi lama. Untuk menghapus field, tandai reserved supaya nomornya tidak pernah dipakai ulang tanpa sengaja.
  • Menambah field kompatibel ke belakang. Beri field baru nomor segar; klien lama tidak melihatnya, server baru memperlakukannya sebagai tidak di-set saat klien lama mengabaikannya. Beginilah API gRPC berevolusi tanpa memecah konsumen yang ter-deploy, disiplin perubahan aditif yang sama dengan versioning REST.
  • proto3 punya default, bukan null. string yang tidak di-set adalah "", int yang tidak di-set 0, bool yang tidak di-set false. Jika Anda harus membedakan "tidak ada" dari "nol", pakai optional (yang menambah pelacakan keberadaan), jebakan yang sama dengan zero value Go, diselesaikan dengan cara yang sama.
Konstruk Artinya Catatan
scalar int32 int64 uint32 sint32 fixed64 float double bool string bytes Pilih sint* untuk angka yang sering negatif; bytes untuk biner mentah.
repeated Daftar terurut dari tipe field Padanan protobuf dari larik/irisan.
enum Set nilai tetap; entri pertama harus 0 Integritas + dokumentasi diri, seperti enum Postgres.
oneof Paling banyak satu dari beberapa field di-set Union bertag, mis. hasil yang entah nilai atau error.
map<k,v> Larik asosiatif Gula di atas message key/value ber-ulang.
optional Menambah pelacakan keberadaan eksplisit ke scalar Membedakan "tidak di-set" dari nilai nol.
The contract is the API

Di REST kontraknya prosa di dokumen Swagger yang mungkin cocok atau tidak dengan kode. Di gRPC .proto adalah kebenaran yang bisa dieksekusi: kedua sisi membuat kode darinya, jadi field yang Anda tambah atau tipe yang Anda ubah tercermin di klien dan server begitu mereka regenerasi. Skema tidak bisa diam-diam menyimpang dari implementasi.

05

Cara Protobuf Meng-encode: Format Wire

Alasan gRPC cepat dan kecil bermuara pada bagaimana protobuf mengubah message menjadi byte. Anda tidak menulis ini manual, tapi memahaminya menjelaskan setiap klaim performa dan setiap jebakan. Trik intinya: tiap field ditulis sebagai tag mungil (yang meng-encode nomor field dan wire type) diikuti nilainya, dan integer dikemas memakai varint yang memakai byte lebih sedikit untuk angka lebih kecil. Tanpa nama field, tanpa kutip, tanpa koma, tanpa spasi.

JSON vs Protobuf, same data
Why the binary form is dramatically smaller
JSON, text, names repeated every time {"id":"u42","role":2,"created_at":1717 careful: every key, quote, brace and comma is bytes on the wire ~52 B Protobuf, field numbers + varints, no names 0A 03 u 4 2 20 02 30 ...varint ~20 B tag len value / tag value Same information, well under half the size, and no text parsing to decode it.
Multiply this saving across millions of messages per second and you see why high-traffic internal systems reach for protobuf over JSON.

Tag: nomor field + wire type dalam satu byte (biasanya)

Setiap field di kabel dimulai dengan tag yang dihitung sebagai (fieldnumber << 3) | wiretype. Tiga bit rendah adalah wire type (cara membaca byte yang menyusul: varint, 64-bit, length-delimited, 32-bit); sisanya nomor field. Jadi decoder membaca tag, tahu "ini field 2, dan length-delimited," dan tahu persis cara mengonsumsi yang datang berikutnya, bahkan jika ia belum pernah melihat field itu sebelumnya (ia bisa melewati field tak dikenal, itulah yang membuat kompatibilitas ke depan bekerja).

Decoding one field
How a tag byte tells the reader what to do next
tag byte = (field_number << 3) | wire_type field number upper bits wire type 3 bits 0 = VARINT, int32/64, bool, enum 1 = I64, fixed64, double 2 = LEN, string, bytes, messages 5 = I32, fixed32, float varint: small numbers cost fewer bytes 1 -> 1 byte  /  300 -> 2 bytes  /  70000 -> 3 bytes each byte uses 7 bits for data + 1 "continue" bit, so values grow their encoding only as needed
Because the reader can identify and skip a field it doesn't recognize, a new server can add fields without breaking old clients, the wire format is self-describing enough to step over the unknown.
The cost of binary: it isn't human-readable

Anda tidak bisa curl endpoint gRPC dan melirik JSON-nya. Debugging butuh tool yang memahami skemanya (grpcurl, reflection, bagian 20). Opasitas itu harga dari kecepatan dan ukurannya; ia alasan utama API publik/ter-debug sering tetap di REST.

06

HTTP/2: Transport di Bawahnya

gRPC tidak menciptakan transport sendiri, ia menumpang HTTP/2, dan hampir setiap keunggulan gRPC (konkurensi, keempat bentuk streaming, overhead rendah) sebenarnya fitur HTTP/2. Yang harus dipahami gagasan sentral HTTP/2: satu koneksi TCP dibagi menjadi banyak stream independen yang disisipkan, masing-masing membawa rangkaian frame biner. Itulah yang membuat satu koneksi bisa membawa ratusan RPC serentak, dan data mengalir dua arah bersamaan.

Multiplexing
One connection, many concurrent RPCs interleaved as frames
HTTP/1.1, one call blocks the line (or needs many connections) call A call B waits call C waits HTTP/2, one connection, 4 streams interleaved ABCD frames from different calls share the wire, in any order No head-of-line blocking at the HTTP layer: a slow call never freezes the others on the connection.
A gRPC call maps to one HTTP/2 stream. Many calls -> many streams -> one connection. This is why a gRPC client keeps a long-lived connection and reuses it.

Cara panggilan gRPC memetakan ke HTTP/2

Secara konkret, RPC adalah POST HTTP/2 ke path berbentuk /package.Service/Method (mis. /user.v1.UserService/GetUser). Metadata gRPC bepergian sebagai header HTTP/2; protobuf terserialisasi bepergian di frame DATA sebagai body; dan status akhir (status code gRPC, bagian 14) tiba di trailer HTTP/2 setelah body. Streaming berarti lebih dari satu message mengalir di satu atau kedua arah pada stream itu sebelum ditutup.

Konsep gRPC Mekanisme HTTP/2
Satu panggilan RPC Satu stream HTTP/2 (request + response)
Method yang dipanggil Header :path = /pkg.Service/Method
Metadata (token auth, trace ID) Header request & response HTTP/2
Message request/response protobuf ber-awalan panjang di frame DATA
Status akhir + pesan Trailer grpc-status / grpc-message
Streaming beberapa frame DATA sebelum stream setengah-menutup
Why this matters for you

Karena gRPC butuh HTTP/2 end to end, proxy/load balancer mana pun di jalurnya harus bicara HTTP/2 dan melakukan balancing L7 (sadar-request), balancer TCP L4 naif akan mem-pin setiap panggilan ke satu backend, karena semuanya berbagi satu koneksi (bagian 17). Dan karena browser tidak bisa mengekspos pembingkaian HTTP/2 mentah ke JS, browser sama sekali tidak bisa bicara gRPC natif (bagian 19). Pilihan transport membentuk seluruh kisah deployment.

Part III / The Four Call Types
07

RPC Unary

Bentuk paling sederhana dan paling umum: satu request, satu response: persis seperti panggilan fungsi normal. Klien mengirim satu message, server melakukan kerjanya dan mengembalikan satu message. ±90% RPC dunia nyata berbentuk unary. Kita akan membangun method GetUser dari user.proto di bagian 04, lengkap, dua bahasa.

Unary
One message each way
Client Server 1 request 1 response
// ===== SERVER =====
package main

import (
    "context"
    "log"
    "net"

    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
    userv1 "example.com/gen/userv1" // generated from user.proto
)

// Embed the generated UnimplementedUserServiceServer for forward-compat.
type server struct {
    userv1.UnimplementedUserServiceServer
}

// The method signature is generated FROM the proto: ctx, *Request -> *Response, error
func (s *server) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error) {
    if req.GetId() == "" {
        return nil, status.Error(codes.InvalidArgument, "id is required") // typed error, sec 14
    }
    // ...real work: query the DB by req.GetId()...
    u := &userv1.User{Id: req.GetId(), Email: "ada@example.com", FullName: "Ada", Role: userv1.Role_ROLE_ADMIN}
    return &userv1.GetUserResponse{User: u}, nil
}

func main() {
    lis, _ := net.Listen("tcp", ":50051")
    s := grpc.NewServer()
    userv1.RegisterUserServiceServer(s, &server{}) // wire the impl to the service
    log.Println("gRPC on :50051")
    s.Serve(lis)
}

// ===== CLIENT =====
func callGetUser() {
    // NewClient replaces the deprecated grpc.Dial; insecure creds for local dev only
    conn, _ := grpc.NewClient("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
    defer conn.Close()

    client := userv1.NewUserServiceClient(conn) // the generated STUB
    ctx, cancel := context.WithTimeout(context.Background(), time.Second) // always a deadline, sec 13
    defer cancel()

    resp, err := client.GetUser(ctx, &userv1.GetUserRequest{Id: "u42"}) // looks local, runs remote
    if err != nil {
        log.Fatalf("GetUser failed: %v", err) // err carries the gRPC status code
    }
    log.Printf("got user: %s", resp.GetUser().GetFullName())
}
# ===== SERVER =====
from concurrent import futures
import grpc
import user_pb2 as pb          # generated: messages
import user_pb2_grpc as pb_grpc  # generated: service base classes

class UserService(pb_grpc.UserServiceServicer):
    # Method signature generated FROM the proto: (self, request, context) -> response
    def GetUser(self, request, context):
        if not request.id:
            context.abort(grpc.StatusCode.INVALID_ARGUMENT, "id is required")  # typed error, sec 14
        # ...real work: query the DB by request.id...
        user = pb.User(id=request.id, email="ada@example.com",
                       full_name="Ada", role=pb.ROLE_ADMIN)
        return pb.GetUserResponse(user=user)

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    pb_grpc.add_UserServiceServicer_to_server(UserService(), server)  # wire impl to service
    server.add_insecure_port("[::]:50051")  # insecure = local dev only
    server.start()
    print("gRPC on :50051")
    server.wait_for_termination()

# ===== CLIENT =====
def call_get_user():
    with grpc.insecure_channel("localhost:50051") as channel:
        stub = pb_grpc.UserServiceStub(channel)        # the generated STUB
        try:
            # timeout= is the deadline, sec 13; looks local, runs remote
            resp = stub.GetUser(pb.GetUserRequest(id="u42"), timeout=1.0)
            print("got user:", resp.user.full_name)
        except grpc.RpcError as e:
            print("failed:", e.code(), e.details())     # carries the gRPC status code

if __name__ == "__main__":
    serve()

Perhatikan simetrinya lintas bahasa: stub hasil generasi di klien, base class servicer/server hasil generasi di server, dan method yang tanda tangannya persis berasal dari proto.

08

Server Streaming

Satu request, aliran respons. Klien bertanya sekali; server mengirim banyak message seiring waktu, lalu menutup stream. Pas untuk: mengembalikan set hasil besar dalam potongan, umpan event langsung, update progres pada pekerjaan panjang, atau paginasi tanpa round trip berulang. Proto menandai response-nya sebagai stream.

Server streaming
Ask once, receive many
Client Server 1 request many responses, then close
// proto:  rpc ListUsers(ListUsersRequest) returns (stream User);

// ===== SERVER: receive one req, call stream.Send(...) repeatedly =====
func (s *server) ListUsers(req *userv1.ListUsersRequest, stream userv1.UserService_ListUsersServer) error {
    for _, u := range queryUsers(req.GetFilter()) { // imagine this yields a big result set
        if err := stream.Send(u); err != nil {       // push one message down the stream
            return err                                // client gone / cancelled
        }
    }
    return nil // returning nil closes the stream cleanly (sends OK trailer)
}

// ===== CLIENT: call once, then Recv() in a loop until io.EOF =====
func listUsers(client userv1.UserServiceClient) {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    stream, _ := client.ListUsers(ctx, &userv1.ListUsersRequest{Filter: "active"})
    for {
        u, err := stream.Recv()
        if err == io.EOF { break } // server closed the stream, we're done
        if err != nil { log.Fatal(err) }
        log.Printf("user: %s", u.GetFullName())
    }
}
# proto:  rpc ListUsers(ListUsersRequest) returns (stream User);

# ===== SERVER: a generator, every `yield` sends one message =====
class UserService(pb_grpc.UserServiceServicer):
    def ListUsers(self, request, context):
        for u in query_users(request.filter):  # big result set
            yield u                             # yielding pushes one message down the stream
        # function returning ends the stream cleanly

# ===== CLIENT: the call returns an ITERABLE of responses =====
def list_users(stub):
    responses = stub.ListUsers(pb.ListUsersRequest(filter="active"), timeout=10.0)
    for u in responses:        # iterate until the server closes the stream
        print("user:", u.full_name)

Go memakai pasangan stream.Send/stream.Recv eksplisit; Python mengekspresikan sisi server sebagai generator (yield) dan sisi klien sebagai iterable polos, idiomatis tiap bahasa, perilaku kabel sama.

09

Client Streaming

Aliran request, satu respons. Klien mengirim banyak message, lalu server membalas sekali dengan ringkasan/hasil. Ideal untuk upload, ingestion batch, atau menggabungkan serangkaian pembacaan menjadi satu jawaban terhitung. Proto menandai request-nya sebagai stream.

Client streaming
Send many, get one back
Client Server many requests, then close 1 response
// proto:  rpc UploadEvents(stream Event) returns (UploadSummary);

// ===== SERVER: Recv() in a loop, then SendAndClose() once at the end =====
func (s *server) UploadEvents(stream userv1.UserService_UploadEventsServer) error {
    count := 0
    for {
        ev, err := stream.Recv()
        if err == io.EOF { // client finished sending, now reply once
            return stream.SendAndClose(&userv1.UploadSummary{Received: int32(count)})
        }
        if err != nil { return err }
        store(ev)
        count++
    }
}

// ===== CLIENT: Send() many, then CloseAndRecv() for the single reply =====
func uploadEvents(client userv1.UserServiceClient, events []*userv1.Event) {
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    stream, _ := client.UploadEvents(ctx)
    for _, ev := range events {
        stream.Send(ev) // push each one up
    }
    summary, err := stream.CloseAndRecv() // close our side, await the summary
    if err != nil { log.Fatal(err) }
    log.Printf("server received %d events", summary.GetReceived())
}
# proto:  rpc UploadEvents(stream Event) returns (UploadSummary);

# ===== SERVER: request_iterator yields incoming messages; return once =====
class UserService(pb_grpc.UserServiceServicer):
    def UploadEvents(self, request_iterator, context):
        count = 0
        for ev in request_iterator:   # iterate the client's stream
            store(ev)
            count += 1
        return pb.UploadSummary(received=count)  # single reply after client closes

# ===== CLIENT: pass an iterable/generator of requests; get one response =====
def upload_events(stub, events):
    def gen():
        for ev in events:
            yield ev
    summary = stub.UploadEvents(gen(), timeout=30.0)  # returns the single summary
    print("server received", summary.received, "events")

Asimetrinya justru intinya: SendAndClose/CloseAndRecv di Go, requestiterator plus return normal di Python, server mengonsumsi seluruh stream request sebelum_ memproduksi satu jawabannya.

10

Streaming Dua Arah (Bidirectional)

Aliran request dan aliran respons, serentak dan independen. Kedua sisi membaca dan menulis di stream yang sama pada waktu yang sama, urutan apa pun, inilah yang dibelikan pembingkaian full-duplex HTTP/2 (bagian 06). Ia bentuk di balik chat, kolaborasi real-time, telemetri langsung dengan pesan kontrol, dan sesi interaktif. Request dan response sama-sama stream di proto.

Bidirectional streaming
Both sides send and receive at once
Client Server interleaved & independent, neither side waits its turn
// proto:  rpc Chat(stream ChatMessage) returns (stream ChatMessage);

// ===== SERVER: loop Recv() and Send() on the same stream =====
func (s *server) Chat(stream userv1.UserService_ChatServer) error {
    for {
        msg, err := stream.Recv()
        if err == io.EOF { return nil } // client closed its send side
        if err != nil { return err }
        // echo back (or broadcast to a room, etc.), can Send anytime, any number
        reply := &userv1.ChatMessage{User: "server", Text: "ack: " + msg.GetText()}
        if err := stream.Send(reply); err != nil { return err }
    }
}

// ===== CLIENT: typically Send in one goroutine, Recv in another =====
func chat(client userv1.UserServiceClient) {
    stream, _ := client.Chat(context.Background())
    go func() { // concurrent receiver
        for {
            in, err := stream.Recv()
            if err != nil { return }
            log.Printf("<< %s", in.GetText())
        }
    }()
    for _, t := range []string{"hi", "how are you", "bye"} {
        stream.Send(&userv1.ChatMessage{User: "ada", Text: t}) // send concurrently
    }
    stream.CloseSend() // signal we're done sending; receiver drains the rest
}
# proto:  rpc Chat(stream ChatMessage) returns (stream ChatMessage);

# ===== SERVER: iterate incoming, yield outgoing, interleaved =====
class UserService(pb_grpc.UserServiceServicer):
    def Chat(self, request_iterator, context):
        for msg in request_iterator:          # read the client's stream
            yield pb.ChatMessage(user="server", text="ack: " + msg.text)  # write back

# ===== CLIENT: pass a request generator; iterate the response stream =====
def chat(stub):
    def outgoing():
        for t in ["hi", "how are you", "bye"]:
            yield pb.ChatMessage(user="ada", text=t)
    responses = stub.Chat(outgoing())   # both directions live at once
    for reply in responses:
        print("<<", reply.text)

# (for true concurrency under load, prefer the async API: grpc.aio)
Streaming gotchas to respect

Stream berumur panjang, jadi ia mengonsumsi koneksi/goroutine selama hidupnya, selalu set deadline atau batas idle, dan tangani klien menghilang di tengah stream. Tidak ada kenop back-pressure otomatis selain flow control HTTP/2, jadi producer cepat bisa menguasai consumer lambat jika Anda tidak mengatur iramanya. Dan satu stream terurut tapi bukan transaksi, jika putus di tengah, Anda telah mengirimkan prefix, jadi rancang message agar bisa dilanjutkan atau idempotent.

The four shapes at a glance
Pick by how many messages flow each way
Unary 1 -> 1 normal call Server stream 1 -> many feeds, large results Client stream many -> 1 uploads, batch Bidirectional many <-> many chat, realtime All four ride the same HTTP/2 stream machinery; the proto's two stream keywords decide the shape. rpc M(Req) returns (Res) | (stream Req) | returns (stream Res) | both
Part IV / The Machinery
11

Alur Kerja .proto -> Kode

Anda tidak pernah menulis stub dan skeleton manual, kompiler, protoc (atau toolchain buf), membaca .proto Anda dan memancarkan source idiomatis untuk tiap bahasa target lewat plugin spesifik bahasa. Inilah langkah yang mengubah kontrak menjadi kode yang bisa dipanggil, dan alasan klien Go dan server Python bisa saling beroperasi mulus: keduanya dibuat dari file yang sama.

Code generation
One contract, many generated clients and servers
user.proto the contract protoc + lang plugins (protoc-gen-go, grpc_tools) Go: *.pb.go messages + stub + server base Py: *_pb2*.py messages + stub + servicer Regenerate after every proto change, the generated files are build artifacts, committed or generated in CI.
// Install the two plugins once (they sit on your PATH):
//   go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
//   go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

// Generate messages (--go_out) AND the service stubs/skeleton (--go-grpc_out):
//   protoc \
//     --go_out=. --go_opt=paths=source_relative \
//     --go-grpc_out=. --go-grpc_opt=paths=source_relative \
//     user.proto

// Produces:
//   user.pb.go, structs for each message (+ getters)
//   user_grpc.pb.go, UserServiceClient (stub) + UserServiceServer (to implement)

// Then in code you simply import the generated package:
//   import userv1 "example.com/gen/userv1"
# Install the tooling once:
#   pip install grpcio grpcio-tools

# Generate messages AND service stubs in one command:
#   python -m grpc_tools.protoc \
#       -I. \
#       --python_out=. \
#       --grpc_python_out=. \
#       user.proto

# Produces:
#   user_pb2.py, message classes
#   user_pb2_grpc.py, UserServiceStub (client) + UserServiceServicer (to implement)

# Then import both in your code:
#   import user_pb2 as pb
#   import user_pb2_grpc as pb_grpc
Use buf in real projects

Invocations protoc mentah cepat tak terkendali. Toolchain buf membungkusnya dengan file konfigurasi, manajemen dependensi, deteksi perubahan yang memecah (ia menggagalkan CI jika Anda melanggar aturan jangan-pernah-pakai-ulang-nomor-field), dan linter. Untuk apa pun di luar mainan, pilih buf, ia mengubah disiplin evolusi skema dari bagian 04 menjadi gerbang otomatis.

12

Channel, Stub & Satu Panggilan End-to-End

Dua objek sisi klien penting, dan orang sering mencampurnya. Channel (Go menyebutnya ClientConn) adalah koneksi berumur panjang yang bisa dipakai ulang ke server; ia mengelola koneksi HTTP/2 yang mendasari, koneksi ulang, dan state load-balancing. Stub adalah objek hasil generasi yang murah yang Anda buat di atas channel untuk benar-benar memanggil method. Aturannya: buat channel sekali dan bagikan; buat stub dengan bebas. Membuka channel per request menghancurkan performa, Anda membuang pemakaian-ulang koneksi yang merupakan seluruh tujuan HTTP/2.

A unary call, end to end
Every step from your method call to the typed reply
1 / you call stub.GetUser(ctx, req) 2 / stub serializes req -> protobuf 3 / client interceptors run 4 / HTTP/2 stream -> :path, DATA 5 / server reads frames 6 / deserialize + interceptors 7 / YOUR handler runs 8 / serialize reply + status The reply retraces the path: serialized on the server, framed over the same stream, deserialized by the stub, and handed back to you as a typed value, or a typed error carrying a status code. channel = the reused connection / stub = the cheap per-call caller on top of it
The performance rule

Satu channel per server, dibagikan di seluruh aplikasi Anda, seumur hidupnya. Stub sekali pakai. Jika latensi Anda misterius jelek, hal pertama yang diperiksa apakah Anda membuat channel (dan karenanya handshake HTTP/2 + TLS baru) di setiap panggilan.

13

Metadata, Deadline & Pembatalan

Ini tool yang mengakui jaringan itu nyata (peringatan bagian 03, dibuat konkret). Metadata adalah kanal sampingan key-value gRPC, padanan header HTTP, untuk hal yang bukan message itu sendiri: token auth, trace/request ID, versi API. Deadline memberi batas waktu absolut pada sebuah panggilan dan, yang krusial, menyebar lintas hop. Pembatalan membuat pemanggil (atau koneksi yang putus) membatalkan kerja yang sedang berjalan supaya server tidak bersusah payah untuk hasil yang tidak diinginkan siapa pun.

Deadline mengalahkan timeout: dan ia menyebar

Deadline gRPC adalah titik waktu absolut, bukan durasi per-hop. Saat service A memanggil B dengan deadline 1 detik dan B memanggil C, budget yang tersisa ikut bepergian, jadi C tahu ia punya, katakanlah, 600ms tersisa, bukan satu detik baru. Ini mencegah kaskade klasik tempat setiap lapisan menunggu timeout penuhnya sendiri dan total latensi menggembung. Selalu set deadline di setiap panggilan. Panggilan tanpa deadline bisa menggantung selamanya, mengunci resource.

// ===== CLIENT: attach a deadline + metadata =====
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) // absolute deadline
defer cancel() // cancel frees resources whether we time out or finish early

ctx = metadata.AppendToOutgoingContext(ctx,
    "authorization", "Bearer "+token,   // auth travels as metadata, sec 16
    "x-request-id", reqID)              // trace id for correlation, like sec 15 of the layers manual

resp, err := client.GetUser(ctx, &userv1.GetUserRequest{Id: "u42"})
if status.Code(err) == codes.DeadlineExceeded {
    log.Println("call timed out") // a network-shaped failure a local call never had
}

// ===== SERVER: read metadata, respect the inherited deadline =====
func (s *server) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error) {
    md, _ := metadata.FromIncomingContext(ctx)
    auth := md.Get("authorization") // verify token here (or in an interceptor, sec 15)

    // ctx already carries the client's remaining deadline + cancellation, // pass it straight to the DB driver so slow work is abandoned automatically.
    if ctx.Err() != nil { return nil, status.FromContextError(ctx.Err()).Err() }
    _ = auth
    return s.lookup(ctx, req.GetId())
}
# ===== CLIENT: attach a deadline (timeout=) + metadata =====
metadata = (
    ("authorization", f"Bearer {token}"),  # auth as metadata, sec 16
    ("x-request-id", req_id),               # trace id for correlation
)
try:
    resp = stub.GetUser(pb.GetUserRequest(id="u42"),
                        timeout=1.0,         # the deadline, in seconds
                        metadata=metadata)
except grpc.RpcError as e:
    if e.code() == grpc.StatusCode.DEADLINE_EXCEEDED:
        print("call timed out")             # a network-shaped failure

# ===== SERVER: read metadata, check for cancellation =====
class UserService(pb_grpc.UserServiceServicer):
    def GetUser(self, request, context):
        md = dict(context.invocation_metadata())
        auth = md.get("authorization")       # verify token (or in an interceptor, sec 15)

        if not context.is_active():          # client gone / deadline passed?
            return pb.GetUserResponse()      # abandon the work
        return self.lookup(request.id)
Pass the context down, always

Di Go, alirkan ctx yang masuk ke setiap panggilan hilir (query DB, RPC keluar). Itulah yang membuat deadline dan pembatalan benar-benar bekerja; jika klien menutup telepon, pembatalannya merambat ke bawah dan membebaskan semuanya. Handler yang mengabaikan ctx terus menggiling kerja yang ditinggalkan.

14

Status Code & Penanganan Error

gRPC tidak memakai status code HTTP. Ia punya set tetap status code sendiri: enum ±16 nilai, dikirim di trailer grpc-status (bagian 06). Setiap panggilan berakhir dengan tepat satu: OK untuk sukses, atau salah satu kode error dengan pesan opsional. Mengembalikan kode yang benar bagian dari kontrak Anda, karena klien (dan kebijakan retry, bagian 17) bercabang padanya.

Kode Artinya Padanan HTTP terdekat
OK Sukses 200
INVALID_ARGUMENT Klien mengirim input buruk (independen dari state sistem) 400
UNAUTHENTICATED Tanpa / kredensial tidak valid 401
PERMISSION_DENIED Terautentikasi tapi tidak diizinkan 403
NOT_FOUND Entitas yang diminta tidak ada 404
ALREADY_EXISTS Create bentrok dengan entitas yang ada 409
FAILED_PRECONDITION Sistem tidak dalam state untuk operasi itu 400/409
RESOURCE_EXHAUSTED Kuota / rate limit tercapai 429
DEADLINE_EXCEEDED Panggilan melewati deadline-nya 504
UNAVAILABLE Sementara, server mati/kelebihan beban; aman untuk retry 503
INTERNAL Bug sungguhan / invariant rusak 500
UNIMPLEMENTED Method tidak diimplementasikan di server ini 501
import (
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

// SERVER: return a typed status, not a bare error string.
func (s *server) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error) {
    if req.GetId() == "" {
        return nil, status.Error(codes.InvalidArgument, "id is required")
    }
    u, found := s.db.Find(req.GetId())
    if !found {
        return nil, status.Errorf(codes.NotFound, "no user with id %q", req.GetId())
    }
    return &userv1.GetUserResponse{User: u}, nil
}

// CLIENT: inspect the code to decide what to do.
resp, err := client.GetUser(ctx, req)
if err != nil {
    st := status.Convert(err)        // pull the status out of the error
    switch st.Code() {
    case codes.NotFound:        // expected, show "not found" in UI
    case codes.Unavailable:     // transient, retry with backoff, sec 17
    default:                    log.Printf("unexpected: %v: %s", st.Code(), st.Message())
    }
}
import grpc

# SERVER: abort with a code + message (or set_code/set_details then return).
class UserService(pb_grpc.UserServiceServicer):
    def GetUser(self, request, context):
        if not request.id:
            context.abort(grpc.StatusCode.INVALID_ARGUMENT, "id is required")
        user = self.db.find(request.id)
        if user is None:
            context.abort(grpc.StatusCode.NOT_FOUND, f"no user with id {request.id!r}")
        return pb.GetUserResponse(user=user)

# CLIENT: catch RpcError and switch on .code()
try:
    resp = stub.GetUser(req, timeout=1.0)
except grpc.RpcError as e:
    if e.code() == grpc.StatusCode.NOT_FOUND:
        ...   # expected, show "not found"
    elif e.code() == grpc.StatusCode.UNAVAILABLE:
        ...   # transient, retry with backoff, sec 17
    else:
        print("unexpected:", e.code(), e.details())
Rich, structured errors

Saat kode + pesan tidak cukup (mis. detail validasi per field, seperti amplop error REST), gRPC mendukung error details: message protobuf bertipe yang ditempel ke status (tipe google.rpc: BadRequest, QuotaFailure, RetryInfo...). Klien men-deserialize-nya sebagai data terstruktur, tidak pernah dengan mem-parse string manusia, prinsip yang sama dengan field code yang bisa dibaca mesin di amplop error manual REST.

15

Interceptor: Middleware-nya gRPC

Jika Anda membaca bab handlers/services/middleware, ini gagasan yang sama persis dengan nama berbeda. Interceptor adalah fungsi yang membungkus setiap RPC, berjalan sebelum dan/atau setelah handler Anda, tempat memusatkan urusan lintas-pemotongan supaya Anda tidak mengulanginya di tiap method: autentikasi, logging, metrik, tracing, pemulihan panic, rate limiting. Ia punya dua rasa: interceptor unary (membungkus panggilan sekali jalan) dan interceptor stream (membungkus panggilan streaming), di sisi klien dan server.

The interceptor chain
Cross-cutting logic wraps the handler, just like HTTP middleware
incoming RPC -> recovercatch panics logging+ metrics, trace authverify token your handlerbusiness logic Order matters (recover outermost, auth nearest the handler), the same ordering discipline as HTTP middleware. <- response unwinds back through each interceptor (logging records status, recover guards the return)
// A unary server interceptor: signature is fixed by the framework.
func authInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler) (any, error) {

    md, _ := metadata.FromIncomingContext(ctx)
    tokens := md.Get("authorization")
    if len(tokens) == 0 || !valid(tokens[0]) {
        return nil, status.Error(codes.Unauthenticated, "missing or invalid token")
    }
    // attach the verified identity for the handler to read from ctx
    ctx = context.WithValue(ctx, userKey{}, parse(tokens[0]))
    return handler(ctx, req) // call the next link / the real handler
}

func loggingInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler) (any, error) {
    start := time.Now()
    resp, err := handler(ctx, req)
    log.Printf("%s took %s -> %s", info.FullMethod, time.Since(start), status.Code(err))
    return resp, err
}

// Register the chain when building the server (outermost listed first):
s := grpc.NewServer(
    grpc.ChainUnaryInterceptor(loggingInterceptor, authInterceptor),
)
import grpc

class AuthInterceptor(grpc.ServerInterceptor):
    def intercept_service(self, continuation, handler_call_details):
        md = dict(handler_call_details.invocation_metadata)
        token = md.get("authorization", "")
        if not valid(token):
            # short-circuit: abort before the handler ever runs
            def deny(request, context):
                context.abort(grpc.StatusCode.UNAUTHENTICATED, "missing or invalid token")
            return grpc.unary_unary_rpc_method_handler(deny)
        return continuation(handler_call_details)  # proceed to next / handler

# Register interceptors when building the server (applied in order):
server = grpc.server(
    futures.ThreadPoolExecutor(max_workers=10),
    interceptors=[AuthInterceptor()],
)

# Client-side interceptors also exist (e.g. to inject auth on every call):
#   channel = grpc.intercept_channel(base_channel, MyClientInterceptor())

Ini rumah gRPC untuk semua yang manual lapisan taruh di middleware: auth, logging, tracing, rate limiting, pemulihan panic, ditulis sekali, diterapkan ke setiap method.

Part V / Production & Interop
16

Autentikasi & Keamanan

Keamanan gRPC terpecah menjadi dua pertanyaan ortogonal: keamanan kanal (apakah koneksinya terenkripsi dan peer-nya benar-benar yang ia klaim? -> TLS / mTLS) dan kredensial panggilan (siapa pemanggil untuk request ini? -> token di metadata). Anda gabungkan keduanya: TLS melindungi pipa, token per-panggilan mengidentifikasi pengguna. Kredensial insecure yang dipakai di contoh awal hanya untuk pengembangan lokal, jangan pernah dibawa ke produksi.

Lapisan Mekanisme Menjawab
Transport TLS: server menyajikan sertifikat terenkripsi? + apakah server-nya asli?
Transport mTLS: kedua sisi menyajikan sertifikat + apakah layanan klien-nya asli? (identitas service-to-service)
Per-panggilan token di metadata (JWT / OAuth bearer) user mana yang membuat panggilan ini? (authn/authz, diverifikasi di interceptor bagian 15)
import "google.golang.org/grpc/credentials"

// ===== SERVER over TLS =====
creds, _ := credentials.NewServerTLSFromFile("server.crt", "server.key")
s := grpc.NewServer(grpc.Creds(creds)) // every connection is now encrypted

// ===== CLIENT over TLS =====
tlsCreds := credentials.NewTLS(&tls.Config{RootCAs: pool}) // trust this CA
conn, _ := grpc.NewClient("api.example.com:443", grpc.WithTransportCredentials(tlsCreds))

// ===== Per-call token (combine with TLS) =====
// Implement credentials.PerRPCCredentials so a fresh token rides on every call:
type tokenCreds struct{ token string }
func (t tokenCreds) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) {
    return map[string]string{"authorization": "Bearer " + t.token}, nil
}
func (t tokenCreds) RequireTransportSecurity() bool { return true } // refuse to send token in cleartext

conn, _ = grpc.NewClient("api.example.com:443",
    grpc.WithTransportCredentials(tlsCreds),
    grpc.WithPerRPCCredentials(tokenCreds{token}),
)
# ===== SERVER over TLS =====
with open("server.key", "rb") as k, open("server.crt", "rb") as c:
    creds = grpc.ssl_server_credentials([(k.read(), c.read())])
server.add_secure_port("[::]:443", creds)   # encrypted port

# ===== CLIENT over TLS =====
with open("ca.crt", "rb") as f:
    channel_creds = grpc.ssl_channel_credentials(root_certificates=f.read())

# ===== Per-call token, composed with the channel credentials =====
class TokenAuth(grpc.AuthMetadataPlugin):
    def __init__(self, token): self.token = token
    def __call__(self, context, callback):
        callback((("authorization", f"Bearer {self.token}"),), None)  # adds metadata per call

call_creds = grpc.metadata_call_credentials(TokenAuth(token))
composite  = grpc.composite_channel_credentials(channel_creds, call_creds)
channel    = grpc.secure_channel("api.example.com:443", composite)
Never send tokens over plaintext

Bearer token seperti kata sandi, siapa pun yang memegangnya bisa menyamar sebagai pemanggil (menggema peringatan pencurian JWT dari manual auth). Selalu wajibkan keamanan transport sebelum menempelkan kredensial. mTLS standar untuk identitas service-to-service di dalam mesh; authz tingkat pengguna tetap menumpang token per-panggilan yang diverifikasi interceptor.

17

Ketahanan & Load Balancing

Karena setiap panggilan jarak jauh bisa gagal dengan cara bentuk jaringan, gRPC produksi bersandar pada beberapa fitur ketahanan, kebanyakan dikonfigurasi, bukan ditulis tangan. Yang tidak jelas adalah load balancing: koneksi gRPC yang berumur panjang dan termultipleks (bagian 06) memecah load balancer naif, dan memahami kenapa esensial untuk men-deploy-nya.

The load-balancing pitfall
An L4 balancer pins every call to one backend
L4 (TCP) balancer, WRONG for gRPC client L4 LBper-conn backend 1 (hot) backend 2 idle all multiplexed calls land on one box L7 / client-side balancing, RIGHT client L7 / meshper-RPC backend 1 backend 2 backend 3 individual RPCs spread across backends
Fixes: an L7 proxy that balances per-request (Envoy/Linkerd), or client-side balancing where the client resolves all backends and round-robins RPCs itself. A plain TCP balancer will funnel an entire connection's traffic to one pod.

Retry, keepalive, health

gRPC mendukung retry deklaratif via service config (kebijakan JSON yang ditempel ke channel): status code mana yang bisa di-retry (biasanya UNAVAILABLE), berapa percobaan, dan exponential backoff, tanpa loop retry di kode Anda. Keepalive ping mendeteksi koneksi mati dan menjaga yang idle tetap hidup lewat NAT/proxy. Layanan health-checking standar membuat load balancer dan probe Kubernetes bisa bertanya "kamu siap?" Bersama-sama ini baseline ketahanannya.

service-config.json / declarative retry policy (language-agnostic)
{
  "methodConfig": [{
    "name": [{ "service": "user.v1.UserService" }],
    "retryPolicy": {
      "maxAttempts": 4,
      "initialBackoff": "0.1s",
      "maxBackoff": "2s",
      "backoffMultiplier": 2,
      "retryableStatusCodes": [ "UNAVAILABLE" ]
    }
  }]
}
// Attach to the channel (Go: grpc.WithDefaultServiceConfig(json);
// Python: grpc.insecure_channel(target, options=[("grpc.service_config", json)]))
// Only retry IDEMPOTENT methods automatically, retrying a non-idempotent
// "charge card" can double-charge (same lesson as POST idempotency keys in REST).
Retries need idempotency

Retry otomatis hanya aman untuk method idempotent. Me-retry "buat pembayaran" saat timeout bisa menagih dua kali, persis bahaya yang manual REST pecahkan dengan idempotency key. Tandai method mana yang aman, dan untuk sisanya, pakai idempotency key atau terima bahwa mereka tidak di-retry otomatis.

18

gRPC vs REST: Memilih

Bukan rivalitas, tool berbeda. Aturan keputusan yang jujur: gRPC untuk lalu lintas internal, throughput tinggi, bertipe, antar-layanan; REST/JSON untuk API publik, menghadap browser, bisa di-debug manusia, ramah cache. Kebanyakan sistem nyata menjalankan keduanya: gRPC antar layanan backend, edge REST (atau GraphQL) untuk dunia luar.

Dimensi gRPC REST / JSON
Payload Biner protobuf, ringkas, cepat Teks JSON, besar, bisa dibaca manusia
Kontrak Dipaksakan .proto; codegen Konvensi + dokumen (OpenAPI opsional)
Transport Hanya HTTP/2 (termultipleks) HTTP apa pun, termasuk 1.1
Streaming Kelas satu, dua arah Tempelan (SSE, polling, WebSocket)
Dukungan browser Tidak natif, butuh gRPC-Web (bagian 19) Universal
Bisa di-debug manusia Butuh tool (grpcurl) curl, browser, devtools
Caching HTTP Tidak benar-benar Matang (ETag, Cache-Control)
Cocok untuk Microservice, internal, latensi rendah, poliglot API publik, pintu depan web/mobile, pihak ketiga
The pragmatic architecture

Bentuk yang sangat umum: klien memukul gateway REST/JSON lewat HTTP/1.1; di belakangnya, gateway itu dan semua layanan internal bicara gRPC satu sama lain. Anda dapat keramahan publik REST di tepi dan kecepatan/ketikan gRPC di inti, dan tool seperti grpc-gateway (bagian 19) bahkan bisa membuat edge REST dari proto yang sama.

19

gRPC-Web & Interop Browser

Batasan keras, dan kejutan yang sering: browser tidak bisa bicara gRPC natif. Alasannya dari bagian 06, gRPC butuh kendali halus atas frame dan trailer HTTP/2, dan fetch/XHR browser tidak mengekspos itu. Jadi bicara ke gRPC dari frontend web butuh lapisan penerjemah.

Bridging to the browser
A proxy translates between browser-friendly and native gRPC
BrowsergRPC-Web client ProxyEnvoy / gateway gRPC servicenative gRPC gRPC-WebHTTP/1.1-friendly native gRPCHTTP/2 + trailers

Pilihan Anda, kira-kira dalam urutan seberapa banyak gRPC yang Anda mau di frontend:

  • gRPC-Web: varian protokol plus klien JS/TS hasil generasi; proxy (Envoy punya filter bawaan) menerjemahkannya ke gRPC sungguhan. Streaming terbatas (server-streaming jalan; dua arah penuh umumnya tidak).
  • Connect (connectrpc), keluarga protokol modern yang berbicara gRPC, gRPC-Web, dan HTTP/JSON miliknya sendiri, sering tanpa proxy terpisah, dari handler yang sama. Makin sering jadi jalur paling ramah.
  • grpc-gateway / transcoding: buat fasad REST+JSON dari .proto Anda (lewat anotasi HTTP). Browser memakai REST polos; gateway mentranscode ke gRPC. Inilah pola "edge REST dari satu proto" yang dirujuk di bagian 18.
Plan the edge before committing

Jika browser harus memanggil layanan Anda langsung, putuskan strategi jembatannya di awal, Anda tidak bisa sekadar mengarahkan fetch ke port gRPC. Untuk layanan internal saja ini tak pernah masalah; untuk apa pun yang menghadap web ini keputusan desain kelas satu.

20

Observabilitas & Debugging

Payload biner berarti Anda tidak bisa melirik lalu lintas seperti JSON (bagian 05), jadi gRPC mengirim ekosistem untuk mengintip ke dalam. Mengetahuinya mengubah service gRPC dari kotak hitam menjadi sesuatu yang bisa Anda congkel, telusuri, dan probe.

Tool / fitur Yang ia berikan
grpcurl curl-nya gRPC, panggil method dari CLI dengan JSON masuk/keluar, daftar service dan method.
Server reflection Membuat klien/tool menemukan service & skema message server saat runtime, jadi grpcurl bekerja tanpa .proto di tangan.
Health checking Service grpc.health.v1.Health standar untuk probe readiness/liveness (K8s, load balancer).
channelz Introspeksi bawaan channel hidup, koneksi, dan statistik per-RPC untuk debugging konektivitas.
Interceptor (bagian 15) Kait untuk metrik (Prometheus), log terstruktur, dan distributed tracing (OpenTelemetry) di setiap panggilan.
Propagasi trace Teruskan trace/request ID lewat metadata (bagian 13) supaya satu request bisa diikuti melewati setiap layanan yang disentuhnya.
grpcurl / debugging from the shell
# List every service the server exposes (needs reflection enabled):
grpcurl localhost:50051 list

# List the methods of one service:
grpcurl localhost:50051 list user.v1.UserService

# Call a unary method with a JSON request, grpcurl turns it into protobuf for you:
grpcurl -d '{"id": "u42"}' localhost:50051 user.v1.UserService/GetUser

# Against a TLS server, drop -plaintext; for local insecure servers, add it:
grpcurl -plaintext -d '{"id":"u42"}' localhost:50051 user.v1.UserService/GetUser
Enable reflection in non-prod

Nyalakan server reflection di dev/staging supaya grpcurl dan tool GUI (seperti mode gRPC Postman atau grpcui) bisa menjelajahi API Anda tanpa Anda mengirim .proto ke mana-mana. Banyak tim menonaktifkannya di produksi untuk menghindari mengiklankan skema, keputusan kecil permukaan serangan.

21

Crib-Sheet Debug

Seluruh manual dipadatkan menjadi yang Anda raih saat tertekan.

Konsep Satu baris
gRPC Panggilan method jarak jauh bertipe, kontrak Protobuf, serialisasi biner, transport HTTP/2.
Protobuf (IDL) .proto adalah kontraknya; kedua sisi membuat kode darinya.
Nomor field Identitas wire sebenarnya, jangan pernah mengubah atau memakai ulang satu pun; menambah field aman.
Format wire tag = (field«3)|wiretype, dikemas varint; kecil + cepat, tidak bisa dibaca manusia.
HTTP/2 Satu koneksi, banyak stream tersisip; satu RPC = satu stream.
Unary 1->1, panggilan normal (±90% RPC).
Server stream 1->banyak, umpan, set hasil besar.
Client stream banyak->1, upload, agregasi batch.
Bidi stream banyak<->banyak, chat, realtime, full-duplex.
Channel vs stub Channel = koneksi dipakai ulang (satu, dibagikan); stub = pemanggil murah per-panggilan.
Metadata Kanel sampingan key-value = header HTTP; membawa token & trace ID.
Deadline Batas waktu absolut, menyebar lintas hop, set satu di setiap panggilan.
Status code Enum milik gRPC sendiri (OK, NOT_FOUND, UNAVAILABLE...), di trailer, bukan kode HTTP.
Interceptor Middleware gRPC: auth, logging, tracing, pemulihan; unary & stream.
Keamanan TLS/mTLS untuk pipa + token-di-metadata untuk pengguna; jangan pernah insecure di produksi.
Load balancing Pakai L7 / sisi klien, balancer TCP L4 mem-pin semuanya ke satu backend.
Retry Deklaratif via service config; hanya auto-retry method idempotent.
Browser Tanpa gRPC natif, pakai gRPC-Web, Connect, atau gateway REST.
Debug grpcurl + reflection; channelz untuk koneksi; health service untuk probe.
vs REST gRPC = internal/bertipe/cepat; REST = publik/ter-debug/cacheable. Jalankan keduanya.

Seluruh topik dalam satu tarikan napas: gRPC membuat klien memanggil method server seolah lokal. Anda mendefinisikan API sekali di .proto (bagian 04), protoc membuat stub dan skeleton server bertipe (bagian 11), argumen diserialkan sebagai protobuf ringkas (bagian 05) dan diangkut lewat stream HTTP/2 termultipleks (bagian 06) dalam satu dari empat bentuk, unary, server-, client-, atau bidirectional-streaming (bagian 07-10). Channel berumur panjang menaungi stub murah per-panggilan (bagian 12); metadata membawa konteks auth dan trace, deadline membatasi dan menyebarkan panggilan, pembatalan merebut kembali kerja (bagian 13); setiap panggilan berakhir dengan status code gRPC (bagian 14); interceptor memusatkan logika lintas-pemotongan (bagian 15); TLS/mTLS plus token per-panggilan mengamankannya (bagian 16); retry deklaratif dan balancing L7/sisi-klien membuatnya tangguh (bagian 17). Rengkuh gRPC di dalam sistem Anda dan REST di tepi publik (bagian 18-19), dan andalkan grpcurl, reflection, dan interceptor untuk mengintip ke dalam (bagian 20).

Berpijak pada dokumen grpc.io & protobuf.dev / MDN untuk latar HTTP/2 / contoh Go 1.22+ (google.golang.org/grpc) / Python 3.11+ (grpcio).