You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 23:20:41