最近公司项目有需要处理文件上传的情况,本地开发遇到一些问题,后面经过排查解决掉。也因此对文件上传的原理产生一些兴趣,于是自己研究了下原生nodejs处理文件上传的情况。
几个重要概念
Content-Type
Content-Type 实体头部用于指示资源的MIME类型 media type 。
在响应中,Content-Type标头告诉客户端实际返回的内容的内容类型。
在请求中 (如POST 或 PUT),客户端告诉服务器实际发送的数据类型。
Content-Type 由三部分组成
- media-type
资源或数据的 MIME type。例如: application/json - charset
字符编码标准。 例如: utf-8 - boundary
对于多部分实体,boundary 是必需的,其包括来自一组字符的1到70个字符。
Content-Disposition
在常规的 HTTP 应答中,Content-Disposition 响应头指示回复的内容该以何种形式展示,是以内联的形式(即网页或者页面的一部分),还是以附件的形式下载并保存到本地
在 multipart/form-data 类型的应答消息体中,Content-Disposition 消息头可以被用在 multipart 消息体的子部分中,用来给出其对应字段的相关信息。各个子部分由在Content-Type 中定义的分隔符分隔。用在消息体自身则无实际意义。
Content-Disposition 消息头最初是在 MIME 标准中定义的,HTTP 表单及 POST 请求只用到了其所有参数的一个子集。只有 form-data 以及可选的 name 和 filename 三个参数可以应用在HTTP场景中。
实际场景应用
在这里,我们以上传一个temp.txt文件举例,temp.txt文件内容为 test:::。
首先 上传文件 我们需要将传给服务端的消息的Content-Type设为 multipart/form-data,在表单中 我们可以指定enctype 值为multipart/form-data
http
.createServer(function(req, res) {
if (req.url == "/upload" && req.method.toLowerCase() === "get") {
//显示一个用于文件上传的form
res.writeHead(200, { "content-type": "text/html" });
res.end(
'<form action="/upload" enctype="multipart/form-data" method="post">' +
'<input type="file" name="upload" multiple="multiple" />' +
'<input type="submit" value="Upload" />' +
"</form>"
);
}
})
.listen(3000);
这个时候 我们在浏览器中访问localhost:3000/upload 就可以访问到该表单。
然后nodejs 里面增加上传文件逻辑处理
if (req.url == "/upload" && req.method.toLowerCase() === "get") {
//显示一个用于文件上传的form
res.writeHead(200, { "content-type": "text/html" });
res.end(
'<form action="/upload" enctype="multipart/form-data" method="post">' +
// '<input type="text" name="description" value="multiple" />' +
'<input type="file" name="upload" multiple="multiple" />' +
'<input type="submit" value="Upload" />' +
"</form>"
);
} else if (req.url == "/upload" && req.method.toLowerCase() === "post") {
if (req.headers["content-type"].indexOf("multipart/form-data") !== -1)
var body = ''
req.on('data', (buffer) => {
body += buffer
})
req.on('end', () => {
res.end(JSON.stringify({
contentType: req.headers['content-type'],
content: body.toString()
}));
})
}
点击选择文件选中temp.txt,然后点击upload。然后页面上显示如下
{
"contentType":"multipart/form-data; boundary=----WebKitFormBoundaryflF412CVstMQDH5m",
"content":"------WebKitFormBoundaryflF412CVstMQDH5m\r\nContent-Disposition: form-data; name=\"upload\"; filename=\"temp.txt\"\r\nContent-Type: text/plain\r\n\r\ntest:::\r\n------WebKitFormBoundaryflF412CVstMQDH5m--\r\n"
}
这里我们将req.headers['content-type']和实体内容打印出来了, 可以看到 contentType里面有个boundary,值和content里面的一串字符串是一样的。当我们上传文件时,我们接收到headers里面的content-type的值就会包含boundary,boundary是用来分隔表单字段的,当你的表单里面有多个内容时 content 里面就会有多个这个值。类似这样
------WebKitFormBoundarymrD3BuxNQlp4oQ2R
Content-Disposition: form-data; name="description"
multiple
------WebKitFormBoundarymrD3BuxNQlp4oQ2R
Content-Disposition: form-data; name="upload"; filename="temp.txt"
Content-Type: text/plain
test:::
------WebKitFormBoundarymrD3BuxNQlp4oQ2R--
从上面的内容我们可以看到 test::: 是我们的文件内容。Content-Disposition 包含了我们的文件名。因此要实现我们的上传文件功能,我们只需要根据boundary 和 Content-Disposition 取到我们的文件名 和 文件内容 然后将文件内容写入到该文件名中即可。 下面是经整理过的完整代码
const http = require("http");
const fs = require("fs");
const querystring = require("querystring");
//用http模块创建一个http服务端
http
.createServer(function(req, res) {
if (req.url == "/upload" && req.method.toLowerCase() === "get") {
//显示一个用于文件上传的form
res.writeHead(200, { "content-type": "text/html" });
res.end(
'<form action="/upload" enctype="multipart/form-data" method="post">' +
'<input type="file" name="upload" multiple="multiple" />' +
'<input type="submit" value="Upload" />' +
"</form>"
);
} else if (req.url == "/upload" && req.method.toLowerCase() === "post") {
if (req.headers["content-type"].indexOf("multipart/form-data") !== -1) {
parseFile(req, res)
}
} else {
res.end("pelease upload file");
}
})
.listen(3000);
function parseFile(req, res) {
req.setEncoding("binary");
let body = ""; // 文件数据
let fileName = ""; // 文件名
// 边界字符串 boundary
const boundary = req.headers["content-type"]
.split("; ")[1]
.replace("boundary=", "");
console.log('req.headers["content-type"]', req.headers["content-type"]);
req.on("data", function(chunk) {
body += chunk;
});
req.on("end", function() {
const file = querystring.parse(body, "\r\n", ":");
console.log('file:::', JSON.stringify(file));
var fileInfo = file["Content-Disposition"].split("; ");
for (value in fileInfo) {
if (fileInfo[value].indexOf("filename=") != -1) {
console.log('fileInfo[value]', fileInfo[value]);
fileName = fileInfo[value].substring(10, fileInfo[value].length - 1);
if (fileName.indexOf("\\") != -1) {
fileName = fileName.substring(fileName.lastIndexOf("\\") + 1);
}
console.log("文件名: " + fileName);
}
}
const entireData = body.toString();
contentType = file["Content-Type"].substring(1);
//获取文件二进制数据开始位置,即contentType的结尾
const upperBoundary = entireData.indexOf(contentType) + contentType.length;
const shorterData = entireData.substring(upperBoundary);
console.log('shorterData', shorterData);
// 替换开始位置的空格
const binaryDataAlmost = shorterData
.replace(/^\s\s*/, "")
.replace(/\s\s*$/, "");
console.log('binaryDataAlmost', binaryDataAlmost);
// 去除数据末尾的额外数据,即: "--"+ boundary + "--"
const binaryData = binaryDataAlmost.substring(
0,
binaryDataAlmost.indexOf("--" + boundary + "--")
);
const bufferData = new Buffer.from(binaryData, "binary");
console.log("bufferData", bufferData);
fs.writeFile(fileName, bufferData, function(err) {
res.end("sucess");
});
});
}
总结
文件上传整个流程其实很好理解,前端上传的时候只需要设置content-type为multipart/form-data。然后服务端拿到headers 里面的boundary 在根据这个去处理得到文件内容即可。以上是nodejs 原生处理文件上传流程,略显复杂。现在koa 结合koa-body 处理文件上传很方便。后续有时间会补充上对应的处理流程