- 文档
- 教程
- 后端
【免费下载链接】nodejs-learning-guide
Nodejs学习笔记以及经验总结,公众号"程序猿小卡"
body-parser是 Express 生态中最常用的请求体解析中间件,两行代码即可覆盖绝大多数 POST 请求的解析场景。本文以 nodejs-learning-guide 仓库中的 进阶/body-parser.md 为骨架,从 HTTP 报文出发,逐步拆解text/plain、application/json、application/x-www-form-urlencoded三类请求体的解析、gbk等非 UTF-8 编码的解码、gzip压缩流的解压,并给出可直接运行的客户端/服务端示例(源码位于 examples/2017.05.20-express-body-parser),帮助你理解其底层实现,读完即可独立实现一个极简版 body-parser。
写在前面:body-parser 是什么
body-parser是非常常用的 Express 中间件,作用是对 HTTP 请求体(request body)进行解析。它的使用非常简单,以下两行代码已经覆盖了大部分的使用场景:
app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false }));以仓库中的 started/server.js 为例,一个最小的 Express 应用只需要挂载bodyParser.urlencoded()即可:
var express = require('express'); var bodyParser = require('body-parser'); var app = express(); app.use(bodyParser.urlencoded({extended: false})); app.get('/test', function (req, res, next) { // 访问地址为:http://127.0.0.1:3030/test?nick=chyingp // 输出为:nick is chyingp res.end(`nick is ${req.query.nick}`); }); app.listen(3030);本文从简单例子出发,探究body-parser的内部实现,而不是重复官方使用文档。为了让读者能亲手验证每一个结论,仓库在 examples/2017.05.20-express-body-parser 下按主题拆分了六组可直接运行的服务端/客户端示例:
| 目录 | 主题 | 对应章节 |
|---|---|---|
| started | body-parser 基础使用 | 开头 |
| parser-text | 解析 text/plain | 一、1 |
| parser-json | 解析 application/json | 一、2 |
| parser-urlencoded | 解析 application/x-www-form-urlencoded | 一、3 |
| parser-with-encoding | 处理 gbk 等非默认编码 | 二 |
| parser-with-gzip | 处理 gzip 压缩请求体 | 三 |
入门基础:先看懂一个 POST 请求报文
在正式讲解前,我们先来看一个 POST 请求的报文:
POST /test HTTP/1.1 Host: 127.0.0.1:3000 Content-Type: text/plain; charset=utf8 Content-Encoding: gzip chyingp其中需要我们注意的有Content-Type、Content-Encoding以及报文主体:
- Content-Type:请求报文主体的类型、编码。常见的类型有
text/plain、application/json、application/x-www-form-urlencoded;常见的编码有utf8、gbk等。它的完整语法形如type/subtype; parameter=value,编码通常以charset参数的形式跟在类型之后(如text/plain; charset=utf8)。 - Content-Encoding:声明报文主体的压缩格式,常见的取值有
gzip、deflate、identity。identity表示不压缩,这也是示例客户端中的默认取值。 - 报文主体:这里是个普通的文本字符串
chyingp。
对服务端而言,"解析请求体"本质上就是回答三个问题:请求体是什么格式(由 Content-Type 决定)、用什么编码(由 Content-Type 的 charset 参数决定)、是否被压缩过(由 Content-Encoding 决定)。body-parser 的全部工作,就是围绕这三个头字段展开的。
body-parser 主要做了什么
body-parser实现的要点如下:
- 处理不同类型的请求体:比如
text、json、urlencoded等,对应的报文主体的格式不同; - 处理不同的编码:比如
utf8、gbk等; - 处理不同的压缩类型:比如
gzip、deflate等; - 其他边界、异常的处理。
下文将逐条展开,用最朴素的原生 Node.js 代码复现每一种能力——你会发现,body-parser 的核心逻辑并不神秘,每一步都是可独立验证的。
一、处理不同类型请求体
为了方便读者测试,以下例子均包含服务端、客户端代码,完整代码见 examples/2017.05.20-express-body-parser 下的对应目录。运行方式为:先node server.js启动服务端(默认监听 3000 端口),再在另一个终端node client.js发起请求,服务端会把解析结果回显在客户端终端中。
解析 text/plain
客户端请求的代码如下(见 parser-text/client.js),采用默认编码,不对请求体进行压缩,请求体类型为text/plain:
var http = require('http'); var options = { hostname: '127.0.0.1', port: '3000', path: '/test', method: 'POST', headers: { 'Content-Type': 'text/plain', 'Content-Encoding': 'identity' } }; var client = http.request(options, (res) => { res.pipe(process.stdout); }); client.end('chyingp');服务端代码如下(见 parser-text/server.js)。text/plain类型处理比较简单,就是 Buffer 的拼接:
var http = require('http'); var parsePostBody = function (req, done) { var arr = []; var chunks; req.on('data', buff => { arr.push(buff); }); req.on('end', () => { chunks = Buffer.concat(arr); done(chunks); }); }; var server = http.createServer(function (req, res) { parsePostBody(req, (chunks) => { var body = chunks.toString(); res.end(`Your nick is ${body}`) }); }); server.listen(3000);这里的实现思路值得记住:请求体可能被拆成多个 TCP 分片到达,因此不能直接读取一次data事件就拿结果,而是把每次收到的 Buffer 依次push进数组,等end事件触发后用Buffer.concat(arr)拼成完整请求体,最后toString()得到字符串。body-parser 内部对请求体的收集逻辑与这段代码一脉相承。
解析 application/json
客户端代码如下(见 parser-json/client.js),把Content-Type换成application/json:
var http = require('http'); var querystring = require('querystring'); var options = { hostname: '127.0.0.1', port: '3000', path: '/test', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Encoding': 'identity' } }; var jsonBody = { nick: 'chyingp' }; var client = http.request(options, (res) => { res.pipe(process.stdout); }); client.end( JSON.stringify(jsonBody) );服务端代码如下(见 parser-json/server.js)。相比text/plain,只是多了个JSON.parse()的过程:
var http = require('http'); var parsePostBody = function (req, done) { var length = req.headers['content-length'] - 0; var arr = []; var chunks; req.on('data', buff => { arr.push(buff); }); req.on('end', () => { chunks = Buffer.concat(arr); done(chunks); }); }; var server = http.createServer(function (req, res) { parsePostBody(req, (chunks) => { var json = JSON.parse( chunks.toString() ); // 关键代码 res.end(`Your nick is ${json.nick}`) }); }); server.listen(3000);注意两点:一是发送端用JSON.stringify(jsonBody)把对象序列化成 JSON 字符串;二是接收端用JSON.parse(chunks.toString())把文本还原成对象,随后就可以像访问普通对象一样取用json.nick。这对应 body-parser 中json解析器的核心动作。
解析 application/x-www-form-urlencoded
客户端代码如下(见 parser-urlencoded/client.js),这里通过querystring对请求体进行格式化,得到类似nick=chyingp的字符串:
var http = require('http'); var querystring = require('querystring'); var options = { hostname: '127.0.0.1', port: '3000', path: '/test', method: 'POST', headers: { 'Content-Type': 'form/x-www-form-urlencoded', 'Content-Encoding': 'identity' } }; var postBody = { nick: 'chyingp' }; var client = http.request(options, (res) => { res.pipe(process.stdout); }); client.end( querystring.stringify(postBody) );提示:示例中的
Content-Type写成了form/x-www-form-urlencoded,这只是为了演示而随意起的类型名。实际生产环境中,浏览器提交表单的标准类型是application/x-www-form-urlencoded,body-parser 的urlencoded解析器也正是匹配后者。
服务端代码如下(见 parser-urlencoded/server.js),同样跟text/plain的解析差不多,就多了个querystring.parse()的调用:
var http = require('http'); var querystring = require('querystring'); var parsePostBody = function (req, done) { var length = req.headers['content-length'] - 0; var arr = []; var chunks; req.on('data', buff => { arr.push(buff); }); req.on('end', () => { chunks = Buffer.concat(arr); done(chunks); }); }; var server = http.createServer(function (req, res) { parsePostBody(req, (chunks) => { var body = querystring.parse( chunks.toString() ); // 关键代码 res.end(`Your nick is ${body.nick}`) }); }); server.listen(3000);querystring.parse()会把nick=chyingp形式的字符串解析成{ nick: 'chyingp' }。这就是 HTML 表单(POST 方法)默认提交格式的解析原理。
小结:三种类型的差异只在一个转换函数
把三个服务端实现放在一起对比,可以看到收集请求体的骨架代码完全一致,差异仅在最后的"文本 → 结构"一步:
| Content-Type | 转换函数 | 产物 |
|---|---|---|
text/plain | chunks.toString() | 字符串 |
application/json | JSON.parse(chunks.toString()) | 对象 |
application/x-www-form-urlencoded | querystring.parse(chunks.toString()) | 对象 |
这正是 body-parser 的设计思路:先统一收集请求体 Buffer,再根据Content-Type分发到不同的解析器,由各解析器完成对应的转换。
二、处理不同编码
很多时候,来自客户端的请求采用的不一定是默认的utf8编码,这个时候就需要对请求体进行解码处理。
客户端请求如下(见 parser-with-encoding/client.js),有两个要点:
- 编码声明:在
Content-Type最后加上;charset=gbk; - 请求体编码:借助
iconv-lite,对请求体进行编码iconv.encode('程序猿小卡', encoding)。
var http = require('http'); var iconv = require('iconv-lite'); var encoding = 'gbk'; // 请求编码 var options = { hostname: '127.0.0.1', port: '3000', path: '/test', method: 'POST', headers: { 'Content-Type': 'text/plain; charset=' + encoding, 'Content-Encoding': 'identity', } }; // 备注:nodejs本身不支持gbk编码,所以请求发送前,需要先进行编码 var buff = iconv.encode('程序猿小卡', encoding); var client = http.request(options, (res) => { res.pipe(process.stdout); }); client.end(buff, encoding);服务端代码如下(见 parser-with-encoding/server.js),这里多了两个步骤:编码判断、解码操作。首先通过Content-Type获取编码类型gbk,然后通过iconv-lite进行反向解码操作:
var http = require('http'); var contentType = require('content-type'); var iconv = require('iconv-lite'); var parsePostBody = function (req, done) { var obj = contentType.parse(req.headers['content-type']); var charset = obj.parameters.charset; // 编码判断:这里获取到的值是 'gbk' var arr = []; var chunks; req.on('data', buff => { arr.push(buff); }); req.on('end', () => { chunks = Buffer.concat(arr); var body = iconv.decode(chunks, charset); // 解码操作 done(body); }); }; var server = http.createServer(function (req, res) { parsePostBody(req, (body) => { res.end(`Your nick is ${body}`) }); }); server.listen(3000);这里值得留意content-type这个模块:contentType.parse()会把text/plain; charset=gbk拆成{ type: 'text/plain', parameters: { charset: 'gbk' } }这样的结构,从而让我们能精确取到charset参数。body-parser 内部正是借助类似机制来判断请求体的编码,并在charset缺省时回退到默认编码(对text、json等类型通常是utf-8)。
另一个关键点是:Node.js 原生并不支持 gbk 编码,UTF-8 之外的中文编码(gbk、gb2312、big5 等)都需要借助iconv-lite这类第三方库完成编码/解码,这就是服务端必须iconv.decode(chunks, charset)的原因。
三、处理不同压缩类型
这里举一个gzip压缩的例子。客户端代码如下(见 parser-with-gzip/client.js),要点如下:
- 压缩类型声明:
Content-Encoding赋值为gzip; - 请求体压缩:通过
zlib模块对请求体进行 gzip 压缩。
var http = require('http'); var zlib = require('zlib'); var options = { hostname: '127.0.0.1', port: '3000', path: '/test', method: 'POST', headers: { 'Content-Type': 'text/plain', 'Content-Encoding': 'gzip' } }; var client = http.request(options, (res) => { res.pipe(process.stdout); }); // 注意:将 Content-Encoding 设置为 gzip 的同时,发送给服务端的数据也应该先进行gzip var buff = zlib.gzipSync('chyingp'); client.end(buff);服务端代码如下(见 parser-with-gzip/server.js),通过zlib模块对请求体进行解压缩操作(gunzip):
var http = require('http'); var zlib = require('zlib'); var parsePostBody = function (req, done) { var length = req.headers['content-length'] - 0; var contentEncoding = req.headers['content-encoding']; var stream = req; // 关键代码如下 if(contentEncoding === 'gzip') { stream = zlib.createGunzip(); req.pipe(stream); } var arr = []; var chunks; stream.on('data', buff => { arr.push(buff); }); stream.on('end', () => { chunks = Buffer.concat(arr); done(chunks); }); stream.on('error', error => console.error(error.message)); }; var server = http.createServer(function (req, res) { parsePostBody(req, (chunks) => { var body = chunks.toString(); res.end(`Your nick is ${body}`) }); }); server.listen(3000);这段代码展示了stream思维:req本身是一个 Readable 流。当检测到Content-Encoding: gzip时,用zlib.createGunzip()创建一个解压转换流,然后req.pipe(stream)让请求体数据流经解压流,再在stream上监听data/end收集解压后的 Buffer。这样"收集请求体"的逻辑只需写一遍,压缩与否只影响数据源是req还是解压后的stream。
从实现上看,body-parser 的处理方式与此一致:它会根据Content-Encoding创建对应的解压流(gzip、deflate等),把请求体接入解压管道后再进入具体的类型解析器。
写在后面:核心不复杂,复杂的是边界
body-parser的核心实现并不复杂,翻看源码后你会发现,更多的代码是在处理异常跟边界。所谓"边界",至少包括以下几类,本文的示例出于演示目的大多刻意避开了:
- 请求体超过
limit上限(body-parser 默认对请求体大小有限制,超限应返回 413 或抛出错误); Content-Type与解析器不匹配(body-parser 靠type-is之类的匹配机制决定是否接管某个请求,不匹配就直接next()放行);JSON.parse失败等解析异常(应返回 400 而非让服务端进程崩溃);charset缺失或不被支持、压缩流解压失败(stream上的error事件需要被监听,正如 gzip 示例中所做的那样);- 空请求体、重复的
content-type头等协议层面的脏数据。
另外,对于 POST 请求还有一个非常常见的Content-Type是multipart/form-data(主要用于文件上传),它的处理相对复杂,body-parser不打算对其进行支持,通常交给multer等专门中间件处理。
相关链接与延伸阅读
- 本文配套的可运行示例代码:examples/2017.05.20-express-body-parser(内含
started、parser-text、parser-json、parser-urlencoded、parser-with-encoding、parser-with-gzip六组客户端/服务端代码); - 正文所依据的原始笔记:进阶/body-parser.md;
- 仓库中与请求体解析相关的延伸主题:文件上传-multer.md(
multipart/form-data的完整处理方案)、express+session实现简易身份认证.md(中间件组合实战)、post-body.md; - 依赖的技术模块:
iconv-lite(gbk 等非 UTF-8 编码的编解码)、content-type(Content-Type 头解析)、Node.js 内置zlib(gzip/deflate 压缩流)。
- 文档
- 教程
- 后端
【免费下载链接】nodejs-learning-guide
Nodejs学习笔记以及经验总结,公众号"程序猿小卡"
相关推荐
GPT-SoVITS 情感控制教程:用参考音频给合成语音加上喜怒哀乐
GPT SoVITS 情感控制教程:用参考音频给合成语音加上喜怒哀乐 从一个具体场景说起 你在做有声书,旁白要生气地说一句“别再跟着我了”。直接用普通 TTS
文档教程后端Windows COM自动化:PythonForWindows COM接口与WMI查询完全教程
Windows COM自动化:PythonForWindows COM接口与WMI查询完全教程 想在 Python 中实现 Windows COM 自动化 吗?
Express-validator中间件深度解析:check、body、query的区别
Express validator中间件深度解析:check、body、query的区别 Express validator是Express.js生态中一个功能
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考