gRPC-Go ile çalışmaya başlama - Akış

1. Giriş

Bu codelab'de, Go ile yazılmış bir rota eşleme uygulamasının temelini oluşturan bir istemci ve sunucu oluşturmak için gRPC-Go'yu kullanacaksınız.

Eğitimin sonunda, bir müşterinin rotasındaki özellikler hakkında bilgi almak, müşterinin rotasının özetini oluşturmak ve trafik güncellemeleri gibi rota bilgilerini sunucu ve diğer müşterilerle paylaşmak için gRPC kullanarak uzak bir sunucuya bağlanan bir istemciniz olacak.

Hizmet, istemci ve sunucu için standart kod oluşturmak üzere kullanılacak bir Protocol Buffers dosyasında tanımlanır. Böylece, bu işlevselliği uygularken zamandan ve emekten tasarruf edersiniz.

Oluşturulan bu kod, yalnızca sunucu ile istemci arasındaki iletişimin karmaşıklıklarını değil, aynı zamanda veri serileştirme ve seri durumdan çıkarma işlemlerini de ele alır.

Neler öğreneceksiniz?

  • Hizmet API'sini tanımlamak için Protocol Buffers'ı kullanma.
  • Otomatik kod oluşturma kullanarak bir Protokol Arabellek tanımından gRPC tabanlı istemci ve sunucu oluşturma.
  • gRPC ile istemci-sunucu akışı iletişimi hakkında bilgi sahibi olmanız gerekir.

Bu codelab, gRPC'ye yeni başlayan veya gRPC hakkında bilgi tazelemek isteyen Go geliştiricilerinin yanı sıra dağıtılmış sistemler oluşturmakla ilgilenen herkes için hazırlanmıştır. Daha önce gRPC deneyimi gerekmez.

2. Başlamadan önce

Ön koşullar

Aşağıdakileri yüklediğinizden emin olun:

  • Go toolchain sürümü 1.24.5 veya daha yeni olmalıdır. Yükleme talimatları için Go'nun Başlangıç bölümüne bakın.
  • Protokol arabelleği derleyicisi, protoc, 3.27.1 veya sonraki sürümler. Yükleme talimatları için derleyicinin yükleme kılavuzuna bakın.
  • Go ve gRPC için protokol arabelleği derleyici eklentileri. Bu eklentileri yüklemek için aşağıdaki komutları çalıştırın:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

PATH değişkeninizi, protokol arabelleği derleyicisinin eklentileri bulabilmesi için güncelleyin:

export PATH="$PATH:$(go env GOPATH)/bin"

Kodu alın

Bu codelab, tamamen sıfırdan başlamanız gerekmemesi için uygulamanın kaynak kodunun tamamlayabileceğiniz bir iskeletini sağlar. Aşağıdaki adımlarda, protokol arabelleği derleyici eklentilerini kullanarak standart gRPC kodu oluşturma da dahil olmak üzere uygulamanın nasıl tamamlanacağı gösterilmektedir.

Öncelikle codelab çalışma dizinini oluşturun ve cd içine gidin:

mkdir streaming-grpc-go-getting-started && cd streaming-grpc-go-getting-started

Codelab'i indirip ayıklayın:

curl -sL https://github.com/grpc-ecosystem/grpc-codelabs/archive/refs/heads/v1.tar.gz \
  | tar xvz --strip-components=4 \
  grpc-codelabs-1/codelabs/grpc-go-streaming/start_here

Alternatif olarak, yalnızca codelab dizinini içeren .zip dosyasını indirebilir ve manuel olarak sıkıştırmasını açabilirsiniz.

Bir uygulamayı yazmayı atlamak isterseniz tamamlanmış kaynak kodunu GitHub'da bulabilirsiniz.

3. İletileri ve hizmetleri tanımlama

