gRPC, memanggil fungsi jarak jauh seperti fungsi lokal.
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.
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.
GetUser or
SendPayment
and the work happens elsewhere. One models nouns; the other models verbs."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.
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.
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.
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.
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.
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.
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.
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:
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,
emaildikirim sebagai field2, 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
reservedsupaya 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.
stringyang tidak di-set adalah"",intyang tidak di-set0,boolyang tidak di-setfalse. Jika Anda harus membedakan "tidak ada" dari "nol", pakaioptional(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. |
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.
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.
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).
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.
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.
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 |
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.
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.
// ===== 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.
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.
// 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.
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.
// 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.
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.
// 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)
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.
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.
// 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
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.
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.
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.
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)
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.
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())
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.
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.
// 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.
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)
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.
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.
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.
{
"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).
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.
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 |
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.
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.
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
.protoAnda (lewat anotasi HTTP). Browser memakai REST polos; gateway mentranscode ke gRPC. Inilah pola "edge REST dari satu proto" yang dirujuk di bagian 18.
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.
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. |
# 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
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.
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).