如何在C#中通过gRPC实现客户端向服务器传输任意文件(附示例)
C# + gRPC 实现任意类型文件传输方案及示例
核心思路
gRPC的**客户端流式传输(Client Streaming)**是处理大文件/任意文件传输的最优方案:客户端将文件分割为固定大小的块,逐个发送给服务端;服务端接收后拼接保存为完整文件,避免一次性加载大文件到内存引发的性能问题。
1. 定义Protobuf协议
创建.proto文件,明确传输的消息结构和服务接口:
syntax = "proto3"; package filetransfer; // 文件传输服务接口 service FileTransferService { // 客户端流式上传文件 rpc UploadFile(stream FileChunk) returns (UploadResponse); } // 文件块消息 message FileChunk { string file_name = 1; // 文件名 int64 file_size = 2; // 文件总大小 bytes chunk_data = 3; // 当前块的二进制数据 int64 chunk_index = 4; // 当前块索引(用于断点续传扩展) } // 上传响应消息 message UploadResponse { bool success = 1; // 上传是否成功 string message = 2; // 响应描述 int64 received_size = 3; // 服务端实际接收字节数 }
通过dotnet工具生成C#代码:
dotnet grpc generate
或在项目文件中配置自动生成:
<ItemGroup> <Protobuf Include="Protos\filetransfer.proto" GrpcServices="Both" /> </ItemGroup>
2. 服务端实现(ASP.NET Core gRPC)
创建ASP.NET Core gRPC项目,实现FileTransferService接口,处理流式请求并保存文件:
using Grpc.Core; using filetransfer; namespace FileTransferServer.Services; public class FileTransferService : FileTransferService.FileTransferServiceBase { private readonly ILogger<FileTransferService> _logger; private readonly string _uploadDir = Path.Combine(Directory.GetCurrentDirectory(), "Uploads"); public FileTransferService(ILogger<FileTransferService> logger) { _logger = logger; // 自动创建上传目录 if (!Directory.Exists(_uploadDir)) Directory.CreateDirectory(_uploadDir); } public override async Task<UploadResponse> UploadFile(IAsyncStreamReader<FileChunk> requestStream, ServerCallContext context) { string? targetFilePath = null; long totalReceivedBytes = 0; long expectedFileSize = 0; try { // 遍历接收所有文件块 await foreach (var chunk in requestStream.ReadAllAsync(context.CancellationToken)) { if (targetFilePath == null) { // 首次接收时初始化文件路径和预期大小 targetFilePath = Path.Combine(_uploadDir, chunk.FileName); expectedFileSize = chunk.FileSize; // 创建新文件并写入第一块 await using var fs = new FileStream(targetFilePath, FileMode.Create, FileAccess.Write); await fs.WriteAsync(chunk.ChunkData.AsMemory(0, chunk.ChunkData.Length), context.CancellationToken); totalReceivedBytes += chunk.ChunkData.Length; } else { // 追加写入后续块 await using var fs = new FileStream(targetFilePath, FileMode.Append, FileAccess.Write); await fs.WriteAsync(chunk.ChunkData.AsMemory(0, chunk.ChunkData.Length), context.CancellationToken); totalReceivedBytes += chunk.ChunkData.Length; } _logger.LogInformation("Received chunk {Index}, total received: {Received}/{Expected}", chunk.ChunkIndex, totalReceivedBytes, expectedFileSize); } // 验证文件完整性 var uploadSuccess = totalReceivedBytes == expectedFileSize; var responseMsg = uploadSuccess ? "File uploaded successfully" : $"Upload incomplete: expected {expectedFileSize} bytes, received {totalReceivedBytes} bytes"; return new UploadResponse { Success = uploadSuccess, Message = responseMsg, ReceivedSize = totalReceivedBytes }; } catch (Exception ex) { _logger.LogError(ex, "Error during file upload"); // 清理未完成的文件 if (targetFilePath != null && File.Exists(targetFilePath)) File.Delete(targetFilePath); return new UploadResponse { Success = false, Message = $"Upload failed: {ex.Message}", ReceivedSize = totalReceivedBytes }; } } }
3. 客户端实现(Console App)
创建控制台项目,引用gRPC客户端包,实现文件分块上传逻辑:
using Grpc.Net.Client; using filetransfer; namespace FileTransferClient; class Program { static async Task Main(string[] args) { // 服务端地址(根据实际部署调整) const string serverAddress = "https://localhost:5001"; using var channel = GrpcChannel.ForAddress(serverAddress); var client = new FileTransferService.FileTransferServiceClient(channel); // 待上传文件路径 var targetFile = @"C:\Documents\demo.zip"; if (!File.Exists(targetFile)) { Console.WriteLine("Error: Target file does not exist."); return; } var fileInfo = new FileInfo(targetFile); var totalFileSize = fileInfo.Length; var fileName = fileInfo.Name; // 分块大小(建议1MB~10MB,根据网络带宽调整) const int chunkSize = 1024 * 1024; // 1MB var totalChunks = (long)Math.Ceiling((double)totalFileSize / chunkSize); // 启动流式上传调用 using var uploadCall = client.UploadFile(); var currentChunkIndex = 0; try { await using var fileStream = new FileStream(targetFile, FileMode.Open, FileAccess.Read); var buffer = new byte[chunkSize]; int bytesRead; // 分块读取并发送文件 while ((bytesRead = await fileStream.ReadAsync(buffer, 0, chunkSize)) > 0) { var chunk = new FileChunk { FileName = fileName, FileSize = totalFileSize, ChunkData = Google.Protobuf.ByteString.CopyFrom(buffer, 0, bytesRead), ChunkIndex = currentChunkIndex++ }; await uploadCall.RequestStream.WriteAsync(chunk); Console.WriteLine($"Sent chunk {currentChunkIndex}/{totalChunks} ({bytesRead} bytes)"); } // 结束流式请求 await uploadCall.RequestStream.CompleteAsync(); var response = await uploadCall.ResponseAsync; Console.WriteLine("\n=== Upload Result ==="); Console.WriteLine($"Success: {response.Success}"); Console.WriteLine($"Message: {response.Message}"); Console.WriteLine($"Server received: {response.ReceivedSize} bytes"); } catch (Exception ex) { Console.WriteLine($"Upload failed: {ex.Message}"); // 可选:添加断点续传逻辑,记录已上传块索引 } } }
关键优化与注意事项
- 分块大小调整:根据网络环境灵活调整,带宽充足时增大分块,弱网环境减小分块避免超时。
- 异常处理与重试:客户端可针对失败块添加重试逻辑,服务端需及时清理未完成的文件。
- 断点续传扩展:在Protobuf中添加已上传块索引字段,服务端跳过已接收的块,实现断点续传。
- 认证授权:通过
ServerCallContext添加JWT认证,限制未授权用户的上传权限。 - 消息压缩:启用gRPC消息压缩减少传输量,客户端和服务端通过配置开启压缩选项。
内容的提问来源于stack exchange,提问作者Laeeq Khan Niazi
相关产品推荐
相关产品推荐