İlk adımınız, Protocol Buffers kullanarak uygulamanın gRPC hizmetini, RPC yöntemlerini, istek ve yanıt mesajı türlerini tanımlamaktır. Hizmetinizde sunulacaklar:

  • Sunucunun uyguladığı ve istemcinin çağırdığı ListFeatures, RecordRoute ve RouteChat adlı RPC yöntemleri.
  • Yukarıdaki yöntemler çağrıldığında istemci ile sunucu arasında değiştirilen veri yapıları olan Point, Feature, Rectangle, RouteNote ve RouteSummary mesaj türleri.

Bu RPC yöntemleri ve mesaj türleri, sağlanan kaynak kodun routeguide/route_guide.proto dosyasında tanımlanır.

Protocol Buffers genellikle protobuf olarak bilinir. gRPC terminolojisi hakkında daha fazla bilgi için gRPC'nin Temel kavramlar, mimari ve yaşam döngüsü başlıklı makalesine bakın.

Mesaj türlerini tanımlama

Kaynak kodun routeguide/route_guide.proto dosyasında önce Point mesaj türünü tanımlayın. Point, haritadaki bir enlem-boylam koordinat çiftini temsil eder. Bu codelab'de koordinatlar için tam sayıları kullanın:

message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

1 ve 2 sayıları, message yapısındaki her alan için benzersiz kimlik numaralarıdır.

Ardından, Feature mesaj türünü tanımlayın. Bir Feature, Point ile belirtilen bir konumdaki bir şeyin adı veya posta adresi için string alanını kullanır:

message Feature {
  // The name or address of the feature.
  string name = 1;

  // The point where the feature is located.
  Point location = 2;
}

Ardından, enlem-boylam dikdörtgenini temsil eden bir Rectangle mesajı gelir. Bu dikdörtgen, çapraz olarak zıt iki nokta ("lo" ve "hi") olarak gösterilir.

message Rectangle {
  // One corner of the rectangle.
  Point lo = 1;

  // The other corner of the rectangle.
  Point hi = 2;
}

Ayrıca belirli bir noktada gönderilen mesajı temsil eden bir RouteNote mesajı.

message RouteNote {
  // The location from which the message is sent.
  Point location = 1;

  // The message to be sent.
  string message = 2;
}

Ayrıca RouteSummary mesajı da gereklidir. Bu mesaj, sonraki bölümde açıklanan bir RecordRoute RPC'ye yanıt olarak alınır. Alınan bireysel puanların sayısı, algılanan özelliklerin sayısı ve her nokta arasındaki mesafenin kümülatif toplamı olarak kapsanan toplam mesafeyi içerir.

message RouteSummary {
  // The number of points received.
  int32 point_count = 1;

  // The number of known features passed while traversing the route.
  int32 feature_count = 2;

  // The distance covered in metres.
  int32 distance = 3;

  // The duration of the traversal in seconds.
  int32 elapsed_time = 4;
}

Hizmet yöntemlerini tanımlama

Bir hizmeti tanımlamak için .proto dosyanızda adlandırılmış bir hizmet belirtirsiniz. route_guide.proto dosyasında, uygulamanın hizmeti tarafından sağlanan bir veya daha fazla yöntemi tanımlayan RouteGuide adlı bir service yapısı bulunur.

Hizmet tanımınızda RPC yöntemlerini tanımlayın, istek ve yanıt türlerini belirtin. Bu codelab bölümünde şunları tanımlayalım:

ListFeatures

Belirtilen Rectangle içinde bulunan Feature'ları alır. Dikdörtgen geniş bir alanı kaplayıp çok sayıda özellik içerebileceğinden sonuçlar tek seferde döndürülmek yerine (ör. tekrarlanan alan içeren bir yanıt mesajında) yayınlanır.

Bu RPC için uygun tür, sunucu tarafı yayın RPC'sidir: İstemci, sunucuya bir istek gönderir ve mesaj dizisini okumak için bir yayın alır. İstemci, döndürülen akıştan mesaj kalmayana kadar okur. Örneğimizde de görebileceğiniz gibi, yanıt türünden önce akış anahtar kelimesini yerleştirerek sunucu tarafı akış yöntemini belirtirsiniz.

