feat: implement client-side streaming file upload example and document it in GRPC.md
This commit is contained in:
+87
-2
@@ -281,13 +281,98 @@ protoc --go_out=. --go-grpc_out=. protoapi.proto
|
||||
|
||||
---
|
||||
|
||||
## 7. 한 걸음 더 나아가기 (다음 단계)
|
||||
## 7. 대용량 데이터 전송을 위한 스트리밍(Streaming) 구현
|
||||
|
||||
일반적인 단발성 요청/응답(Unary) 통신은 전송할 전체 데이터를 단일 메모리에 전부 올려 적재한 상태에서 동작하므로, 펌웨어나 대형 이미지 같은 대용량 데이터를 다룰 때 메모리 고갈(OOM)이나 네트워크 대역폭 병목을 초래하기 쉽습니다. gRPC는 HTTP/2 프로토콜의 스트림(Stream) 채널을 기본 가용하므로, 데이터를 일정 크기(Chunk) 단위로 쪼개 연속적으로 전송할 수 있는 강력한 **스트리밍(Streaming)** 기법을 지원합니다. 본 예제에서는 클라이언트가 가상의 10KB 파일을 조각내어 연속 송출하는 **클라이언트 스트리밍(Client-side Streaming)** 기법을 구현했습니다.
|
||||
|
||||
### 7.1 스키마 설계 (`protoapi.proto`)
|
||||
데이터를 전송할 때 파일 식별 메타와 조각난 데이터 조각(`bytes`)을 순차적으로 실어 보낼 수 있도록 인터페이스 명세를 아래와 같이 선언해 둡니다:
|
||||
```proto
|
||||
message FileChunk {
|
||||
string FileName = 1;
|
||||
bytes Content = 2; // 쪼개진 실제 데이터 알맹이
|
||||
}
|
||||
|
||||
message UploadStatus {
|
||||
bool Success = 1;
|
||||
string Message = 2;
|
||||
int64 BytesUploaded = 3; // 서버가 조립 완료한 누적 수신량
|
||||
}
|
||||
```
|
||||
* **설계 포인트**: 원격 호출 함수의 입력 매개변수 앞에 `stream` 키워드를 정의함으로써 단발성이 아닌 스트림 채널을 활성화하도록 컴파일러에 제약합니다.
|
||||
|
||||
### 7.2 서버 수신 핸들러 구현 ([server.go](../examples/grpcentity/server.go))
|
||||
```go
|
||||
func (IoTServer) UploadFile(stream protoapi.IoTService_UploadFileServer) error {
|
||||
var totalBytes int64
|
||||
var fileName string
|
||||
|
||||
for {
|
||||
chunk, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
// io.EOF는 클라이언트가 데이터 전송 완료 후 채널을 닫았음을 의미함
|
||||
fmt.Printf("File upload completed. Received %d bytes for file '%s'\n", totalBytes, fileName)
|
||||
return stream.SendAndClose(&protoapi.UploadStatus{
|
||||
Success: true,
|
||||
Message: fmt.Sprintf("File '%s' uploaded successfully.", fileName),
|
||||
BytesUploaded: totalBytes,
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if fileName == "" {
|
||||
fileName = chunk.GetFileName()
|
||||
}
|
||||
totalBytes += int64(len(chunk.GetContent()))
|
||||
}
|
||||
}
|
||||
```
|
||||
* **쉬운 설명**: 서버는 통신 채널에 조각 상자가 도달할 때까지 루프를 통해 대기(`stream.Recv()`)합니다. 더 들어올 데이터가 없어 마감 표시(`io.EOF`)가 수신되면 루프를 빠져나와 총 수령 바이트 크기와 정합성 지표를 적재한 성공 영수증을 클라이언트에게 돌려주며 최종 처리 채널을 마감합니다.
|
||||
* **상세 설명**: `stream.Recv()`는 수신 파이프라인 버퍼에서 이벤트를 논블로킹 대기(Blocking Wait) 형태로 반환받는 동작 방식입니다. 클라이언트가 송출 완료 신호(Half-close)를 보내면 `io.EOF` 로 캡처되며, 이후 수집된 누적 메타 정보를 바탕으로 `SendAndClose()`를 기동해 단일 응답 객체를 전송하고 통신 리소스를 해제합니다.
|
||||
|
||||
### 7.3 클라이언트 송신 기동 ([client.go](../examples/grpcentity/client.go))
|
||||
```go
|
||||
func AskUploadFile(ctx context.Context, m protoapi.IoTServiceClient, fileName string, fileData []byte) (*protoapi.UploadStatus, error) {
|
||||
stream, err := m.UploadFile(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chunkSize := 1024 // 1KB 크기 단위로 조각 설정
|
||||
totalBytes := len(fileData)
|
||||
|
||||
for i := 0; i < totalBytes; i += chunkSize {
|
||||
end := i + chunkSize
|
||||
if end > totalBytes {
|
||||
end = totalBytes
|
||||
}
|
||||
|
||||
err := stream.Send(&protoapi.FileChunk{
|
||||
FileName: fileName,
|
||||
Content: fileData[i:end],
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return stream.CloseAndRecv()
|
||||
}
|
||||
```
|
||||
* **쉬운 설명**: 클라이언트는 전송할 데이터를 준비한 뒤 이를 1KB 단위의 작은 상자들로 조각조각 잘라냅니다. 그 후 연속적인 루프를 돌면서 채널을 타고 상자들을 차례대로 전송(`stream.Send`)합니다. 전송이 모두 끝나면 통신선을 닫고 서버가 수집 집계를 마치고 영수증을 돌려줄 때까지 대기(`CloseAndRecv`)합니다.
|
||||
* **상세 설명**: gRPC 컴파일러가 도출한 스트림 인스턴스의 `Send()` 메서드를 사용하여 순차적인 패킷 세그먼트를 윈도우 슬라이싱 크기만큼 나누어 송신합니다. 데이터 분할 루프가 마감되면 `CloseAndRecv()`를 트리거하여 스트림의 송신 플래그를 차단(Half-close)하고 서버로부터 결과 영수증이 도달할 때까지 동기식으로 블로킹 대기합니다.
|
||||
|
||||
---
|
||||
|
||||
## 8. 한 걸음 더 나아가기 (다음 단계)
|
||||
|
||||
본 기초 실습을 끝마치셨다면, 아래 과제를 해결해보세요:
|
||||
**약속 스펙 확장해 보기**: [protoapi.proto](../examples/grpcentity/protoapi.proto) 파일에 새로운 환경 데이터(예: 미세먼지 수치 `double Dust = 4;`)를 슬쩍 얹어본 뒤, 직접 번역기를 새로 돌리고 Go 소스코드를 고치며 확장해 봅니다.
|
||||
|
||||
---
|
||||
|
||||
## 8. 참고 자료
|
||||
## 9. 참고 자료
|
||||
|
||||
* [gRPC와 REST의 차이점 (AWS)](https://aws.amazon.com/ko/compare/the-difference-between-grpc-and-rest/): 두 방식의 특징과 언제 어떤 기술을 선택해야 하는지 친절하게 정리된 공식 블로그 자료입니다.
|
||||
|
||||
Reference in New Issue
Block a user