Node.js如何解析Form上传?

2017 年 8 月 1 日 前端之巅 黄俊涛

本文为作者投稿。

NPM 和 GitHub 里的开源组件帮我们解决了很多繁琐的工作,但是也让我们失去了很多深入技术细节的机会。在现有组件无法满足我们需求的时候,就需要我们来自己动手丰衣足食了。

作者前段时间遇到了一个需要手动解析 Form 表单上传的机会,也借此为各位解析一下 Node.js 解析 Form 上传的实现细节。


背景


半年前微信小程序的推出,掀起了一股小程序开发的热潮,作者一样收到了来自产品妹子的小程序开发需求。

需求中涉及到照片 / 视频上传的功能,而我们本身是全国最大的照片上传服务产品 --QQ 空间相册,我们各主要上传渠道的上传都采用了 socket 分片上传技术,包括客户端和 PC 浏览器端。微信小程序提供的上传接口只能是 Form 表单上传,且又没有提供读取本地文件内容的接口,打消了我们通过客户端分片上传的念头。

所以现在的问题是从小程序发出的是 Form 表单的上传请求,后端提供的是基于 socket 的分片上传服务,我们需要把两者对接起来。

我们的解决方案就是在 Node.js 层加一个适配接入,接收来自小程序的 Form 上传请求,然后解析图片内容,并分片上传到后端。


方案


现在的任务就分为两部分:

  1. 解析 Form 上传表单,将图片 / 视频内容解析出来;

  2. 将图片 / 视频内容拆分成固定大小的小片数据包,通过后端提供的私有协议接口上传;

那在这里我们又面临了一个选择,是等整个 Form 表单接收完成再进行分片上传,还是收到 Form 表单就开始上传呢?

前者我们可以直接引入现成的 multipart 的 Node.js 解析组件,临时存储到本地或者内存,然后进行上传,但是缺点是整个上传过程变成了两个串行过程,上传延时会增加,另外我们同时支持图片和视频的上传,会导致 Node.js 侧的内存占用比较高。

而后者则是尽量同步,接收到图片 / 视频 part 我们就可以开始上传,只是没有刚好满足我们需求的组件来支持。基于以上对比我们选择后面的方案,自己来解析 Form 上传表单,整个上传流程可以看下面的时序图。


Form 表单


Form 表单我们平时并不少接触,但是需要自己去解析的场景并不多,在解析 Form 表单前,我们先简单看一下 Form 表单的结构是怎么样的。

--${bound}
Content-Disposition: form-data; name="Filename"

IMG-0719.jpg
--${bound}
Content-Disposition: form-data; name="filedata"; filename="IMG-0719.jpg"
Content-Type: application/octet-stream

file content
--${bound}
Content-Disposition: form-data; name="Upload"

Submit Query
--${bound}--

可以看到每一个表单 field 都是以一个 --{bound} 开头的,然后是一些属性信息,属性和内容之间都有一个空行,整个 Form 表单则以 --{bound}-- 结束。那 bound 是一个什么存在呢?答案就在 Http 请求头里,因为 Form 请求的 Content-type 一般是这样的:

Content-Type: multipart/form-data; boundary=--49348984387434655602146

--49348984387434655602146 就是我们要找的 bound,作用就是将 Form 表单里的 field 分隔开来,所以这个串一般比较复杂,并且有足够的长度。

再整理一下我们的思路:

  1. 先从 Http 请求头里找到 Content-type,并渠道 boundary 的内容,该内容为表单分隔符;

  2. 对表单内容从前往后查找 --{bound}--{bound}----{bound}--{bound}或者 --{bound}-- 之间的内容就是单个表单 field 内容;

  3. 单个表单内容第一个空行之前的部分为表单头部,头部以换行符分隔,空行后面的内容则为 field 内容;


解析


Form 表单是流式的,表单 field 的查找其实是线性的,且是有状态的,整个解析过程我们可以看成是一个有限状态机,而 Form 请求的 data 事件则是这个状态机的驱动器。

可以看到解析器可能存在 5 个状态,分别为:

  1. 空闲,即初始状态;

  2. 解析到 field,即进入到待解析 field 内容的状态,后面的内容都存入临时 buffer,每次 data 事件都会更新这个 buffer,并触发一次检查是否有一个空行存在,即一组\r\n\r\n

  3. 解析 field 头部,在遇到\r\n\r\n之后,临时 buffer 空行前面的内容即为 field 头部,根据换行拆分出单个头部,基于字符串解析就能获取到头部信息,这里就不详解了;

  4. 解析 field 内容,再遇到\r\n\r\n之后,临时 buffer 空行后面的内容就包含了 field 的内容,但是要注意,空行后面的内容并不是全都是当前 field 的内容,我们需要对临时 buffer 空行后面的内容中寻找--{bound}或者--{bound}--,如果查找到--{bound},则分隔符前面的内容为当前 field 内容,分隔符后面的内容为下一个 field,那解析器则进入到状态 2 《解析到 field》;如果查找到--{bound}--,则意味着当前 field 为 Form 表单最后一个 field,分隔符前面的内容为 field 内容,同时进入到状态 5 《Form 结束》 ;如果临时 buffer 里--{bound}--{bound}--都没有找到,意味着当前 field 内容还没有接受完成,那么空行后面的内容均为 field 内容,每次 data 事件则会触发上述状态转换查询逻辑进行一遍;

  5. Form 结束,即整个 Form 表单解析完成了。