rpc ListFeatures(Rectangle) returns (stream Feature) {}

RecordRoute

Geçilen bir rotadaki Point akışını kabul eder ve geçiş tamamlandığında RouteSummary döndürür.

Bu durumda istemci tarafı akış RPC'si uygun görünmektedir: İstemci, bir ileti dizisi yazar ve bunları sağlanan bir akışı kullanarak tekrar sunucuya gönderir. İstemci, mesajları yazmayı bitirdikten sonra sunucunun hepsini okumasını ve yanıtını döndürmesini bekler. İstemci tarafı yayın yöntemini, yayın anahtar kelimesini istek türünün önüne yerleştirerek belirtirsiniz.

rpc RecordRoute(stream Point) returns (RouteSummary) {}

RouteChat

Bir rota geçilirken gönderilen RouteNote akışını kabul ederken diğer RouteNote'ları (ör. diğer kullanıcılardan) alır.

Bu, çift yönlü akışın tam olarak kullanım alanıdır. Çift yönlü akış RPC'sinde her iki taraf da okuma/yazma akışı kullanarak bir ileti dizisi gönderir. İki akış bağımsız olarak çalışır. Bu nedenle, istemciler ve sunucular istedikleri sırada okuma ve yazma işlemi yapabilir.

Örneğin, sunucu yanıtlarını yazmadan önce tüm istemci mesajlarını almayı bekleyebilir veya alternatif olarak bir mesajı okuyup ardından bir mesaj yazabilir ya da okuma ve yazma işlemlerinin başka bir kombinasyonunu kullanabilir.

Her akıştaki iletilerin sırası korunur. Bu tür bir yöntemi, hem istekten hem de yanıttan önce yayın anahtar kelimesini yerleştirerek belirtirsiniz.

rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}

4. İstemci ve sunucu kodu oluşturma

Ardından, protokol arabellek derleyicisini kullanarak .proto dosyasından hem istemci hem de sunucu için standart gRPC kodunu oluşturun. routeguide dizininde şunu çalıştırın:

protoc --go_out=. --go_opt=paths=source_relative \
       --go-grpc_out=. --go-grpc_opt=paths=source_relative \
       route_guide.proto

Bu komut aşağıdaki dosyaları oluşturur:

  • route_guide.pb.go, uygulamanın mesaj türlerini oluşturma ve verilerine erişme işlevlerini ve mesajları temsil eden türlerin tanımını içerir.
  • route_guide_grpc.pb.go: İstemcinin hizmetin uzak gRPC yöntemini çağırmak için kullandığı işlevleri ve sunucunun bu uzak hizmeti sağlamak için kullandığı işlevleri içerir.

Ardından, istemci bir istek gönderdiğinde sunucunun yanıt verebilmesi için yöntemleri sunucu tarafında uygulayacağız.

5. Hizmeti uygulama

Öncelikle RouteGuide sunucuyu nasıl oluşturduğumuza bakalım. RouteGuide hizmetimizin işini yapmasını sağlamak için iki bölüm vardır:

  • Hizmet tanımımızdan oluşturulan hizmet arayüzünü uygulama: Hizmetimizin asıl "işini" yapma.
  • İstemcilerden gelen istekleri dinlemek ve bunları doğru hizmet uygulamasına göndermek için bir gRPC sunucusu çalıştırma.

server/server.go içinde RouteGuide'ı uygulayalım.

RouteGuide'ı uygulama

Oluşturulan RouteGuideService arayüzünü uygulamamız gerekir. Uygulama bu şekilde görünür.

type routeGuideServer struct {
        ...
}
...
func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
        ...
}
...

func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error {
        ...
}
...

func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
        ...
}

Her RPC uygulamasını ayrıntılı olarak inceleyelim.

Sunucu tarafında yayın RPC'si

Yayın RPC'lerimizden biriyle başlayın. ListFeatures, sunucu tarafı akış RPC'si olduğundan istemcimize birden fazla Feature göndermemiz gerekir.

