1. 소개
이 Codelab에서는 gRPC-Go를 사용하여 Go로 작성된 경로 매핑 애플리케이션의 기반을 형성하는 클라이언트와 서버를 만듭니다.
튜토리얼이 끝나면 gRPC를 사용하여 원격 서버에 연결하여 클라이언트 경로의 기능에 관한 정보를 가져오고, 클라이언트 경로의 요약을 만들고, 서버 및 다른 클라이언트와 트래픽 업데이트와 같은 경로 정보를 교환하는 클라이언트가 있습니다.
서비스는 프로토콜 버퍼 파일에 정의되며, 이 파일은 클라이언트와 서버가 서로 통신할 수 있도록 상용구 코드를 생성하는 데 사용되므로 이 기능을 구현하는 데 드는 시간과 노력을 절약할 수 있습니다.
이 생성된 코드는 서버와 클라이언트 간의 복잡한 통신뿐만 아니라 데이터 직렬화 및 역직렬화도 처리합니다.
학습할 내용
- 프로토콜 버퍼를 사용하여 서비스 API를 정의하는 방법
- 자동 코드 생성을 사용하여 프로토콜 버퍼 정의에서 gRPC 기반 클라이언트와 서버를 빌드하는 방법
- gRPC를 사용한 클라이언트-서버 스트리밍 통신에 대한 이해
이 Codelab은 gRPC를 처음 접하거나 gRPC를 다시 한번 살펴보고 싶은 Go 개발자 또는 분산 시스템을 빌드하는 데 관심이 있는 모든 사용자를 대상으로 합니다. 이전 gRPC 경험은 필요하지 않습니다.
2. 시작하기 전에
기본 요건
다음이 설치되어 있는지 확인합니다.
- Go 도구 모음 버전 1.24.5 이상 설치 안내는 Go의 시작하기를 참고하세요.
- 프로토콜 버퍼 컴파일러
protoc
버전 3.27.1 이상 설치 안내는 컴파일러의 설치 가이드를 참고하세요. - 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
변수를 업데이트합니다.
export PATH="$PATH:$(go env GOPATH)/bin"
코드 가져오기
처음부터 완전히 시작하지 않아도 되도록 이 Codelab에서는 애플리케이션 소스 코드의 스캐폴드를 제공하여 이를 완성할 수 있습니다. 다음 단계에서는 프로토콜 버퍼 컴파일러 플러그인을 사용하여 상용구 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
또는 codelab 디렉터리만 포함된 .zip 파일을 다운로드하고 직접 압축을 해제할 수 있습니다.
구현을 입력하는 것을 건너뛰려면 완료된 소스 코드를 GitHub에서 확인하세요.
3. 메시지 및 서비스 정의
첫 번째 단계는 프로토콜 버퍼를 사용하여 애플리케이션의 gRPC 서비스, RPC 메서드, 요청 및 응답 메시지 유형을 정의하는 것입니다. 서비스에서 제공하는 기능:
- 서버가 구현하고 클라이언트가 호출하는 RPC 메서드
ListFeatures
,RecordRoute
,RouteChat
- 위의 메서드를 호출할 때 클라이언트와 서버 간에 교환되는 데이터 구조인 메시지 유형
Point
,Feature
,Rectangle
,RouteNote
,RouteSummary
이러한 RPC 메서드와 메시지 유형은 모두 제공된 소스 코드의 routeguide/route_guide.proto
파일에 정의됩니다.
프로토콜 버퍼는 일반적으로 protobufs로 알려져 있습니다. gRPC 용어에 대한 자세한 내용은 gRPC의 핵심 개념, 아키텍처, 수명 주기를 참고하세요.
메시지 유형 정의
소스 코드의 routeguide/route_guide.proto
파일에서 먼저 Point
메시지 유형을 정의합니다. Point
는 지도상의 위도-경도 좌표 쌍을 나타냅니다. 이 Codelab에서는 좌표에 정수를 사용합니다.
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
1
및 2
은 message
구조의 각 필드에 대한 고유 ID 번호입니다.
다음으로 Feature
메시지 유형을 정의합니다. Feature
는 Point
로 지정된 위치에 있는 항목의 이름이나 우편 주소에 string
필드를 사용합니다.
message Feature {
// The name or address of the feature.
string name = 1;
// The point where the feature is located.
Point location = 2;
}
다음은 위도-경도 사각형을 나타내는 Rectangle
메시지입니다. 이 메시지는 대각선으로 반대되는 두 점 '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
메시지도 필요합니다. 이 메시지는 다음 섹션에서 설명하는 RecordRoute
RPC에 대한 응답으로 수신됩니다. 여기에는 수신된 개별 포인트 수, 감지된 특징 수, 각 포인트 간 거리의 누적 합계로 계산된 총 이동 거리가 포함됩니다.
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
파일에는 애플리케이션 서비스에서 제공하는 하나 이상의 메서드를 정의하는 RouteGuide
라는 service
구조가 있습니다.
서비스 정의 내에서 RPC
메서드를 정의하여 요청 및 응답 유형을 지정합니다. Codelab의 이 섹션에서는 다음을 정의합니다.
ListFeatures
지정된 Rectangle
내에서 사용할 수 있는 Feature
을 가져옵니다. 결과는 한 번에 반환되지 않고 스트리밍됩니다 (예: 반복 필드가 있는 응답 메시지). 사각형이 넓은 영역을 포함하고 많은 수의 기능을 포함할 수 있기 때문입니다.
이 RPC에 적합한 유형은 서버 측 스트리밍 RPC입니다. 클라이언트는 서버에 요청을 보내고 일련의 메시지를 다시 읽는 스트림을 가져옵니다. 클라이언트는 메시지가 더 이상 없을 때까지 반환된 스트림을 읽습니다. 예에서 볼 수 있듯이 응답 유형 앞에 stream 키워드를 배치하여 서버 측 스트리밍 메서드를 지정합니다.
rpc ListFeatures(Rectangle) returns (stream Feature) {}
RecordRoute
이동 중인 경로에서 Point
스트림을 수락하고 이동이 완료되면 RouteSummary
를 반환합니다.
이 경우 클라이언트 측 스트리밍 RPC가 적합해 보입니다. 클라이언트는 일련의 메시지를 작성한 후 제공된 스트림을 사용하여 서버에 이를 보냅니다. 클라이언트는 메시지 작성을 완료한 후 서버가 메시지를 모두 읽고 응답을 반환할 때까지 대기합니다. 요청 유형 앞에 스트림 키워드를 배치하여 클라이언트 측 스트리밍 메서드를 지정합니다.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
RouteChat
경로가 이동되는 동안 전송된 RouteNote
스트림을 수락하고 다른 RouteNote
(예: 다른 사용자로부터)를 수신합니다.
이것이 바로 양방향 스트리밍의 사용 사례입니다. 양방향 스트리밍 RPC에서는 양쪽에서 읽기-쓰기 스트림을 사용하여 일련의 메시지를 보냅니다. 두 스트림은 독립적으로 작동하므로 클라이언트와 서버는 원하는 순서로 읽고 쓸 수 있습니다.
예를 들어 서버는 응답을 작성하기 전에 모든 클라이언트 메시지를 수신할 때까지 기다리거나 메시지를 읽은 다음 메시지를 작성하거나 읽기와 쓰기의 다른 조합을 사용할 수 있습니다.
각 스트림의 메시지 순서는 유지됩니다. 요청과 응답 앞에 모두 스트림 키워드를 배치하여 이 유형의 메서드를 지정합니다.
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
4. 클라이언트 및 서버 코드 생성
그런 다음 프로토콜 버퍼 컴파일러를 사용하여 .proto
파일에서 클라이언트와 서버 모두의 상용구 gRPC 코드를 생성합니다. 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
서비스가 작업을 수행하도록 하는 데는 두 가지 부분이 있습니다.
- 서비스 정의에서 생성된 서비스 인터페이스 구현: 서비스의 실제 '작업'을 실행합니다.
- 클라이언트의 요청을 수신하고 올바른 서비스 구현으로 디스패치하는 gRPC 서버를 실행합니다.
server/server.go
에서 RouteGuide를 구현해 보겠습니다.
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
}
메서드 매개변수에서 간단한 요청 및 응답 객체를 가져오는 대신 이번에는 요청 객체 (클라이언트가 Features
을 찾으려는 Rectangle
)와 응답을 작성하는 특수 RouteGuide_ListFeaturesServer
객체를 가져옵니다. 메서드에서 반환해야 하는 Feature
객체를 최대한 많이 채우고 Send()
메서드를 사용하여 RouteGuide_ListFeaturesServer
에 씁니다. 마지막으로 간단한 RPC에서와 마찬가지로 nil
오류를 반환하여 응답 작성을 완료했음을 gRPC에 알립니다. 이 호출에서 오류가 발생하면 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
의 Recv()
메서드를 사용하여 메시지가 더 이상 없을 때까지 클라이언트의 요청을 요청 객체 (이 경우 Point
)에 반복적으로 읽어옵니다. 서버는 각 호출 후 Recv()
에서 반환된 오류를 확인해야 합니다. nil
인 경우 스트림이 여전히 양호하므로 계속 읽을 수 있습니다. io.EOF
인 경우 메시지 스트림이 종료되었으므로 서버가 RouteSummary
를 반환할 수 있습니다. 다른 값이 있으면 gRPC 레이어에서 RPC 상태로 변환되도록 오류를 '있는 그대로' 반환합니다.
양방향 스트리밍 RPC
마지막으로 양방향 스트리밍 RPC 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
스트림이 표시됩니다. 하지만 이번에는 클라이언트가 메시지 스트림에 메시지를 계속 쓰는 동안 메서드의 스트림을 통해 값을 반환합니다. 여기서 읽기 및 쓰기 구문은 클라이언트 스트리밍 메서드와 매우 유사하지만 서버는 여러 응답을 쓰기 때문에 SendAndClose()
대신 스트림의 send()
메서드를 사용합니다. 각 측면은 항상 작성된 순서대로 다른 측면의 메시지를 받지만 클라이언트와 서버는 어떤 순서로든 읽고 쓸 수 있습니다. 스트림은 완전히 독립적으로 작동합니다.
서버 시작
모든 메서드를 구현한 후에는 클라이언트가 실제로 서비스를 사용할 수 있도록 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()
에서 이루어지는 작업은 다음과 같습니다.
lis, err := net.Listen(...)
를 사용하여 원격 클라이언트 요청을 수신하는 데 사용할 TCP 포트를 지정합니다. 기본적으로 애플리케이션은port
변수에 의해 지정되거나 서버를 실행할 때 명령줄에--port
스위치를 전달하여 지정된 TCP 포트50051
를 사용합니다. TCP 포트를 열 수 없으면 애플리케이션이 심각한 오류로 종료됩니다.grpc.NewServer(...)
를 사용하여 gRPC 서버 인스턴스를 만들고 이 인스턴스의 이름을grpcServer
로 지정합니다.- 애플리케이션의 API 서비스를 나타내는 구조체인
routeGuideServer
에 대한 포인터를 만들고 포인터 이름을s.
로 지정합니다. s.loadFeatures()
을 사용하여s.savedFeatures
배열을 채웁니다.- gRPC 서버에 서비스 구현을 등록합니다.
- 포트 세부정보를 사용하여 서버에서
Serve()
을 호출하여 클라이언트 요청을 차단 대기합니다. 이는 프로세스가 종료되거나Stop()
이 호출될 때까지 계속됩니다.
loadFeatures()
함수는 server/testdata.go
에서 좌표-위치 매핑을 가져옵니다.
6. 클라이언트 만들기
이제 클라이언트 코드를 구현할 client/client.go
를 수정합니다.
원격 서비스의 메서드를 호출하려면 먼저 서버와 통신할 gRPC 채널을 만들어야 합니다. 클라이언트의 main()
함수에서 서버의 타겟 URI 문자열 (이 경우 주소와 포트 번호)을 grpc.NewClient()
에 전달하여 이를 만듭니다.
// 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
서비스에는 사용자 인증 정보가 필요하지 않습니다.
gRPC 채널이 설정되면 Go 함수 호출을 통해 RPC를 실행할 클라이언트 스텁이 필요합니다. 애플리케이션의 .proto
파일에서 생성된 route_guide_grpc.pb.go
파일에서 제공하는 NewRouteGuideClient
메서드를 사용하여 스텁을 가져옵니다.
import (pb "github.com/grpc-ecosystem/codelabs/getting_started_streaming/routeguide")
client := pb.NewRouteGuideClient(conn)
서비스 메서드 호출
이제 서비스 메서드를 호출하는 방법을 살펴보겠습니다. gRPC-Go에서 RPC는 차단/동기 모드로 작동합니다. 즉, RPC 호출은 서버가 응답할 때까지 기다리며 응답 또는 오류를 반환합니다.
서버 측 스트리밍 RPC
여기에서 지리적 Feature
객체의 스트림을 반환하는 서버 측 스트리밍 메서드 ListFeatures
를 호출합니다.
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_ListFeaturesClient
의 Recv()
메서드를 사용하여 더 이상 메시지가 없을 때까지 응답 프로토콜 버퍼 객체 (이 경우 Feature
)에 대한 서버의 응답을 반복적으로 읽어옵니다. 클라이언트는 각 호출 후 Recv()
에서 반환된 오류 err를 확인해야 합니다. 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에 알려야 합니다. CloseAndRecv()
에서 반환된 err에서 RPC 상태를 가져옵니다. 상태가 nil
이면 CloseAndRecv()
의 첫 번째 반환 값은 유효한 서버 응답입니다.
양방향 스트리밍 RPC
마지막으로 양방향 스트리밍 RPC 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)