diff --git a/docs/GRPC.md b/docs/GRPC.md index ec506dc..0c50950 100644 --- a/docs/GRPC.md +++ b/docs/GRPC.md @@ -344,7 +344,47 @@ var ( storeMu sync.RWMutex ) -// 2. 파일 리스트 조회 Unary RPC +// 2. 파일 업로드 Client Streaming RPC +func (IoTServer) UploadFile(stream protoapi.IoTService_UploadFileServer) error { + var totalBytes int64 + var fileName string + var buffer []byte + + for { + chunk, err := stream.Recv() + if err == io.EOF { + fmt.Printf("File upload completed. Received %d bytes for file '%s'\n", totalBytes, fileName) + + if fileName != "" { + storeMu.Lock() + fileStore[fileName] = &UploadedFile{ + FileName: fileName, + Content: buffer, + UploadedAt: time.Now().Unix(), + } + storeMu.Unlock() + } + + return stream.SendAndClose(&protoapi.UploadStatus{ + Success: true, + Message: fmt.Sprintf("File '%s' uploaded successfully.", fileName), + BytesUploaded: totalBytes, + }) + } + if err != nil { + fmt.Println("File upload error:", err) + return err + } + + if fileName == "" { + fileName = chunk.GetFileName() + } + buffer = append(buffer, chunk.GetContent()...) + totalBytes += int64(len(chunk.GetContent())) + } +} + +// 3. 파일 리스트 조회 Unary RPC func (IoTServer) ListFiles(ctx context.Context, r *protoapi.EmptyRequest) (*protoapi.FileList, error) { storeMu.RLock() defer storeMu.RUnlock() @@ -360,7 +400,7 @@ func (IoTServer) ListFiles(ctx context.Context, r *protoapi.EmptyRequest) (*prot return &protoapi.FileList{Files: files}, nil } -// 3. 파일 다운로드 Server Streaming RPC +// 4. 파일 다운로드 Server Streaming RPC func (IoTServer) DownloadFile(r *protoapi.DownloadRequest, stream protoapi.IoTService_DownloadFileServer) error { storeMu.RLock() f, exists := fileStore[r.GetFileName()] @@ -390,9 +430,11 @@ func (IoTServer) DownloadFile(r *protoapi.DownloadRequest, stream protoapi.IoTSe } ``` * **쉬운 설명**: + * **파일 업로드**: 클라이언트가 쪼개서 던지는 파일 조각 상자(`stream.Recv()`)들을 루프를 돌며 계속 수집하여 하나의 임시 보관 버퍼(`buffer`)에 합칩니다. 마지막 조각 전송 완료(`io.EOF`) 신호가 오면, 모인 바이트들을 파일 이름과 함께 서버의 보관함(`fileStore`)에 안전하게 저장하고 영수증을 클라이언트에게 발행합니다. * **목록 조회**: 서버의 파일 보관함(`fileStore`)을 열고 그 안에 든 모든 파일의 메타데이터(이름, 크기, 업로드 시각)를 리스트로 포장해 한번에 리턴해 줍니다. * **파일 다운로드**: 보관함에서 요청받은 파일을 찾은 뒤, 파일 내용 전체를 1KB 크기의 패킷 조각들로 잘라 통로를 타고 차례대로 연속 전송(`stream.Send()`)해 줍니다. * **상세 설명**: + * **파일 업로드**: 수신 파이프라인 스트림의 `Recv()`를 호출하여 개별 `FileChunk` 객체들을 수령합니다. 수신 스트림이 종료(`io.EOF`)되면 버퍼링된 바이트 데이터와 타임스탬프를 묶어 스레드 동기화 락(`storeMu.Lock()`)을 획득하고 인메모리 맵에 적재한 뒤, `SendAndClose`로 마감 처리합니다. * **목록 조회**: 동시 접근 보호(Race condition 방지)를 위해 읽기 전용 락(`RLock`)을 획득한 후 인메모리 맵을 순회하며 메타데이터 구조체 목록을 집계해 반환합니다. * **파일 다운로드**: 대상 파일 쿼리 실패 시 gRPC 표준 에러(`codes.NotFound`)를 반환합니다. 검증 통과 시 루프 내에서 가상 윈도우 슬라이싱을 집행해 청크 구조체를 구성하고, `stream.Send()`로 직렬화 패킷을 클라이언트 버퍼 큐에 기입합니다. @@ -400,12 +442,40 @@ func (IoTServer) DownloadFile(r *protoapi.DownloadRequest, stream protoapi.IoTSe 클라이언트는 업로드에 성공한 뒤, 서버에 파일 목록 조회를 요구하고, 다운로드 스트림을 개설해 조각 데이터를 재조립하여 무결성을 검사합니다. ```go -// 1. 파일 목록 조회 호출 +// 1. 파일 업로드 송신 (클라이언트 스트리밍) +func AskUploadFile(ctx context.Context, m protoapi.IoTServiceClient, fileName string, dummyData []byte) (*protoapi.UploadStatus, error) { + stream, err := m.UploadFile(ctx) + if err != nil { + return nil, err + } + + chunkSize := 1024 // 1KB 단위 분할 송신 + totalBytes := len(dummyData) + + for i := 0; i < totalBytes; i += chunkSize { + end := i + chunkSize + if end > totalBytes { + end = totalBytes + } + + err := stream.Send(&protoapi.FileChunk{ + FileName: fileName, + Content: dummyData[i:end], + }) + if err != nil { + return nil, err + } + } + + return stream.CloseAndRecv() +} + +// 2. 파일 목록 조회 호출 func AskListFiles(ctx context.Context, m protoapi.IoTServiceClient) (*protoapi.FileList, error) { return m.ListFiles(ctx, &protoapi.EmptyRequest{}) } -// 2. 파일 다운로드 수신 및 조립 +// 3. 파일 다운로드 수신 및 조립 func AskDownloadFile(ctx context.Context, m protoapi.IoTServiceClient, fileName string) ([]byte, error) { stream, err := m.DownloadFile(ctx, &protoapi.DownloadRequest{FileName: fileName}) if err != nil { @@ -427,9 +497,11 @@ func AskDownloadFile(ctx context.Context, m protoapi.IoTServiceClient, fileName } ``` * **쉬운 설명**: + * **파일 업로드**: 보낼 파일을 1KB 조각 크기로 나누어 준비한 뒤, gRPC 전용 파이프 스트림 통로에 차례대로 흘려보냅니다(`stream.Send()`). 모든 조각을 던진 후 채널을 끊고 영수증(`UploadStatus`)을 받습니다. * **목록 조회**: 서버에게 "보관 중인 파일 이름 목록을 달라"고 요구하여 화면에 출력합니다. * **파일 다운로드**: 다운로드 통로를 열어 서버가 던져주는 조각들을 계속 수령(`stream.Recv()`)하여 버퍼에 차곡차곡 합칩니다. 서버가 보내기를 끝마치면(`io.EOF`) 조립을 중단하고 최종 완성된 온전한 바이트 파일을 최종 사용처에 반환합니다. * **상세 설명**: + * **파일 업로드**: `UploadFile` 채널을 기동하여 클라이언트 사이드 스트림 핸들을 획득합니다. 슬라이스 윈도우 방식으로 데이터를 순차 분할 송출하고, `CloseAndRecv` 메서드를 최종 호출해 스트림 종결 프레임을 송신한 뒤 서버의 단발성 최종 회신 상태를 획득합니다. * **목록 조회**: 빈 메시지(`EmptyRequest`)를 동봉해 Unary RPC 채널을 트리거하고 메타데이터 배열 결과를 동기 획득합니다. * **파일 다운로드**: 서버 스트리밍 엔드포인트 기동 후, `stream.Recv()` 블로킹 수신 루프에 진입합니다. 채널 해제 지점(`io.EOF`)에 도달할 때까지 메모리 버퍼 슬라이스에 청크 바이트 배열을 병합 누적하여 재조립(Reassembly)을 마친 후 반환합니다.