func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
  for _, feature := range s.savedFeatures {
    if inRange(feature.Location, rect) {
      if err := stream.Send(feature); err != nil {
        return err
      }
    }
  }
  return nil
}

Gördüğünüz gibi, yöntem parametrelerimizde basit istek ve yanıt nesneleri almak yerine bu kez bir istek nesnesi (istemcimizin Rectangle içinde bulmak istediği Features) ve yanıtlarımızı yazmak için özel bir RouteGuide_ListFeaturesServer nesnesi alıyoruz. Bu yöntemde, döndürmemiz gereken sayıda Feature nesnesi doldururuz ve bunları Send() yöntemini kullanarak RouteGuide_ListFeaturesServer'ye yazarız. Son olarak, basit RPC'mizde olduğu gibi, yanıt yazmayı bitirdiğimizi gRPC'ye bildirmek için nil hatası döndürüyoruz. Bu çağrıda herhangi bir hata oluşursa sıfır olmayan bir hata döndürürüz. gRPC katmanı, bunu kablo üzerinden gönderilecek uygun bir RPC durumuna çevirir.

İstemci tarafı yayın RPC'si

Şimdi biraz daha karmaşık bir yönteme, istemci tarafı akış yöntemine RecordRoute bakalım. Bu yöntemde istemciden bir Points akışı alıp yolculuklarıyla ilgili bilgileri içeren tek bir RouteSummary döndürüyoruz. Gördüğünüz gibi, bu kez yöntemde hiç istek parametresi yok. Bunun yerine, sunucunun hem mesaj okumak hem de yazmak için kullanabileceği bir RouteGuide_RecordRouteServer akışı alır. Sunucu, Recv() yöntemini kullanarak istemci mesajlarını alabilir ve SendAndClose() yöntemini kullanarak tek yanıtını döndürebilir.

func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error {
  var pointCount, featureCount, distance int32
  var lastPoint *pb.Point
  startTime := time.Now()
  for {
    point, err := stream.Recv()
    if err == io.EOF {
      endTime := time.Now()
      return stream.SendAndClose(&pb.RouteSummary{
        PointCount:   pointCount,
        FeatureCount: featureCount,
        Distance:     distance,
        ElapsedTime:  int32(endTime.Sub(startTime).Seconds()),
      })
    }
    if err != nil {
      return err
    }
    pointCount++
    for _, feature := range s.savedFeatures {
      if proto.Equal(feature.Location, point) {
        featureCount++
      }
    }
    if lastPoint != nil {
      distance += calcDistance(lastPoint, point)
    }
    lastPoint = point
  }
}

Yöntem gövdesinde, istemcimizin isteklerini bir istek nesnesine (bu örnekte Point) tekrar tekrar okumak için RouteGuide_RecordRouteServer'ın Recv() yöntemini kullanırız. Bu işlem, başka mesaj kalmayana kadar devam eder. Sunucunun, her çağrıdan sonra Recv()'dan döndürülen hatayı kontrol etmesi gerekir. Bu nil ise akış hala iyidir ve okumaya devam edebilir; io.EOF ise ileti akışı sona ermiştir ve sunucu RouteSummary değerini döndürebilir. Başka bir değer varsa gRPC katmanı tarafından RPC durumuna çevrilmesi için hatayı "olduğu gibi" döndürürüz.

Çift yönlü akış RPC'si

Son olarak, çift yönlü akış RPC'mize RouteChat() bakalım.

func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
  for {
    in, err := stream.Recv()
    if err == io.EOF {
      return nil
    }
    if err != nil {
      return err
    }
    key := serialize(in.Location)

    s.mu.Lock()
    s.routeNotes[key] = append(s.routeNotes[key], in)
    // Note: this copy prevents blocking other clients while serving this one.
    // We don't need to do a deep copy, because elements in the slice are
    // insert-only and never modified.
    rn := make([]*pb.RouteNote, len(s.routeNotes[key]))
    copy(rn, s.routeNotes[key])
    s.mu.Unlock()

    for _, note := range rn {
      if err := stream.Send(note); err != nil {
        return err
      }
    }
  }
}

