客户端到服务器文件/数据流





5.00/5 (13投票s)
您的 API 和 Web 客户端表单、XHR、Blob 和拖放文件/数据上传的一站式指南
目录
引言
尝试找到一份权威的参考指南,说明如何将客户端文件流式传输到服务器,这是一项艰巨的任务,因此有了这篇文章。
本文演示了
- 从 C# 客户端上传文件
- 从浏览器页面上传文件
- 使用
Form
元素 - 将 XHR 与
Form
元素一起使用 - 上传“blob”数据
- 文件拖放
- 使用
为了保持简单
- 所有描述的变体都由一个后端端点处理。
- 前端只使用了简单的 JavaScript。演示实际上只是一个 HTML 文件。
- 我还演示了如何为正在上传的文件/blob 添加其他元数据。
为什么要使用流?
虽然答案应该是显而易见的,但主要原因是客户端和服务器端都不需要将整个文件加载到内存中 - 相反,流会将大型文件的数据分解成小块。从客户端读取文件到服务器将内容保存到文件的整个过程都作为“流式数据”进行管理,并且双方最多只需要一个大小为流块大小的缓冲区。
我如何弄清楚这一切
将这些信息拼凑在一起涉及大量的谷歌搜索。以下是我发现最有用的链接
服务器 URL
服务器设置为使用 IIS,因此到处使用的 URL 是 https:///FileStreamServer/file/upload,因为这是一篇演示文章,所以在示例中硬编码了。显然,在现实生活中,您会以不同的方式实现这一点!
服务器
服务器是用 .NET Core 3.1 实现的。API 端点很简单
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace FileStreamServer.Controllers
{
[ApiController]
[Route("[controller]")]
public class FileController : ControllerBase
{
public FileController()
{
}
[HttpPost("upload")]
public async Task<IActionResult> Upload([FromForm] DocumentUpload docInfo)
{
IActionResult ret;
if (docInfo == null || docInfo.File == null)
{
ret = BadRequest("Filename not specified.");
}
else
{
var fn = Path.GetFileNameWithoutExtension(docInfo.File.FileName);
var ext = Path.GetExtension(docInfo.File.FileName);
string outputPathAndFile = $@"c:\projects\{fn}-{docInfo.Id}{ext}";
using (FileStream output = System.IO.File.Create(outputPathAndFile))
{
await docInfo.File.CopyToAsync(output);
}
ret = Ok();
}
return ret;
}
}
}
此实现的关键点如下
[FromForm]
属性会告知端点处理程序将接收表单数据。DocumentUpload
类是“文件”和表单元数据的容器。
DocumentUpload 类
public class DocumentUpload
{
public IFormFile File { get; set; }
public string Id { get; set; }
}
属性名称必须与前端使用的命名约定匹配!本示例说明了仅指定一个文件以及元数据仅包含“Id
”值的预期。
处理大文件
这其中更复杂的部分实际上是配置 ASP.NET Core 以接受大文件。首先,必须修改 web.config 文件。在 system.webServer
部分,我们必须增加请求限制
<security>
<requestFiltering>
<!-- 4 GB is the max we can set but we use 2GB 2147483647
because that is the form limit -->
<requestLimits maxAllowedContentLength="2147483647" />
</requestFiltering>
</security>
其次,需要设置表单选项。我选择在 Startup 代码中执行此操作
public void ConfigureServices(IServiceCollection services)
{
...
services.Configure<FormOptions>(x =>
{
// int.MaxValue is 2GB.
x.ValueLengthLimit = int.MaxValue;
x.MultipartBodyLengthLimit = int.MaxValue;
});
...
}
因为 int.MaxValue
的最大值为 2GB,所以上传文件的大小限制在 2GB 左右。由于编码开销,实际可上传的文件大小小于 2GB,但我还没有弄清楚具体是多少。
C# 客户端
一个非常简单的 C# 控制台客户端,用于上传我的一只猫的照片(文件包含在文章下载中),它就是全部内容
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
namespace FileStreamClient
{
class Program
{
static void Main(string[] args)
{
var task = Task.Run(async () => await UploadAsync
("https:///FileStreamServer/file/upload", "cat.png"));
task.Wait();
}
// https://stackoverflow.com/a/16925159
// This was an alternate that is a lot more complicated:
// https://stackoverflow.com/a/2996904
// and that I couldn't get to work on the server-side.
private async static Task<Stream> UploadAsync(string url, string filename)
{
using var fileStream = new FileStream("cat.png", FileMode.Open, FileAccess.Read);
using var fileStreamContent = new StreamContent(fileStream);
using var stringContent = new StringContent("13");
using var client = new HttpClient();
using var formData = new MultipartFormDataContent();
formData.Add(stringContent, "Id");
formData.Add(fileStreamContent, "File", filename);
var response = await client.PostAsync(url, formData);
Stream ret = await response.Content.ReadAsStreamAsync();
return ret;
}
}
}
请注意,内容字符串“Id
”和文件流内容“File
”的名称与服务器上 DocumentUpload
类中的属性相匹配。
客户端网页
对于 Web 客户端,我想演示支持几种不同的内容
- 带有提交按钮的直接表单上传
- 用 XHR 上传实现替换标准提交过程
- 将数据作为 blob 上传
- 通过拖放上传文件
为了保持简单,不支持多文件。
文章下载中提供的 HTML 文件可以直接在浏览器中打开,例如: file:///C:/projects/FileStreaming/FileStreamClient/upload.html
带有提交按钮的直接表单上传
这是一个非常简单的过程,但有一个缺点是 action 会将浏览器重定向到上传 URL,这真的不是我们想要的,除非您想显示“您的文档已上传”之类的页面。
<form id="uploadForm" action="https:///FileStreamServer/file/upload"
method="post" enctype="multipart/form-data">
<div>
<input id="id" placeholder="ID" type="text" name="id" value="1" />
</div>
<div style="margin-top:5px">
<input id="file" style="width:300px" type="file" name="file" />
</div>
<div style="margin-top:5px">
<button type="submit">Upload</button>
</div>
</form>
就是这样。请注意,name
标签与服务器上的 DocumentUpload
类的名称(不区分大小写)相匹配。
用 XHR 上传实现替换标准提交过程
此实现需要更改 form
标签并实现 XHR 上传代码。
<form id="uploadForm" onsubmit="xhrUpload(); return false;" action="#">
<div>
<input id="id" placeholder="ID" type="text" name="id" value="1" />
</div>
<div style="margin-top:5px">
<input id="file" style="width:300px" type="file" name="file" />
</div>
<div style="margin-top:5px">
<button type="submit">Upload</button>
</div>
</form>
<div style="margin-top:5px">
<button onclick="xhrUpload()">Upload using XHR</button>
</div>
请注意,使用 XHR 上传的按钮不属于表单!
JavaScript 实现
function xhrUpload() {
const form = document.getElementById("uploadForm");
const xhr = new XMLHttpRequest();
responseHandler(xhr);
xhr.open("POST", "https:///FileStreamServer/file/upload");
const formData = new FormData(form);
xhr.send(formData);
}
function responseHandler(xhr) {
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
uploadResponse(xhr);
}
}
}
function uploadResponse(xhr) {
if (xhr.status >= 200 && xhr.status < 300) {
alert("Upload successful.");
} else {
alert("Upload failed: " + xhr.responseText);
}
}
这段代码中最有趣的部分是这个
const form = document.getElementById("uploadForm");
...
const formData = new FormData(form);
因为无论输入的 id
值和选择的文件在实例化 FormData
对象时都会被应用。
将数据作为 Blob 上传
HTML
<div style="margin-top:15px">
<input id="data" placeholder="some data" type="text" value="The quick brown fox" />
</div>
<div style="margin-top:5px">
<button onclick="uploadData()">Upload Data</button>
</div>
JavaScript
function uploadData() {
const id = document.getElementById("id").value;
const data = document.getElementById("data").value;
const blob = new Blob([data]);
var xhr = new XMLHttpRequest();
responseHandler(xhr);
xhr.open("POST", "https:///FileStreamServer/file/upload");
var formData = new FormData();
formData.append("Id", id);
formData.append("File", blob, "data.txt");
xhr.send(formData);
}
请注意,这里 FormData
是在没有引用表单的情况下实例化的,而是以编程方式应用表单数据。另请注意,文件名是硬编码的。此代码还重用了前面定义的 responseHandler
。
通过拖放上传文件
HTML
<div ondrop="dropFile(event);" ondragover="allowDrop(event);" style="margin-top:15px;
width:200px; height:200px; border-style:solid; border-width: 1px; text-align:center">
<div>Drag & drop file here</div>
</div>
这里重要的是,要使拖放起作用,ondrop
和 ondragover
都必须有实现。
JavaScript
function allowDrop(e) {
e.preventDefault();
}
function dropFile(e) {
e.preventDefault();
const dt = e.dataTransfer;
// We could implement multiple files here.
const file = dt.files[0];
const id = document.getElementById("id").value;
uploadFile(id, file);
}
function uploadFile(id, file) {
var xhr = new XMLHttpRequest();
responseHandler(xhr);
xhr.open("POST", "https:///FileStreamServer/file/upload");
var formData = new FormData();
formData.append("Id", id);
formData.append("File", file, file.name);
xhr.send(formData);
}
请注意,我们调用了 preventDefault
,因为这对于防止浏览器实际尝试渲染文件是必需的。
这段代码的另一个有趣部分是我们如何获取文件对象
const dt = e.dataTransfer;
const file = dt.files[0];
我肯定不会通过搜索网络上的示例来弄清楚这一点,因为我很少在我构建的前端实现拖放功能。
结论
就是这样。一篇关于使用表单、XHR 或拖放上传文件/数据的单一参考文章。
历史
- 2021 年 12 月 15 日:初始版本