在Web应用中实现文件上传时,用户最关心的往往是“传到哪了”。传统的表单提交或Ajax上传,前端很难知道服务端真实的处理进度。借助ASP.NET Core中的SignalR,我们可以在C#后端边接收文件边计算百分比,并主动推送到前端页面,从而实现平滑的实时进度条。

一、为什么用SignalR做进度推送
过去常见的做法是前端定时调用一个查询接口,问“现在传了多少”。这种方式叫轮询,缺点很明显:如果间隔设得长,进度更新迟钝;间隔短,服务端要频繁处理无意义的请求,浪费资源。SignalR基于WebSocket(在不支持时自动降级为长轮询等),建立连接后服务端可以随时向客户端发消息,属于真正的服务端推送。
在文件上传场景里,C#服务端通常负责接收并保存文件。只要我们在写入流的循环中加入进度计算,就能通过SignalR的Hub上下文把进度广播出去。这样前端不用猜,后端推多少前端就显示多少,体验更流畅,架构也更清晰。
二、后端Hub与上传接口设计
先定义一个简单的进度Hub,不需要写复杂方法,因为主要是服务端主动推,客户端只需连接并监听。代码如下:
using Microsoft.AspNetCore.SignalR;
public class ProgressHub : Hub
{
// 客户端连接后可在此处理,但推送由其他接口发起
}
接着在控制器中注入IHubContext<ProgressHub>,在上传Action里读取请求流并分块写入文件。每写完一块,就计算已接收字节与总大小的比例,调用Clients.Caller.SendAsync把百分比发给当前连接。注意这里用Caller,只推给上传者自己,避免泄露他人进度。
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using System.IO;
using System.Threading.Tasks;
[ApiController]
[Route("api/upload")]
public class UploadController : ControllerBase
{
private readonly IHubContext<ProgressHub> _hubContext;
public UploadController(IHubContext<ProgressHub> hubContext)
{
_hubContext = hubContext;
}
[HttpPost]
public async Task<IActionResult> Upload(long totalSize)
{
var filePath = Path.Combine(Path.GetTempPath(), "bigfile.tmp");
using var fileStream = System.IO.File.Create(filePath);
var buffer = new byte[8192];
long received = 0;
int read;
while ((read = await Request.Body.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
await fileStream.WriteAsync(buffer, 0, read);
received += read;
int percent = (int)(received * 100 / totalSize);
// 推送给当前连接
await _hubContext.Clients.Caller.SendAsync("ReceiveProgress", percent);
}
return Ok(new { done = true });
}
}
上面代码使用固定8KB缓冲区循环读取请求体,每次累加received并算出百分比。实际上totalSize应由前端在请求头或查询参数中带上,后端据此计算。若前端采用分块上传,也可改为每块确认后推送,逻辑类似。
这种写法的优点是内存占用低,文件不会被一次性加载进内存;缺点是如果网络断开,已传部分需前端重传或后端做断点续传标记。生产环境建议结合唯一上传ID,将进度与ID绑定,用Clients.Client(connectionId)精准推送。
三、前端连接SignalR并更新进度条
前端使用官方@microsoft/signalr库,先建立连接,再监听ReceiveProgress方法。上传时用fetch或XMLHttpRequest发送原始二进制流,并在URL里带上totalSize。示例代码如下:
const connection = new signalR.HubConnectionBuilder()
.withUrl("/progressHub")
.build();
connection.on("ReceiveProgress", (percent) => {
document.getElementById("bar").style.width = percent + "%";
document.getElementById("text").innerText = percent + "%";
});
await connection.start();
async function uploadFile(file) {
await fetch("/api/upload?totalSize=" + file.size, {
method: "POST",
body: file
});
}
页面上只需一个div作为进度条容器,宽度随percent变化。因为SignalR连接和文件上传是两个请求,但同属一个浏览器会话,后端用Caller能正确找到连接。若用户开多个标签页,应让前端在连接时传一个上传令牌,后端用Group管理,避免串进度。
从实践看,这种方式比轮询简单且高效。前端代码量不多,主要工作都在C#后端的计算与推送。需要注意的是,SignalR默认消息有大小限制,进度数字很小不受影响;但若附带额外日志,应控制负载。
四、常见坑与优化建议
第一,IIS或Kestrel对请求体大小有限制,大文件要在Program.cs里调高限制,例如使用builder.Services.Configure<KestrelServerOptions>或修改web.config。第二,如果用了负载均衡,SignalR需配置背板(如Redis)才能保证多实例间推送可达。第三,进度百分比用int即可,不必传小数,减少带宽。
另外,前端应在上传前先确保SignalR连接已开启,否则会丢早期进度。可在start()之后禁用上传按钮,连接断开时利用connection.onclose自动重连并提示用户。下表列出轮询与SignalR推送的对比:
| 方式 | 实时性 | 服务端开销 | 实现复杂度 |
|---|---|---|---|
| 前端轮询 | 低 | 高 | 低 |
| SignalR推送 | 高 | 低 | 中 |
综合来看,C#配合SignalR做文件上传进度实时推送,既符合现代Web交互预期,也减轻了服务器无效请求。只要处理好连接管理和大文件限制,就能稳定支撑业务。
C#SignalRfile_upload_progress修改时间:2026-07-31 16:45:29