C#如何通过gRPC流式上传和下载大文件

来源:网站建设作者:大卫头衔:程序员
导读:本期聚焦于小伙伴创作的《C#如何通过gRPC流式上传和下载大文件》,敬请观看详情,探索知识的价值。以下视频、文章将为您系统阐述其核心内容与价值。如果您觉得《C#如何通过gRPC流式上传和下载大文件》有用,将其分享出去将是对创作者最好的鼓励。

在C#项目开发中,大文件传输是常见的业务场景,传统的单次请求传输方式会将整个文件加载到内存中,不仅占用大量内存资源,还容易因为网络波动导致传输失败。gRPC的流式传输特性支持分块处理文件数据,能够很好地解决这些问题,下面介绍具体的实现方式。

C#如何通过gRPC流式上传和下载大文件

环境准备

首先需要创建gRPC项目,安装对应的NuGet包,服务端和客户端都需要安装Grpc.AspNetCoreGoogle.Protobuf包,客户端如果是控制台程序还需要安装Grpc.Net.Client

定义proto文件

在proto文件中定义流式传输的服务方法,分别定义上传和下载的RPC方法,上传使用客户端流,下载使用服务端流,同时定义对应的请求和响应消息结构。

syntax = "proto3";

option csharp_namespace = "GrpcFileTransfer";

package file_transfer;

// 文件块消息,用于传输分块的文件数据
message FileChunk {
  string file_name = 1; // 文件名
  bytes content = 2;    // 文件块内容
  int32 chunk_index = 3;// 块索引
  bool is_last = 4;     // 是否为最后一块
}

// 上传响应消息
message UploadReply {
  bool success = 1;     // 是否上传成功
  string message = 2;   // 响应消息
}

// 下载请求消息
message DownloadRequest {
  string file_name = 1; // 要下载的文件名
}

// 文件传输服务定义
service FileTransfer {
  // 客户端流式上传,客户端发送多个FileChunk,服务端返回单个UploadReply
  rpc UploadFile (stream FileChunk) returns (UploadReply);
  // 服务端流式下载,客户端发送DownloadRequest,服务端返回多个FileChunk
  rpc DownloadFile (DownloadRequest) returns (stream FileChunk);
}

服务端实现

实现proto文件中定义的FileTransfer服务,分别处理上传和下载的流式逻辑。

上传功能实现

上传功能需要接收客户端发送的多个文件块,依次写入到本地文件中,直到接收到最后一个块为止。

using Grpc.Core;
using GrpcFileTransfer;

namespace GrpcFileTransferServer.Services
{
    public class FileTransferService : FileTransfer.FileTransferBase
    {
        private readonly IWebHostEnvironment _env;
        private readonly ILogger<FileTransferService> _logger;

        public FileTransferService(IWebHostEnvironment env, ILogger<FileTransferService> logger)
        {
            _env = env;
            _logger = logger;
        }

        // 实现上传文件方法
        public override async Task<UploadReply> UploadFile(IAsyncStreamReader<FileChunk> requestStream, ServerCallContext context)
        {
            string? fileName = null;
            string? savePath = null;
            FileStream? fileStream = null;
            try
            {
                // 循环读取客户端发送的每一个文件块
                while (await requestStream.MoveNext(context.CancellationToken))
                {
                    var chunk = requestStream.Current;
                    // 第一个块时初始化文件信息
                    if (fileName == null)
                    {
                        fileName = chunk.FileName;
                        // 保存路径为项目根目录下的Uploads文件夹
                        var uploadDir = Path.Combine(_env.ContentRootPath, "Uploads");
                        if (!Directory.Exists(uploadDir))
                        {
                            Directory.CreateDirectory(uploadDir);
                        }
                        savePath = Path.Combine(uploadDir, fileName);
                        fileStream = new FileStream(savePath, FileMode.Create, FileAccess.Write);
                        _logger.LogInformation($"开始接收文件:{fileName}");
                    }
                    // 写入文件块内容
                    if (fileStream != null && chunk.Content != null)
                    {
                        await fileStream.WriteAsync(chunk.Content.ToByteArray(), context.CancellationToken);
                        _logger.LogInformation($"接收文件块:{chunk.ChunkIndex},大小:{chunk.Content.Length}字节");
                    }
                    // 如果是最后一块,结束写入
                    if (chunk.IsLast)
                    {
                        _logger.LogInformation($"文件{fileName}上传完成");
                        break;
                    }
                }
                return new UploadReply { Success = true, Message = "文件上传成功" };
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "文件上传失败");
                return new UploadReply { Success = false, Message = $"上传失败:{ex.Message}" };
            }
            finally
            {
                fileStream?.Close();
                fileStream?.Dispose();
            }
        }
    }
}

下载功能实现

下载功能需要读取本地文件,将文件分块后依次发送给客户端,直到文件读取完毕。

