Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nodejs教程16:POST文件上传 #47

Open
chencl1986 opened this issue Mar 18, 2019 · 2 comments
Open

Nodejs教程16:POST文件上传 #47

chencl1986 opened this issue Mar 18, 2019 · 2 comments

Comments

@chencl1986
Copy link
Owner

阅读更多系列文章请访问我的GitHub博客,示例代码请访问这里

简单的文件上传例子

处理文件上传数据,也是前后端交互中重要的功能,它的处理方式与数据不同。

接下来,通过一个例子查看服务端接收到的文件上传数据。

首先,在post_file.html中,新建一个用与上传文件的表单:

form的属性enctype="multipart/form-data"代表表单上传的是文件。

enctype的默认值为enctype="application/x-www-form-urlencoded"表示上传的是数据类型,此时服务端接收到的数据为“username=lee&password=123456&file=upload.txt”。

代码示例:/lesson16/post_file.html

<form action="http://localhost:8080/upload" method="POST" enctype="multipart/form-data">
  用户:<input type="text" name="username" value="lee"><br/>
  密码:<input type="password" name="password" value="123456"><br/>
  <input type="file" name="file" id=""><br/>
  <input type="submit" value="提交">
</form>

其次,在server.js中,查看接收到的表单提交数据:

代码示例:/lesson16/server.js

const http = require('http')

const server = http.createServer((req, res) => {
  let arr = []

  req.on('data', (buffer) => {
    arr.push(buffer)
  })

  req.on('end', () => {
    let buffer = Buffer.concat(arr)

    console.log(buffer.toString())
  })
})

server.listen(8080)

最后,在表单中上传/lesson16/upload.txt文件,并查看打印出的结果:

------WebKitFormBoundaryL5AGcit70yhKB92Y
Content-Disposition: form-data; name="username"

lee
------WebKitFormBoundaryL5AGcit70yhKB92Y
Content-Disposition: form-data; name="password"

123456
Content-Disposition: form-data; name="file"; filename="upload.txt"
Content-Type: text/plain

upload
------WebKitFormBoundaryL5AGcit70yhKB92Y--

文件上传数据分析

通过分析上面这个例子中,服务端接收到的数据,可以得到以下信息:

  1. 表单上传的数据,被分隔符“------WebKitFormBoundaryL5AGcit70yhKB92Y”隔开,分隔符在每次上传时都不同。分隔符数据可以从req.headers['content-type']中获取,如:const boundary = '--' + req.headers['content-type'].split('; ')[1].split('=')[1]
  2. 前两段数据中,分别可以获取到表单上传的字段名name="username",以及数据“lee”。
  3. 第三段数据中,多了一个字段filename="upload.txt",它表示的是文件的原始名称。以及可以获取到文件类型“Content-Type: text/plain”,表示这是一个文本文件。最后是文件的内容“upload”。

由此可以看出,文件上传数据虽然有些乱,但还是有规律的,那么处理思路就是按照规律,将数据切割之后,取出其中有用的部分。

文件上传数据简化

先回顾一下上面的数据,并将回车符标记出来:

------WebKitFormBoundaryL5AGcit70yhKB92Y\r\n
Content-Disposition: form-data; name="username"\r\n
\r\n
lee\r\n
------WebKitFormBoundaryL5AGcit70yhKB92Y\r\n
Content-Disposition: form-data; name="password"\r\n
\r\n
123456\r\n
Content-Disposition: form-data; name="file"; filename="upload.txt"\r\n
Content-Type: text/plain\r\n
\r\n
upload\r\n
------WebKitFormBoundaryL5AGcit70yhKB92Y--

可以看出,每段数据的结构其实是这样的:

------WebKitFormBoundaryL5AGcit70yhKB92Y\r\nContent-Disposition: form-data; name="username"\r\n\r\nlee\r\n

将每段上传数据简化如下:

<分隔符>\r\n字段头\r\n\r\n内容\r\n

也就是说,整个表单的数据,就是按照这样的数据格式组装而成。

需要注意的是,在表单数据的结尾不再是\r\n,而是“--”。

文件上传数据处理步骤

  1. 用<分隔符>切分数据:
[
  ‘’,
  "\r\n字段信息\r\n\r\n内容\r\n",
  "\r\n字段信息\r\n\r\n内容\r\n",
  "\r\n字段信息\r\n\r\n内容\r\n",
  '--'
]
  1. 删除数组头尾数据:
[
  "\r\n字段信息\r\n\r\n内容\r\n",
  "\r\n字段信息\r\n\r\n内容\r\n",
  "\r\n字段信息\r\n\r\n内容\r\n",
]
  1. 将每一项数据头尾的的\r\n删除:
[
  "字段信息\r\n\r\n内容",
  "字段信息\r\n\r\n内容",
  "字段信息\r\n\r\n内容",
]
  1. 将每一项数据中间的\r\n\r\n删除,得到最终结果:
[
	"字段信息", "内容",
	"字段信息", "内容",
	"字段信息", "内容",
]