Bu kez, istemci tarafı akış örneğimizde olduğu gibi mesaj okumak ve yazmak için kullanılabilecek bir RouteGuide_RouteChatServer akışı elde ederiz. Ancak bu kez, istemci mesaj akışına mesaj yazmaya devam ederken değerleri yöntemimizin akışı üzerinden döndürüyoruz. Burada okuma ve yazma söz dizimi, istemci akışı yöntemimize çok benzer. Ancak sunucu, birden fazla yanıt yazdığı için SendAndClose() yerine akışın send() yöntemini kullanır. Her iki taraf da diğer tarafın iletilerini her zaman yazıldıkları sırayla alsa da hem istemci hem de sunucu, iletileri herhangi bir sırada okuyup yazabilir. Akışlar tamamen bağımsız olarak çalışır.

Sunucuyu başlatma

Tüm yöntemlerimizi uyguladıktan sonra, istemcilerin hizmetimizi kullanabilmesi için bir gRPC sunucusu da başlatmamız gerekir. Aşağıdaki snippet'te, RouteGuide hizmetimiz için bunu nasıl yaptığımız gösterilmektedir:

lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
if err != nil {
  log.Fatalf("failed to listen: %v", err)
}

grpcServer := grpc.NewServer()

s := &routeGuideServer{routeNotes: make(map[string][]*pb.RouteNote)}
s.loadFeatures()
pb.RegisterRouteGuideServer(grpcServer, s)
grpcServer.Serve(lis)

main() bölgesinde adım adım neler oluyor:

  1. lis, err := net.Listen(...) kullanarak uzak istemci isteklerini dinlemek için kullanılacak TCP bağlantı noktasını belirtin. Varsayılan olarak uygulama, sunucu çalıştırılırken komut satırında --port anahtarı iletilerek veya port değişkeniyle belirtildiği gibi 50051 TCP bağlantı noktasını kullanır. TCP bağlantı noktası açılamazsa uygulama ölümcül bir hatayla sonlanır.
  2. grpc.NewServer(...) kullanarak gRPC sunucusunun bir örneğini oluşturun ve bu örneği grpcServer olarak adlandırın.
  3. Uygulamanın API hizmetini temsil eden bir yapı olan routeGuideServer için s. adlı bir işaretçi oluşturun.
  4. Diziyi doldurmak için s.loadFeatures() kullanın s.savedFeatures.
  5. Hizmet uygulamamızı gRPC sunucusuna kaydedin.
  6. İstemci istekleri için engelleme beklemesi yapmak üzere bağlantı noktası ayrıntılarımızla sunucuda Serve() işlevini çağırın. Bu işlem, süreç sonlandırılana veya Serve() işlevi çağrılana kadar devam eder.Stop()

loadFeatures() işlevi, koordinat-konum eşlemelerini server/testdata.go kaynağından alır.

6. İstemciyi oluşturma

Şimdi istemci kodunu uygulayacağınız yer olan client/client.go dosyasını düzenleyin.

Uzak hizmetin yöntemlerini çağırmak için öncelikle sunucuyla iletişim kuracak bir gRPC kanalı oluşturmamız gerekir. Bunu, istemcinin main() işlevinde sunucunun hedef URI dizesini (bu örnekte yalnızca adres ve bağlantı noktası numarası) grpc.NewClient()'ya ileterek oluştururuz:

// Set up a connection to the gRPC server.
conn, err := grpc.NewClient("dns:///"+*serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
  log.Fatalf("fail to dial: %v", err)
}
defer conn.Close()

serverAddr değişkeniyle tanımlanan sunucunun adresi varsayılan olarak localhost:50051'dir ve istemci çalıştırılırken komut satırındaki --addr anahtarıyla geçersiz kılınabilir.

İstemcinin, TLS veya JWT kimlik bilgileri gibi kimlik doğrulama kimlik bilgileri gerektiren bir hizmete bağlanması gerekiyorsa istemci, gerekli kimlik bilgilerini içeren bir DialOptions nesnesini grpc.NewClient için parametre olarak iletebilir. RouteGuide hizmeti herhangi bir kimlik bilgisi gerektirmez.