// 在FileTransferService类中添加下载方法实现
public override async Task DownloadFile(DownloadRequest request, IServerStreamWriter<FileChunk> responseStream, ServerCallContext context)
{
    var fileName = request.FileName;
    // 文件存储路径,和服务端上传路径一致
    var filePath = Path.Combine(_env.ContentRootPath, "Uploads", fileName);
    if (!File.Exists(filePath))
    {
        throw new RpcException(new Status(StatusCode.NotFound, $"文件{fileName}不存在"));
    }
    int chunkSize = 1024 * 64; // 每块64KB
    int chunkIndex = 0;
    using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
    {
        byte[] buffer = new byte[chunkSize];
        int bytesRead;
        // 循环读取文件内容,分块发送
        while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length, context.CancellationToken)) > 0)
        {
            bool isLast = fileStream.Position >= fileStream.Length;
            var chunk = new FileChunk
            {
                FileName = fileName,
                Content = Google.Protobuf.ByteString.CopyFrom(buffer, 0, bytesRead),
                ChunkIndex = chunkIndex,
                IsLast = isLast
            };
            await responseStream.WriteAsync(chunk, context.CancellationToken);
            _logger.LogInformation($"发送文件块:{chunkIndex},大小:{bytesRead}字节");
            chunkIndex++;
            if (isLast)
            {
                break;
            }
        }
    }
    _logger.LogInformation($"文件{fileName}下载完成");
}

客户端实现

客户端需要分别实现上传和下载的调用逻辑,处理流式数据的发送和接收。

上传客户端实现

using Grpc.Core;
using GrpcFileTransfer;
using Grpc.Net.Client;

namespace GrpcFileTransferClient
{
    class Program
    {
        static async Task Main(string[] args)
        {
            // 创建gRPC通道,地址为服务端地址
            using var channel = GrpcChannel.ForAddress("https://localhost:5001");
            var client = new FileTransfer.FileTransferClient(channel);
            // 调用上传方法,获取请求流
            using var call = client.UploadFile();
            var requestStream = call.RequestStream;
            string filePath = "D:\test\large_file.zip"; // 要上传的大文件路径
            string fileName = Path.GetFileName(filePath);
            int chunkSize = 1024 * 64; // 每块64KB
            int chunkIndex = 0;
            using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
            {
                byte[] buffer = new byte[chunkSize];
                int bytesRead;
                // 循环读取文件,分块发送到服务端
                while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
                {
                    bool isLast = fileStream.Position >= fileStream.Length;
                    var chunk = new FileChunk
                    {
                        FileName = fileName,
                        Content = Google.Protobuf.ByteString.CopyFrom(buffer, 0, bytesRead),
                        ChunkIndex = chunkIndex,
                        IsLast = isLast
                    };
                    await requestStream.WriteAsync(chunk);
                    Console.WriteLine($"发送文件块:{chunkIndex},大小:{bytesRead}字节");
                    chunkIndex++;
                }
            }
            // 完成请求流写入,等待服务端响应
            await requestStream.CompleteAsync();
            var reply = await call.ResponseAsync;
            Console.WriteLine($"上传结果:{reply.Success},消息:{reply.Message}");
        }
    }
}

下载客户端实现

// 在客户端Program类中添加下载方法
static async Task DownloadFile(GrpcChannel channel)
{
    var client = new FileTransfer.FileTransferClient(channel);
    string fileName = "large_file.zip"; // 要下载的文件名
    string savePath = Path.Combine("D:\test\download", fileName); // 保存路径
    var downloadDir = Path.GetDirectoryName(savePath);
    if (!Directory.Exists(downloadDir))
    {
        Directory.CreateDirectory(downloadDir!);
    }
    // 调用下载方法,获取响应流
    using var call = client.DownloadFile(new DownloadRequest { FileName = fileName });
    var responseStream = call.ResponseStream;
    FileStream? fileStream = null;
    try
    {
        // 循环读取服务端发送的文件块
        while (await responseStream.MoveNext(CancellationToken.None))
        {
            var chunk = responseStream.Current;
            if (fileStream == null)
            {
                fileStream = new FileStream(savePath, FileMode.Create, FileAccess.Write);
                Console.WriteLine($"开始下载文件:{chunk.FileName}");
            }
            // 写入文件块内容
            await fileStream.WriteAsync(chunk.Content.ToByteArray());
            Console.WriteLine($"接收文件块:{chunk.ChunkIndex},大小:{chunk.Content.Length}字节");
            if (chunk.IsLast)
            {
                Console.WriteLine($"文件{chunk.FileName}下载完成");
                break;
            }
        }
    }
    finally
    {
        fileStream?.Close();
        fileStream?.Dispose();
    }
}

注意事项

  • 流式传输的块大小需要合理设置,过小的块会导致请求次数过多,过大的块会占用过多内存,通常建议设置为32KB到128KB之间。
  • 需要在服务端和客户端都添加取消令牌的处理,当传输过程中用户主动取消或者网络中断时,能够及时释放资源。
  • 大文件传输时建议添加断点续传的逻辑,记录已传输的块索引,传输中断后可以从中断的位置继续传输,避免重复传输整个文件。
  • 需要注意gRPC默认的消息大小限制,如果文件块大小超过默认限制,需要在服务端和客户端配置更大的消息接收大小。

gRPCC#文件流式传输流式上传流式下载修改时间:2026-07-13 06:27:36

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。