Kendi .proto sözleşmeni yaz, protoc ile kod üret, sunucu ve istemciyi kur. Unary'den bidirectional streaming'e 4 RPC tipini komut-komut, satır-satır.
İzole bir klasör ve sanal Python ortamı. Bitince silersin, sistem Python'un kirlenmez.
mkdir grpc-demo
cd grpc-demo
python3 -m venv .venv
source .venv/bin/activate
Windows'ta: .venv\Scripts\activate
pip install grpcio grpcio-tools
Kısaca teorik zemin. Sonra tamamen pratik.
gRPC, Google'ın geliştirdiği, HTTP/2 üzerinde çalışan bir RPC (Remote Procedure Call) çerçevesidir. Servisler arası iletişimde REST'e göre avantajları var: binary format (daha küçük mesajlar), multiplexing (tek bağlantıda paralel çağrılar), native streaming desteği, her dilde çalışan kod üretimi.
Protocol Buffers (protobuf) ise gRPC'nin mesaj formatıdır. Bir .proto dosyasında servis ve mesaj yapısını tanımlarsın; protoc bu dosyadan seçtiğin dilde (Python, Go, Java…) client ve server kodunu üretir. JSON/XML'e kıyasla hem daha küçük (binary) hem daha hızlı (parse edilmesi için).
REST'te sözleşme dokümandır (OpenAPI), gRPC'de sözleşme .proto dosyasıdır ve her iki taraf o dosyadan aynı kodu üretir — yanlış tip göndermek derleme zamanında yakalanır.
Bu tutorial'da şunu yapacaksın: tek bir chat.proto dosyasında 4 farklı RPC tipi tanımlayıp hem sunucu hem istemci kodunu çalışır hale getireceksin.
Her şeyin başı burası. Sunucu ne yapacak, istemci ne gönderecek, ne alacak — hepsi burada tanımlı. Kod yazmaya başlamadan önce bu dosyayı bitirirsin.
syntax = "proto3";
package chat;
service Chat {
// Unary: tek istek gönder, tek yanıt al
rpc SayHello (HelloRequest) returns (HelloReply);
// Server streaming: tek istek gönder, birden fazla yanıt al
rpc ListGreetings (HelloRequest) returns (stream HelloReply);
// Client streaming: birden fazla istek gönder, tek yanıt al
rpc SendManyGreetings (stream HelloRequest) returns (HelloReply);
// Bidirectional streaming: her iki yönde de akış
rpc ChatStream (stream HelloRequest) returns (stream HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
Bu dosya değişmeden sunucu ve istemci farklı dillerde yazılabilir. Go sunucu, Python istemci — aynı chat.proto'dan üretilen kod, farklı dillerde birbiriyle konuşabilir. Sözleşme bozmak (alan numarası, tip değiştirmek) backward-incompatible'dır.
Bir komut — iki Python dosyası. Bunları elle yazmazsın, proto'dan üretirsin.
python -m grpc_tools.protoc \
-I. \
--python_out=. \
--grpc_python_out=. \
chat.proto
Komut çalıştıktan sonra dizinde iki yeni dosya belirir:
| Dosya | Ne İçeriyor | Sen Bunu Kullanacaksın |
|---|---|---|
| chat_pb2.py | HelloRequest ve HelloReply sınıfları + binary serileştirme kodu | chat_pb2.HelloRequest(name="...") ile |
| chat_pb2_grpc.py | ChatServicer (sunucu tabanı) + ChatStub (istemci) + kayıt fonksiyonu | class ChatServicer(chat_pb2_grpc.ChatServicer): ile |
Bu iki dosyayı elle düzenleme. Proto değişirse komutu tekrar çalıştır, üretilen dosyaları sil/yenile. Elle yapılan değişiklikler sonraki üretimde silinir. Genelde bu dosyalar versiyonlamaya dahil edilmez (.gitignore'a eklenir).
Üretilen ChatServicer tabanını extend edip 4 metodu implement ediyorsun. Her metodun imzası farklı — normal fonksiyon, yield'layan fonksiyon veya iterator alan fonksiyon.
import grpc
from concurrent import futures
import chat_pb2
import chat_pb2_grpc
class ChatServicer(chat_pb2_grpc.ChatServicer):
def SayHello(self, request, context):
# Unary: tek request, tek response döndür
return chat_pb2.HelloReply(message=f"Merhaba, {request.name}!")
def ListGreetings(self, request, context):
# Server streaming: yield ile birden fazla mesaj gönder
diller = ["Türkçe: Merhaba", "İngilizce: Hello", "Japonca: Konnichiwa"]
for dil in diller:
yield chat_pb2.HelloReply(message=f"{dil}, {request.name}!")
def SendManyGreetings(self, request_iterator, context):
# Client streaming: request_iterator'dan tüm isimleri topla
isimler = [req.name for req in request_iterator]
return chat_pb2.HelloReply(
message=f"Hepsine selam: {', '.join(isimler)}"
)
def ChatStream(self, request_iterator, context):
# Bidi streaming: her isteğe karşılık bir yanıt yield'la
for request in request_iterator:
yield chat_pb2.HelloReply(message=f"Echo: {request.name}")
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
chat_pb2_grpc.add_ChatServicer_to_server(ChatServicer(), server)
server.add_insecure_port('[::]:50051')
server.start()
print("gRPC sunucu :50051 portunda başladı")
server.wait_for_termination()
if __name__ == '__main__':
serve()
Server streaming metodları normal return yerine yield kullanır çünkü gönderilecek mesaj sayısı önceden bilinmiyor. Python'un generator semantiği ile gRPC'nin akış mekanizması arasında doğal bir eşleşme var: her yield bir DATA frame'i anlamına gelir.
Bir kanal aç, stub oluştur, 4 farklı mod için ayrı fonksiyon yaz. Komut satırı argümanı hangi modu çalıştıracağını belirler.
import grpc
import sys
import chat_pb2
import chat_pb2_grpc
def run_unary(stub):
# Tek istek, tek yanıt — en basit RPC tipi
reply = stub.SayHello(chat_pb2.HelloRequest(name="Dünya"))
print(f"[unary] {reply.message}")
def run_server_stream(stub):
# Sunucu birden fazla yanıt akıtır, for ile tüketilir
for reply in stub.ListGreetings(chat_pb2.HelloRequest(name="Ali")):
print(f"[server-stream] {reply.message}")
def run_client_stream(stub):
# Generator ile birden fazla istek gönderilir, tek yanıt alınır
isimler = ["Ali", "Veli", "Ayşe"]
istekler = (chat_pb2.HelloRequest(name=n) for n in isimler)
reply = stub.SendManyGreetings(istekler)
print(f"[client-stream] {reply.message}")
def run_bidi(stub):
# Her iki yönde de akış: generator → iterator
def istekler():
for isim in ["Ali", "Veli", "Ayşe"]:
yield chat_pb2.HelloRequest(name=isim)
for reply in stub.ChatStream(istekler()):
print(f"[bidi] {reply.message}")
def main():
mod = sys.argv[1] if len(sys.argv) > 1 else "unary"
with grpc.insecure_channel("localhost:50051") as channel:
stub = chat_pb2_grpc.ChatStub(channel)
if mod == "unary":
run_unary(stub)
elif mod == "server-stream":
run_server_stream(stub)
elif mod == "client-stream":
run_client_stream(stub)
elif mod == "bidi":
run_bidi(stub)
else:
print(f"Bilinmeyen mod: {mod}")
print("Kullanım: python client.py [unary|server-stream|client-stream|bidi]")
if __name__ == '__main__':
main()
Şu an dizinde 5 dosya olmalı. Kim ne, nereye ait, hangisini commit etmeli misin?
| Dosya | Ne | Durum |
|---|---|---|
| chat.proto | Servis sözleşmesi — tek kaynak doğrusu | Commit'le, versiyonla |
| chat_pb2.py | Mesaj sınıfları — proto'dan üretildi | Genelde .gitignore'a ekle |
| chat_pb2_grpc.py | Stub + Servicer + kayıt fonksiyonu — proto'dan üretildi | Genelde .gitignore'a ekle |
| server.py | gRPC sunucu implementasyonu | Commit'le |
| client.py | gRPC istemci — 4 mod | Commit'le |
Tartışmalı. Küçük projelerde kolaylık için commit edilebilir. Büyük projelerde CI'da protoc çalıştırılır, üretilen dosyalar .gitignore'a alınır. İkinci yaklaşım daha temiz — proto değişince üretilen dosyalar eski kalma riski ortadan kalkar.
Birinci terminalde sunucuyu başlat ve açık bırak. İkinci terminalde 4 modu sırayla dene.
python server.py
gRPC sunucu :50051 portunda başladı
Sunucu bu terminalde askıda kalacak. Durdurmak için Ctrl+C.
python client.py unary
[unary] Merhaba, Dünya!
python client.py server-stream
[server-stream] Türkçe: Merhaba, Ali!
[server-stream] İngilizce: Hello, Ali!
[server-stream] Japonca: Konnichiwa, Ali!
3 ayrı DATA frame geldi — biri hemen ardından diğeri. Sunucu liste bitene kadar akıtmaya devam etti.
python client.py client-stream
[client-stream] Hepsine selam: Ali, Veli, Ayşe
İstemci 3 istek gönderdi, sunucu hepsini topladıktan sonra tek bir yanıt döndürdü. Sunucu istemci akışını kapatana kadar (generator bitti) bekleyebilir.
python client.py bidi
[bidi] Echo: Ali
[bidi] Echo: Veli
[bidi] Echo: Ayşe
Her istek gönderilir gönderilmez sunucu yanıt verdi. Gerçek bir chat senaryosunda her iki taraf birbirinden bağımsız olarak mesaj gönderebilir.
Unary bir çağrı yapıldığında ağ üzerinde ne gidip geldi? gRPC'ye özgü adımlar mavi ile işaretli.
1. İstemci TCP bağlantısı açar 2. HTTP/2 handshake (PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n) Her iki taraf SETTINGS frame gönderir → window size, max frame size, max concurrent streams 3. İstemci HEADERS frame gönderir: :method = POST :path = /chat.Chat/SayHello :scheme = http content-type = application/grpc grpc-timeout = 5S (isteğe bağlı) te = trailers 4. İstemci DATA frame gönderir: ┌─[5 byte prefix]─┬─[protobuf payload]─┐ │ compressed flag │ message length (4B)│ HelloRequest bytes │ └─────────────────┴────────────────────┘ END_STREAM flag ile stream'i kapatır (unary'de) 5. Sunucu isteği işler, ChatServicer.SayHello() çağrılır 6. Sunucu HEADERS frame gönderir: :status = 200 content-type = application/grpc 7. Sunucu DATA frame gönderir: └─ Length-Prefixed HelloReply bytes 8. Sunucu HEADERS frame gönderir (trailers): grpc-status = 0 (OK) grpc-message = "" END_STREAM flag ile stream kapanır 9. İstemci yanıtı deserialize eder → HelloReply nesnesi
Server streaming'de adım 7 birden fazla kez tekrar eder — her yield için ayrı bir DATA frame. Trailers (adım 8) yalnızca sunucu akışı bitirince gelir. Client streaming'de ise adım 4 birden fazla kez tekrar eder ve adım 5 ancak istemci END_STREAM gönderince çalışır.
gRPC kendi wire protokolünü icat etmedi. HTTP/2 multiplexing, flow control, header sıkıştırma (HPACK) — bunların hepsi HTTP/2'den geliyor. gRPC bunların üzerine: :path'ı /paket.Servis/Metod biçimine sabitleyip, protobuf payload'ı 5 byte length-prefix ile sarmak gibi kurallar koyuyor.
Bu tutorial boyunca insecure_channel ve add_insecure_port kullandık. Prod'da bu ikisi olmamalı.
gRPC'yi TLS ile güvenliğe almak için grpc.ssl_channel_credentials() ve grpc.ssl_server_credentials() kullanırsın. mTLS (karşılıklı TLS) ile hem sunucu istemciyi, hem istemci sunucuyu doğrular.
# ca.crt, client.crt, client.key mTLS tutorial'ından geliyor
with open('../mtls/ca.crt', 'rb') as f: ca_cert = f.read()
with open('../mtls/client.crt', 'rb') as f: client_cert = f.read()
with open('../mtls/client.key', 'rb') as f: client_key = f.read()
creds = grpc.ssl_channel_credentials(
root_certificates=ca_cert,
private_key=client_key,
certificate_chain=client_cert,
)
with grpc.secure_channel('localhost:50051', creds) as channel:
stub = chat_pb2_grpc.ChatStub(channel)
# ... aynı çağrılar
Sertifika nasıl üretilir, CA nasıl kurulur, handshake'te ne olur? Her adım açıklamalı olarak:
.proto dosyasında "kim ne gönderir, kim ne alır" yazan bir sözleşme yaz; protoc bunu Python koduna çevirir; gRPC bu kodu HTTP/2 üzerinden binary ile çalıştırır — unary'den bidirectional streaming'e dört farklı iletişim desenini aynı altyapıda, aynı sözleşmeyle kullanabilirsin.