文件上传都发生了啥
一直在用组件库做文件上传,那里面的原理到底是啥,自己写能不能写一个upload
框出来呢?
(一)基本原理
浏览器端提供了一个表单,在用户提交请求后,将文件数据和其他表单信息编码并上传至服务器端,服务器端将上传的内容进行解码了,提取出 HTML 表单中的信息,将文件数据存入磁盘或数据库。
就是编码-解码-放数据库其实。
浏览器采用默认的编码方式是 application/x-www-form-urlencoded , 可以通过指定 form 标签中的 enctype 属性使浏览器知道此表单是用 multipart/form-data 方式编码如:
<form enctype="multipart/form-data"
action="${pageContext.request.contextPath}/servlet/uploadServlet2" method="post" >
(二)详解
2.1编码篇
我们在上传文件的时候用的编码都是form-data
的形式,不上传文件的时候都是用application/x...
的格式,这是为啥呢?
总的来说,application
传输二进制文件或者非ASCII码字符的时候会对数据进行大量转义,导致传输效率低下,而且也不支持多文件传输。
相对的,form-data
的数据处理形式就比较适合传输文件,他会把表单数据分成多个部分,每个部分有自己的头部信息和数据内容,而且还用分隔符分割,数据内容可以是文本或二进制数据,可以是文件或表单字段的值。他还支持多文件多表单数据的传输,所以经常用他来传输文件。
但大文件上传就不用这个了,用的是application/octet-stream
,后面再说。
2.1.1 常见的application/x-www-form-urlencoded
如果你打开network就知道很多表单提交请求都是用的application/x-www-form-urlencoded
这个编码方式,编码结果就是name=xiaoming&age=18
,
在这种编码格式中,所有字段名和值都被URL编码,并使用等号(=)分隔。每个字段之间使用与号(&)分隔。但让他来处理数据庞大的二进制数据或者包含非 ASCII 字符的数据的时候就会显得力不从心。因为它是一种文本格式,只能处理 ASCII 字符集中的字符,无法处理二进制数据。在该编码格式中,所有的非 ASCII 字符都需要进行编码,这会导致二进制数据被大量转义,使得数据量变得非常大,传输效率低下。另外,该编码格式也不支持多部分传输,无法将多个文件或多个表单字段一起传输。因此,如果需要传输二进制文件或多个表单字段,应该使用其他编码格式,如 multipart/form-data。该编码格式支持二进制数据和多部分传输,可以更有效地传输文件和表单数据。
2.1.2 multipart/form-data
使用这个编码的时候,会把表单数据分成多个部分,每个部分都有自己的头部信息和数据内容。各部分之间用一个不可能出现的分隔符分隔,分隔符由一个随机字符串和两个连字符组成,例如:–boundary。每部分都对应表单中的一个input
区
这种编码方式先定义好 一个不可能在数据中出现的字符串作为分界符,然后用它将各个数据段 分开,而对于每个数据段都对应着 HTML 页面表单中的一个 Input 区,包括一个 content-disposition 属性,说明了这个数据段的一些信息,如果这个数据段的内容是一个文件,还会有 Content-Type 属性,然后就是数据本身。
头部信息有这个部分的类型、名称和其他元数据。对于文件上传还包含了文件名和文件类型等信息。数据内容可以是文件或者表单的值。
multipart/form-data 是一种常见的编码格式,用于在 HTTP 协议中传输二进制数据和多部分数据。它通常用于上传文件或提交包含多个表单字段的表单数据。
在 multipart/form-data 编码中,数据被分成多个部分,每个部分都有自己的头部信息和数据内容。每个部分之间用一个特殊的分隔符分隔,分隔符由一个随机字符串和两个连字符组成,例如:–boundary。
每个部分的头部信息包含了该部分的类型、名称和其他元数据。对于文件上传,头部信息还包含了文件名和文件类型等信息。数据内容可以是文本或二进制数据,可以是文件或表单字段的值。
multipart/form-data 编码格式支持上传多个文件和多个表单字段,可以同时上传多个文件和表单字段。它还支持断点续传和上传进度显示等功能,可以更好地满足文件上传的需求。
需要注意的是,multipart/form-data 编码格式会增加数据传输的开销,因为每个部分都需要添加头部信息和分隔符。因此,在传输大文件或大量数据时,应该考虑使用其他编码格式,如 application/octet-stream。
2.1.3 思考
也许你有疑问?那可以用 application/json
吗?
其实我认为,无论你用什么都可以传,只不过会要综合考虑一些因素的话,multipart/form-data
更好。例如我们知道了文件是以二进制的形式存在,application/json
是以文本形式进行传输,那么某种意义上我们确实可以将文件转成例如文本形式的 Base64
形式。但是呢,你转成这样的形式,后端也需要按照你这样传输的形式,做特殊的解析。并且文本在传输过程中是相比二进制效率低的,那么对于我们动辄几十 M 几百 M 的文件来说是速度是更慢的。
以上为什么文件传输要用 multipart/form-data
我还可以举个例子,例如你在中国,你想要去美洲,我们的 multipart/form-data
相当于是选择飞机,而 application/json
相当于高铁,但是呢?中国和美洲之间没有高铁啊,你执意要坐高铁去,你可以花昂贵的代价(后端额外解析你的文本)造高铁去美洲,但是你有更加廉价的方式坐飞机(使用 multipart/form-data
)去美洲(去传输文件)。你图啥?
为什么要编码?
- 传递过程中要进行编码来制定发送的文件数据规则,以便于后端能够实现一套对应的解析规则。
- 传递的数据规则里包含所传递文件的基本信息 ,如文件名与文件类型,以便后端写出正确格式的文件。
2.1.4 实践一下
如果一不小心真用这个来传文件了会怎么样?
首先我们先写下最简单的一个表单提交方式。
<form action="http://localhost:7787/files" method="POST"><input name="file" type="file" id="file"><input type="submit" value="提交">
</form>
我们选择文件后上传,发现后端返回了文件不存在。
不用着急,熟悉的同学可能立马知道是啥原因了。
我们打开控制台,由于表单提交会进行网页跳转,因此我们勾选 preserve log
来进行日志追踪。
我们可以发现其实 FormData
中 file
字段显示的是文件名,并没有将真正的内容进行传输。再看请求头。
发现是请求头和预期不符,也印证了 application/x-www-form-urlencoded
无法进行文件上传。
我们加上请求头,再次请求。
<form action="http://localhost:7787/files" enctype="multipart/form-data" method="POST"><input name="file" type="file" id="file"><input type="submit" value="提交">
</form>
发现文件上传成功,简单的表单上传就是像以上一样简单。但是你得熟记文件上传的格式以及类型。
2.2 提交的时候
在文件上载和表单提交的过程中,有两个指的关心的问题,一是 上载的数据是是采用的那种方式的编码,这个问题的可以从 Content-Type
中得到答案,另一个是问题是上载的数据量有多少即 Content-Length
, 知道了它,就知道了 HttpServletRequest
的实例(java)中有多少数据可以读取出来。
2.2.1 后端不解析的话会拿到啥?
在开头的时候就说了整个文件上传的流程就是表单提交-编码-解析-保存,
下面的内容示范了后端不去处理上传的数据内容时会受到什么样的数据。
ps:form 表单提交操作网页会造成整体刷新,所以一般比较少用,而是利用熟悉的异步请求操作 AJAX 来完成上传动作,而一个新的问题出现了,不使用 form 表单,那文件编码该怎么处理呢?
答案是自己生成 FormData 的实例咯,
const form = new FormData()
然后用
form.append('file': fileData)
来放数据
<form method="POST" enctype="multipart/form-data"> <input type="file" name="file" value="请选择文件"><br /><input type="submit">
</form>
下面提供NodeJs
示例:
//上传接口逻辑
if (url === "/upload" && method === "POST") {// 定义一个缓存区const arr = [];req.on("data", (buffer) => {// 将前端传来的数据进行存储进缓存区arr.push(buffer);});req.on("end", () => {// 前端请求结束后进行数据解析 处理const buffer = Buffer.concat(arr);// 将数据变成string类型const content = buffer.toString();// 从传来的数存进test的文件里fileStream("test").write(buffer);// 返回前端请求完成res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });res.end("上传完成");});
}
这里的服务端代码先将前端上传的数据内容毫不处理直接写入一个名为 test 的文件内,以便我们查看前端到底传来了什么样的数据。
上传上面的html文件,这是前端的部分请求头。
Conten-Type
中,前面的是数据类型,后面就是分隔符啦。
这是后端得到的数据(请求体)
------WebKitFormBoundary7YGEQ1Wf4VuKd0cE
Content-Disposition: form-data; name="file"; filename="index.html"
Content-Type: text/html// 这里是文件内容
<html><head><title>上传文件</title></head><body><form method="POST" enctype="multipart/form-data"><input type="file" name="file" value="请选择文件"><br /><input type="submit"></form></body>
</html>
------WebKitFormBoundary7YGEQ1Wf4VuKd0cE--
2.2.2 后端是如何解析的
从上面的示例可以看到,后端拿到的请求体其实跟我上传拿到文件内容差不多,但多了一些东西,所以后端解析器的目的就是去掉这几行内容,并且在这几行简要信息里摘出文件名,以便写文件。
先是第一行和最后一行的 WebKitFormBoundary
码,第二行的 ContentDisposition
,该行包含一些文件名等基本信息,还有第三行文件内容类型,所以后端如果要获取到正确的文件内容则需要自己去除(直接用 indexof + length + substr 就可以)由浏览器在上传时所添加的进来的几行内容,而保留有效文件内容后进行写文件操作,完成上传目的。
自己写一个解析器:拆成数组,然后字符串操作进行删和拿。
/*** @step1 过滤第一行* @step2 过滤最后一行* @step3 过滤最先出现Content-Disposition的一行* @step4 过滤最先出现Content-Type:的一行*/
const decodeContent = content => {let lines = content.split('\n');const findFlagNo = (arr, flag) => arr.findIndex(o => o.includes(flag));// 查找 ----- Content-Disposition Content-Type 位置并且删除const startNo = findFlagNo(lines, '------');lines.splice(startNo, 1);const ContentDispositionNo = findFlagNo(lines, 'Content-Disposition');lines.splice(ContentDispositionNo, 1);const ContentTypeNo = findFlagNo(lines, 'Content-Type');lines.splice(ContentTypeNo, 1);// 最后的 ----- 要在数组末往前找const endNo = lines.length - findFlagNo(lines.reverse(), '------') - 1;// 先反转回来lines.reverse().splice(endNo, 1);return Buffer.from(lines.join('\n'));}
一个简单的解析器完成了,一般情况下你所使用的框架会解决解码这一部分问题,无论是 Nodejs 或是 Java,他们的本质都是摘出有效的文件内容然后写进新文件里,从而达到文件上传的目的。
最终的服务器代码如下:
// 最终的服务端代码
if(url ==='/upload' && method === 'POST') {
//文件类型
const arr = []
req.on('data', (buffer) => {arr.push(buffer);
})
req.on('end', () => {const buffer = Buffer.concat(arr);const content = buffer.toString();const result = decodeContent(content);const fileName = content.match(/(?<=filename=").*?(?=")/)[0];fileStream(fileName).write(result);res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });res.end('上传完成')
})
}
2.2.3 不用formdata,直接用file对象呢
上面那些编码的意义都是规范,都是为了前后端更好地进行协作开发。
当我们遇到了无form表单的情况时,该怎么上传文件。
文件上传的实质是上传文件的内容以及文件的格式,当我们使用 HTML 提供的 Input 上传文件的时候,它将文件的内容读进内存里,那我们直接将内存里的数据当成普通的数据提交到服务端可以么?
上面的代码中,直接用input框的file对象写了。
然后直接跟之前提到的后端代码一样,什么解析都不做,直接写到txt文件里,打开你就会发现这就是自己上传的文件,一模一样,后台解析都不用解析。
简单介绍一下file对象
用上面的代码尝试一下。
没上传任何内容打印了一下 file 变量,是 undefined,然后我上传了一张图片,再次打印后 file 变量是一个 File 函数构造出的对象了,它里面有文件的一些简略信息,如大小 size,文件名 name 以及文件格式 type 等,而且文件内容也在这个对象里,只不过以 ArrayBuffer 的方式在文件的原型链上体现,看看下面对于 File 对象的操作。
上面这些数字其实就是文件的内容,大家都知道数据是 0,1 组成的世界,而 ArrayBuffer 则是更多的数字来体现的数据世界,它和二进制的目的是一样的,它被用来表示通用的、固定长度的原始二进制数据缓冲区。说到这里则必须要提起一个新的概念,浏览器的提供的 Blob 接口。
blob对象
Blob 对象表示一个不可变、原始数据的类文件对象。上面的 file 变量的构造函数 File 就是继承与基于 Blob,继承了 blob 的功能并将其扩展使其支持用户系统上的文件。看看下面的 Blob 与 File 的示例。
上面我先打印了一下 file 与浏览器提供的构造函数 File 和 Blob 的关系,然后自行构建了自定义的 myfile 对象和 myblob 的对象,看得出自行构建的 File 对象下会多出一些文件相关的属性,而 Blob 对象则只是基本的 size 与 type 属性。当打印 arrayBuffer 函数的返回值时发现其内容也是完全一致的。
其实说到这里很多人对于 Blob 是个啥还是一知半解的,简单理解一下,它的构造结果是一块内存区,这块内存区以特定的格式存储我们所要上传的文件二进制数据,当我们上传文件时上传这块内存区里的数据即可。
参考文章1
参考文章1
相关文章:

