feat: implement ListFiles and DownloadFile RPCs with documentation updates in GRPC.md
This commit is contained in:
+113
-48
@@ -283,86 +283,151 @@ protoc --go_out=. --go-grpc_out=. protoapi.proto
|
||||
|
||||
## 7. 대용량 데이터 전송을 위한 스트리밍(Streaming) 구현
|
||||
|
||||
일반적인 단발성 요청/응답(Unary) 통신은 전송할 전체 데이터를 단일 메모리에 전부 올려 적재한 상태에서 동작하므로, 펌웨어나 대형 이미지 같은 대용량 데이터를 다룰 때 메모리 고갈(OOM)이나 네트워크 대역폭 병목을 초래하기 쉽습니다. gRPC는 HTTP/2 프로토콜의 스트림(Stream) 채널을 기본 가용하므로, 데이터를 일정 크기(Chunk) 단위로 쪼개 연속적으로 전송할 수 있는 강력한 **스트리밍(Streaming)** 기법을 지원합니다. 본 예제에서는 클라이언트가 가상의 10KB 파일을 조각내어 연속 송출하는 **클라이언트 스트리밍(Client-side Streaming)** 기법을 구현했습니다.
|
||||
일반적인 단발성 요청/응답(Unary) 통신은 전송할 전체 데이터를 단일 메모리에 전부 올려 적재한 상태에서 동작하므로, 펌웨어나 대형 이미지 같은 대용량 데이터를 다룰 때 메모리 고갈(OOM)이나 네트워크 대역폭 병목을 초래하기 쉽습니다. gRPC는 HTTP/2 프로토콜의 스트림(Stream) 채널을 기본 가용하므로, 데이터를 일정 크기(Chunk) 단위로 쪼개 연속적으로 전송할 수 있는 강력한 **스트리밍(Streaming)** 기법을 지원합니다. 본 예제에서는 클라이언트가 파일을 조각내어 보내는 **클라이언트 스트리밍(Client Streaming)**, 업로드된 파일 메타 정보를 모아 한 번에 내려주는 **단일 조회(Unary RPC)**, 그리고 서버가 데이터를 쪼개어 클라이언트에게 보내는 **서버 스트리밍(Server Streaming)**까지 모두 종합 설계하여 탑재했습니다.
|
||||
|
||||
### 7.1 스키마 설계 (`protoapi.proto`)
|
||||
데이터를 전송할 때 파일 식별 메타와 조각난 데이터 조각(`bytes`)을 순차적으로 실어 보낼 수 있도록 인터페이스 명세를 아래와 같이 선언해 둡니다:
|
||||
업로드와 리스트 조회, 그리고 다운로드를 위한 gRPC 메시지 규격을 명세합니다:
|
||||
```proto
|
||||
service IoTService {
|
||||
// ... 기존 RPC ...
|
||||
rpc UploadFile (stream FileChunk) returns (UploadStatus);
|
||||
rpc ListFiles (EmptyRequest) returns (FileList);
|
||||
rpc DownloadFile (DownloadRequest) returns (stream FileChunk);
|
||||
}
|
||||
|
||||
message FileChunk {
|
||||
string FileName = 1;
|
||||
bytes Content = 2; // 쪼개진 실제 데이터 알맹이
|
||||
bytes Content = 2; // 쪼개진 바이너리 데이터 조각
|
||||
}
|
||||
|
||||
message UploadStatus {
|
||||
bool Success = 1;
|
||||
string Message = 2;
|
||||
int64 BytesUploaded = 3; // 서버가 조립 완료한 누적 수신량
|
||||
int64 BytesUploaded = 3;
|
||||
}
|
||||
|
||||
message EmptyRequest {}
|
||||
|
||||
message FileMetadata {
|
||||
string FileName = 1;
|
||||
int64 FileSize = 2;
|
||||
int64 UploadedAt = 3;
|
||||
}
|
||||
|
||||
message FileList {
|
||||
repeated FileMetadata Files = 1;
|
||||
}
|
||||
|
||||
message DownloadRequest {
|
||||
string FileName = 1;
|
||||
}
|
||||
```
|
||||
* **설계 포인트**: 원격 호출 함수의 입력 매개변수 앞에 `stream` 키워드를 정의함으로써 단발성이 아닌 스트림 채널을 활성화하도록 컴파일러에 제약합니다.
|
||||
* **설계 포인트**: `stream` 키워드가 들어간 위치에 주목합니다. `UploadFile`은 입력에 `stream`이 붙어 클라이언트 스트리밍을, `DownloadFile`은 반환(returns)에 `stream`이 붙어 서버 스트리밍 채널을 개설합니다.
|
||||
|
||||
### 7.2 서버 저장 및 송수신 구현 ([server.go](../examples/grpcentity/server.go))
|
||||
인메모리 파일 저장소(`fileStore`)를 구현하고 목록 조회(`ListFiles`) 및 서버 다운로드 스트리밍(`DownloadFile`) 핸들러를 정의합니다:
|
||||
|
||||
### 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()))
|
||||
}
|
||||
// 1. 인메모리 파일 보관소
|
||||
type UploadedFile struct {
|
||||
FileName string
|
||||
Content []byte
|
||||
UploadedAt int64
|
||||
}
|
||||
```
|
||||
* **쉬운 설명**: 서버는 통신 채널에 조각 상자가 도달할 때까지 루프를 통해 대기(`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
|
||||
var (
|
||||
fileStore = make(map[string]*UploadedFile)
|
||||
storeMu sync.RWMutex
|
||||
)
|
||||
|
||||
// 2. 파일 리스트 조회 Unary RPC
|
||||
func (IoTServer) ListFiles(ctx context.Context, r *protoapi.EmptyRequest) (*protoapi.FileList, error) {
|
||||
storeMu.RLock()
|
||||
defer storeMu.RUnlock()
|
||||
|
||||
var files []*protoapi.FileMetadata
|
||||
for _, f := range fileStore {
|
||||
files = append(files, &protoapi.FileMetadata{
|
||||
FileName: f.FileName,
|
||||
FileSize: int64(len(f.Content)),
|
||||
UploadedAt: f.UploadedAt,
|
||||
})
|
||||
}
|
||||
return &protoapi.FileList{Files: files}, nil
|
||||
}
|
||||
|
||||
// 3. 파일 다운로드 Server Streaming RPC
|
||||
func (IoTServer) DownloadFile(r *protoapi.DownloadRequest, stream protoapi.IoTService_DownloadFileServer) error {
|
||||
storeMu.RLock()
|
||||
f, exists := fileStore[r.GetFileName()]
|
||||
storeMu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return status.Errorf(codes.NotFound, "file %s not found", r.GetFileName())
|
||||
}
|
||||
|
||||
chunkSize := 1024 // 1KB 크기 단위로 조각 설정
|
||||
totalBytes := len(fileData)
|
||||
chunkSize := 1024 // 1KB 단위 분할 송출
|
||||
totalBytes := len(f.Content)
|
||||
|
||||
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],
|
||||
FileName: f.FileName,
|
||||
Content: f.Content[i:end],
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
* **쉬운 설명**:
|
||||
* **목록 조회**: 서버의 파일 보관함(`fileStore`)을 열고 그 안에 든 모든 파일의 메타데이터(이름, 크기, 업로드 시각)를 리스트로 포장해 한번에 리턴해 줍니다.
|
||||
* **파일 다운로드**: 보관함에서 요청받은 파일을 찾은 뒤, 파일 내용 전체를 1KB 크기의 패킷 조각들로 잘라 통로를 타고 차례대로 연속 전송(`stream.Send()`)해 줍니다.
|
||||
* **상세 설명**:
|
||||
* **목록 조회**: 동시 접근 보호(Race condition 방지)를 위해 읽기 전용 락(`RLock`)을 획득한 후 인메모리 맵을 순회하며 메타데이터 구조체 목록을 집계해 반환합니다.
|
||||
* **파일 다운로드**: 대상 파일 쿼리 실패 시 gRPC 표준 에러(`codes.NotFound`)를 반환합니다. 검증 통과 시 루프 내에서 가상 윈도우 슬라이싱을 집행해 청크 구조체를 구성하고, `stream.Send()`로 직렬화 패킷을 클라이언트 버퍼 큐에 기입합니다.
|
||||
|
||||
### 7.3 클라이언트 송수신 기동 ([client.go](../examples/grpcentity/client.go))
|
||||
클라이언트는 업로드에 성공한 뒤, 서버에 파일 목록 조회를 요구하고, 다운로드 스트림을 개설해 조각 데이터를 재조립하여 무결성을 검사합니다.
|
||||
|
||||
```go
|
||||
// 1. 파일 목록 조회 호출
|
||||
func AskListFiles(ctx context.Context, m protoapi.IoTServiceClient) (*protoapi.FileList, error) {
|
||||
return m.ListFiles(ctx, &protoapi.EmptyRequest{})
|
||||
}
|
||||
|
||||
// 2. 파일 다운로드 수신 및 조립
|
||||
func AskDownloadFile(ctx context.Context, m protoapi.IoTServiceClient, fileName string) ([]byte, error) {
|
||||
stream, err := m.DownloadFile(ctx, &protoapi.DownloadRequest{FileName: fileName})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var buffer []byte
|
||||
for {
|
||||
chunk, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
break // 서버가 송신 완료하고 채널을 닫음
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buffer = append(buffer, chunk.GetContent()...)
|
||||
}
|
||||
|
||||
return stream.CloseAndRecv()
|
||||
return buffer, nil
|
||||
}
|
||||
```
|
||||
* **쉬운 설명**: 클라이언트는 전송할 데이터를 준비한 뒤 이를 1KB 단위의 작은 상자들로 조각조각 잘라냅니다. 그 후 연속적인 루프를 돌면서 채널을 타고 상자들을 차례대로 전송(`stream.Send`)합니다. 전송이 모두 끝나면 통신선을 닫고 서버가 수집 집계를 마치고 영수증을 돌려줄 때까지 대기(`CloseAndRecv`)합니다.
|
||||
* **상세 설명**: gRPC 컴파일러가 도출한 스트림 인스턴스의 `Send()` 메서드를 사용하여 순차적인 패킷 세그먼트를 윈도우 슬라이싱 크기만큼 나누어 송신합니다. 데이터 분할 루프가 마감되면 `CloseAndRecv()`를 트리거하여 스트림의 송신 플래그를 차단(Half-close)하고 서버로부터 결과 영수증이 도달할 때까지 동기식으로 블로킹 대기합니다.
|
||||
* **쉬운 설명**:
|
||||
* **목록 조회**: 서버에게 "보관 중인 파일 이름 목록을 달라"고 요구하여 화면에 출력합니다.
|
||||
* **파일 다운로드**: 다운로드 통로를 열어 서버가 던져주는 조각들을 계속 수령(`stream.Recv()`)하여 버퍼에 차곡차곡 합칩니다. 서버가 보내기를 끝마치면(`io.EOF`) 조립을 중단하고 최종 완성된 온전한 바이트 파일을 최종 사용처에 반환합니다.
|
||||
* **상세 설명**:
|
||||
* **목록 조회**: 빈 메시지(`EmptyRequest`)를 동봉해 Unary RPC 채널을 트리거하고 메타데이터 배열 결과를 동기 획득합니다.
|
||||
* **파일 다운로드**: 서버 스트리밍 엔드포인트 기동 후, `stream.Recv()` 블로킹 수신 루프에 진입합니다. 채널 해제 지점(`io.EOF`)에 도달할 때까지 메모리 버퍼 슬라이스에 청크 바이트 배열을 병합 누적하여 재조립(Reassembly)을 마친 후 반환합니다.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"grpccanary/examples/grpcentity/protoapi"
|
||||
"io"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
@@ -64,6 +66,31 @@ func AskUploadFile(ctx context.Context, m protoapi.IoTServiceClient, fileName st
|
||||
return stream.CloseAndRecv()
|
||||
}
|
||||
|
||||
func AskListFiles(ctx context.Context, m protoapi.IoTServiceClient) (*protoapi.FileList, error) {
|
||||
return m.ListFiles(ctx, &protoapi.EmptyRequest{})
|
||||
}
|
||||
|
||||
func AskDownloadFile(ctx context.Context, m protoapi.IoTServiceClient, fileName string) ([]byte, error) {
|
||||
stream, err := m.DownloadFile(ctx, &protoapi.DownloadRequest{FileName: fileName})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var buffer []byte
|
||||
for {
|
||||
chunk, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buffer = append(buffer, chunk.GetContent()...)
|
||||
}
|
||||
|
||||
return buffer, nil
|
||||
}
|
||||
|
||||
func ClientRun(addr string) {
|
||||
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
@@ -109,4 +136,39 @@ func ClientRun(addr string) {
|
||||
fmt.Println("Upload Success:", status.Success)
|
||||
fmt.Println("Upload Message:", status.Message)
|
||||
fmt.Printf("Uploaded Bytes: %d bytes\n", status.BytesUploaded)
|
||||
|
||||
// 5단계: 파일 리스트 조회 실행 예제
|
||||
fmt.Println("Querying uploaded files metadata from Server...")
|
||||
list, err := AskListFiles(context.Background(), client)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to list files:", err)
|
||||
return
|
||||
}
|
||||
for i, f := range list.GetFiles() {
|
||||
fmt.Printf("[%d] Name: %s, Size: %d bytes, UploadedAt: %s\n",
|
||||
i+1, f.GetFileName(), f.GetFileSize(), time.Unix(f.GetUploadedAt(), 0).Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
|
||||
// 6단계: 파일 다운로드 스트리밍 실행 예제
|
||||
fmt.Println("Downloading file 'firmware.bin' via Server Streaming...")
|
||||
downloadedData, err := AskDownloadFile(context.Background(), client, "firmware.bin")
|
||||
if err != nil {
|
||||
fmt.Println("Failed to download file:", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Download completed. Received %d bytes.\n", len(downloadedData))
|
||||
|
||||
// 데이터 정합성(Integrity) 검증
|
||||
isMatch := true
|
||||
if len(dummyData) != len(downloadedData) {
|
||||
isMatch = false
|
||||
} else {
|
||||
for i := range dummyData {
|
||||
if dummyData[i] != downloadedData[i] {
|
||||
isMatch = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("Data Integrity Checked (Upload vs Download matches?): %t\n", isMatch)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ service IoTService {
|
||||
rpc UpdateSensingData (SensingData) returns (SensingResponse);
|
||||
rpc GetRandomPass (RequestPass) returns (RandomPass);
|
||||
rpc UploadFile (stream FileChunk) returns (UploadStatus);
|
||||
rpc ListFiles (EmptyRequest) returns (FileList);
|
||||
rpc DownloadFile (DownloadRequest) returns (stream FileChunk);
|
||||
}
|
||||
|
||||
message FileChunk {
|
||||
@@ -46,4 +48,20 @@ message RequestPass {
|
||||
|
||||
message RandomPass {
|
||||
string Password = 1;
|
||||
}
|
||||
|
||||
message EmptyRequest {}
|
||||
|
||||
message FileMetadata {
|
||||
string FileName = 1;
|
||||
int64 FileSize = 2;
|
||||
int64 UploadedAt = 3;
|
||||
}
|
||||
|
||||
message FileList {
|
||||
repeated FileMetadata Files = 1;
|
||||
}
|
||||
|
||||
message DownloadRequest {
|
||||
string FileName = 1;
|
||||
}
|
||||
@@ -429,6 +429,190 @@ func (x *RandomPass) GetPassword() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
type EmptyRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *EmptyRequest) Reset() {
|
||||
*x = EmptyRequest{}
|
||||
mi := &file_protoapi_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *EmptyRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*EmptyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *EmptyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protoapi_proto_msgTypes[8]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use EmptyRequest.ProtoReflect.Descriptor instead.
|
||||
func (*EmptyRequest) Descriptor() ([]byte, []int) {
|
||||
return file_protoapi_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
type FileMetadata struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
FileName string `protobuf:"bytes,1,opt,name=FileName,proto3" json:"FileName,omitempty"`
|
||||
FileSize int64 `protobuf:"varint,2,opt,name=FileSize,proto3" json:"FileSize,omitempty"`
|
||||
UploadedAt int64 `protobuf:"varint,3,opt,name=UploadedAt,proto3" json:"UploadedAt,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *FileMetadata) Reset() {
|
||||
*x = FileMetadata{}
|
||||
mi := &file_protoapi_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *FileMetadata) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*FileMetadata) ProtoMessage() {}
|
||||
|
||||
func (x *FileMetadata) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protoapi_proto_msgTypes[9]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use FileMetadata.ProtoReflect.Descriptor instead.
|
||||
func (*FileMetadata) Descriptor() ([]byte, []int) {
|
||||
return file_protoapi_proto_rawDescGZIP(), []int{9}
|
||||
}
|
||||
|
||||
func (x *FileMetadata) GetFileName() string {
|
||||
if x != nil {
|
||||
return x.FileName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *FileMetadata) GetFileSize() int64 {
|
||||
if x != nil {
|
||||
return x.FileSize
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *FileMetadata) GetUploadedAt() int64 {
|
||||
if x != nil {
|
||||
return x.UploadedAt
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type FileList struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Files []*FileMetadata `protobuf:"bytes,1,rep,name=Files,proto3" json:"Files,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *FileList) Reset() {
|
||||
*x = FileList{}
|
||||
mi := &file_protoapi_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *FileList) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*FileList) ProtoMessage() {}
|
||||
|
||||
func (x *FileList) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protoapi_proto_msgTypes[10]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use FileList.ProtoReflect.Descriptor instead.
|
||||
func (*FileList) Descriptor() ([]byte, []int) {
|
||||
return file_protoapi_proto_rawDescGZIP(), []int{10}
|
||||
}
|
||||
|
||||
func (x *FileList) GetFiles() []*FileMetadata {
|
||||
if x != nil {
|
||||
return x.Files
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DownloadRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
FileName string `protobuf:"bytes,1,opt,name=FileName,proto3" json:"FileName,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DownloadRequest) Reset() {
|
||||
*x = DownloadRequest{}
|
||||
mi := &file_protoapi_proto_msgTypes[11]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DownloadRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DownloadRequest) ProtoMessage() {}
|
||||
|
||||
func (x *DownloadRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_protoapi_proto_msgTypes[11]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DownloadRequest.ProtoReflect.Descriptor instead.
|
||||
func (*DownloadRequest) Descriptor() ([]byte, []int) {
|
||||
return file_protoapi_proto_rawDescGZIP(), []int{11}
|
||||
}
|
||||
|
||||
func (x *DownloadRequest) GetFileName() string {
|
||||
if x != nil {
|
||||
return x.FileName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_protoapi_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_protoapi_proto_rawDesc = "" +
|
||||
@@ -457,7 +641,18 @@ const file_protoapi_proto_rawDesc = "" +
|
||||
"\x06Length\x18\b \x01(\x03R\x06Length\"(\n" +
|
||||
"\n" +
|
||||
"RandomPass\x12\x1a\n" +
|
||||
"\bPassword\x18\x01 \x01(\tR\bPassword2\xc0\x01\n" +
|
||||
"\bPassword\x18\x01 \x01(\tR\bPassword\"\x0e\n" +
|
||||
"\fEmptyRequest\"f\n" +
|
||||
"\fFileMetadata\x12\x1a\n" +
|
||||
"\bFileName\x18\x01 \x01(\tR\bFileName\x12\x1a\n" +
|
||||
"\bFileSize\x18\x02 \x01(\x03R\bFileSize\x12\x1e\n" +
|
||||
"\n" +
|
||||
"UploadedAt\x18\x03 \x01(\x03R\n" +
|
||||
"UploadedAt\"/\n" +
|
||||
"\bFileList\x12#\n" +
|
||||
"\x05Files\x18\x01 \x03(\v2\r.FileMetadataR\x05Files\"-\n" +
|
||||
"\x0fDownloadRequest\x12\x1a\n" +
|
||||
"\bFileName\x18\x01 \x01(\tR\bFileName2\x97\x02\n" +
|
||||
"\n" +
|
||||
"IoTService\x12&\n" +
|
||||
"\aGetDate\x12\x10.RequestDateTime\x1a\t.DateTime\x123\n" +
|
||||
@@ -465,7 +660,10 @@ const file_protoapi_proto_rawDesc = "" +
|
||||
"\rGetRandomPass\x12\f.RequestPass\x1a\v.RandomPass\x12)\n" +
|
||||
"\n" +
|
||||
"UploadFile\x12\n" +
|
||||
".FileChunk\x1a\r.UploadStatus(\x01B\x16Z\x14./protoapi/;protoapib\x06proto3"
|
||||
".FileChunk\x1a\r.UploadStatus(\x01\x12%\n" +
|
||||
"\tListFiles\x12\r.EmptyRequest\x1a\t.FileList\x12.\n" +
|
||||
"\fDownloadFile\x12\x10.DownloadRequest\x1a\n" +
|
||||
".FileChunk0\x01B\x16Z\x14./protoapi/;protoapib\x06proto3"
|
||||
|
||||
var (
|
||||
file_protoapi_proto_rawDescOnce sync.Once
|
||||
@@ -479,7 +677,7 @@ func file_protoapi_proto_rawDescGZIP() []byte {
|
||||
return file_protoapi_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_protoapi_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
|
||||
var file_protoapi_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
|
||||
var file_protoapi_proto_goTypes = []any{
|
||||
(*FileChunk)(nil), // 0: FileChunk
|
||||
(*UploadStatus)(nil), // 1: UploadStatus
|
||||
@@ -489,21 +687,30 @@ var file_protoapi_proto_goTypes = []any{
|
||||
(*RequestDateTime)(nil), // 5: RequestDateTime
|
||||
(*RequestPass)(nil), // 6: RequestPass
|
||||
(*RandomPass)(nil), // 7: RandomPass
|
||||
(*EmptyRequest)(nil), // 8: EmptyRequest
|
||||
(*FileMetadata)(nil), // 9: FileMetadata
|
||||
(*FileList)(nil), // 10: FileList
|
||||
(*DownloadRequest)(nil), // 11: DownloadRequest
|
||||
}
|
||||
var file_protoapi_proto_depIdxs = []int32{
|
||||
5, // 0: IoTService.GetDate:input_type -> RequestDateTime
|
||||
2, // 1: IoTService.UpdateSensingData:input_type -> SensingData
|
||||
6, // 2: IoTService.GetRandomPass:input_type -> RequestPass
|
||||
0, // 3: IoTService.UploadFile:input_type -> FileChunk
|
||||
4, // 4: IoTService.GetDate:output_type -> DateTime
|
||||
3, // 5: IoTService.UpdateSensingData:output_type -> SensingResponse
|
||||
7, // 6: IoTService.GetRandomPass:output_type -> RandomPass
|
||||
1, // 7: IoTService.UploadFile:output_type -> UploadStatus
|
||||
4, // [4:8] is the sub-list for method output_type
|
||||
0, // [0:4] is the sub-list for method input_type
|
||||
0, // [0:0] is the sub-list for extension type_name
|
||||
0, // [0:0] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
9, // 0: FileList.Files:type_name -> FileMetadata
|
||||
5, // 1: IoTService.GetDate:input_type -> RequestDateTime
|
||||
2, // 2: IoTService.UpdateSensingData:input_type -> SensingData
|
||||
6, // 3: IoTService.GetRandomPass:input_type -> RequestPass
|
||||
0, // 4: IoTService.UploadFile:input_type -> FileChunk
|
||||
8, // 5: IoTService.ListFiles:input_type -> EmptyRequest
|
||||
11, // 6: IoTService.DownloadFile:input_type -> DownloadRequest
|
||||
4, // 7: IoTService.GetDate:output_type -> DateTime
|
||||
3, // 8: IoTService.UpdateSensingData:output_type -> SensingResponse
|
||||
7, // 9: IoTService.GetRandomPass:output_type -> RandomPass
|
||||
1, // 10: IoTService.UploadFile:output_type -> UploadStatus
|
||||
10, // 11: IoTService.ListFiles:output_type -> FileList
|
||||
0, // 12: IoTService.DownloadFile:output_type -> FileChunk
|
||||
7, // [7:13] is the sub-list for method output_type
|
||||
1, // [1:7] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_protoapi_proto_init() }
|
||||
@@ -517,7 +724,7 @@ func file_protoapi_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_protoapi_proto_rawDesc), len(file_protoapi_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 8,
|
||||
NumMessages: 12,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
||||
@@ -23,6 +23,8 @@ const (
|
||||
IoTService_UpdateSensingData_FullMethodName = "/IoTService/UpdateSensingData"
|
||||
IoTService_GetRandomPass_FullMethodName = "/IoTService/GetRandomPass"
|
||||
IoTService_UploadFile_FullMethodName = "/IoTService/UploadFile"
|
||||
IoTService_ListFiles_FullMethodName = "/IoTService/ListFiles"
|
||||
IoTService_DownloadFile_FullMethodName = "/IoTService/DownloadFile"
|
||||
)
|
||||
|
||||
// IoTServiceClient is the client API for IoTService service.
|
||||
@@ -33,6 +35,8 @@ type IoTServiceClient interface {
|
||||
UpdateSensingData(ctx context.Context, in *SensingData, opts ...grpc.CallOption) (*SensingResponse, error)
|
||||
GetRandomPass(ctx context.Context, in *RequestPass, opts ...grpc.CallOption) (*RandomPass, error)
|
||||
UploadFile(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[FileChunk, UploadStatus], error)
|
||||
ListFiles(ctx context.Context, in *EmptyRequest, opts ...grpc.CallOption) (*FileList, error)
|
||||
DownloadFile(ctx context.Context, in *DownloadRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[FileChunk], error)
|
||||
}
|
||||
|
||||
type ioTServiceClient struct {
|
||||
@@ -86,6 +90,35 @@ func (c *ioTServiceClient) UploadFile(ctx context.Context, opts ...grpc.CallOpti
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type IoTService_UploadFileClient = grpc.ClientStreamingClient[FileChunk, UploadStatus]
|
||||
|
||||
func (c *ioTServiceClient) ListFiles(ctx context.Context, in *EmptyRequest, opts ...grpc.CallOption) (*FileList, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(FileList)
|
||||
err := c.cc.Invoke(ctx, IoTService_ListFiles_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *ioTServiceClient) DownloadFile(ctx context.Context, in *DownloadRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[FileChunk], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &IoTService_ServiceDesc.Streams[1], IoTService_DownloadFile_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[DownloadRequest, FileChunk]{ClientStream: 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
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type IoTService_DownloadFileClient = grpc.ServerStreamingClient[FileChunk]
|
||||
|
||||
// IoTServiceServer is the server API for IoTService service.
|
||||
// All implementations must embed UnimplementedIoTServiceServer
|
||||
// for forward compatibility.
|
||||
@@ -94,6 +127,8 @@ type IoTServiceServer interface {
|
||||
UpdateSensingData(context.Context, *SensingData) (*SensingResponse, error)
|
||||
GetRandomPass(context.Context, *RequestPass) (*RandomPass, error)
|
||||
UploadFile(grpc.ClientStreamingServer[FileChunk, UploadStatus]) error
|
||||
ListFiles(context.Context, *EmptyRequest) (*FileList, error)
|
||||
DownloadFile(*DownloadRequest, grpc.ServerStreamingServer[FileChunk]) error
|
||||
mustEmbedUnimplementedIoTServiceServer()
|
||||
}
|
||||
|
||||
@@ -116,6 +151,12 @@ func (UnimplementedIoTServiceServer) GetRandomPass(context.Context, *RequestPass
|
||||
func (UnimplementedIoTServiceServer) UploadFile(grpc.ClientStreamingServer[FileChunk, UploadStatus]) error {
|
||||
return status.Error(codes.Unimplemented, "method UploadFile not implemented")
|
||||
}
|
||||
func (UnimplementedIoTServiceServer) ListFiles(context.Context, *EmptyRequest) (*FileList, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListFiles not implemented")
|
||||
}
|
||||
func (UnimplementedIoTServiceServer) DownloadFile(*DownloadRequest, grpc.ServerStreamingServer[FileChunk]) error {
|
||||
return status.Error(codes.Unimplemented, "method DownloadFile not implemented")
|
||||
}
|
||||
func (UnimplementedIoTServiceServer) mustEmbedUnimplementedIoTServiceServer() {}
|
||||
func (UnimplementedIoTServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
@@ -198,6 +239,35 @@ func _IoTService_UploadFile_Handler(srv interface{}, stream grpc.ServerStream) e
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type IoTService_UploadFileServer = grpc.ClientStreamingServer[FileChunk, UploadStatus]
|
||||
|
||||
func _IoTService_ListFiles_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(EmptyRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(IoTServiceServer).ListFiles(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: IoTService_ListFiles_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(IoTServiceServer).ListFiles(ctx, req.(*EmptyRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _IoTService_DownloadFile_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
m := new(DownloadRequest)
|
||||
if err := stream.RecvMsg(m); err != nil {
|
||||
return err
|
||||
}
|
||||
return srv.(IoTServiceServer).DownloadFile(m, &grpc.GenericServerStream[DownloadRequest, FileChunk]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type IoTService_DownloadFileServer = grpc.ServerStreamingServer[FileChunk]
|
||||
|
||||
// IoTService_ServiceDesc is the grpc.ServiceDesc for IoTService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@@ -217,6 +287,10 @@ var IoTService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "GetRandomPass",
|
||||
Handler: _IoTService_GetRandomPass_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListFiles",
|
||||
Handler: _IoTService_ListFiles_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
@@ -224,6 +298,11 @@ var IoTService_ServiceDesc = grpc.ServiceDesc{
|
||||
Handler: _IoTService_UploadFile_Handler,
|
||||
ClientStreams: true,
|
||||
},
|
||||
{
|
||||
StreamName: "DownloadFile",
|
||||
Handler: _IoTService_DownloadFile_Handler,
|
||||
ServerStreams: true,
|
||||
},
|
||||
},
|
||||
Metadata: "protoapi.proto",
|
||||
}
|
||||
|
||||
@@ -7,16 +7,30 @@ import (
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/reflection"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var min = 0
|
||||
var max = 100
|
||||
var port = ":8080"
|
||||
|
||||
type UploadedFile struct {
|
||||
FileName string
|
||||
Content []byte
|
||||
UploadedAt int64
|
||||
}
|
||||
|
||||
var (
|
||||
fileStore = make(map[string]*UploadedFile)
|
||||
storeMu sync.RWMutex
|
||||
)
|
||||
|
||||
func random(min, max int, src rand.Source) int {
|
||||
return rand.New(src).Intn(max-min) + min
|
||||
}
|
||||
@@ -89,11 +103,23 @@ func (IoTServer) GetRandomPass(ctx context.Context, r *protoapi.RequestPass) (*p
|
||||
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),
|
||||
@@ -108,10 +134,57 @@ func (IoTServer) UploadFile(stream protoapi.IoTService_UploadFileServer) error {
|
||||
if fileName == "" {
|
||||
fileName = chunk.GetFileName()
|
||||
}
|
||||
buffer = append(buffer, chunk.GetContent()...)
|
||||
totalBytes += int64(len(chunk.GetContent()))
|
||||
}
|
||||
}
|
||||
|
||||
func (IoTServer) ListFiles(ctx context.Context, r *protoapi.EmptyRequest) (*protoapi.FileList, error) {
|
||||
storeMu.RLock()
|
||||
defer storeMu.RUnlock()
|
||||
|
||||
var files []*protoapi.FileMetadata
|
||||
for _, f := range fileStore {
|
||||
files = append(files, &protoapi.FileMetadata{
|
||||
FileName: f.FileName,
|
||||
FileSize: int64(len(f.Content)),
|
||||
UploadedAt: f.UploadedAt,
|
||||
})
|
||||
}
|
||||
|
||||
return &protoapi.FileList{Files: files}, nil
|
||||
}
|
||||
|
||||
func (IoTServer) DownloadFile(r *protoapi.DownloadRequest, stream protoapi.IoTService_DownloadFileServer) error {
|
||||
storeMu.RLock()
|
||||
f, exists := fileStore[r.GetFileName()]
|
||||
storeMu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return status.Errorf(codes.NotFound, "file %s not found", r.GetFileName())
|
||||
}
|
||||
|
||||
chunkSize := 1024 // 1KB 청크 단위
|
||||
totalBytes := len(f.Content)
|
||||
|
||||
for i := 0; i < totalBytes; i += chunkSize {
|
||||
end := i + chunkSize
|
||||
if end > totalBytes {
|
||||
end = totalBytes
|
||||
}
|
||||
|
||||
err := stream.Send(&protoapi.FileChunk{
|
||||
FileName: f.FileName,
|
||||
Content: f.Content[i:end],
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ServerRun(addr string) {
|
||||
server := grpc.NewServer()
|
||||
var iotServer IoTServer
|
||||
|
||||
Reference in New Issue
Block a user