gRPC kanalı ayarlandıktan sonra, Go işlev çağrıları aracılığıyla RPC'ler gerçekleştirmek için bir istemci stub'ına ihtiyacımız vardır. Stub, uygulamanın .proto dosyasından oluşturulan route_guide_grpc.pb.go dosyası tarafından sağlanan NewRouteGuideClient yöntemi kullanılarak alınır.

import (pb "github.com/grpc-ecosystem/codelabs/getting_started_streaming/routeguide")

client := pb.NewRouteGuideClient(conn)

Hizmet yöntemlerini çağırma

Şimdi de hizmet yöntemlerimizi nasıl çağırdığımıza bakalım. gRPC-Go'da RPC'ler, engelleme/eşzamanlı modda çalışır. Bu, RPC çağrısının sunucunun yanıt vermesini beklediği ve yanıt ya da hata döndüreceği anlamına gelir.

Sunucu tarafında yayın RPC'si

Burada, coğrafi Feature nesnelerinin akışını döndüren sunucu tarafı akış yöntemi ListFeatures çağrılır.

rect := &pb.Rectangle{ ... }  // initialize a pb.Rectangle
log.Printf("Looking for features within %v", rect)
stream, err := client.ListFeatures(context.Background(), rect)
if err != nil {
  log.Fatalf("client.ListFeatures failed: %v", err)
}
for {
  // For server-to-client streaming RPCs, you call stream.Recv() until it
  // returns io.EOF.
  feature, err := stream.Recv()
  if err == io.EOF {
    break
  }
  if err != nil {
    log.Fatalf("client.ListFeatures failed: %v", err)
  }
  log.Printf("Feature: name: %q, point:(%v, %v)", feature.GetName(),
    feature.GetLocation().GetLatitude(), feature.GetLocation().GetLongitude())
}

Basit RPC'de olduğu gibi, yönteme bir bağlam ve bir istek iletiyoruz. Ancak yanıt nesnesi yerine RouteGuide_ListFeaturesClient örneği alıyoruz. İstemci, sunucunun yanıtlarını okumak için RouteGuide_ListFeaturesClient akışını kullanabilir. Sunucunun bir yanıt protokol arabelleği nesnesine (bu durumda Feature) verdiği yanıtları, başka mesaj kalmayana kadar tekrar tekrar okumak için RouteGuide_ListFeaturesClient'nın Recv() yöntemini kullanırız: İstemcinin her çağrıdan sonra Recv()'dan döndürülen err hatasını kontrol etmesi gerekir. nil ise akış hâlâ iyi durumdadır ve okumaya devam edebilir. io.EOF ise ileti akışı sona ermiştir. Aksi takdirde, err üzerinden iletilen bir RPC hatası olmalıdır.

İstemci tarafı yayın RPC'si

İstemci tarafı akış yöntemi RecordRoute, sunucu tarafı yöntemine benzer. Ancak bu yönteme yalnızca bir bağlam iletiriz ve RouteGuide_RecordRouteClient akışı geri alırız. Bu akışı hem yazmak hem de okumak için kullanabiliriz.

// Create a random number of random points
r := rand.New(rand.NewSource(time.Now().UnixNano()))
pointCount := int(r.Int31n(100)) + 2 // Traverse at least two points
var points []*pb.Point
for i := 0; i < pointCount; i++ {
  points = append(points, randomPoint(r))
}
log.Printf("Traversing %d points.", len(points))
c2sStream, err := client.RecordRoute(context.TODO())
if err != nil {
  log.Fatalf("client.RecordRoute failed: %v", err)
}
// Stream each point to the server.
for _, point := range points {
  if err := c2sStream.Send(point); err != nil {
    log.Fatalf("client.RecordRoute: stream.Send(%v) failed: %v", point, err)
  }
}
// Close the stream and receive the RouteSummary from the server.
reply, err := c2sStream.CloseAndRecv()
if err != nil {
  log.Fatalf("client.RecordRoute failed: %v", err)
}
log.Printf("Route summary: %v", reply)

