1. บทนำ
ในโค้ดแล็บนี้ คุณจะได้ใช้ gRPC-Go เพื่อสร้างไคลเอ็นต์และเซิร์ฟเวอร์ซึ่งเป็นรากฐานของแอปพลิเคชันการแมปเส้นทางที่เขียนด้วย Go
เมื่อจบบทแนะนำนี้ คุณจะมีไคลเอ็นต์ที่เชื่อมต่อกับเซิร์ฟเวอร์ระยะไกลโดยใช้ gRPC เพื่อรับข้อมูลเกี่ยวกับฟีเจอร์ในเส้นทางของไคลเอ็นต์ สร้างข้อมูลสรุปของเส้นทางของไคลเอ็นต์ และแลกเปลี่ยนข้อมูลเส้นทาง เช่น ข้อมูลอัปเดตการจราจร กับเซิร์ฟเวอร์และไคลเอ็นต์อื่นๆ
บริการนี้กำหนดไว้ในไฟล์ Protocol Buffers ซึ่งจะใช้เพื่อสร้างโค้ดมาตรฐานสำหรับไคลเอ็นต์และเซิร์ฟเวอร์เพื่อให้สื่อสารกันได้ ซึ่งจะช่วยประหยัดเวลาและแรงในการติดตั้งใช้งานฟังก์ชันดังกล่าว
โค้ดที่สร้างขึ้นนี้ไม่เพียงแต่จัดการความซับซ้อนของการสื่อสารระหว่างเซิร์ฟเวอร์และไคลเอ็นต์เท่านั้น แต่ยังจัดการการซีเรียลไลซ์และการดีซีเรียลไลซ์ข้อมูลด้วย
สิ่งที่คุณจะได้เรียนรู้
- วิธีใช้ Protocol Buffers เพื่อกำหนด API ของบริการ
- วิธีสร้างไคลเอ็นต์และเซิร์ฟเวอร์ที่ใช้ gRPC จากคำจำกัดความของ Protocol Buffers โดยใช้การสร้างโค้ดอัตโนมัติ
- ความเข้าใจเกี่ยวกับการสื่อสารแบบสตรีมมิงไคลเอ็นต์-เซิร์ฟเวอร์ด้วย gRPC
Codelab นี้มีไว้สำหรับนักพัฒนา Go ที่เพิ่งเริ่มใช้ gRPC หรือต้องการทบทวน gRPC หรือใครก็ตามที่สนใจสร้างระบบแบบกระจาย ไม่จำเป็นต้องมีประสบการณ์เกี่ยวกับ gRPC มาก่อน
2. ก่อนเริ่มต้น
ข้อกำหนดเบื้องต้น
ตรวจสอบว่าคุณได้ติดตั้งสิ่งต่อไปนี้แล้ว
- ชุดเครื่องมือ Go เวอร์ชัน 1.24.5 ขึ้นไป โปรดดูวิธีการติดตั้งที่เริ่มต้นใช้งานของ Go
- คอมไพเลอร์บัฟเฟอร์โปรโตคอล protocเวอร์ชัน 3.27.1 ขึ้นไป ดูวิธีการติดตั้งได้ในคู่มือการติดตั้งของคอมไพเลอร์
- ปลั๊กอินคอมไพเลอร์ Protocol Buffer สำหรับ Go และ gRPC หากต้องการติดตั้งปลั๊กอินเหล่านี้ ให้เรียกใช้คำสั่งต่อไปนี้
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
อัปเดตตัวแปร PATH เพื่อให้คอมไพเลอร์ Protocol Buffer ค้นหาปลั๊กอินได้
export PATH="$PATH:$(go env GOPATH)/bin"
รับโค้ด
Codelab นี้มีโครงสร้างของซอร์สโค้ดของแอปพลิเคชันเพื่อให้คุณทำต่อได้ คุณจึงไม่ต้องเริ่มต้นจากศูนย์ ขั้นตอนต่อไปนี้จะแสดงวิธีสมัครแอปพลิเคชันให้เสร็จสมบูรณ์ ซึ่งรวมถึงการใช้ปลั๊กอินคอมไพเลอร์ Protocol Buffer เพื่อสร้างโค้ด gRPC ที่ซ้ำกัน
ก่อนอื่น ให้สร้างไดเรกทอรีการทำงานของ Codelab แล้ว cd เข้าไป
mkdir streaming-grpc-go-getting-started && cd streaming-grpc-go-getting-started
ดาวน์โหลดและแตกไฟล์ Codelab โดยทำดังนี้
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
หรือคุณจะดาวน์โหลดไฟล์ .zip ที่มีเฉพาะไดเรกทอรี Codelab แล้วแตกไฟล์ด้วยตนเองก็ได้
ซอร์สโค้ดที่เสร็จสมบูรณ์แล้วพร้อมใช้งานใน GitHub หากคุณต้องการข้ามการพิมพ์การติดตั้งใช้งาน
3. กำหนดข้อความและบริการ
ขั้นตอนแรกคือการกำหนดบริการ gRPC ของแอปพลิเคชัน เมธอด RPC และประเภทข้อความคำขอและการตอบกลับโดยใช้ Protocol Buffers บริการของคุณจะให้ข้อมูลต่อไปนี้
- เมธอด RPC ที่เรียกว่า ListFeatures,RecordRouteและRouteChatซึ่งเซิร์ฟเวอร์ใช้และไคลเอ็นต์เรียก
- ประเภทข้อความ Point,Feature,Rectangle,RouteNoteและRouteSummaryซึ่งเป็นโครงสร้างข้อมูลที่แลกเปลี่ยนระหว่างไคลเอ็นต์และเซิร์ฟเวอร์เมื่อเรียกใช้เมธอดข้างต้น
วิธีการ RPC และประเภทข้อความทั้งหมดจะกำหนดไว้ในrouteguide/route_guide.protoของซอร์สโค้ดที่ระบุ
Protocol Buffers เรียกกันโดยทั่วไปว่า protobuf ดูข้อมูลเพิ่มเติมเกี่ยวกับคำศัพท์ gRPC ได้ที่แนวคิดหลัก สถาปัตยกรรม และวงจรของ gRPC
กำหนดประเภทข้อความ
ในrouteguide/route_guide.protoไฟล์ของซอร์สโค้ด ให้กำหนดPointประเภทข้อความก่อน Point แสดงคู่พิกัดละติจูด-ลองจิจูดบนแผนที่ สำหรับ Codelab นี้ ให้ใช้จำนวนเต็มสำหรับพิกัด
message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}
หมายเลข 1 และ 2 เป็นหมายเลขรหัสที่ไม่ซ้ำกันสำหรับแต่ละฟิลด์ในโครงสร้าง message
จากนั้นกำหนดFeatureประเภทข้อความ Feature ใช้ฟิลด์ string สำหรับชื่อหรือที่อยู่ไปรษณีย์ของสิ่งหนึ่งๆ ในสถานที่ที่ระบุโดย Point ดังนี้
message Feature {
  // The name or address of the feature.
  string name = 1;
  // The point where the feature is located.
  Point location = 2;
}
ถัดมาคือRectangleข้อความซึ่งแสดงสี่เหลี่ยมผืนผ้าละติจูด-ลองจิจูด ซึ่งแสดงเป็นจุด 2 จุดที่อยู่ตรงข้ามกันในแนวทแยง "lo" และ "hi"
message Rectangle {
  // One corner of the rectangle.
  Point lo = 1;
  // The other corner of the rectangle.
  Point hi = 2;
}
รวมถึงข้อความ RouteNote ซึ่งแสดงถึงข้อความที่ส่งขณะอยู่ที่จุดหนึ่งๆ
message RouteNote {
  // The location from which the message is sent.
  Point location = 1;
  // The message to be sent.
  string message = 2;
}
นอกจากนี้ เรายังต้องการRouteSummaryข้อความด้วย คุณจะได้รับข้อความนี้เป็นการตอบกลับ RPC ของ RecordRoute ซึ่งจะอธิบายในส่วนถัดไป โดยจะมีจำนวนจุดแต่ละจุดที่ได้รับ จำนวนฟีเจอร์ที่ตรวจพบ และระยะทางทั้งหมดที่ครอบคลุมเป็นผลรวมสะสมของระยะทางระหว่างแต่ละจุด
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;
}
กำหนดวิธีการบริการ
หากต้องการกำหนดบริการ ให้ระบุบริการที่มีชื่อในไฟล์ .proto ไฟล์ route_guide.proto มีโครงสร้าง service ชื่อ RouteGuide ซึ่งกำหนดวิธีการอย่างน้อย 1 วิธีที่บริการของแอปพลิเคชันมีให้
กำหนดRPCภายในคำจำกัดความของบริการ โดยระบุประเภทคำขอและการตอบกลับ  ในส่วนนี้ของโค้ดแล็บ เราจะกำหนดค่าต่อไปนี้
ListFeatures
รับ Feature ที่พร้อมให้บริการภายใน Rectangle ที่ระบุ ระบบจะสตรีมผลลัพธ์แทนที่จะแสดงผลพร้อมกัน (เช่น ในข้อความตอบกลับที่มีฟิลด์ที่ซ้ำกัน) เนื่องจากสี่เหลี่ยมผืนผ้าอาจครอบคลุมพื้นที่ขนาดใหญ่และมีฟีเจอร์จำนวนมาก
ประเภทที่เหมาะสมสำหรับ RPC นี้คือ RPC แบบสตรีมมิงฝั่งเซิร์ฟเวอร์ ซึ่งไคลเอ็นต์จะส่งคำขอไปยังเซิร์ฟเวอร์และรับสตรีมเพื่ออ่านลำดับข้อความกลับ ไคลเอ็นต์จะอ่านจากสตรีมที่ส่งคืนจนกว่าจะไม่มีข้อความเหลือ ดังที่เห็นในตัวอย่าง คุณระบุวิธีการสตรีมฝั่งเซิร์ฟเวอร์ได้โดยวางคีย์เวิร์ดสตรีมไว้ก่อนประเภทการตอบกลับ
rpc ListFeatures(Rectangle) returns (stream Feature) {}
RecordRoute
ยอมรับสตรีมของ Point ในเส้นทางที่กำลังเดินทาง และแสดงผล RouteSummary เมื่อการเดินทางเสร็จสมบูรณ์
RPC การสตรีมฝั่งไคลเอ็นต์ดูเหมือนจะเหมาะสมในกรณีนี้ โดยไคลเอ็นต์จะเขียนลำดับข้อความและส่งไปยังเซิร์ฟเวอร์โดยใช้สตรีมที่ระบุอีกครั้ง เมื่อไคลเอ็นต์เขียนข้อความเสร็จแล้ว ไคลเอ็นต์จะรอให้เซิร์ฟเวอร์อ่านข้อความทั้งหมดและส่งการตอบกลับ คุณระบุวิธีการสตรีมฝั่งไคลเอ็นต์ได้โดยวางคีย์เวิร์ดสตรีมไว้ก่อนประเภทคำขอ
rpc RecordRoute(stream Point) returns (RouteSummary) {}
RouteChat
ยอมรับสตรีมของ RouteNote ที่ส่งขณะที่กำลังเดินทางตามเส้นทาง ขณะที่รับ RouteNote อื่นๆ (เช่น จากผู้ใช้รายอื่น)
นี่เป็นกรณีการใช้งานที่เหมาะสมสำหรับการสตรีมแบบ 2 ทาง RPC การสตรีมแบบ 2 ทางจะให้ทั้ง 2 ฝ่ายส่งลำดับข้อความโดยใช้สตรีมแบบอ่าน-เขียน สตรีมทั้ง 2 รายการทำงานแยกกัน ดังนั้นไคลเอ็นต์และเซิร์ฟเวอร์จึงอ่านและเขียนได้ตามลำดับที่ต้องการ
เช่น เซิร์ฟเวอร์อาจรอรับข้อความไคลเอ็นต์ทั้งหมดก่อนที่จะเขียนการตอบกลับ หรืออาจอ่านข้อความแล้วเขียนข้อความสลับกัน หรืออาจใช้การอ่านและการเขียนแบบอื่นๆ
ระบบจะรักษลําดับของข้อความในแต่ละสตรีมไว้ คุณระบุประเภทเมธอดนี้ได้โดยวางคีย์เวิร์ดสตรีมไว้ก่อนทั้งคำขอและการตอบกลับ
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
4. สร้างโค้ดไคลเอ็นต์และเซิร์ฟเวอร์
จากนั้นสร้างโค้ด gRPC มาตรฐานสำหรับทั้งไคลเอ็นต์และเซิร์ฟเวอร์จากไฟล์ .proto โดยใช้คอมไพเลอร์ Protocol Buffer ในไดเรกทอรี routeguide ให้เรียกใช้คำสั่งต่อไปนี้
protoc --go_out=. --go_opt=paths=source_relative \
       --go-grpc_out=. --go-grpc_opt=paths=source_relative \
       route_guide.proto
