FormData 接口提供了一种表示表单数据的键值对的构造方式,经过它的数据可以使用 XMLHttpRequest.send() 方法送出,本接口和此方法都相当简单直接。如果送出时的编码类型被设为 "multipart/form-data",它会使用和表单一样的格式。

如果你想构建一个简单的GET请求,并且通过<form>的形式带有查询参数,可以将它直接传递给URLSearchParams

更多解释MDN: https://developer.mozilla.org/zh-CN/docs/Web/API/FormData

分块(分片,统称分块了,确实只是发送一块数据)文件上传主要分2部分。

1. 前端js用file.slice可以从文件中切出一块一块的数据,然后用FormData包装一下,用XMLHttpRequest把切出来的数据块,一块一块send到server.

2. Server接收到的每一块都是一个multipart/form-data Form表单。可以在表单里放很多附属信息,文件名,大小,块大小,块索引,最总带上这块切出来的二进制数据。

multipart/form-data 数据

POST /upload HTTP/.
Host: localhost:
Content-Length:
User-Agent: Mozilla/. (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36 Content-Type: multipart/form-data; boundary=----WebKitFormBoundarymtng0xrR3ASR7wx7 ------WebKitFormBoundaryHdBeczaB5xBq6d55
Content-Disposition: form-data; name="file_name" apache-maven-..-bin.zip
------WebKitFormBoundaryHdBeczaB5xBq6d55
Content-Disposition: form-data; name="file_size" ------WebKitFormBoundaryHdBeczaB5xBq6d55
Content-Disposition: form-data; name="block_size" ------WebKitFormBoundaryHdBeczaB5xBq6d55
Content-Disposition: form-data; name="total_blocks" ------WebKitFormBoundaryHdBeczaB5xBq6d55
Content-Disposition: form-data; name="break_error" true
------WebKitFormBoundaryHdBeczaB5xBq6d55
Content-Disposition: form-data; name="index" ------WebKitFormBoundaryHdBeczaB5xBq6d55
Content-Disposition: form-data; name="data"; filename="blob"
Content-Type: application/octet-stream
(binary)

在Server存储文件,基本也就2种方案:

1. 直接创建一个对应大小的文件,按照每块数据的offset位置,写进去。

2. 每个传过来的数据块,保存成一个单独的数据块文件,最后把所有文件块合并成文件。

我这里只是做了一份简单的演示代码,基本上是不能用于生产环境的。

Index.html,直接把js写进去了

 <!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
<title>Multil-Blocks upload</title>
</head> <body>
<h2>Multil-Blocks upload</h2> <input id="file" type="file" /> <input type="checkbox" id="multil_block_file">multil block file</input>
<button type="button" onclick="on_block_upload()">Block upload</button>
<button type="button" onclick="on_concurrency_upload()">Concurrency upload</button>
<hr/> <div>
<label>File name: </label><span id="file_name"></span>
</div>
<div>
<label>File size: </label><span id="file_size"></span>
</div>
<div>
<label>Split blocks: </label><span id="block_count"></span>
</div> <hr/> <p id="upload_info"></p> <script>
var Block_Size = 1024 * 1024 * 2; var el_file = document.getElementById('file');
var el_multil_block_file = document.getElementById('multil_block_file');
var el_file_name = document.getElementById('file_name');
var el_file_size = document.getElementById('file_size');
var el_block_count = document.getElementById('block_count');
var el_upload_info = document.getElementById('upload_info'); var file = null;
var total_blocks = 0;
var block_index = -1;
var block_index_random_arr = [];
var form_data = null; el_file.onchange = function() {
if (this.files.length === 0) return; file = this.files[0];
total_blocks = Math.ceil( file.size / Block_Size ); el_file_name.innerText = file.name;
el_file_size.innerText = file.size;
el_block_count.innerText = total_blocks;
} function print_info(msg) {
el_upload_info.innerHTML += `${msg}<br/>`;
} function done() {
file = null;
total_blocks = 0;
block_index = -1;
form_data = null; el_file.value = '';
} function get_base_form_data() {
var base_data = new FormData();
base_data.append('file_name', file.name);
base_data.append('file_size', file.size);
base_data.append('block_size', Block_Size);
base_data.append('total_blocks', total_blocks);
base_data.append('break_error', true);
base_data.append('index', 0);
base_data.append('data', null); return base_data
} function build_block_index_random_arr() {
block_index_random_arr = new Array(total_blocks).fill(0).map((v,i) => i);
block_index_random_arr.sort((n, m) => Math.random() > .5 ? -1 : 1); print_info(`Upload sequence: ${block_index_random_arr}`);
} function post(index, success_cb, failed_cb) {
if (!form_data) {
form_data = get_base_form_data();
}
var start = index * Block_Size;
var end = Math.min(file.size, start + Block_Size); form_data.set('index', index);
form_data.set('data', file.slice(start, end)); print_info(`Post ${index}/${total_blocks}, offset: ${start} -- ${end}`); var xhr = new XMLHttpRequest();
xhr.open('POST', '/upload', true);
/*
Browser-based general content types
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarysXH5DIES2XFMuLXL Error content type:
xhr.setRequestHeader('Content-Type', 'multipart/form-data');
Content-Type: multipart/form-data;
*/
xhr.onreadystatechange = function() { if (xhr.readyState === XMLHttpRequest.DONE) { if (xhr.status >= 200 && xhr.status < 300 && success_cb) {
return success_cb();
} if (xhr.status >= 400 && failed_cb) {
failed_cb();
}
}
} // xhr.onerror event
xhr.send(form_data);
} function block_upload() {
if (!file) {
return;
}
if (block_index + 1 >= total_blocks) {
return done();
} block_index++;
var index = block_index_random_arr[block_index]; post(index, block_upload);
} function concurrency_upload() {
if (!file || total_blocks === 0) {
return;
} build_block_index_random_arr(); form_data = get_base_form_data();
form_data.set('break_error', false);
form_data.set('multil_block', el_multil_block_file.checked); for (var i of block_index_random_arr) {
((idx) => {
post(idx, null, function() {
print_info(`Failed: ${idx}`);
setTimeout(() => post(idx), 1000);
});
})(i);
}
} function on_block_upload() {
if (file) {
print_info('Block upload'); form_data = get_base_form_data();
form_data.set('multil_block', el_multil_block_file.checked); build_block_index_random_arr(); block_index = -1;
block_upload();
}
} function on_concurrency_upload() {
if (file) {
print_info('Concurrency upload');
concurrency_upload();
}
}
</script> </body>
</html>

简单的Go server和保存文件,基本忽略所有的错误处理

 package main

 import (
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"syscall"
"text/template"
) type MultilBlockFile struct {
FileName string
Size int64
BlockSize int64
TotalBlocks int
Index int
Bufs []byte
BreakError bool
} func fileIsExist(f string) bool {
_, err := os.Stat(f)
return err == nil || os.IsExist(err)
} func lockFile(f *os.File) error {
err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
if err != nil {
return fmt.Errorf("get flock failed. err: %s", err)
} return nil
} func unlockFile(f *os.File) error {
defer f.Close()
return syscall.Flock(int(f.Fd()), syscall.LOCK_UN)
} func singleFileSave(mbf *MultilBlockFile) error { dir, _ := filepath.Abs(filepath.Dir(os.Args[0]))
filePath := path.Join(dir, "tmp", mbf.FileName) offset := int64(mbf.Index) * mbf.BlockSize fmt.Println(">>> Single file save ---------------------")
fmt.Printf("Save file: %s \n", filePath)
fmt.Printf("File offset: %d \n", offset) var f *os.File
var needTruncate bool = false
if !fileIsExist(filePath) {
needTruncate = true
} f, _ = os.OpenFile(filePath, syscall.O_CREAT|syscall.O_WRONLY, 0777) err := lockFile(f)
if err != nil {
if mbf.BreakError {
log.Fatalf("get flock failed. err: %s", err)
} else {
return err
}
} if needTruncate {
f.Truncate(mbf.Size)
} f.WriteAt(mbf.Bufs, offset) unlockFile(f) return nil
} func multilBlocksSave(mbf *MultilBlockFile) error {
dir, _ := filepath.Abs(filepath.Dir(os.Args[0]))
tmpFolderPath := path.Join(dir, "tmp")
tmpFileName := fmt.Sprintf("%s.%d", mbf.FileName, mbf.Index)
fileBlockPath := path.Join(tmpFolderPath, tmpFileName) f, _ := os.OpenFile(fileBlockPath, syscall.O_CREAT|syscall.O_WRONLY|syscall.O_TRUNC, 0777)
defer f.Close() f.Write(mbf.Bufs)
f.Close() re := regexp.MustCompile(`(?i:^` + mbf.FileName + `).\d$`) files, _ := ioutil.ReadDir(tmpFolderPath)
matchFiles := make(map[string]bool) for _, file := range files {
if file.IsDir() {
continue
} fname := file.Name()
if re.MatchString(fname) {
matchFiles[fname] = true
}
} if len(matchFiles) >= mbf.TotalBlocks {
lastFile, _ := os.OpenFile(path.Join(tmpFolderPath, mbf.FileName), syscall.O_CREAT|syscall.O_WRONLY, 0777)
lockFile(lastFile) lastFile.Truncate(mbf.Size) for name := range matchFiles {
tmpPath := path.Join(tmpFolderPath, name) idxStr := name[strings.LastIndex(name, ".")+1:]
idx, _ := strconv.ParseInt(idxStr, 10, 32) fmt.Printf("Match file: %s index: %d \n", name, idx) data, _ := ioutil.ReadFile(tmpPath) lastFile.WriteAt(data, idx*mbf.BlockSize) os.Remove(tmpPath)
}
unlockFile(lastFile)
} return nil
} func indexHandle(w http.ResponseWriter, r *http.Request) {
tmp, _ := template.ParseFiles("./static/index.html")
tmp.Execute(w, "Index")
} func uploadHandle(w http.ResponseWriter, r *http.Request) { var mbf MultilBlockFile
mbf.FileName = r.FormValue("file_name")
mbf.Size, _ = strconv.ParseInt(r.FormValue("file_size"), 10, 64)
mbf.BlockSize, _ = strconv.ParseInt(r.FormValue("block_size"), 10, 64)
mbf.BreakError, _ = strconv.ParseBool(r.FormValue("break_error")) var i int64
i, _ = strconv.ParseInt(r.FormValue("total_blocks"), 10, 32)
mbf.TotalBlocks = int(i) i, _ = strconv.ParseInt(r.FormValue("index"), 10, 32)
mbf.Index = int(i) d, _, _ := r.FormFile("data")
mbf.Bufs, _ = ioutil.ReadAll(d) fmt.Printf(">>> Upload --------------------- \n")
fmt.Printf("File name: %s \n", mbf.FileName)
fmt.Printf("Size: %d \n", mbf.Size)
fmt.Printf("Block size: %d \n", mbf.BlockSize)
fmt.Printf("Total blocks: %d \n", mbf.TotalBlocks)
fmt.Printf("Index: %d \n", mbf.Index)
fmt.Println("Bufs len:", len(mbf.Bufs)) multilBlockFile, _ := strconv.ParseBool(r.FormValue("multil_block")) var err error
if multilBlockFile {
err = multilBlocksSave(&mbf)
} else {
err = singleFileSave(&mbf)
} if !mbf.BreakError && err != nil {
w.WriteHeader(400)
fmt.Fprintf(w, fmt.Sprintf("%s", err))
return
} fmt.Fprintf(w, "ok")
} func main() {
println("Listen on 8080") http.HandleFunc("/", indexHandle)
http.HandleFunc("/upload", uploadHandle) log.Fatal(http.ListenAndServe(":8080", nil))
}

目录截图,比较乱来

 

FormData/Go分片/分块文件上传的更多相关文章

  1. 利用Formdata实现form提交文件上传不跳转页面

    作者:幻月九十链接:https://www.zhihu.com/question/19631256/answer/119911045来源:知乎著作权归作者所有,转载请联系作者获得授权. $('form ...

  2. 使用PHP和HTML5 FormData实现无刷新文件上传教程

    无刷新文件上传是一个常见而又有点复杂的问题,常见的解决方案是构造 iframe 方式实现. 在 HTML5 中提供了一个 FormData 对象 API,通过 FormData 可以方便地构造一个表单 ...

  3. FormData+Ajax 实现多文件上传 学习使用FormData对象

    FormData对象是为序列化表以及创建与表单格式相同的数据(当然是用于XHR传输)提供便利. 今天我们使用dropzone和FormData实现多文件上传功能. var SAMP = null; / ...

  4. FormData序列化及file文件上传

    表单数据上传 情况一: 一.当表单文件处于无任何处理状态时,用submit提交直接上传; 但这种方式上传,数据无任何处理:(极少使用): 但是传统的表单提交会导致页面刷新,但是有些情况下,我们并不希望 ...

  5. 传统表单提交文件上传,以及FormData异步ajax上传文件

    传统的文件上传: 只用将form表单的entype修改成multipart/form-data,然后就可以进行文件上传,这种方式常用并且简单. 以下是另一种方式FormData,有时候我们需要ajax ...

  6. Java结合WebUploader文件上传

    之前自己写小项目的时候也碰到过文件上传的问题,没有找到很好的解决方案.虽然之前网找各种解决方案的时候也看到过WebUploader,但没有进一步深究.这次稍微深入了解了些,这里也做个小结. 简单的文件 ...

  7. PHP中使用 TUS 协议来实现可恢复文件上传

    曾经尝试过用PHP上传大文件吗?想知道您是否可以从上次中断的地方继续上传,而不会在遇到任何中断的情况下再次重新上传整个数据?如果您觉得这个场景很熟悉,请接着往下阅读. 文件上传是我们几乎所有现代Web ...

  8. PHP简单文件上传

    一个简单的PHP上传文件的例子: upload.html <html> <body> <form action="upload.php" method ...

  9. Spring MVC使用commons fileupload实现文件上传功能

    通过Maven建立Spring MVC项目,引入了Spring相关jar依赖. 1.为了使用commons fileupload组件,需要在pom.xml中添加依赖: <properties&g ...

随机推荐

  1. 阿里巴巴-德鲁伊druid连接池配置

    阿里巴巴推出的国产数据库连接池,据网上测试对比,比目前的DBCP或C3P0数据库连接池性能更好,Druid与其他数据库连接池使用方法基本一样(与DBCP非常相似),将数据库的连接信息全部配置给Data ...

  2. 阿里云vpc网络SNAT实现内网实例通外网

    需求场景: 因费用和安全考虑,内网部分机器没有分配公网IP,没绑定弹性公网IP,没有购买NAT服务,但是内网机器需要访问外网部分资源,如发送邮件. 操作步骤如下: 1.查看外网上的转发功能的开启没开启 ...

  3. SurfaceView和TextureView的区别

    SurfaceView和TextureView均继承于android.view.View,与其它View不同的是,两者都能在独立的线程中绘制和渲染,在专用的GPU线程中大大提高渲染的性能.Surfac ...

  4. Java并发编程(01):线程的创建方式,状态周期管理

    本文源码:GitHub·点这里 || GitEE·点这里 一.并发编程简介 1.基础概念 程序 与计算机系统操作有关的计算机程序.规程.规则,以及可能有的文件.文档及数据. 进程 进程是计算机中的程序 ...

  5. Ansible-基本概述

    为什么要自动化运维 纯手动软件安装部署方式 我们以 10 台机器部署 Nginx 为例.部署步骤如下: 1.通过 ssh 登录一台机器: 2.yum install -y nginx 或者 获取安装包 ...

  6. BTrace实战

    BTrace在解决现场问题的时候非常有用. 1.概述 1.1下载 https://github.com/btraceio/btrace,最新版本是1.3.9 目前1.3.x系列最低支持JDK1.7,要 ...

  7. 关于使用fastjson出现的问题:com.alibaba.fastjson.JSONException: syntax error, expect {, actual string, pos 1, fastjson-version 1.2.44

    先说下需求:是从redis中根据keys批量获取数据集合,再通过fastjson转为对象集合 代码如下: 在postman测试后,出现错误如下: 刚开始以为是使用fstjson方法不对,后面先通过打断 ...

  8. @常见的远程服务器连接工具:Xshell与secureCRT的比较!!!(对于刚接触的测试小白很有帮助哦)

    现在比较受欢迎的终端模拟器软件当属xshell和securecrt了. XShell绝对首选,免费版也没什么限制,随便改字体随便改颜色随便改大小随便改字符集,多窗口,也比较小巧,而SecureCRT界 ...

  9. php -v 找不到命令

    [root@localhost htdocs]# php -v -bash: php: command not found export PATH=$PATH:/usr/local/php7/bin ...

  10. nes 红白机模拟器 第7篇 编译使用方法

    模拟器,基于 InfoNES ,作者添加修改以下功能: 1, joypad 真实手柄驱动程序(字符型设备驱动) 2,原始图像只有256*240 ,添加 图像放大算法,这里实现了2种,a, 最近邻插值 ...