들어가기에 앞서
먼저 본 글은 https://grpc.io/docs/languages/go/basics/ gRPC의 Basics tutorial의 코드 인것을 밝힙니다.
gRPC를 한 번 실습해보았지만 공식 Docs를 읽어보지는 않아서 번역기 등등.. 많은 방법을 통해 혼자 번역, 분석, 주석을 단 글 인점을 알아주시길 바라고, 글이 길지만 누군가에겐 도움이 되지 않을까 하는 마음에 올려봅니다..
먼저 Github에 존재하는 gRPC를 clone해옵니다. 그리고 현재 Basics tutorial이기 때문에 examples/route_guide안에서만 실습을 할 예정입니다.
* 전체 코드를 올린 것이 아니라 부분 부분 잘라서 올려 전체 코드랑 같이봐야 이해하기 편할 거 같습니다 *
1. Protocol Buffers
마이크로 서비스 아키텍처(MSA)를 하는 기업이 gRPC를 선호하는 이유이자 gRPC만의 장점인 Protocol Buffers ( Protobuf)를 작성해보겠습니다.
protobuf란 무엇이고 왜 gRPC에 쓰이는가?
먼저 서비스를 정의합니다.
service RouteGuide {
...
}
1-1. 단순 RPC(Unary RPC)
- Client가 Stub을 사용하여 서버에 요청을 보내고, 응답이 돌아올 때 까지 기다리는 방식입니다.
//단순 RPC(Unary RPC)
// 특정 위치의 특징(Feature)을 가져옵니다.
rpc GetFeature(Point) returns (Feature) {}
*Stub이란 추상 함수라고 생각하면 될 듯 하다, Feature은 밑에 존재하는 메시지 유형이다, Point도 메시지 유형이다.
1-2. 서버 측 스트리밍 RPC(Server Streaming RPC)
*Steaming이란: 데이터를 연손적으로 요청 응답하는 방식(지속적인 전달) (여기서 비동기가 필요할 거 같다는 생각이 든다)
TCP의 기본방식이다-> 스트리밍 방식으로 전송과 흐름제어, 혼잡 제어
// 주어진 사각형 내에 있는 사용 가능한 특징(Feature)을 스트리밍 방식으로 가져옵니다.
rpc ListFeatures(Rectangle) returns (stream Feature) {}
* 서버 측 스트리밍인 것을 알 수 있는 방법 반환 형식이 stream이다 서버가 연속적으로 응답하기 때문이다.
1-3. 클라이언트 측 스트리밍 RPC(Client Streaming RPC)
- Client가 메시지 시퀸스(여러개의 메시지가 연속적으로 나열된 데이터 시퀀스)를 작성하여 Server에 전송합니다.
// 경로를 따라 이동 중인 Point들의 스트림을 받아서, 이동이 완료되면 RouteSummary를 반환합니다.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
- 이번에는 Client가 연속적으로 응답을 보낸다는 것을 알 수 있다.
1-4. 양방향 스트리밍 RPC(Bidirectional Streaming RPC)
- 양쪽 모두가 읽기 및 쓰기 스트림을 사용해 메시지 시퀀스를 전송합니다.
// 경로가 횡단되는 동안 전송되는 RouteNote 메시지를 받으면서 동시에 다른 RouteNote 메시지를 반환합니다.
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
1-5 message
- 그냥 이런 형식이구나 하고 넘어가면 될 듯 하다. 모르시겠으면 위에 블로그를 보고 오셔도 됩니다.
// 좌표는 E7 표현법에서의 위도와 경도 쌍으로 표시됩니다.
// (10의 7제곱을 곱하고 가장 가까운 정수로 반올림한 값입니다).
// 위도는 +/- 90도 범위 내에 있어야 하며, 경도는 +/- 180도 범위 내에 있어야 합니다 (포함됨).
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
2. pb.go 살펴보기
*굳이 궁금하지 않다면 뛰어넘어도 되는 부분입니다 tutorial에도 없는 부분이기도 합니다 , 저는 궁금해서 코드를 따라가 보겠습니다.
* 이미 클론이 되어 있다면 생성이 되어 있겠지만 tutorial를 따라 가보겠습니다.
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
routeguide/route_guide.proto
protoc(protobuf 컴파일러를 사용하여 프로토콜 버퍼 정의 파일(Protocol Buffers Definition Files)를 생성합니다.
- route_guide.pb.go, route_guide_grpc.pb.go 파일이 만들어집니다.
route_guide.pb.go
- 요청과 응답 메시지 유형을 채우고 직렬화하고 검색하는 데 사용되는 모든 프로토콜 버퍼 코드를 포함합니다.
route_guide.grpc.go
- RouteGuide Service에 정의된 메서드를 호출하기 위한 Client용 Interface타입
- RouteGuide Service에서 정의된 Method를 구현하기 위한 Server용 Interface타입
들어가기 전에 제 생각을 말씀드리자면 pb.go 파일은 DTO,
grpc.pb.go는 API Interface느낌이라고 생각하면 조금 편했던 것 같습니다.
2-1 route_guide.pb.go
- 코드가 너무 길다보니 Point에 관해서만 보고 지나가겠습니다. 시작하는 주석은 그냥 메모로써 크게 신경쓸 필요없습니다.
*Point만 보면 되는 이유는 메소드들이 비슷비슷한 역할을 하기 때문입니다. (전체 코드를 보면 이해가 갈 것임)
- 요청 및 응답 메시지 유형을 채우고 직렬화하고 검색하기 위한 모든 프로토콜 버퍼 코드를 포함합니다.
type Point struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
//위까지는 protobuf에서 내부적으로 사용되니는 필드들이다
Latitude int32 `protobuf:"varint,1,opt,name=latitude,proto3" json:"latitude,omitempty"`
Longitude int32 `protobuf:"varint,2,opt,name=longitude,proto3" json:"longitude,omitempty"`
}
하나하나 살펴보자면 일단 주석위에 줄은 일반적으로 사용자가 직접 조작할 필요가 없습니다.
*굳이 이건 자세히 볼 필요 없음
state : 내부 상태 변수, sizeCache: 메시지 크기를 캐싱하기위한 내부 캐시, protobuf 메시지에 포함되는 알 수 없는 피드들을 저장
varint: 필드 값을 인 코딩딩해서 표현한다는 의미
이건 자세히 봅시다.
1, 2 : 숫자는 필드 번호
opt: 필드가 선택적인 것을 나타냄, 값이 지정 안되면 default값 사용
name: 필드 이름을 명시함
json: 만약 JSON으로 바뀌면 이렇게 나타낼 것이다.
logutude: JSON 필드 이름
omitempty: 필드 값이 Empty이면 해당 필드를 JSON 표현에서 제외한다.
2- 2 Rest()
- 한마디로 정의하면: 메시지의 필드들을 초기화하는 역할.
- 자세히 보자면...
UnsafeEnabled: 프로토콜 버퍼의 안전하지 않은 기능을 사용할 때만 해당 코드 블록이 실행되도록 합니다.
mi는 따라가서 해석하려면 protoimpl라이브러리도 봐야하기에 간단히 MessageInfo → 메시지가 직렬화 및 역직렬화되는 과정에서 필요한 정보를 제공하고, 필드에 대한 접근과 조작을 가능하게 합니다. ms = MessageState →메시지가 생성되고 수정되는 과정에서 필요한 정보를 관리하며, 필드 값을 가져오거나 설정하는 등의 작업을 수행할 수 있습니다.
func (x *Point) Reset() { //x라는 pointer reciver
*x = Point{}
if protoimpl.UnsafeEnabled {
mi := &file_examples_route_guide_routeguide_route_guide_proto_msgTypes[0] //mi -> MessageInfo -> protobuf message reset!! & save
//- > 필드 갯수, 필드 속성이 담김
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) // ms -> MessageState => 필드값, 수정여부, 메시지 크기 등 정보가 담김
ms.StoreMessageInfo(mi) // -> ms에 mi의 정보를 저장합니다
}
}
2-3 String()
func (x *Point) String() string { //반환값을 String으로 바꾼다.
return protoimpl.X.MessageStringOf(x)
}
2-4 ProtoReflect()
- 메시지의 reflection을 제공하는 것입니다. (직접 reflection이란 무엇인가를 찾아보면 이해가 편합니다.)
- 반환 형식을 자세히 볼 필요가 있다. reflection정보를 반환하고 있다. 이를 (ProtoReflect()메소드를)사용해 필드에 동적으로 접근하거나 그것을 바탕으로 수정이 가능하다.
func (x *Point) ProtoReflect() protoreflect.Message {
mi := &file_examples_route_guide_routeguide_route_guide_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil { //말 그대로 안전하지 않으면 -> 안전 모드이다
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) //Pointer가 가르키는 것을 가져와 ms에 저장
if ms.LoadMessageInfo() == nil { //메시지 상태에 로드되지 않았을 경우 로드해줌
ms.StoreMessageInfo(mi)
}
return ms //반환
}
return mi.MessageOf(x)
}
2-5 Descriptor()
- 메시지 직렬화, 역직렬화 메소드
*여기서 디스크립터란→ 메시지 구조와 필드에 대한 정보를 담고 있는 데이터 (간단한 DTO)라고 생각하자
// Deprecated: Use Point.ProtoReflect.Descriptor instead.
func (*Point) Descriptor() ([]byte, []int) { //protobuf 직렬화 역 직렬화시 사용
return file_examples_route_guide_routeguide_route_guide_proto_rawDescGZIP(),[]int{0}
}
func (x *Point) GetLatitude() int32 { //Getter
if x != nil {
return x.Latitude
}
return 0
}
func (x *Point) GetLongitude() int32 { //Getter
if x != nil {
return x.Longitude
}
return 0
자 이제 pb.go는 다 보았다고 할 수 있습니다. 왜냐하면 Getter, Setter ⇒ DTO이기 때문에 이제 다른 protobuf의 Getter, Setter가 반복적으로 나타날 뿐이기 때문입니다.
3. grpc.pb.go 살펴보기
- 3번째는 하다보니 주석이 많아졌습니다 주석을 위주로 보시면 될 듯 합니다.
- grpc.pb.go RouterGuideServer → UnimplementeRouteGuideServer→ RouteGuideClient → routeGuildeCloent → 각 메서드 구현
type RouteGuideServer interface {
GetFeature(context.Context, *Point) (*Feature, error)
ListFeatures(*Rectangle, RouteGuide_ListFeaturesServer) error
RecordRoute(RouteGuide_RecordRouteServer) error
RouteChat(RouteGuide_RouteChatServer) error
mustEmbedUnimplementedRouteGuideServer()
}
3-1 RouteGuideServer GetFeature ⇒ Unary RPC ⇒ 단일 RPC
- Context의 사용 이유 : 요청이기 때문에 서버에서 생명주기, 작업취소, 값 전달 등등 처리 하도록 *단일 RPC에서 자주 사용, !!! 메서드가 빠르게 처리되어 응답이 빠르게 반한되는 것이라면 Context를 쓸 필요가 없고, Client가 응답을 받은 후 작업을 종료하는 경우 쓸 이유가 없다.
3-2 ListFeatures ⇒ Server Streaming RPC
- 반환값 RouteGuide_ListFeaturesServer(이건 나중에 보겠다 지나가자) , error
type RouteGuideServer interface {
GetFeature(context.Context, *Point) (*Feature, error)
ListFeatures(*Rectangle, RouteGuide_ListFeaturesServer) error
RecordRoute(RouteGuide_RecordRouteServer) error
RouteChat(RouteGuide_RouteChatServer) error
mustEmbedUnimplementedRouteGuideServer()
}
..밑에 interface들도 똑같은 느낌이다 지나가자 노가다다..
3-3 UnimplementedRouteGuideServer
- 간단하게 하자면 그냥 예외처리입니다. 인터페이스가 구현되지 않았다는 에러들입니다.
func (UnimplementedRouteGuideServer) GetFeature(context.Context, *Point) (*Feature, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetFeature not implemented")
}
func (UnimplementedRouteGuideServer) ListFeatures(*Rectangle, RouteGuide_ListFeaturesServer) error {
return status.Errorf(codes.Unimplemented, "method ListFeatures not implemented")
}
func (UnimplementedRouteGuideServer) RecordRoute(RouteGuide_RecordRouteServer) error {
return status.Errorf(codes.Unimplemented, "method RecordRoute not implemented")
}
func (UnimplementedRouteGuideServer) RouteChat(RouteGuide_RouteChatServer) error {
return status.Errorf(codes.Unimplemented, "method RouteChat not implemented")
}
func (UnimplementedRouteGuideServer) mustEmbedUnimplementedRouteGuideServer() {}
type UnsafeRouteGuideServer interface {
mustEmbedUnimplementedRouteGuideServer()
}
3-4 RouteGuideClient
- Client의 interface이다 ctx등등 인자값을 보면 알 수 있다.
type RouteGuideClient interface {
GetFeature(ctx context.Context, in *Point, opts ...grpc.CallOption) (*Feature, error)
//ctx context.Context = > Context의 용도는 작업취소, 값 전달, 타임아웃 기한 설정, 컨텍스트 계층 구조 비등기 느낌입니다.
//...grpc.CallOption은 gRPC호출에 적용할 수 있는 다양한 옵션들을 나타낸다.
// 호출 타임아웃, 인증설정, 메타데이터 등과 같은 추가 정보를 지정하는데 사용 됨 ... 은 옵션을 여러 개 지정할 수 있다는 표현이다
ListFeatures(ctx context.Context, in *Rectangle, opts ...grpc.CallOption) (RouteGuide_ListFeaturesClient, error)
RecordRoute(ctx context.Context, opts ...grpc.CallOption) (RouteGuide_RecordRouteClient, error)
RouteChat(ctx context.Context, opts ...grpc.CallOption) (RouteGuide_RouteChatClient, error)
}
3-5 routeGuideClient struct
- 구조체와 생성자이다 순서 → 생성자 → cc를 받아 구조체의 인터페이스를 생성(라이브러리다 코드가 너무 길기 때문에 Skip) → RouteGuideClient로 형변환 후 반환
type routeGuideClient struct { //gRPC클라이언트를 나타냄
cc grpc.ClientConnInterface
}
func NewRouteGuideClient(cc grpc.ClientConnInterface) RouteGuideClient { //생성자
return &routeGuideClient{cc}
}
주요 메소드들 일단 GetFeature을 보면 알 수 있기 때문에 Stream하나만 보겠습니다
func (c *routeGuideClient) ListFeatures(ctx context.Context, in *Rectangle, opts ...grpc.CallOption) (RouteGuide_ListFeaturesClient, error) {
stream, err := c.cc.NewStream(ctx, &RouteGuide_ServiceDesc.Streams[0], RouteGuide_ListFeatures_FullMethodName, opts...)
if err != nil {
return nil, err
}
x := &routeGuideListFeaturesClient{stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
코드를 보면아시겠지만 새로운Stream을 만들고 보내고 받을때 열려있는지 닫혀있는지를 체크하고 반환합니다.
3-6 Handler
- Client 요청 후 디코딩후Reacangle 객체로 변환하고 서버 스트림 객체를 생성 Client로부터 요청을 받고 routeGuidlListFeaturesServer를 통해 응답을 전송합니다 .
func _RouteGuide_ListFeatures_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(Rectangle)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(RouteGuideServer).ListFeatures(m, &routeGuideListFeaturesServer{stream})
}
이제 대충 grpc.pb.go, pb.go에 대한 역할과 동작과정을 알아보았고 어떻게 Stream이 생기며, Client ↔Server가 상호작용할까 라는 생각을 해보았습니다.
이제 직접 상호작용하는 코드를 살펴보겠습니다.
4. Client 및 Server 생성
* 여기부터도 주석이 많습니다 주석을 보면서 따라오시면 좋을 것 같고, 전체 코드를 보면서 따라오면 더 이해하기 편하지 않을까 합니다.
4-1 Server 생성
func main() {
flag.Parse() //keyFile같은 플래그 값들이 파싱되어 저장된다
lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
//지정포트 번호로 연결 준비
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
var opts []grpc.ServerOption //서버옵션
if *tls { //TLS를 설정
if *certFile == "" {
*certFile = data.Path("x509/server_cert.pem")
}
if *keyFile == "" {
*keyFile = data.Path("x509/server_key.pem")
}
creds, err := credentials.NewServerTLSFromFile(*certFile, *keyFile)
//TLS설정
if err != nil {
log.Fatalf("Failed to generate credentials: %v", err)
}
opts = []grpc.ServerOption{grpc.Creds(creds)}
}
grpcServer := grpc.NewServer(opts...) //서버 인스턴스 생성
pb.RegisterRouteGuideServer(grpcServer, newServer()) //서비스 구현을 gRPC서버에 등록합니다.
grpcServer.Serve(lis) //프로세스가 종료되거나 호출 될 Serve()때까지 차단 대기를 수행하기 위해 포트 세부 정보로 서버를 호출합니다.
}
4-2 Client 생성
func main() { //클라이언트를 만듭니다.
flag.Parse()
var opts []grpc.DialOption //인증 자격 증명
if *tls {
if *caFile == "" {
*caFile = data.Path("x509/ca_cert.pem")
}
creds, err := credentials.NewClientTLSFromFile(*caFile, *serverHostOverride)
if err != nil {
log.Fatalf("Failed to create TLS credentials: %v", err)
}
opts = append(opts, grpc.WithTransportCredentials(creds))
} else {
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
}
//TLS설정
conn, err := grpc.Dial(*serverAddr, opts...) //서버에 연결
if err != nil {
log.Fatalf("fail to dial: %v", err)
}
defer conn.Close()
client := pb.NewRouteGuideClient(conn)
//client 인스턴스 생성 RPC 호출가능하도록
// Looking for a valid feature
printFeature(client, &pb.Point{Latitude: 409146138, Longitude: -746188906})
// Feature missing.
printFeature(client, &pb.Point{Latitude: 0, Longitude: 0})
// Looking for features between 40, -75 and 42, -73.
printFeatures(client, &pb.Rectangle{
Lo: &pb.Point{Latitude: 400000000, Longitude: -750000000},
Hi: &pb.Point{Latitude: 420000000, Longitude: -730000000},
})
// RecordRoute
runRecordRoute(client)
// RouteChat
runRouteChat(client)
}
자 이제 따라가봅시다.
먼저 생성까지는 볼 것이없고 printFeature(client…)부터 보죠
4-3 printFeature() - 단일 RPC (Client)
func printFeature(client pb.RouteGuideClient, point *pb.Point) {
//인자 값으로 client, point를 받음
log.Printf("Getting feature for point (%d, %d)", point.Latitude, point.Longitude)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
//10초동안 조회하는 기능 10초로 설정합니다.
defer cancel()
// context 사용 취소, 자원 해제
feature, err := client.GetFeature(ctx, point)
//GetFeature의 RPC 메서드를 실행
if err != nil {
log.Fatalf("client.GetFeature failed: %v", err)
}
log.Println(feature)
}
4-3 GetFeature (grpc.pb.go) - 단일 RPC (Server)
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 //없다면 좌표를 반환
}
// Looking for a valid feature printFeature(client, &pb.Point{Latitude: 409146138, Longitude: -746188906}
// Feature missing. printFeature(client, &pb.Point{Latitude: 0, Longitude: 0})
좌표가 없을때
4-4 printFeatures() - 서버측 스트리밍 (Client)
// printFeatures lists all the features within the given bounding Rectangle.
func printFeatures(client pb.RouteGuideClient, rect *pb.Rectangle) {
log.Printf("Looking for features within %v", rect)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
//10초
defer cancel()
stream, err := client.ListFeatures(ctx, rect)
if err != nil {
log.Fatalf("client.ListFeatures failed: %v", err)
}
for {
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())
}
}
4-4 ListFeautures(grpc.pb.go) - 서버측 스트리밍 (Server)
// ListFeatures lists all features contained within the given bounding Rectangle.
func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
for _, feature := range s.savedFeatures {
if inRange(feature.Location, rect) { //for문으로 계속 보냄 사각형안에 있는 영역들을
if err := stream.Send(feature); err != nil {
return err
}
}
}
return nil
}
결과 값…
4-5 runRecoreRoute() - 클라이언트 측 스트리밍 RPC (Client)
func runRecordRoute(client pb.RouteGuideClient) {
// 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
//0~99 2를 더해 두 점을 지나도록 설계
var points []*pb.Point
for i := 0; i < pointCount; i++ {
points = append(points, randomPoint(r))//points에 랜덤한 포인터를 더한다
}
log.Printf("Traversing %d points.", len(points)) //길이
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
//10초
defer cancel()
stream, err := client.RecordRoute(ctx)
//스트림 열고
if err != nil {
log.Fatalf("client.RecordRoute failed: %v", err)
}
for _, point := range points { //어디 점을 지나는지
if err := stream.Send(point); err != nil {
log.Fatalf("client.RecordRoute: stream.Send(%v) failed: %v", point, err)
}
}
reply, err := stream.CloseAndRecv() //스트림닫기
if err != nil {
log.Fatalf("client.RecordRoute failed: %v", err)
}
log.Printf("Route summary: %v", reply)
}
4-5 클라이언트 측 스트리밍 RPC (Server)
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++ //message First message.. 등등 존재하는 좌표면 featureCount++함
}
}
if lastPoint != nil {
distance += calcDistance(lastPoint, point) //거리계산을 적용해서 더해준다
}
lastPoint = point //lastPoint를 최신화시켜준다
}
}
결과 값…
4-6 runRouteChat() - 양방향 스트리밍 RPC (Client)
func runRouteChat(client pb.RouteGuideClient) {
notes := []*pb.RouteNote{
{Location: &pb.Point{Latitude: 0, Longitude: 1}, Message: "First message"},
{Location: &pb.Point{Latitude: 0, Longitude: 2}, Message: "Second message"},
{Location: &pb.Point{Latitude: 0, Longitude: 3}, Message: "Third message"},
{Location: &pb.Point{Latitude: 0, Longitude: 1}, Message: "Fourth message"},
{Location: &pb.Point{Latitude: 0, Longitude: 2}, Message: "Fifth message"},
{Location: &pb.Point{Latitude: 0, Longitude: 3}, Message: "Sixth message"},
}
//RouteNote라는 구조체 생성 (0,1) Message: First Message
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
stream, err := client.RouteChat(ctx)
//stream열고
if err != nil {
log.Fatalf("client.RouteChat failed: %v", err)
}
waitc := make(chan struct{})
//채널의 응답을 대기하는데 사용 채널 생성
go func() { //고루틴 생성하기 위한 문법
for {
in, err := stream.Recv() //서버로부터 반복적으로 메시지를 읽는다
if err == io.EOF {
// read done.
close(waitc) //EOF면 닫기
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)
}
}()
for _, note := range notes {
if err := stream.Send(note); err != nil {
log.Fatalf("client.RouteChat: stream.Send(%v) failed: %v", note, err)
}
}
stream.CloseSend()
<-waitc //waitc채널에서 대기하며 응답 완료를 기다립니다.
}
4-6 RouteChat() - 양방향 스트리밍 RPC (Server)
func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
for {
in, err := stream.Recv() //Client로부터 메시지를 읽어옵니다
if err == io.EOF { //다보냈다면
return nil
}
if err != nil { //에러가 있따면
return err
}
key := serialize(in.Location)
s.mu.Lock() //Mutxt 동시성 해결하기 충돌 방지
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])) // rn을만들고
copy(rn, s.routeNotes[key]) //rn에 옮깁니다.
s.mu.Unlock() //풀어서 다른 client의 메시지도 읽어 옵니다.
for _, note := range rn { //client에게 메시지를 보냅니다
if err := stream.Send(note); err != nil {
return err
}
}
}
}
이렇게 함으로써 Client에게 메시지를 보내면서 또한 맵에 메시지를 추가할 수 있습니다.
이제 튜토리얼이 끝났는데.. 이해하기 어려울 것이라고 생각합니다. 하나하나 따라가다보니 시간도 오래 걸리고
그래도 이렇게 하니 라이브러리를 쓸 때 이해가기 조금 편한 감은 있는 것 같습니다.
오타나 잘 못 이해한 부분이 있다면 댓글 부탁드립니다!!
'Go' 카테고리의 다른 글
[Go] 블록체인 공부가 막막하다면 (0) | 2023.09.13 |
---|---|
[Go] Go Clean Architecture란 무엇인가? (0) | 2023.05.01 |
[Go] Go의 웹 프레임워크 Gin이란? (0) | 2023.04.29 |
[Go] Go로 로그 패키지 만들기 (1) (1) | 2023.03.07 |
[Go] golang slice안에 특징 (0) | 2023.02.16 |