คำสั่งนี้จะสร้างไฟล์ต่อไปนี้
- route_guide.pb.goซึ่งมีฟังก์ชันในการสร้างประเภทข้อความของแอปพลิเคชันและเข้าถึงข้อมูล รวมถึงคำจำกัดความของประเภทที่แสดงถึงข้อความ
- route_guide_grpc.pb.goซึ่งมีฟังก์ชันที่ไคลเอ็นต์ใช้เรียกเมธอด gRPC ระยะไกลของบริการ และฟังก์ชันที่เซิร์ฟเวอร์ใช้เพื่อให้บริการระยะไกลนั้น
จากนั้นเราจะใช้เมธอดในฝั่งเซิร์ฟเวอร์เพื่อให้เมื่อไคลเอ็นต์ส่งคำขอ เซิร์ฟเวอร์จะตอบกลับได้
5. ติดตั้งใช้งานบริการ
ก่อนอื่นมาดูวิธีสร้างRouteGuideเซิร์ฟเวอร์กัน การทำให้RouteGuideบริการของเราทำงานได้ตามที่ควรจะเป็นมี 2 ส่วน ดังนี้
- การติดตั้งใช้งานอินเทอร์เฟซบริการที่สร้างขึ้นจากคำจำกัดความของบริการ: การ "ทำงาน" จริงของบริการ
- เรียกใช้เซิร์ฟเวอร์ gRPC เพื่อรอรับคำขอจากไคลเอ็นต์และส่งคำขอไปยังการติดตั้งใช้งานบริการที่ถูกต้อง
มาใช้ RouteGuide ใน server/server.go กัน
ใช้ RouteGuide
เราต้องติดตั้งใช้งานอินเทอร์เฟซ RouteGuideService ที่สร้างขึ้น การติดตั้งใช้งานจะมีลักษณะดังนี้
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 {
        ...
}
มาดูรายละเอียดการติดตั้งใช้งาน RPC แต่ละรายการกัน
RPC การสตรีมฝั่งเซิร์ฟเวอร์
เริ่มต้นด้วย RPC การสตรีมรายการใดรายการหนึ่งของเรา ListFeatures เป็น RPC การสตรีมฝั่งเซิร์ฟเวอร์ ดังนั้นเราจึงต้องส่ง Feature หลายรายการกลับไปยังไคลเอ็นต์
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
}
ดังที่เห็นได้ว่าแทนที่จะรับออบเจ็กต์คำขอและการตอบกลับแบบง่ายๆ ในพารามิเตอร์ของเมธอด คราวนี้เราจะได้รับออบเจ็กต์คำขอ (Rectangle ซึ่งไคลเอ็นต์ต้องการค้นหา Features) และออบเจ็กต์ RouteGuide_ListFeaturesServer พิเศษเพื่อเขียนการตอบกลับ ในเมธอดนี้ เราจะป้อนข้อมูลออบเจ็กต์ Feature ให้มากที่สุดเท่าที่จำเป็นต้องส่งคืน โดยเขียนลงใน RouteGuide_ListFeaturesServer โดยใช้เมธอด Send() ของออบเจ็กต์ สุดท้ายนี้ เราจะส่งคืนnilข้อผิดพลาดเพื่อแจ้งให้ gRPC ทราบว่าเราเขียนการตอบกลับเสร็จแล้ว เช่นเดียวกับใน RPC อย่างง่าย หากเกิดข้อผิดพลาดในการเรียกนี้ เราจะแสดงข้อผิดพลาดที่ไม่ใช่ nil โดยเลเยอร์ gRPC จะแปลข้อผิดพลาดเป็นสถานะ RPC ที่เหมาะสมเพื่อส่งผ่านสาย
RPC การสตรีมฝั่งไคลเอ็นต์
ตอนนี้เรามาดูสิ่งที่ซับซ้อนขึ้นเล็กน้อยกัน นั่นคือวิธีการสตรีมฝั่งไคลเอ็นต์ RecordRoute ซึ่งเราจะรับสตรีมของ Points จากไคลเอ็นต์และส่งคืน RouteSummary รายการเดียวพร้อมข้อมูลเกี่ยวกับการเดินทาง คุณจะเห็นว่าครั้งนี้เมธอดไม่มีพารามิเตอร์คำขอเลย แต่จะได้รับสตรีม RouteGuide_RecordRouteServer ซึ่งเซิร์ฟเวอร์ใช้ได้ทั้งอ่านและเขียนข้อความ โดยจะรับข้อความไคลเอ็นต์ได้โดยใช้เมธอด Recv() และส่งคืนการตอบกลับรายการเดียวโดยใช้เมธอด SendAndClose()
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
  }
}
ในส่วนเนื้อหาของเมธอด เราใช้เมธอด RouteGuide_RecordRouteServer's Recv() เพื่ออ่านคำขอของไคลเอ็นต์ซ้ำๆ ไปยังออบเจ็กต์คำขอ (ในกรณีนี้คือ Point) จนกว่าจะไม่มีข้อความอีกต่อไป เซิร์ฟเวอร์ต้องตรวจสอบข้อผิดพลาดที่ส่งคืนจาก Recv() หลังจากการเรียกแต่ละครั้ง หากเป็น nil แสดงว่าสตรีมยังคงใช้งานได้และอ่านต่อไปได้ หากเป็น io.EOF แสดงว่าสตรีมข้อความสิ้นสุดแล้วและเซิร์ฟเวอร์สามารถส่งคืน RouteSummary ได้ หากมีค่าอื่น เราจะแสดงข้อผิดพลาด "ตามที่เป็น" เพื่อให้เลเยอร์ gRPC แปลงเป็นสถานะ RPC
RPC การสตรีมแบบ 2 ทาง
สุดท้าย มาดู RPC แบบสตรีมมิงแบบ 2 ทาง RouteChat() กัน
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
      }
    }
  }
}
คราวนี้เราจะได้รับสตรีม RouteGuide_RouteChatServer ซึ่งสามารถใช้เพื่ออ่านและเขียนข้อความได้เช่นเดียวกับตัวอย่างการสตรีมฝั่งไคลเอ็นต์ แต่คราวนี้เราจะแสดงค่าผ่านสตรีมของเมธอดในขณะที่ไคลเอ็นต์ยังคงเขียนข้อความลงในสตรีมข้อความ ไวยากรณ์สำหรับการอ่านและเขียนที่นี่คล้ายกับวิธีการสตรีมมิงฝั่งไคลเอ็นต์มาก ยกเว้นว่าเซิร์ฟเวอร์จะใช้วิธี send() ของสตรีมแทน SendAndClose() เนื่องจากเซิร์ฟเวอร์เขียนการตอบกลับหลายรายการ แม้ว่าแต่ละฝ่ายจะได้รับข้อความของอีกฝ่ายตามลำดับที่เขียนเสมอ แต่ทั้งไคลเอ็นต์และเซิร์ฟเวอร์สามารถอ่านและเขียนได้ตามลำดับใดก็ได้ โดยสตรีมจะทำงานอย่างอิสระโดยสมบูรณ์
เริ่มเซิร์ฟเวอร์
เมื่อเราได้ใช้ทุกวิธีแล้ว เราก็ต้องเริ่มเซิร์ฟเวอร์ gRPC เพื่อให้ไคลเอ็นต์ใช้บริการของเราได้จริง ข้อมูลโค้ดต่อไปนี้แสดงวิธีที่เราดำเนินการนี้สำหรับบริการ RouteGuide
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() มีดังนี้
- ระบุพอร์ต TCP ที่จะใช้เพื่อรอคำขอของไคลเอ็นต์ระยะไกลโดยใช้ lis, err := net.Listen(...)โดยค่าเริ่มต้น แอปพลิเคชันจะใช้พอร์ต TCP50051ตามที่ระบุโดยตัวแปรportหรือโดยการส่งสวิตช์--portในบรรทัดคำสั่งเมื่อเรียกใช้เซิร์ฟเวอร์ หากเปิดพอร์ต TCP ไม่ได้ แอปพลิเคชันจะสิ้นสุดด้วยข้อผิดพลาดร้ายแรง
- สร้างอินสแตนซ์ของเซิร์ฟเวอร์ gRPC โดยใช้ grpc.NewServer(...)และตั้งชื่ออินสแตนซ์นี้ว่าgrpcServer
- สร้างพอยน์เตอร์ไปยัง routeGuideServerซึ่งเป็นโครงสร้างที่แสดงถึงบริการ API ของแอปพลิเคชัน โดยตั้งชื่อพอยน์เตอร์ว่าs.
- ใช้ s.loadFeatures()เพื่อสร้างอาร์เรย์s.savedFeatures
- ลงทะเบียนการติดตั้งใช้งานบริการกับเซิร์ฟเวอร์ gRPC
- เรียกใช้ Serve()ในเซิร์ฟเวอร์พร้อมรายละเอียดพอร์ตเพื่อรอการบล็อกสำหรับคำขอของไคลเอ็นต์ ซึ่งจะดำเนินการต่อไปจนกว่าจะมีการสิ้นสุดกระบวนการหรือเรียกใช้Stop()
ฟังก์ชัน loadFeatures() จะได้รับการแมปพิกัดกับสถานที่จาก server/testdata.go
6. สร้างไคลเอ็นต์
ตอนนี้ให้แก้ไข client/client.go ซึ่งเป็นที่ที่คุณจะใช้โค้ดฝั่งไคลเอ็นต์
หากต้องการเรียกใช้เมธอดของบริการระยะไกล เราต้องสร้างแชแนล gRPC ก่อนเพื่อสื่อสารกับเซิร์ฟเวอร์ เราสร้างสิ่งนี้โดยส่งสตริง URI เป้าหมายของเซิร์ฟเวอร์ (ซึ่งในกรณีนี้คือที่อยู่และหมายเลขพอร์ต) ไปยัง grpc.NewClient() ในฟังก์ชัน main() ของไคลเอ็นต์ดังนี้
// 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 จะเป็น localhost:50051 โดยค่าเริ่มต้น และสามารถลบล้างได้โดยใช้สวิตช์ --addr ในบรรทัดคำสั่งเมื่อเรียกใช้ไคลเอ็นต์
หากไคลเอ็นต์ต้องการเชื่อมต่อกับบริการที่ต้องใช้ข้อมูลเข้าสู่ระบบการตรวจสอบสิทธิ์ เช่น ข้อมูลเข้าสู่ระบบ TLS หรือ JWT ไคลเอ็นต์จะส่งออบเจ็กต์ DialOptions เป็นพารามิเตอร์ไปยัง grpc.NewClient ที่มีข้อมูลเข้าสู่ระบบที่จำเป็นได้ RouteGuide ไม่ต้องใช้ข้อมูลเข้าสู่ระบบใดๆ
เมื่อตั้งค่า Channel ของ gRPC แล้ว เราต้องมี Stub ของไคลเอ็นต์เพื่อเรียกใช้ RPC ผ่านการเรียกฟังก์ชัน Go เราได้รับสแต็บโดยใช้เมธอด NewRouteGuideClient ที่ระบุไว้ในไฟล์ route_guide_grpc.pb.go ซึ่งสร้างจากไฟล์ .proto ของแอปพลิเคชัน
import (pb "github.com/grpc-ecosystem/codelabs/getting_started_streaming/routeguide")
client := pb.NewRouteGuideClient(conn)
วิธีการเรียกใช้บริการ
ตอนนี้มาดูวิธีเรียกใช้เมธอดบริการกัน ใน gRPC-Go, RPC จะทํางานในโหมดบล็อก/ซิงโครนัส ซึ่งหมายความว่าการเรียก RPC จะรอให้เซิร์ฟเวอร์ตอบกลับ และจะส่งคืนการตอบกลับหรือข้อผิดพลาด
RPC การสตรีมฝั่งเซิร์ฟเวอร์
ในส่วนนี้ เราจะเรียกใช้เมธอดการสตรีมฝั่งเซิร์ฟเวอร์ ListFeatures ซึ่งจะแสดงผลสตรีมของออบเจ็กต์ Feature ทางภูมิศาสตร์
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())
}
เช่นเดียวกับใน RPC อย่างง่าย เราจะส่งบริบทและคำขอไปยังเมธอด แต่แทนที่จะได้รับออบเจ็กต์การตอบกลับ เรากลับได้รับอินสแตนซ์ของ RouteGuide_ListFeaturesClient ไคลเอ็นต์สามารถใช้ RouteGuide_ListFeaturesClient สตรีมเพื่ออ่านการตอบกลับของเซิร์ฟเวอร์ได้ เราใช้วิธีการ RouteGuide_ListFeaturesClientRecv() เพื่ออ่านการตอบกลับของเซิร์ฟเวอร์ซ้ำๆ ไปยังออบเจ็กต์ Protocol Buffer ของการตอบกลับ (ในกรณีนี้คือ Feature) จนกว่าจะไม่มีข้อความอีกต่อไป โดยไคลเอ็นต์จะต้องตรวจสอบข้อผิดพลาด err ที่ส่งกลับจาก Recv() หลังจากการเรียกใช้แต่ละครั้ง หากเป็น nil แสดงว่าสตรีมยังคงใช้งานได้และอ่านต่อไปได้ หากเป็น io.EOF แสดงว่าสตรีมข้อความสิ้นสุดแล้ว มิเช่นนั้นจะต้องมีข้อผิดพลาด RPC ซึ่งจะส่งผ่านทาง err
RPC การสตรีมฝั่งไคลเอ็นต์
วิธีการสตรีมฝั่งไคลเอ็นต์ RecordRoute คล้ายกับวิธีการฝั่งเซิร์ฟเวอร์ ยกเว้นว่าเราจะส่งเฉพาะบริบทไปยังวิธีการดังกล่าวและรับสตรีม RouteGuide_RecordRouteClient กลับมา ซึ่งเราสามารถใช้เพื่อทั้งเขียนและอ่านข้อความได้
// 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 มีเมธอด Send() ที่เราใช้ส่งคำขอไปยังเซิร์ฟเวอร์ได้ เมื่อเขียนคำขอของไคลเอ็นต์ไปยังสตรีมโดยใช้ Send() เสร็จแล้ว เราต้องเรียกใช้ CloseAndRecv() ในสตรีมเพื่อให้ gRPC ทราบว่าเราเขียนเสร็จแล้วและคาดว่าจะได้รับคำตอบ เราได้รับสถานะ RPC จากข้อผิดพลาดที่ส่งคืนจาก CloseAndRecv() หากสถานะเป็น nil ค่าที่ส่งคืนแรกจาก CloseAndRecv() จะเป็นการตอบกลับที่ถูกต้องของเซิร์ฟเวอร์
RPC การสตรีมแบบ 2 ทาง
สุดท้าย มาดู RPC แบบสตรีมมิงแบบ 2 ทาง RouteChat() กัน เช่นเดียวกับกรณีของ RecordRoute เราจะส่งเฉพาะออบเจ็กต์บริบทไปยังเมธอดและรับสตรีมกลับมาซึ่งเราใช้ได้ทั้งในการเขียนและอ่านข้อความ แต่คราวนี้เราจะแสดงค่าผ่านสตรีมของเมธอดในขณะที่เซิร์ฟเวอร์ยังคงเขียนข้อความลงในสตรีมข้อความ
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
ไวยากรณ์สำหรับการอ่านและการเขียนที่นี่คล้ายกับวิธีการสตรีมมิงฝั่งไคลเอ็นต์ของเรามาก ยกเว้นว่าเราจะใช้วิธี CloseSend() ของสตรีมเมื่อการเรียกเสร็จสิ้นแล้ว แม้ว่าแต่ละฝ่ายจะได้รับข้อความของอีกฝ่ายตามลำดับที่เขียนเสมอ แต่ทั้งไคลเอ็นต์และเซิร์ฟเวอร์สามารถอ่านและเขียนได้ตามลำดับใดก็ได้ โดยสตรีมจะทำงานอย่างอิสระโดยสมบูรณ์
7. ลองเลย
ยืนยันว่าเซิร์ฟเวอร์และไคลเอ็นต์ทำงานร่วมกันได้อย่างถูกต้องโดยการเรียกใช้คำสั่งต่อไปนี้ในไดเรกทอรีการทำงานของแอปพลิเคชัน
- เรียกใช้เซิร์ฟเวอร์ในเทอร์มินัลหนึ่ง
cd server go run .
- เรียกใช้ไคลเอ็นต์จากเทอร์มินัลอื่นโดยใช้คำสั่งต่อไปนี้
cd client go run .
คุณจะเห็นเอาต์พุตลักษณะนี้ โดยระบบจะละเว้นการประทับเวลาเพื่อความชัดเจน
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. ขั้นตอนถัดไป
- ดูวิธีการทำงานของ gRPC ในข้อมูลเบื้องต้นเกี่ยวกับ gRPC และแนวคิดหลัก
- ดูบทแนะนำเกี่ยวกับพื้นฐาน
- สำรวจข้อมูลอ้างอิงของ API