Buffer的数据处理

由于文件都是二进制数据,不能直接将其转换为字符串后再进行处理,否则数据会出错,因此要通过Buffer模块进行数据处理操作。

Buffer模块提供了indexOf方法获取Buffer数据中,其参数所在位置的index值。

Buffer模块提供了slice方法,可通过index值切分Buffer数据。

先测试一下这两个方法:

示例代码:/lesson16/buffer.js

let buffer = Buffer.from('lee\r\nchen\r\ntest')

const index = buffer.indexOf('\r\n')

console.log(index)
console.log(buffer.slice(0, index).toString())

可以看到打印结果分别为3和"lee",也就是说,我们先找到了"\r\n"所在的index为3,之后从Buffer数据的index为0的位置,切割到index为3的位置,得到了正确的结果。

由此,可以封装一个专门用于切割Buffer数据的方法:

示例代码:/lesson16/bufferSplit.js

module.exports = function bufferSplit(buffer, separator) {
  let result = [];
  let index = 0;

  while ((index = buffer.indexOf(separator)) != -1) {
    result.push(buffer.slice(0, index));
    buffer = buffer.slice(index + separator.length);
  }
  result.push(buffer);

  return result;
}

有了bufferSplit方法,就可以正式开始处理数据了。

文件上传数据处理

根据上面的思路,就可以实现一个完整的文件上传流程。

代码示例:/lesson16/server.js

const http = require('http')
const fs = require('fs')
const bufferSplit = require('./bufferSplit')

const server = http.createServer((req, res) => {
  const boundary = `--${req.headers['content-type'].split('; ')[1].split('=')[1]}`  // 获取分隔符
  let arr = []

  req.on('data', (buffer) => {
    arr.push(buffer)
  })

  req.on('end', () => {
    const buffer = Buffer.concat(arr)
    console.log(buffer.toString())

    // 1. 用<分隔符>切分数据
    let result = bufferSplit(buffer, boundary)
    console.log(result.map(item => item.toString()))

    // 2. 删除数组头尾数据
    result.pop()
    result.shift()
    console.log(result.map(item => item.toString()))

    // 3. 将每一项数据头尾的的\r\n删除
    result = result.map(item => item.slice(2, item.length - 2))
    console.log(result.map(item => item.toString()))

    // 4. 将每一项数据中间的\r\n\r\n删除,得到最终结果
    result.forEach(item => {
      console.log(bufferSplit(item, '\r\n\r\n').map(item => item.toString()))

      let [info, data] = bufferSplit(item, '\r\n\r\n')  // 数据中含有文件信息,保持为Buffer类型

      info = info.toString()  // info为字段信息,这是字符串类型数据,直接转换成字符串,若为文件信息,则数据中含有一个回车符\r\n,可以据此判断数据为文件还是为普通数据。

      if (info.indexOf('\r\n') >= 0) {  // 若为文件信息,则将Buffer转为文件保存
        // 获取字段名
        let infoResult = info.split('\r\n')[0].split('; ')
        let name = infoResult[1].split('=')[1]
        name = name.substring(1, name.length - 1)

        // 获取文件名
        let filename = infoResult[2].split('=')[1]
        filename = filename.substring(1, filename.length - 1)
        console.log(name)
        console.log(filename)

        // 将文件存储到服务器
        fs.writeFile(`./upload/${filename}`, data, err => {
          if (err) {
            console.log(err)
          } else {
            console.log('文件上传成功')
          }
        })
      } else {  // 若为数据,则直接获取字段名称和值
        let name = info.split('; ')[1].split('=')[1]
        name = name.substring(1, name.length - 1)
        const value = data.toString()
        console.log(name, value)
      }
    })
  })
})

server.listen(8080)
@pengchengzhong
Copy link

请问为什么我跟着你写的代码运行http://localhost:8080/post_file.html不显示
加入这段代码才可以加载出页面。不做buffer处理之前上传文件哪儿也提示失败
if(req.url !== '/favicon.ico'){
fs.readFile(./${req.url},(error,buffer)=>{
if (error) { // 若读取错误,则向前端返回404状态码,以及内容Not Found。
res.writeHead(404)
res.write('Not Found')
} else { // 若读取成功,则向前端返回读取到的文件。
res.write(buffer)
}
res.end() // 关闭连接。
})
}

@pengchengzhong
Copy link

请问为什么我跟着你写的代码运行http://localhost:8080/post_file.html不显示
加入这段代码才可以加载出页面。不做buffer处理之前上传文件哪儿也提示失败
if(req.url !== '/favicon.ico'){
fs.readFile(./${req.url},(error,buffer)=>{
if (error) { // 若读取错误,则向前端返回404状态码,以及内容Not Found。
res.writeHead(404)
res.write('Not Found')
} else { // 若读取成功,则向前端返回读取到的文件。
res.write(buffer)
}
res.end() // 关闭连接。
})
}

我这边直接运行github lesson16代码也报错

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants