Làm quen với gRPC-Go

1. Giới thiệu

Trong lớp học lập trình này, bạn sẽ sử dụng gRPC-Go để tạo một ứng dụng khách và máy chủ tạo thành nền tảng của một ứng dụng lập bản đồ tuyến đường được viết bằng Go.

Khi kết thúc hướng dẫn này, bạn sẽ có một ứng dụng kết nối với một máy chủ từ xa bằng gRPC để lấy tên hoặc địa chỉ bưu chính của nội dung nằm tại một toạ độ cụ thể trên bản đồ. Một ứng dụng hoàn chỉnh có thể sử dụng thiết kế máy chủ-máy khách này để liệt kê hoặc tóm tắt các địa điểm yêu thích dọc theo một tuyến đường.

Dịch vụ này được xác định trong một tệp Protocol Buffers. Tệp này sẽ được dùng để tạo mã chuẩn cho ứng dụng và máy chủ để chúng có thể giao tiếp với nhau, giúp bạn tiết kiệm thời gian và công sức khi triển khai chức năng đó.

Mã được tạo này không chỉ xử lý sự phức tạp của việc giao tiếp giữa máy chủ và ứng dụng mà còn xử lý quá trình chuyển đổi dữ liệu thành chuỗi và chuyển đổi chuỗi thành dữ liệu.

Kiến thức bạn sẽ học được

  • Cách sử dụng Protocol Buffers để xác định một API dịch vụ.
  • Cách tạo máy khách và máy chủ dựa trên gRPC từ một định nghĩa Protocol Buffers bằng cách sử dụng tính năng tạo mã tự động.
  • Hiểu rõ về giao tiếp máy khách-máy chủ bằng gRPC.

Lớp học lập trình này dành cho những nhà phát triển Go mới làm quen với gRPC hoặc muốn tìm hiểu lại về gRPC, hoặc bất kỳ ai khác quan tâm đến việc xây dựng hệ thống phân tán. Bạn không cần có kinh nghiệm sử dụng gRPC.

2. Trước khi bắt đầu

Điều kiện tiên quyết

Đảm bảo bạn đã cài đặt những thứ sau:

  • Chuỗi công cụ Go phiên bản 1.24.5 trở lên. Để biết hướng dẫn cài đặt, hãy xem phần Bắt đầu của Go.
  • Trình biên dịch vùng đệm giao thức, protoc, phiên bản 3.27.1 trở lên. Để biết hướng dẫn cài đặt, hãy xem hướng dẫn cài đặt của trình biên dịch.
  • Trình bổ trợ trình biên dịch vùng đệm giao thức cho Go và gRPC. Để cài đặt các trình bổ trợ này, hãy chạy các lệnh sau:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

Cập nhật biến PATH để trình biên dịch vùng đệm giao thức có thể tìm thấy các trình bổ trợ:

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

Lấy mã

Để bạn không phải bắt đầu hoàn toàn từ đầu, lớp học lập trình này cung cấp một khung mã nguồn ứng dụng để bạn hoàn tất. Các bước sau đây sẽ hướng dẫn bạn cách hoàn tất ứng dụng, bao gồm cả việc sử dụng trình bổ trợ trình biên dịch vùng đệm giao thức để tạo mã gRPC chung.

Tải mã nguồn này xuống dưới dạng tệp lưu trữ .ZIP từ GitHub rồi giải nén nội dung của tệp.

Ngoài ra, bạn có thể xem mã nguồn hoàn chỉnh trên GitHub nếu muốn bỏ qua bước nhập nội dung triển khai.

3. Xác định dịch vụ

Bước đầu tiên là xác định dịch vụ gRPC của ứng dụng, phương thức RPC và các loại thông báo yêu cầu và phản hồi bằng cách sử dụng Protocol Buffers. Dịch vụ của bạn sẽ cung cấp:

  • Một phương thức RPC có tên là GetFeature mà máy chủ triển khai và máy khách gọi.
  • Các loại thông báo PointFeature là các cấu trúc dữ liệu được trao đổi giữa ứng dụng và máy chủ khi sử dụng phương thức GetFeature. Ứng dụng cung cấp toạ độ trên bản đồ dưới dạng Point trong yêu cầu GetFeature gửi đến máy chủ, còn máy chủ sẽ trả lời bằng một Feature tương ứng mô tả mọi thứ nằm ở toạ độ đó.

Phương thức RPC này và các loại thông báo của phương thức này sẽ được xác định trong tệp routeguide/route_guide.proto của mã nguồn được cung cấp.

Vùng đệm giao thức thường được gọi là protobuf. Để biết thêm thông tin về thuật ngữ gRPC, hãy xem phần Các khái niệm, cấu trúc và vòng đời cốt lõi của gRPC.

Loại thông báo

Trong tệp routeguide/route_guide.proto của mã nguồn, trước tiên hãy xác định kiểu thông báo Point. Point biểu thị một cặp toạ độ vĩ độ và kinh độ trên bản đồ. Trong lớp học lập trình này, hãy sử dụng số nguyên cho toạ độ:

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