文件上传都发生了啥
一直在用组件库做文件上传,那里面的原理到底是啥,自己写能不能写一个upload框出来呢? (一)基本原理 浏览器端提供了一个表单,在用户提交请求后,将文件数据和其他表单信息编码并上传至服务器端࿰…...

【vim进阶】vim编辑器的多文件操作(如何打开多个文件,如何进行文件间的切换,如何关闭其中的某一个文件)
一、如何打开多个文件? 方法一:启动打开 现在有多个文件 file1 ,file2 , … ,filen. 现在举例打开两个文件 file1,file2 vim file1 file2该方式打开文件,显示屏默认显示第一个文件也就是 file1。 方法二ÿ…...

ToBeWritten之车辆通信
也许每个人出生的时候都以为这世界都是为他一个人而存在的,当他发现自己错的时候,他便开始长大 少走了弯路,也就错过了风景,无论如何,感谢经历 转移发布平台通知:将不再在CSDN博客发布新文章,敬…...
自定义 Jackson 的 ObjectMapper, springboot多个模块共同引用,爽
springboot多个模块共同引用自定义ObjectMapper 🚃统一配置示例自定义 Jackson 的 ObjectMapper更改时区为东八区, 优点是在多个模块中都可以使用同一种方式来进行配置,方便维护和修改 统一配置 假设有一个 Spring Boot 项目,包含多个模块&…...

