[C#] .NET FileUpload gPRC Service Poc
- gRPC
- 2023. 2. 2. 20:09
개요
- fileUploadService를 grpc를 이용하여 구현하였습니다.
- MSDN에 제시된 fileupload 샘플은 {-FileBuffer를 한번에 전송하여 서버에서 한번에 처리-}하는 코드 입니다.
- 위 예제의 경우 한번에 fileBuffer를 http에 올리기 때문에 grpc대역폭제한으로 대용량 파일 처리를 할 수 없고 전송된 파일을 어디까지 쓰고 있는지 Status Check가 어렵다는 단점이 있습니다.
- Client에서 form 데이터내에 Buffer를 일정한 byte로 분해하여 서버로 전송하여 서버에 전송된 byte를 모아서 write 하는 FileStreamming 기능을 구현하였습니다.
1. Client Fileupload form 개발.
.razor
파일에 form<InputFile>
을 추가해 줍니다.OnChange
event Handler를 만들어 줍니다.
<p>
<label>
<InputFile OnChange="@LoadFiles" />
</label>
</p>
private async Task LoadFiles(InputFileChangeEventArgs e)
{
// Form파일이 변경되면 발생하는 이벤트입니다.
// MultiFile Getter -> e.GetMultipleFiles(count);
// SingleFile Getter -> e.File
}
2. Client , Server Grpc protoBuf 개발.
- 양방향 스트리밍을 개발하기 위하여 grpc의 request, response에 stream으로 변경합니다.
- 예시 양방향 ->
rpc UploadFileStream(stream UploadFileRequest) returns (stream UploadFileResponse) {}
- 예시 Client Streamming->
rpc UploadFileStream(stream UploadFileRequest) returns (UploadFileResponse) {}
- 예시 Server Streamming->
rpc UploadFileStream(stream UploadFileRequest) returns (UploadFileResponse) {}
- protoBuf를 아래와 같이 만들어 줍니다.
- request의 buffer는 파일을 분할한 Buffer입니다.
- {+request+}의 fileTotalSize는 파일의 총 Buffer Size입니다. buffer의 총합(전송된 양의 총합)과 fileTotalSize이 같아지면 파일 전송은 완료되는 개념입니다.
- request의 fileName은 파일의 원본 이름입니다.
- response의 fileName은 파일의 원본 이름입니다.
- response의 fileTotalSize은 파일의 총 Buffer Size이름입니다.
- response의 currentBufferSize은 지금까지 write한 Buffer의 합입니다. (예시 1mb/sec 로 전송시 5초후 currentBufferSize는 5120 * 1024byte 입니다)
syntax = "proto3";
option csharp_namespace = "GrpcService1";
package upload;
// The FileDownload service definition.
service Uploader {
// Download a file
rpc UploadFileStream(stream UploadFileRequest) returns (stream UploadFileResponse) {}
}
// The request message containing file data, and file name
message UploadFileRequest {
bytes buffer = 1;
int64 fileTotalSize= 2;
string fileName = 3;
}
// The response from the upload containing the filePath
message UploadFileResponse {
string filePath =1;
int64 fileTotalSize= 2;
int64 currentBufferSize = 3;
}
3. Client Grpc service 개발.
public class TestGprcService
{
private readonly static int _1KB = 1 * 1024; // 파일의 사이즈 Config 상수 입니다.
private readonly static int _1MB = _1KB * 1024; // 파일의 사이즈 Config 상수 입니다.
private readonly static int _1GB = _1MB * 1024; // 파일의 사이즈 Config 상수 입니다.
// IBrowserFile? file = Form Data 입니다.
// Reciver 발생시 UI에 알려주기위한 EnventCallback 입니다.
public async Task FileUpload(IBrowserFile? file, Action<float, string> streamingReciverCallBack)
{
using var channel = GrpcChannel.ForAddress("https://localhost:7273", new GrpcChannelOptions()
{
MaxReceiveMessageSize = _1GB,
MaxSendMessageSize = _1GB,
}); //Grpc Channel 생성시 MesageSize를 지정해 주어야 합니다. 최대크기는 4GB라고 합니다.
// mutipartform에서 지원할 파일의 최대 사이즈 입니다
// 이부분을 수정하면 업로드 할 파일의 최대사이즈를 제어 할 수 있습니다.
var stream = file!.OpenReadStream(_1GB);
// 매 리퀘스트 마다 전송할 buffer의 크기 입니다.
// 이 부분의 크기를 증가시키면 업로드 속도가 항샹 됩니다.
var buffer = new byte[1024 * 10];
var client = new Uploader.UploaderClient(channel);
// 기존 GRPC와 다른 점은 Stream을 열어둔 상태에서 데이터를 흘려 보내야
// Streamming이 동작한다는 점이 다른 점 입니다.
// `UploadFileStream`이라는 제가 만든 stream을 열어주었습니다.
using var stremmingGrpc = client.UploadFileStream();
// streamming이 진행되면 받게될 리시버를 새로운 Task에서 대기 상태로 생성합니다.
// server쪽에서 `WriteAsync`를 발생시키면 아래 리시버에 들어옵니다.
var taskReciver = async () =>
{
await foreach (var grpcRes in stremmingGrpc.ResponseStream.ReadAllAsync())
{
var percent = ((double)grpcRes.CurrentBufferSize / grpcRes.FileTotalSize) * 100;
streamingReciverCallBack.Invoke(float.Parse(percent.ToString()), grpcRes.FilePath);
}
};
_ = taskReciver.Invoke();
// fileBuffer를 Memory Stream에 분해하여 흘려보냅니다.
// `WriteAsync`를 발생시키면 Server쪽 `ReadAllAsync`에서 응답합니다.
var bytesRead = 0;
while ((bytesRead = await stream.ReadAsync(buffer)) != 0)
{
using var ms = new MemoryStream(bytesRead);
await ms.WriteAsync(buffer, 0, bytesRead);
await stremmingGrpc.RequestStream.WriteAsync(new UploadFileRequest()
{
Buffer = ByteString.CopyFrom(ms.ToArray()),
FileTotalSize = file.Size,
FileName = file.Name
});
}
// 모든 Server Client Stream을 닫습니다.
await stremmingGrpc.RequestStream.CompleteAsync();
await Task.CompletedTask;
}
}
4. Server GrpcService 개발
// requestStream = request가 들어오는 parameter 입니다.
// 일반 controller와 다르게 requestStream로 부터 stream데이터가 흘러 들어오는 형식입니다.
// responseStream= response를 흘려 보내는 parameter 입니다.
// 일반 controller와 다르게 returen으로 데이터를 반환하지 않고 parameter에 Stream을 통해서 데이터를 흘려 보냅니다.
public override async Task UploadFileStream(IAsyncStreamReader<UploadFileRequest> requestStream, IServerStreamWriter<UploadFileResponse> responseStream, ServerCallContext context)
{
try
{
string uploadFileName = string.Empty;
long totalReadBufferSize = 0;
// 분할하여 들어오는 FileStream을 모아두는 List 입니다.
List<byte[]> buffers = new List<byte[]>();
/// `awaite foreach ReadAllAsync()`로 request 대기 상태로 만들어 줍니다.
await foreach (var chunkMsg in requestStream.ReadAllAsync())
{
// 받아온 데이터를 마지막에 병합하기 위한 데이터 수집로직을 만듭니다.
var buffer = chunkMsg.Buffer;
buffers.Add(buffer.ToArray());
uploadFileName = chunkMsg.FileName;
totalReadBufferSize += buffer.Count();
// 데이터 수집이 끝나면 서버에서 수집된 데이터를 Client에 수집된 양을 전달합니다.
await responseStream.WriteAsync(new UploadFileResponse()
{
FilePath = uploadFileName,
FileTotalSize = chunkMsg.FileTotalSize,
CurrentBufferSize = totalReadBufferSize,
});
}
// 파일 전송이 끝나면 완료 메세지를 Client쪽으로 전달합니다.
await responseStream.WriteAsync(new UploadFileResponse()
{
FilePath = uploadFileName,
FileTotalSize = totalReadBufferSize,
CurrentBufferSize = totalReadBufferSize,
});
// 수집된 Buffer를 임시파일로 write하고 임시파일을 원본 파일명으로 복원합니다.
var directoryPath = Path.Combine("D:\\\\Temp\\\\upload");
var tempFilepath = Path.Combine(directoryPath, Path.GetRandomFileName());
var orgFilePath = Path.Combine(directoryPath, uploadFileName);
_mkdir(directoryPath);
using FileStream fileStream = new(tempFilepath, FileMode.Create);
await fileStream.WriteAsync(buffers.SelectMany(t => t).ToArray());
fileStream.Close();
File.Move(tempFilepath, orgFilePath, true);
}
catch (Exception ex)
{
}
}
- Client Ui - Client GrpcService 연동
- ui에 formFile OnChanged 이벤트가 있는 곳으로 이동합니다.
// 업로드 되는 State를 체크하기 위한 변수를 만들어 줍니다.
// isLoading = 파일 로딩 중
// progressPercent = 업로드 된 퍼센트
private bool isLoading;
private float progressPercent = 0;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
TestGprcService gprcService = new TestGprcService(); // Di로 분리
await gprcService.FileUpload(e.File, (float percent, string filePath) =>
{
progressPercent = percent;
StateHasChanged();
});
isLoading = false;
}
728x90
'gRPC' 카테고리의 다른 글
[인공지능] Tensorflow Serving gRPC C# 구현 (0) | 2023.02.27 |
---|---|
[gRPC] gRPC C# Server/Client 예제 (0) | 2023.02.26 |
[C#] .NET의 gRPC 인터셉터 - 클라이언트 측 인터셉터 (0) | 2022.04.27 |
[C#] .NET의 gRPC 서버 측 인터셉터 (0) | 2022.04.22 |
[gRPC] gRPC 프로토콜 버퍼란? (0) | 2022.03.17 |
이 글을 공유하기