Các số 12 là số nhận dạng duy nhất cho từng trường trong cấu trúc message.

Tiếp theo, hãy xác định loại thông báo Feature. Feature sử dụng trường string cho tên hoặc địa chỉ bưu chính của một thứ gì đó tại một vị trí do Point chỉ định:

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

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

Phương thức dịch vụ

Tệp route_guide.proto có cấu trúc service tên là RouteGuide, xác định một hoặc nhiều phương thức do dịch vụ của ứng dụng cung cấp.

Thêm phương thức rpc GetFeature vào bên trong định nghĩa RouteGuide. Như đã giải thích trước đó, phương thức này sẽ tra cứu tên hoặc địa chỉ của một vị trí từ một tập hợp toạ độ nhất định, vì vậy, hãy để GetFeature trả về một Feature cho một Point nhất định:

service RouteGuide {
  // Definition of the service goes here

  // Obtains the feature at a given position.
  rpc GetFeature(Point) returns (Feature) {}
}

Đây là một phương thức RPC đơn phương: một RPC đơn giản, trong đó ứng dụng gửi một yêu cầu đến máy chủ và đợi phản hồi quay lại, giống như một lệnh gọi hàm cục bộ.

4. Tạo mã máy khách và máy chủ

Tiếp theo, hãy tạo mã gRPC chung cho cả máy khách và máy chủ từ tệp .proto bằng trình biên dịch vùng đệm giao thức. Trong thư mục routeguide, hãy chạy:

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

Lệnh này tạo ra các tệp sau:

  • route_guide.pb.go, chứa các hàm để tạo kiểu thông báo của ứng dụng và truy cập vào dữ liệu của các kiểu thông báo đó.
  • route_guide_grpc.pb.go, chứa các hàm mà ứng dụng sử dụng để gọi phương thức gRPC từ xa của dịch vụ và các hàm mà máy chủ sử dụng để cung cấp dịch vụ từ xa đó.

Tiếp theo, chúng ta sẽ triển khai phương thức GetFeature ở phía máy chủ để khi ứng dụng gửi yêu cầu, máy chủ có thể trả lời.

5. Triển khai dịch vụ

Hàm GetFeature ở phía máy chủ là nơi thực hiện công việc chính: hàm này nhận một thông báo Point từ máy khách và trả về thông tin vị trí tương ứng trong một thông báo Feature từ danh sách các địa điểm đã biết. Dưới đây là cách triển khai hàm trong server/server.go:

func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) {
  for _, feature := range s.savedFeatures {
    if proto.Equal(feature.Location, point) {
      return feature, nil
    }
  }
  // No feature was found, return an unnamed feature
  return &pb.Feature{Location: point}, nil
}

Khi phương thức này được gọi sau một yêu cầu từ máy khách từ xa, hàm sẽ được truyền một đối tượng Context mô tả lệnh gọi RPC và một đối tượng bộ đệm giao thức Point từ yêu cầu của máy khách đó. Hàm này trả về một đối tượng Feature của vùng đệm giao thức cho vị trí được tra cứu và một error nếu cần.

Trong phương thức này, hãy điền thông tin thích hợp cho Point đã cho vào đối tượng Feature, rồi return đối tượng đó cùng với lỗi nil để cho gRPC biết rằng bạn đã hoàn tất việc xử lý RPC và đối tượng Feature có thể được trả về cho máy khách.

Phương thức GetFeature yêu cầu bạn tạo và đăng ký một đối tượng routeGuideServer để các yêu cầu của ứng dụng khách về việc tra cứu vị trí có thể được định tuyến đến hàm đó. Thao tác này được thực hiện trong main():

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

  var opts []grpc.ServerOption
  grpcServer := grpc.NewServer(opts...)

  s := &routeGuideServer{}
  s.loadFeatures()
  pb.RegisterRouteGuideServer(grpcServer, s)
  grpcServer.Serve(lis)
}

Sau đây là những gì diễn ra trong main(), từng bước:

  1. Chỉ định cổng TCP cần dùng để theo dõi các yêu cầu của máy khách từ xa bằng cách sử dụng lis, err := net.Listen(...). Theo mặc định, ứng dụng sẽ dùng cổng TCP 50051 như được chỉ định bởi biến port hoặc bằng cách truyền công tắc --port trên dòng lệnh khi chạy máy chủ. Nếu không mở được cổng TCP, ứng dụng sẽ kết thúc bằng một lỗi nghiêm trọng.
  2. Tạo một phiên bản của máy chủ gRPC bằng cách sử dụng grpc.NewServer(...), đặt tên cho phiên bản này là grpcServer.
  3. Tạo một con trỏ đến routeGuideServer, một cấu trúc đại diện cho dịch vụ API của ứng dụng, đặt tên cho con trỏ là s.
  4. Sử dụng s.loadFeatures() để điền vào mảng s.savedFeatures các vị trí có thể tra cứu thông qua GetFeature.
  5. Đăng ký dịch vụ API với máy chủ gRPC để các lệnh gọi RPC đến GetFeature được định tuyến đến hàm thích hợp.
  6. Gọi Serve() trên máy chủ bằng thông tin chi tiết về cổng của chúng tôi để thực hiện một lệnh chờ chặn cho các yêu cầu của ứng dụng; lệnh này sẽ tiếp tục cho đến khi quy trình bị huỷ hoặc Stop() được gọi.