再回到我们的上传场景中,我们需要将收到的图片 / 视频内容同步打包传给后端,意味着在状态 4 的处理中,如果 field 是图片 / 视频内容,就需要将收到的文件部分打包传输。在查找到--{bound}--{bound}--的情况下比较好处理,分隔符前的内容都是文件内容,直接拆分打包传输就可以了,但是在--{bound}--{bound}--都没有找到的时候,我们将哪部分内容进行打包呢?

虽然没有查找到这两种分隔符,但是有可能当前 buffer 的结尾已经包含了部分分隔符的内容,所以我们需要预留出一部分 buffer,这段 buffer 前面的部分认为是文件内容是安全的,这段 buffer 只能等与后续收到的 data 拼接在一起才知道是不是文件内容。因为预留的这段 buffer 的目的是为了防止将分隔符当做文件内容传输了,所以预留的这段 buffer 长度应该不短于 buffer 长度减一,即 buffer 的长度为bound.length + 4 - 1,4 代表 4 个-长度。


总结


本文只是根据实际需求和应用场景介绍了 Form 表单的解析细节,并附带介绍了在 Form 表单流式接收的过程中流式代理上传的方案。

目前前端技术栈越来越丰富,前端社区越来越活跃,我们有海量的开源组件,唾手可得的框架库以及全链路支持的工具集,但是我们仍然需要掌握能探究技术细节的能力,在关键技术节点上往往还是要回归到技术细节本身。



前端之巅



关注「前端之巅」,紧跟前端发展,共享一线技术。各位淀粉投稿请发邮件到 editors@cn.infoq.com,注明“前端之巅投稿”。


登录查看更多
0

相关内容

Node.js 是一个在浏览器外部创建互联网应用程序的框架,它基于 Google 开发的 V8 JavaScript 引擎,轻量,高效,事件驱动,非阻塞I/O,特别适合运行于跨分布式设备的实时数据处理程序。
【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
56+阅读 · 2020年6月26日
【实用书】Python爬虫Web抓取数据,第二版,306页pdf
专知会员服务
115+阅读 · 2020年5月10日
Python分布式计算,171页pdf,Distributed Computing with Python
专知会员服务
105+阅读 · 2020年5月3日
【2020新书】如何认真写好的代码和软件,318页pdf
专知会员服务
63+阅读 · 2020年3月26日
【干货书】流畅Python,766页pdf,中英文版
专知会员服务
223+阅读 · 2020年3月22日
【资源】100+本免费数据科学书
专知会员服务
105+阅读 · 2020年3月17日
Keras作者François Chollet推荐的开源图像搜索引擎项目Sis
专知会员服务
29+阅读 · 2019年10月17日
在K8S上运行Kafka合适吗?会遇到哪些陷阱?
DBAplus社群
9+阅读 · 2019年9月4日
阿里技术大牛:一份架构师成神路线图!
51CTO博客
29+阅读 · 2019年7月6日
PHP使用Redis实现订阅发布与批量发送短信
安全优佳
7+阅读 · 2019年5月5日
7 款实用到哭的App,只说一遍
高效率工具搜罗
84+阅读 · 2019年4月30日
使用 Canal 实现数据异构
性能与架构
20+阅读 · 2019年3月4日
如何用GitLab本地私有化部署代码库?
Python程序员
9+阅读 · 2018年12月29日
解析京东大数据下高效图像特征提取方案
京东大数据
4+阅读 · 2017年9月29日
咦,用浏览器做人脸检测,竟然这么简单?
机械鸡
4+阅读 · 2017年9月11日
The Evolved Transformer
Arxiv
5+阅读 · 2019年1月30日
Arxiv
5+阅读 · 2018年10月23日
Arxiv
3+阅读 · 2012年11月20日
VIP会员
相关VIP内容
【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
56+阅读 · 2020年6月26日
【实用书】Python爬虫Web抓取数据,第二版,306页pdf
专知会员服务
115+阅读 · 2020年5月10日
Python分布式计算,171页pdf,Distributed Computing with Python
专知会员服务
105+阅读 · 2020年5月3日
【2020新书】如何认真写好的代码和软件,318页pdf
专知会员服务
63+阅读 · 2020年3月26日
【干货书】流畅Python,766页pdf,中英文版
专知会员服务
223+阅读 · 2020年3月22日
【资源】100+本免费数据科学书
专知会员服务
105+阅读 · 2020年3月17日
Keras作者François Chollet推荐的开源图像搜索引擎项目Sis
专知会员服务
29+阅读 · 2019年10月17日
相关资讯
在K8S上运行Kafka合适吗?会遇到哪些陷阱?
DBAplus社群
9+阅读 · 2019年9月4日
阿里技术大牛:一份架构师成神路线图!
51CTO博客
29+阅读 · 2019年7月6日
PHP使用Redis实现订阅发布与批量发送短信
安全优佳
7+阅读 · 2019年5月5日
7 款实用到哭的App,只说一遍
高效率工具搜罗
84+阅读 · 2019年4月30日
使用 Canal 实现数据异构
性能与架构
20+阅读 · 2019年3月4日
如何用GitLab本地私有化部署代码库?
Python程序员
9+阅读 · 2018年12月29日
解析京东大数据下高效图像特征提取方案
京东大数据
4+阅读 · 2017年9月29日
咦,用浏览器做人脸检测,竟然这么简单?
机械鸡
4+阅读 · 2017年9月11日
Top
微信扫码咨询专知VIP会员