深入koa-bodyparser原理解析
一、前置知识
在理解koa-bodyparser原理之前,首先需要了解部分http相关的知识。
1、报文主体
http报文主要分为请求报文和响应报文,koa-bodyparser主要针对请求报文的处理。
请求报文主要由以下三个部分组成:
- 报文头部
- 空行
- 报文主体
而koa-bodyparser中的body指的就是请求报文中的报文主体部分。
2、服务器端获取报文主体流程
http底层采用tcp提供可靠的字节流服务,简单而言就是报文主体部分会被转化为二进制数据在网络中传输,所以服务器端首先需要拿到二进制流数据。
谈到网络传输,当然会涉及到传输速度的优化,而其中一种优化方式就是对内容进行压缩编码,常用的压缩编码方式有:
- gzip
- compress
- deflate
- identity(不执行压缩或不会变化的默认编码格式)
服务器端会根据报文头部信息中的content-encoding确认采用何种解压编码。
接下来就需要将二进制数据转换为相应的字符,而字符也有不同的字符编码方式,例如对于中文字符处理差异巨大的utf-8和gbk,utf-8编码汉字通常需要三个字节,而gbk只需要两个字节。所以还需要在请求报文的头部信息中设置content-type使用的字符编码信息(默认情况下采用的是utf-8),这样服务器端就可以利用相应的字符规则进行解码,得到正确的字符串。
拿到字符串之后,服务器端又要问了:客户端,你这一段字符串是啥意思啊?
根据不同的应用场景,客户端会对字符串采用不同的编码方式,常见的编码方式有:
- url编码方式: a=1&b=2
- json编码方式: {a:1,b:2}
客户端会将采用的字符串编码方式设置在请求报文头部信息的content-type属性中,这样服务器端根据相应的字符串编码规则进行解码,就能够明白客户端所传递的信息了。
下面一步步分析koa-bodyparser是如何处理这一系列操作,从而得到报文主体内容。
二、获取二进制数据流
nodejs中获取请求报文主体二进制数据流主要通过监听request对象的data事件完成:
// 示例一 const http = require('http') http.createserver((req, res) => { const body = [] req.on('data', chunk => { body.push(chunk) }) req.on('end', () => { const chunks = buffer.concat(body) // 接收到的二进制数据流 // 利用res.end进行响应处理 res.end(chunks.tostring()) }) }).listen(1234)
而koa-bodyparser主要是对的封装,而【co-body】中主要是采用模块获取请求报文主体的二进制数据流,【row-body】主要是对上述示例代码的封装和健壮性处理。
三、内容解码
客户端会将内容编码的方式放入请求报文头部信息content-encoding属性中,服务器端接收报文主体的二进制数据了时,会根据该头部信息进行解压操作,当然服务器端可以在响应报文头部信息accept-encoding属性中添加支持的解压方式。
而【row-body】主要采用 模块进行解压处理。
四、字符解码
一般而言,utf-8是互联网中主流的字符编码方式,前面也提到了还有gbk编码方式,相比较utf-8,它编码中文只需要2个字节,那么在字符解码时误用utf-8解码gbk编码的字符,就会出现中文乱码的问题。
nodejs主要通过buffer处理二进制数据流,但是它并不支持gbk字符编码方式,需要通过 iconv-lite 模块进行处理。
【示例一】中的代码就存在没有正确处理字符编码的问题,那么报文主体中的字符采用gbk编码方式,必然会出现中文乱码:
const request = require('request') const iconv = require('iconv-lite') request.post({ url: 'http://localhost:1234/', body: iconv.encode('中文', 'gbk'), headers: { 'content-type': 'text/plain;charset=gbk' } }, (error, response, body) => { console.log(body) // 发生中文乱码情况 })
nodejs中的buffer默认是采用utf-8字符编码处理,这里借助【iconv-lite】模块处理不同的字符编码方式:
const chunks = buffer.concat(body) res.end(iconv.decode(chunks, charset)) // charset通过content-type得到
五、字符串解码
前面已经提到了字符串的二种编码方式,它们对应的content-type分别为:
- url编码 application/x-www-form-urlencoded
- json编码 application/json
对于前端来说,url编码并不陌生,经常会用于url拼接操作,唯一需要注意的是不要忘记对键值对进行decodeuricomponent()处理。
当客户端发送请求主体时,需要进行编码操作:
'a=1&b=2&c=3'
服务器端再根据url编码规则解码,得到相应的对象。
// url编码方式 简单的解码方法实现 function decode (qs, sep = '&', eq = '=') { const obj = {} qs = qs.split(sep) for (let i = 0, max = qs.length; i < max; i++) { const item = qs[i] const index = item.indexof(eq) let key, value if (~index) { key = item.substr(0, index) value = item.substr(index + 1) } else { key = item value = '' } key = decodeuricomponent(key) value = decodeuricomponent(value) if (!obj.hasownproperty(key)) { obj[key] = value } } return obj } console.log(decode('a=1&b=2&c=3')) // { a: '1', b: '2', c: '3' }
url编码方式适合处理简单的键值对数据,并且很多框架的ajax中的content-type默认值都是它,但是对于复杂的嵌套对象就不太好处理了,这时就需要json编码方式大显身手了。
客户端发送请求主体时,只需要采用json.stringify进行编码。服务器端只需要采用json.parse进行解码即可:
const strictjsonreg = /^[\x20\x09\x0a\x0d]*(\[|\{)/; function parse(str) { if (!strict) return str ? json.parse(str) : str; // 严格模式下,总是返回一个对象 if (!str) return {}; // 是否为合法的json字符串 if (!strictjsonreg.test(str)) { throw new error('invalid json, only supports object and array'); } return json.parse(str); }
除了上述两种字符串编码方式,koa-bodyparser还支持不采用任何字符串编码方式的普通字符串。
三种字符串编码的处理方式由【co-body】模块提供,koa-bodyparser中通过判断当前content-type类型,调用不同的处理方式,将获取到的结果挂载在ctx.request.body:
return async function bodyparser(ctx, next) { if (ctx.request.body !== undefined) return await next(); if (ctx.disablebodyparser) return await next(); try { // 最重要的一步 将解析的内容挂载到koa的上下文中 const res = await parsebody(ctx); ctx.request.body = 'parsed' in res ? res.parsed : {}; if (ctx.request.rawbody === undefined) ctx.request.rawbody = res.raw; // 保存原始字符串 } catch (err) { if (onerror) { onerror(err, ctx); } else { throw err; } } await next(); }; async function parsebody(ctx) { if (enablejson && ((detectjson && detectjson(ctx)) || ctx.request.is(jsontypes))) { return await parse.json(ctx, jsonopts); // application/json等json type } if (enableform && ctx.request.is(formtypes)) { return await parse.form(ctx, formopts); // application/x-www-form-urlencoded } if (enabletext && ctx.request.is(texttypes)) { return await parse.text(ctx, textopts) || ''; // text/plain } return {}; } };
其实还有一种比较常见的content-type,当采用表单上传时,报文主体中会包含多个实体主体:
------webkitformboundaryqsagmb6us6f7s3sf content-disposition: form-data; name="image"; filename="image.png" content-type: image/png ------webkitformboundaryqsagmb6us6f7s3sf content-disposition: form-data; name="text" ------webkitformboundaryqsagmb6us6f7s3sf--
这种方式处理相对比较复杂,koa-bodyparser中并没有提供该content-type的解析。(下一篇中应该会介绍^_^)
五、总结
以上便是koa-bodyparser的核心实现原理,其中涉及到很多关于http的基础知识,对于http不太熟悉的同学,可以推荐看一波入门级宝典【图解http】。
最后留图一张:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。