[C#] .NET FileUpload gPRC Service Poc

개요

  • 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)
    {

    }
}
  1. 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

이 글을 공유하기

댓글

Designed by JB FACTORY