gRPC双向流传文件需定义rpc TransferFiles(stream FileChunk) returns (stream FileStatus),双方均用stream修饰;FileChunk含bytes data、filename、is_last,FileStatus含progress、message、success;客户端须调CompleteAsync(),服务端应流式落盘并校验,避免内存溢出与超时失败。

gRPC 双向流怎么定义才能传文件
双向流必须用 stream 修饰请求和响应类型,不能只流一边。常见错误是把 UploadRequest 设成单条消息、只让服务端返回 stream UploadResponse——这其实是服务器流,不是双向流,无法实时反馈进度或中断传输。
正确做法是在 .proto 里声明:
rpc TransferFiles(stream FileChunk) returns (stream FileStatus);其中
FileChunk 包含 bytes data、string filename、bool is_last 等字段;FileStatus 包含 int32 progress、string message、bool success。
-
bytes data字段实际对应 C# 的ReadOnlyMemory<byte>或ByteString,别直接用byte[]做属性(序列化会出问题) - 别在单个
FileChunk里塞整个文件——gRPC 默认有 4MB 消息上限,大文件必须分块,建议每块 128KB~512KB - 客户端写完所有
FileChunk后,必须显式调用await writer.CompleteAsync(),否则服务端收不到 EOF,await foreach会一直挂起
C# 客户端怎么边发边收状态
关键不是“发完再收”,而是用同一个 AsyncDuplexStreamingCall<FileChunk, FileStatus> 实例同时读写。很多人误以为要开两个独立流,结果状态对不上、超时错乱。
典型写法是启动一个发送任务 + 一个接收任务,并用 CancellationToken 协同控制:
var call = client.TransferFiles();
_ = Task.Run(async () => {
foreach (var chunk in chunks) {
await call.RequestStream.WriteAsync(chunk);
await Task.Delay(1); // 防止单次压满缓冲区
}
await call.RequestStream.CompleteAsync();
});
await foreach (var status in call.ResponseStream.ReadAllAsync(ct)) {
Console.WriteLine($"[{status.Progress}%] {status.Message}");
}- 不要在
WriteAsync后立刻await ReadAsync——这不是同步 RPC,响应顺序不保证与请求一一对应 - 如果服务端返回
status.Success == false,应主动取消CancellationToken并 await call.RequestStream.CompleteAsync(),避免继续发包 - 注意
ReadAllAsync()是扩展方法,需引用Grpc.Net.Client2.47+,旧版本得手动写while (await responseStream.MoveNext())
服务端如何避免内存爆掉或丢块
最常踩的坑:把所有 FileChunk.data 全 accumulate 到一个 List<byte[]> 再拼接——上传 1GB 文件就吃掉 1.5GB 内存,还可能触发 GC 中断流。
正确姿势是流式落盘 + 分块校验:
await foreach (var chunk in requestStream.ReadAllAsync(ct))
{
if (chunk.IsLast)
{
await fileStream.FlushAsync();
break;
}
await fileStream.WriteAsync(chunk.Data.Memory, ct);
}- 用
FileStream构造时加FileOptions.Asynchronous | FileOptions.SequentialScan,提升大文件写入性能 -
chunk.Data是ByteString,转Memory<byte>用.Memory属性,别用.ToByteArray()(触发复制) - 服务端别在循环里 new
FileStream——每个连接复用一个实例,否则 Windows 下快速创建/销毁文件句柄会耗尽 - 如果客户端网络抖动,gRPC 会重传部分帧,但不会保证 exactly-once;业务层需靠
chunk.SequenceNumber或哈希校验去重
为什么传一半就报 “Status(StatusCode=Internal, Detail="Error starting gRPC call")”
这个错误几乎全是服务端未捕获异常导致的,比如 FileStream 路径不可写、磁盘满、chunk.Data 为 null 时没判空就调 .Memory。
更隐蔽的是超时:默认客户端和服务端都设了 100 秒超时,传大文件很容易触发。必须显式配置:
var channel = GrpcChannel.ForAddress("https://...", new GrpcChannelOptions
{
HttpHandler = new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(5) },
DisposeHttpClient = true
});
// 调用时传 cancellation token with longer timeout
await foreach (var s in call.ResponseStream.ReadAllAsync(ct)) { ... }- 服务端 Kestrel 也要调大超时:
options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(10) - 别依赖
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true)走明文 HTTP/2——生产环境必须用 TLS,否则某些代理会截断长流 - 调试时用
grpc_cli测试基础流通不通:grpc_cli call localhost:5001 TransferFiles @request.json,排除客户端逻辑干扰
实际跑通的关键不在“怎么写语法”,而在于两边对 chunk 边界、错误传播、超时协同的理解是否一致——少一个 CompleteAsync(),或服务端少一个 try/catch,整条流就静默失败。