【面试】Redis面试题
文章目录概述什么是Redis?Redis有哪些优缺点?使用redis有哪些好处?为什么要用 Redis / 为什么要用缓存为什么要用 Redis 而不用 map/guava 做缓存?Redis为什么这么快Redis的应用场景持久化什么是Redis持久化?Redis 的持久化机制是…...

前端后端交互系列之原生Ajax的使用
目录前言一,Ajax概述二,基础知识之Http协议2.1 请求报文2.2 响应报文2.3 如何查看通信报文三,Ajax简单案例3.1 Express框架创建服务端3.2 Ajax案例后台准备3.3 Ajax案例前台准备3.4 发送get请求3.5 发送带有参数的Ajax请求3.6 发送post请求3.…...

openGauss 5.0企业版主从部署,实战狂飙
📢📢📢📣📣📣 哈喽!大家好,我是【IT邦德】,江湖人称jeames007,10余年DBA及大数据工作经验 一位上进心十足的【大数据领域博主】!😜&am…...
Vue中props组件和slot标签的区别
在 Vue 中,props 和 slot 都是组件之间进行通信的机制,它们的作用和应用场景有一些区别: props 是一种组件的数据传递机制,通过在父组件中以属性的形式向子组件传递数据。子组件接收这些数据,并可以进行相应的处理和渲…...

