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

环境准备
首先需要创建gRPC项目,安装对应的NuGet包,服务端和客户端都需要安装Grpc.AspNetCore和Google.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默认的消息大小限制,如果文件块大小超过默认限制,需要在服务端和客户端配置更大的消息接收大小。