RouteGuide_RecordRouteClient, sunucuya istek göndermek için kullanabileceğimiz bir Send() yöntemine sahiptir. Send() kullanarak istemcimizin isteklerini akışa yazmayı bitirdikten sonra, yazma işlemini bitirdiğimizi ve yanıt beklediğimizi gRPC'ye bildirmek için akışta CloseAndRecv() işlevini çağırmamız gerekir. RPC durumumuzu CloseAndRecv() tarafından döndürülen hatadan alıyoruz. Durum nil ise CloseAndRecv() işlevinin ilk dönüş değeri geçerli bir sunucu yanıtı olur.

Çift yönlü akış RPC'si

Son olarak, çift yönlü akış RPC'mize RouteChat() bakalım. RecordRoute örneğinde olduğu gibi, yönteme yalnızca bir bağlam nesnesi iletiyor ve hem mesaj yazmak hem de okumak için kullanabileceğimiz bir akış geri alıyoruz. Ancak bu kez, sunucu ileti akışına ileti yazmaya devam ederken değerleri yöntemimizin akışı üzerinden döndürüyoruz.

biDiStream, err := client.RouteChat(context.Background())
if err != nil {
  log.Fatalf("client.RouteChat failed: %v", err)
}
// this channel is used to wait for the receive goroutine to finish.
recvDoneCh := make(chan struct{})
// receive goroutine.
go func() {
  for {
    in, err := biDiStream.Recv()
    if err == io.EOF {
      // read done.
      close(recvDoneCh)
      return
    }
    if err != nil {
      log.Fatalf("client.RouteChat failed: %v", err)
    }
    log.Printf("Got message %s at point(%d, %d)", in.Message, in.Location.Latitude, in.Location.Longitude)
  }
}()
// send messages simultaneously.
for _, note := range notes {
  if err := biDiStream.Send(note); err != nil {
    log.Fatalf("client.RouteChat: stream.Send(%v) failed: %v", note, err)
  }
}
biDiStream.CloseSend()
// wait for the receive goroutine to finish.
<-recvDoneCh

Burada okuma ve yazma söz dizimi, istemci tarafı akış yöntemimize çok benzer. Tek fark, çağrımızı tamamladıktan sonra akışın CloseSend() yöntemini kullanmamızdır. Her iki taraf da diğer tarafın iletilerini her zaman yazıldıkları sırayla alsa da hem istemci hem de sunucu, iletileri herhangi bir sırada okuyup yazabilir. Akışlar tamamen bağımsız olarak çalışır.

7. Deneyin

Uygulamanın çalışma dizininde aşağıdaki komutları çalıştırarak sunucu ve istemcinin birbirleriyle doğru şekilde çalıştığını onaylayın:

  1. Sunucuyu bir terminalde çalıştırın:
cd server
go run .
  1. İstemciyi başka bir terminalden çalıştırma:
cd client
go run .

Aşağıdakine benzer bir çıkış görürsünüz. Zaman damgaları, netlik için çıkarılmıştır:

Looking for features within lo:<latitude:400000000 longitude:-750000000 > hi:<latitude:420000000 longitude:-730000000 >
name:"Patriots Path, Mendham, NJ 07945, USA" location:<latitude:407838351 longitude:-746143763 >
...
name:"3 Hasta Way, Newton, NJ 07860, USA" location:<latitude:410248224 longitude:-747127767 >
Traversing 56 points.
Route summary: point_count:56 distance:497013163
Got message First message at point(0, 1)
Got message Second message at point(0, 2)
Got message Third message at point(0, 3)
Got message First message at point(0, 1)
Got message Fourth message at point(0, 1)
Got message Second message at point(0, 2)
Got message Fifth message at point(0, 2)
Got message Third message at point(0, 3)
Got message Sixth message at point(0, 3)

8. Sırada ne var?