基于Windows下VSCode搭建Vue开发环境
一、准备工作 VSCode编辑器安装:https://code.visualstudio.com/Node.js安装:https://blog.csdn.net/qq_40197828/article/details/78302124VSCode插件安装:Vetur和ESlint 二、更换淘宝镜像源 更换镜像源命令:npm install -g c…...

Android开发 Dialog对话框 DatePickerDialog
1. AlertDialog AlertDialog是弹出的提醒对话框,有提示,确认,选择等功能。 没有公开的构造方法,一般用AlertDialog.Builder来完成参数设置,最后调用create方法创建。 参数设置常用的方法: 代码ÿ…...
开心档开发入门网之C++ Web 编程
C Web 编程什么是 CGI?公共网关接口(CGI),是一套标准,定义了信息是如何在 Web 服务器和客户端脚本之间进行交换的。CGI 规范目前是由 NCSA 维护的,NCSA 定义 CGI 如下:公共网关接口(…...

C# 和 VB .NET 的纯 FFmpeg 包装器:CSFFmpeg Crack
用于 C# 和 VB .NET 的纯 FFmpeg 包装器buildbuildpassingpassing releasereleasev1.0.3.0v1.0.3.0用于 C# 和 VB .NET Framework(WinForm 和 WPF)和 .NET Core 的纯 FFmpeg 包装器。 截图 主要 Winform 示例有据可查的例子目录: 关于截图好处…...
python外篇(序列化和非序列化)
目录 概念阐述 pickle json msgpack 概念阐述 序列化是指将对象转化为可存储或可传输的数据格式,例如将 Python 对象转化为二进制、JSON 或 XML 等格式,以便于将其存储到文件中或在网络上传输。在Python中,可以使用pickle、json、msgpac…...

Linux总结(二)
基础IO 1.什么叫文件? 我们需要在操作系统的角度理解文件。 文件 = 文件内容 + 属性(所以即使是空文件,也会占空间,因为我们是需要保存文件属性的,属性也是数据,所以占空间) C/C++程序默认会打开三个文件流,叫做标准输入(stdin),标准输出(stdout),标准错误(std…...

【4.1】Socket编程、TCP挥手
TCP连接断开 四次挥手 四次挥手过程 客户端发送FIN报文,客户端进入FIN_WAIT_1状态。 服务端接收报文,发送ACK报文,服务端进入CLOSE_WAIT状态。 客户端收到ACK报文,进入FIN_WAIT_2状态。 服务端处理完数据后,也发送…...

【竞赛经历】CSDN第41期竞赛题解
1 前言 本次的竞赛主要是最后一题,对于完全不懂珠算的人来说还是有点困难的,仅理解题目的意思就花了很多时间,最后侥幸拿了第一个前三。。。 2 题解 本次竞赛分为编程题部分和非编程题部分,其中非编程题部分比较简单。 2.1 非编…...

【Linux学习】信号——预备知识 | 信号产生 | 核心转储
🐱作者:一只大喵咪1201 🐱专栏:《Linux学习》 🔥格言:你只管努力,剩下的交给时间! 信号🔔信号🎵预备知识🎵信号处理方法的注册🔔信号…...

2023中国程序员薪酬报告出炉,你拖后腿了吗?
程序员薪资高已是公认的事实,但是具体高到什么程度呢?近期,全球人力服务公司 Michael Page Internatioal 就发布了《2023 中国大陆薪酬报告》,揭示了中国程序员的薪酬情况。 该报告中一共调研了国内 7 个行业以及 6 大城市不同职…...

Mac下Python3安装及基于Idea开发
本篇文章带大家基于Mac OS操作系统,下载、安装Python环境,并基于Idea编写第一个Demo。 Python3安装 访问Python官网:https://www.python.org/。找到“Download”菜单,点击下载: 此处下载的为Mac的安装包,…...

2017年 团体程序设计天梯赛——题解集
前言: Hello各位童学大家好!😊😊,茫茫题海你我相遇即是缘分呐,或许日复一日的刷题已经让你感到疲惫甚至厌倦了,但是我们真的真的已经达到了我们自身极限了吗?少一点自我感动…...
后进先出(LIFO)详解
LIFO 是 Last In, First Out 的缩写,中文译为后进先出。这是一种数据结构的工作原则,类似于一摞盘子或一叠书本: 最后放进去的元素最先出来 -想象往筒状容器里放盘子: (1)你放进的最后一个盘子(…...

大话软工笔记—需求分析概述
需求分析,就是要对需求调研收集到的资料信息逐个地进行拆分、研究,从大量的不确定“需求”中确定出哪些需求最终要转换为确定的“功能需求”。 需求分析的作用非常重要,后续设计的依据主要来自于需求分析的成果,包括: 项目的目的…...

渗透实战PortSwigger靶场-XSS Lab 14:大多数标签和属性被阻止
<script>标签被拦截 我们需要把全部可用的 tag 和 event 进行暴力破解 XSS cheat sheet: https://portswigger.net/web-security/cross-site-scripting/cheat-sheet 通过爆破发现body可以用 再把全部 events 放进去爆破 这些 event 全部可用 <body onres…...
【算法训练营Day07】字符串part1
文章目录 反转字符串反转字符串II替换数字 反转字符串 题目链接:344. 反转字符串 双指针法,两个指针的元素直接调转即可 class Solution {public void reverseString(char[] s) {int head 0;int end s.length - 1;while(head < end) {char temp …...

12.找到字符串中所有字母异位词
🧠 题目解析 题目描述: 给定两个字符串 s 和 p,找出 s 中所有 p 的字母异位词的起始索引。 返回的答案以数组形式表示。 字母异位词定义: 若两个字符串包含的字符种类和出现次数完全相同,顺序无所谓,则互为…...
Qt 事件处理中 return 的深入解析
Qt 事件处理中 return 的深入解析 在 Qt 事件处理中,return 语句的使用是另一个关键概念,它与 event->accept()/event->ignore() 密切相关但作用不同。让我们详细分析一下它们之间的关系和工作原理。 核心区别:不同层级的事件处理 方…...
深度学习之模型压缩三驾马车:模型剪枝、模型量化、知识蒸馏
一、引言 在深度学习中,我们训练出的神经网络往往非常庞大(比如像 ResNet、YOLOv8、Vision Transformer),虽然精度很高,但“太重”了,运行起来很慢,占用内存大,不适合部署到手机、摄…...

C++实现分布式网络通信框架RPC(2)——rpc发布端
有了上篇文章的项目的基本知识的了解,现在我们就开始构建项目。 目录 一、构建工程目录 二、本地服务发布成RPC服务 2.1理解RPC发布 2.2实现 三、Mprpc框架的基础类设计 3.1框架的初始化类 MprpcApplication 代码实现 3.2读取配置文件类 MprpcConfig 代码实现…...

《信号与系统》第 6 章 信号与系统的时域和频域特性
目录 6.0 引言 6.1 傅里叶变换的模和相位表示 6.2 线性时不变系统频率响应的模和相位表示 6.2.1 线性与非线性相位 6.2.2 群时延 6.2.3 对数模和相位图 6.3 理想频率选择性滤波器的时域特性 6.4 非理想滤波器的时域和频域特性讨论 6.5 一阶与二阶连续时间系统 6.5.1 …...

倒装芯片凸点成型工艺
UBM(Under Bump Metallization)与Bump(焊球)形成工艺流程。我们可以将整张流程图分为三大阶段来理解: 🔧 一、UBM(Under Bump Metallization)工艺流程(黄色区域ÿ…...