65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (13投票s)

2021 年 12 月 15 日

CPOL

5分钟阅读

viewsIcon

30400

downloadIcon

392

您的 API 和 Web 客户端表单、XHR、Blob 和拖放文件/数据上传的一站式指南

目录

引言

尝试找到一份权威的参考指南,说明如何将客户端文件流式传输到服务器,这是一项艰巨的任务,因此有了这篇文章。

本文演示了

  1. 从 C# 客户端上传文件
  2. 从浏览器页面上传文件
    1. 使用 Form 元素
    2. 将 XHR 与 Form 元素一起使用
    3. 上传“blob”数据
    4. 文件拖放

为了保持简单

  1. 所有描述的变体都由一个后端端点处理。
  2. 前端只使用了简单的 JavaScript。演示实际上只是一个 HTML 文件。
  3. 我还演示了如何为正在上传的文件/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;
    }
  }
}

此实现的关键点如下

  1. [FromForm] 属性会告知端点处理程序将接收表单数据。
  2. 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 客户端,我想演示支持几种不同的内容

  1. 带有提交按钮的直接表单上传
  2. 用 XHR 上传实现替换标准提交过程
  3. 将数据作为 blob 上传
  4. 通过拖放上传文件

为了保持简单,不支持多文件。

文章下载中提供的 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>

这里重要的是,要使拖放起作用,ondropondragover 都必须有实现。

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 日:初始版本
© . All rights reserved.