Hàm loadFeatures() lấy các mối liên kết từ toạ độ đến vị trí từ server/testdata.go.

6. Tạo ứng dụng

Bây giờ, hãy chỉnh sửa client/client.go. Đây là nơi bạn sẽ triển khai mã ứng dụng.

Để gọi các phương thức của dịch vụ từ xa, trước tiên, chúng ta cần tạo một kênh gRPC để giao tiếp với máy chủ. Chúng ta tạo điều này bằng cách truyền chuỗi URI đích của máy chủ (trong trường hợp này, chỉ là địa chỉ và số cổng) đến grpc.NewClient() trong hàm main() của máy khách như sau:

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

Địa chỉ của máy chủ, được xác định bằng biến serverAddr, theo mặc định là localhost:50051 và có thể bị ghi đè bằng công tắc --addr trên dòng lệnh khi chạy ứng dụng.

Nếu cần kết nối với một dịch vụ yêu cầu thông tin xác thực, chẳng hạn như thông tin xác thực TLS hoặc JWT, ứng dụng có thể truyền đối tượng DialOptions làm tham số đến grpc.NewClient có chứa thông tin xác thực bắt buộc. Dịch vụ RouteGuide không yêu cầu cung cấp thông tin đăng nhập.

Sau khi thiết lập kênh gRPC, chúng ta cần một stub ứng dụng để thực hiện các RPC thông qua lệnh gọi hàm Go. Chúng ta sẽ nhận được stub bằng phương thức NewRouteGuideClient do tệp route_guide_grpc.pb.go tạo từ tệp .proto của ứng dụng cung cấp.

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

client := pb.NewRouteGuideClient(conn)

Gọi các phương thức dịch vụ

Trong gRPC-Go, các RPC hoạt động ở chế độ chặn/đồng bộ, tức là lệnh gọi RPC sẽ đợi máy chủ phản hồi và sẽ trả về phản hồi hoặc lỗi.

Simple RPC

Việc gọi RPC đơn giản GetFeature gần như đơn giản như gọi một phương thức cục bộ, trong trường hợp này là client.GetFeature:

point := &pb.Point{Latitude: 409146138, Longitude: -746188906}
log.Printf("Getting feature for point (%d, %d)", point.Latitude, point.Longitude)

// Call GetFeature method on the client.
feature, err := client.GetFeature(context.TODO(), point)
if err != nil {
  log.Fatalf("client.GetFeature failed: %v", err)
}

Ứng dụng gọi phương thức trên phần giữ chỗ đã tạo trước đó. Đối với các tham số của phương thức, ứng dụng sẽ tạo và điền sẵn một đối tượng vùng đệm giao thức yêu cầu Point. Bạn cũng truyền một đối tượng context.Context cho phép chúng tôi thay đổi hành vi của RPC nếu cần, chẳng hạn như xác định giới hạn thời gian cho lệnh gọi hoặc huỷ RPC đang diễn ra. Nếu lệnh gọi không trả về lỗi, thì ứng dụng có thể đọc thông tin phản hồi từ máy chủ từ giá trị trả về đầu tiên:

log.Println(feature)

Nhìn chung, hàm main() của ứng dụng sẽ có dạng như sau:

func main() {
        flag.Parse()

        // 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()

        // Create a new RouteGuide stub.
        client := pb.NewRouteGuideClient(conn)

        point := &pb.Point{Latitude: 409146138, Longitude: -746188906}
        log.Printf("Getting feature for point (%d, %d)", point.Latitude, point.Longitude)

        // Call GetFeature method on the client.
        feature, err := client.GetFeature(context.TODO(), point)
        if err != nil {
                log.Fatalf("client.GetFeature failed: %v", err)
        }
        log.Println(feature)
}

7. Dùng thử

Xác nhận rằng máy chủ và máy khách đang hoạt động với nhau một cách chính xác bằng cách thực thi các lệnh sau trong thư mục làm việc của ứng dụng:

  1. Chạy máy chủ trong một dòng lệnh:
cd server
go run .
  1. Chạy ứng dụng từ một thiết bị đầu cuối khác:
cd client
go run .

Bạn sẽ thấy kết quả như sau, trong đó dấu thời gian đã bị bỏ qua để đảm bảo sự rõ ràng:

Getting feature for point (409146138, -746188906)
name:"Berkshire Valley Management Area Trail, Jefferson, NJ, USA" location:<latitude:409146138 longitude:-746188906 >
Getting feature for point (0, 0)
location:<>

8. Bước tiếp theo