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 thông tin về các đối tượng trên tuyến đường của ứng dụng, tạo bản tóm tắt về tuyến đường của ứng dụng và trao đổi thông tin về tuyến đường (chẳng hạn như thông tin cập nhật về tình trạng giao thông) với máy chủ và các ứng dụng khác.
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 truyền phát trực tiếp giữa máy khách và 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.
Trước tiên, hãy tạo thư mục làm việc của lớp học lập trình và cd
vào thư mục đó:
mkdir streaming-grpc-go-getting-started && cd streaming-grpc-go-getting-started
Tải và giải nén lớp học lập trình:
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
Ngoài ra, bạn có thể tải tệp .zip chỉ chứa thư mục codelab xuống rồi giải nén theo cách thủ công.
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 thông báo và dịch vụ
Bước đầu tiên là xác định dịch vụ gRPC của ứng dụng, các 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:
- Các phương thức RPC được gọi là
ListFeatures
,RecordRoute
vàRouteChat
mà máy chủ triển khai và ứng dụng gọi. - Các loại thông báo
Point
,Feature
,Rectangle
,RouteNote
vàRouteSummary
là các cấu trúc dữ liệu được trao đổi giữa máy khách và máy chủ khi gọi các phương thức ở trên.
Tất cả các phương thức RPC này và các loại thông báo của phương thức 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.
Xác định các 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ố 1
và 2
là các 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 đối tượng 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;
}
Tiếp theo là thông báo Rectangle
biểu thị một hình chữ nhật vĩ độ – kinh độ, được biểu thị dưới dạng hai điểm đối diện theo đường chéo "lo" và "hi".
message Rectangle {
// One corner of the rectangle.
Point lo = 1;
// The other corner of the rectangle.
Point hi = 2;
}
Ngoài ra, RouteNote
là một thông báo đại diện cho thông báo được gửi tại một thời điểm nhất định.
message RouteNote {
// The location from which the message is sent.
Point location = 1;
// The message to be sent.
string message = 2;
}
Chúng tôi cũng sẽ yêu cầu bạn gửi một thông báo RouteSummary
. Thông báo này được nhận để phản hồi một RPC RecordRoute
. RPC này sẽ được giải thích trong phần tiếp theo. Tệp này chứa số lượng điểm riêng lẻ nhận được, số lượng đối tượng được phát hiện và tổng khoảng cách đã đi được tính bằng tổng tích luỹ khoảng cách giữa mỗi điểm.
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;
}
Xác định các phương thức dịch vụ
Để xác định một dịch vụ, bạn chỉ định một dịch vụ có tên trong tệp .proto. 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.
Xác định các phương thức RPC
trong định nghĩa dịch vụ của bạn, chỉ định các loại yêu cầu và phản hồi của các phương thức đó. Trong phần này của lớp học lập trình, hãy xác định:
ListFeatures
Lấy các Feature
có sẵn trong Rectangle
đã cho. Kết quả được truyền trực tuyến thay vì trả về ngay lập tức (ví dụ: trong một thông báo phản hồi có trường lặp lại), vì hình chữ nhật có thể bao phủ một khu vực rộng lớn và chứa một số lượng lớn các đối tượng.
Một loại phù hợp cho RPC này là RPC truyền trực tuyến phía máy chủ: ứng dụng gửi yêu cầu đến máy chủ và nhận một luồng để đọc lại một chuỗi thông báo. Ứng dụng đọc từ luồng được trả về cho đến khi không còn thông báo nào nữa. Như bạn có thể thấy trong ví dụ của chúng tôi, bạn chỉ định một phương thức truyền trực tuyến phía máy chủ bằng cách đặt từ khoá truyền trực tuyến trước loại phản hồi.
rpc ListFeatures(Rectangle) returns (stream Feature) {}
RecordRoute
Chấp nhận một luồng Point
trên một tuyến đường đang được đi qua, trả về một RouteSummary
khi quá trình đi qua hoàn tất.
RPC truyền trực tuyến phía máy khách có vẻ phù hợp trong trường hợp này: máy khách ghi một chuỗi thông báo và gửi chúng đến máy chủ, một lần nữa sử dụng một luồng được cung cấp. Sau khi hoàn tất việc ghi các thông báo, ứng dụng sẽ đợi máy chủ đọc tất cả các thông báo đó và trả về phản hồi. Bạn chỉ định phương thức phát trực tuyến phía máy khách bằng cách đặt từ khoá stream trước loại yêu cầu.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
RouteChat
Chấp nhận luồng RouteNote
được gửi trong khi một tuyến đường đang được đi qua, đồng thời nhận các RouteNote
khác (ví dụ: từ những người dùng khác).
Đây chính xác là loại trường hợp sử dụng cho truyền phát trực tiếp hai chiều. RPC truyền trực tuyến hai chiều có cả hai bên gửi một chuỗi thông báo bằng cách sử dụng luồng đọc-ghi. Hai luồng này hoạt động độc lập, vì vậy, máy khách và máy chủ có thể đọc và ghi theo bất kỳ thứ tự nào mà chúng muốn.
Ví dụ: máy chủ có thể đợi nhận tất cả các thông báo của ứng dụng trước khi ghi phản hồi, hoặc có thể lần lượt đọc một thông báo rồi ghi một thông báo, hoặc một số tổ hợp khác giữa đọc và ghi.
Thứ tự của các thông báo trong mỗi luồng được giữ nguyên. Bạn chỉ định loại phương thức này bằng cách đặt từ khoá luồng trước cả yêu cầu và phản hồi.
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
4. Tạo mã máy khách và mã 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 các loại thông báo của ứng dụng và truy cập vào dữ liệu cũng như định nghĩa của các loại đại diện cho 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 các phương thức ở phía máy chủ để khi máy khách gửi yêu cầu, máy chủ có thể trả lời.
5. Triển khai dịch vụ
Trước tiên, hãy xem cách chúng ta tạo một máy chủ RouteGuide
. Có hai phần để dịch vụ RouteGuide
thực hiện công việc của mình:
- Triển khai giao diện dịch vụ được tạo từ định nghĩa dịch vụ của chúng ta: thực hiện "công việc" thực tế của dịch vụ.
- Chạy một máy chủ gRPC để lắng nghe các yêu cầu từ ứng dụng và gửi các yêu cầu đó đến đúng quy trình triển khai dịch vụ.
Hãy triển khai RouteGuide trong server/server.go
.
Triển khai RouteGuide
Chúng ta cần triển khai giao diện RouteGuideService
đã tạo. Đây là cách triển khai.
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 {
...
}
Hãy cùng tìm hiểu chi tiết về từng cách triển khai RPC.
RPC truyền trực tuyến phía máy chủ
Bắt đầu bằng một trong các RPC truyền phát trực tiếp của chúng tôi. ListFeatures
là một RPC truyền trực tuyến phía máy chủ, vì vậy chúng ta cần gửi lại nhiều Feature
cho ứng dụng.
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
}
Như bạn thấy, thay vì nhận các đối tượng yêu cầu và phản hồi đơn giản trong các tham số phương thức, lần này chúng ta nhận được một đối tượng yêu cầu (Rectangle
mà ứng dụng muốn tìm Features
) và một đối tượng RouteGuide_ListFeaturesServer
đặc biệt để ghi các phản hồi. Trong phương thức này, chúng ta sẽ điền sẵn nhiều đối tượng Feature
nhất có thể để trả về, ghi các đối tượng đó vào RouteGuide_ListFeaturesServer
bằng phương thức Send()
của đối tượng này. Cuối cùng, như trong RPC đơn giản của chúng ta, chúng ta sẽ trả về một lỗi nil
để cho gRPC biết rằng chúng ta đã hoàn tất việc ghi phản hồi. Nếu có lỗi xảy ra trong lệnh gọi này, chúng tôi sẽ trả về một lỗi không phải là nil; lớp gRPC sẽ dịch lỗi đó thành một trạng thái RPC thích hợp để gửi qua mạng.
RPC truyền phát trực tiếp phía máy khách
Bây giờ, hãy xem xét một phương thức phức tạp hơn một chút: phương thức truyền trực tuyến phía máy khách RecordRoute
, trong đó chúng ta nhận được một luồng Points
từ máy khách và trả về một RouteSummary
duy nhất có thông tin về chuyến đi của họ. Như bạn có thể thấy, lần này phương thức không có tham số yêu cầu nào. Thay vào đó, nó nhận được một luồng RouteGuide_RecordRouteServer
mà máy chủ có thể dùng để vừa đọc vừa ghi thông báo – máy chủ có thể nhận thông báo của ứng dụng bằng phương thức Recv()
và trả về phản hồi duy nhất bằng phương thức 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
}
}
Trong phần nội dung phương thức, chúng ta sử dụng phương thức Recv()
của RouteGuide_RecordRouteServer
để liên tục đọc các yêu cầu của ứng dụng vào một đối tượng yêu cầu (trong trường hợp này là Point
) cho đến khi không còn thông báo nào nữa: máy chủ cần kiểm tra lỗi được trả về từ Recv()
sau mỗi lệnh gọi. Nếu là nil
, luồng vẫn hoạt động tốt và có thể tiếp tục đọc; nếu là io.EOF
, luồng thông báo đã kết thúc và máy chủ có thể trả về RouteSummary
. Nếu có bất kỳ giá trị nào khác, chúng ta sẽ trả về lỗi "nguyên trạng" để lỗi đó được dịch thành trạng thái RPC theo lớp gRPC.
RPC truyền trực tuyến hai chiều
Cuối cùng, hãy xem RPC truyền phát trực tiếp hai chiều 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
}
}
}
}
Lần này, chúng ta sẽ nhận được một luồng RouteGuide_RouteChatServer
. Giống như trong ví dụ về truyền phát trực tiếp phía máy khách, luồng này có thể dùng để đọc và ghi tin nhắn. Tuy nhiên, lần này chúng ta sẽ trả về các giá trị thông qua luồng của phương thức trong khi máy khách vẫn đang ghi thông báo vào luồng thông báo của họ. Cú pháp để đọc và ghi ở đây rất giống với phương thức truyền trực tuyến của máy khách, ngoại trừ việc máy chủ sử dụng phương thức send()
của luồng thay vì SendAndClose()
vì máy chủ đang ghi nhiều phản hồi. Mặc dù mỗi bên sẽ luôn nhận được tin nhắn của bên kia theo thứ tự chúng được viết, nhưng cả máy khách và máy chủ đều có thể đọc và ghi theo bất kỳ thứ tự nào – các luồng hoạt động hoàn toàn độc lập.
Khởi động máy chủ
Sau khi triển khai tất cả các phương thức, chúng ta cũng cần khởi động một máy chủ gRPC để máy khách có thể thực sự sử dụng dịch vụ của chúng ta. Đoạn mã sau đây cho biết cách chúng tôi thực hiện việc này cho dịch vụ 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)
Sau đây là những gì diễn ra trong main()
, từng bước:
- 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 TCP50051
như được chỉ định bởi biếnport
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. - 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
. - 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.
- Sử dụng
s.loadFeatures()
để điền vào mảngs.savedFeatures
. - Đăng ký việc triển khai dịch vụ của chúng tôi với máy chủ gRPC.
- 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ặcStop()
đượ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:
// 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()
Đị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, thì ứng dụng có thể truyền đối tượng DialOptions
làm tham số đến grpc.NewClient
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
cung cấp, được tạo từ tệp .proto
của ứng dụng.
import (pb "github.com/grpc-ecosystem/codelabs/getting_started_streaming/routeguide")
client := pb.NewRouteGuideClient(conn)
Gọi các phương thức dịch vụ
Bây giờ, hãy xem cách chúng ta 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.
RPC truyền trực tuyến phía máy chủ
Đây là nơi chúng ta gọi phương thức truyền trực tuyến phía máy chủ ListFeatures
, phương thức này trả về một luồng các đối tượng địa lý 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())
}
Giống như trong RPC đơn giản, chúng ta truyền cho phương thức một ngữ cảnh và một yêu cầu. Tuy nhiên, thay vì nhận lại một đối tượng phản hồi, chúng ta sẽ nhận lại một thực thể của RouteGuide_ListFeaturesClient
. Ứng dụng có thể dùng luồng RouteGuide_ListFeaturesClient
để đọc các phản hồi của máy chủ. Chúng ta sử dụng phương thức Recv()
của RouteGuide_ListFeaturesClient
để đọc đi đọc lại các phản hồi của máy chủ vào một đối tượng bộ đệm giao thức phản hồi (trong trường hợp này là Feature
) cho đến khi không còn thông báo nào nữa: ứng dụng cần kiểm tra lỗi err được trả về từ Recv()
sau mỗi lệnh gọi. Nếu là nil
, luồng vẫn hoạt động tốt và có thể tiếp tục đọc; nếu là io.EOF
thì luồng thông báo đã kết thúc; nếu không, phải có lỗi RPC, được truyền qua err
.
RPC truyền phát trực tiếp phía máy khách
Phương thức truyền trực tuyến phía máy khách RecordRoute
tương tự như phương thức phía máy chủ, ngoại trừ việc chúng ta chỉ truyền cho phương thức này một ngữ cảnh và nhận lại một luồng RouteGuide_RecordRouteClient
. Chúng ta có thể dùng luồng này để vừa ghi vừa đọc thông báo.
// 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
có một phương thức Send()
mà chúng ta có thể dùng để gửi yêu cầu đến máy chủ. Sau khi hoàn tất việc ghi các yêu cầu của ứng dụng vào luồng bằng Send()
, chúng ta cần gọi CloseAndRecv()
trên luồng để cho gRPC biết rằng chúng ta đã hoàn tất việc ghi và đang chờ nhận phản hồi. Chúng ta lấy trạng thái RPC từ err được trả về từ CloseAndRecv()
. Nếu trạng thái là nil
, thì giá trị trả về đầu tiên từ CloseAndRecv()
sẽ là một phản hồi hợp lệ của máy chủ.
RPC truyền trực tuyến hai chiều
Cuối cùng, hãy xem RPC truyền phát trực tiếp hai chiều RouteChat()
. Như trong trường hợp của RecordRoute
, chúng ta chỉ truyền cho phương thức một đối tượng bối cảnh và nhận lại một luồng mà chúng ta có thể dùng để vừa ghi vừa đọc tin nhắn. Tuy nhiên, lần này chúng ta sẽ trả về các giá trị thông qua luồng của phương thức trong khi máy chủ vẫn đang ghi thông báo vào luồng thông báo của chúng.
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
Cú pháp để đọc và ghi ở đây rất giống với phương thức truyền phát trực tiếp phía máy khách của chúng ta, ngoại trừ việc chúng ta sử dụng phương thức CloseSend()
của luồng sau khi hoàn tất lệnh gọi. Mặc dù mỗi bên sẽ luôn nhận được tin nhắn của bên kia theo thứ tự chúng được viết, nhưng cả máy khách và máy chủ đều có thể đọc và ghi theo bất kỳ thứ tự nào – các luồng hoạt động hoàn toàn độc lập.
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:
- Chạy máy chủ trong một dòng lệnh:
cd server go run .
- 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:
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. Bước tiếp theo
- Tìm hiểu cách hoạt động của gRPC trong phần Giới thiệu về gRPC và Các khái niệm cốt lõi.
- Làm theo hướng dẫn cơ bản.
- Khám phá tài liệu tham khảo về API.