深入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的封装,而【co-body】中主要是采用raw-body模块获取请求报文主体的二进制数据流,【row-body】主要是对上述示例代码的封装和健壮性处理。

三、内容解码

客户端会将内容编码的方式放入请求报文头部信息Content-Encoding属性中,服务器端接收报文主体的二进制数据了时,会根据该头部信息进行解压操作,当然服务器端可以在响应报文头部信息Accept-Encoding属性中添加支持的解压方式。

而【row-body】主要采用 inflation模块进行解压处理。

四、字符解码

一般而言,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】。

最后留图一张:

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 玩转Koa之koa-router原理解析

    一.前言 Koa为了保持自身的简洁,并没有捆绑中间件.但是在实际的开发中,我们需要和形形色色的中间件打交道,本文将要分析的是经常用到的路由中间件 -- koa-router. 如果你对Koa的原理还不了解的话,可以先查看Koa原理解析. 二.koa-router概述 koa-router的源码只有两个文件:router.js和layer.js,分别对应Router对象和Layer对象. Layer对象是对单个路由的管理,其中包含的信息有路由路径(path).路由请求方法(method)和路由执行

  • Java 并发编程:volatile的使用及其原理解析

    Java并发编程系列[未完]: •Java 并发编程:核心理论 •Java并发编程:Synchronized及其实现原理 •Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) •Java 并发编程:线程间的协作(wait/notify/sleep/yield/join) •Java 并发编程:volatile的使用及其原理 一.volatile的作用 在<Java并发编程:核心理论>一文中,我们已经提到过可见性.有序性及原子性问题,通常情况下我们可以通过Synchroniz

  • Java 8 动态类型语言Lambda表达式实现原理解析

    Java 8支持动态语言,看到了很酷的Lambda表达式,对一直以静态类型语言自居的Java,让人看到了Java虚拟机可以支持动态语言的目标. import java.util.function.Consumer; public class Lambda { public static void main(String[] args) { Consumer<String> c = s -> System.out.println(s); c.accept("hello lambd

  • asp防止上传图片木马原理解析

    首先判断文件大小: if file.filesize<10 then Response.Write("<script>alert('您没有选择上传文件')</script>") Response.Write("<script>history.go(-1)</script>") Response.End() end if 将文件上传到服务器后,判断用户文件中的危险操作字符: set MyFile = server.

  • Android中微信抢红包插件原理解析及开发思路

    一.前言 自从去年中微信添加抢红包的功能,微信的电商之旅算是正式开始正式火爆起来.但是作为Android开发者来说,我们在抢红包的同时意识到了很多问题,就是手动去抢红包的速度慢了,当然这些有很多原因导致了.或许是网络的原因,而且这个也是最大的原因.但是其他的不可忽略的因素也是要考虑到进去的,比如在手机充电锁屏的时候,我们并不知道有人已经开始发红包了,那么这时候也是让我们丧失了一大批红包的原因.那么关于网络的问题,我们开发者可能用相关技术无法解决(当然在Google和Facebook看来的话,他们

  • JavaScript中的return布尔值的用法和原理解析

    首先return作为返回关键字,他有以下两种返回方式 1.返回控制与函数结果 语法为:return 表达式; 语句结束函数执行,返回调用函数,而且把表达式的值作为函数的结果 2.返回控制无函数结果 语法为:return;在大多数情况下,为事件处理函数返回false,可以防止默认的事件行为.例如,默认情况下点击一个<A>元素,页面会跳转到该元素href属性指定的页. 例如:<a href="http:www.baidu.com;alert(11);return false;ale

  • 使用BootStrap实现标签切换原理解析

    原理解析 1.定义一无序列表.取id值为myTab. 2.定义列表中a标签的目标地址为我们要展示的div对应的id 3.为a标签注册对应的事件(就是js的代码) 效果如图 代码如下 html代码部分 <ul id="myTab" class="nav nav-tabs"> <li role="presentation" class="active"><a href="#home"

  • 详解关于react-redux中的connect用法介绍及原理解析

    关于react-redux的一个流程图 流程图 connect用法介绍 connect方法声明: connect([mapStateToProps], [mapDispatchToProps], [mergeProps],[options]) 作用:连接React组件与 Redux store. 参数说明: mapStateToProps(state, ownProps) : stateProps 这个函数允许我们将 store 中的数据作为 props 绑定到组件上. const mapSta

  • Android ListView 单条刷新方法实践及原理解析

    对于使用listView配合adapter进行刷新的方法大家都不陌生,先刷新adapter里的数据,然后调用notifydatasetchange通知listView刷新界面. 方法虽然简单,但这里面涉及到一个效率的问题,调用notifydatasetchange其实会导致adpter的getView方法被多次调用 (画面上能显示多少就会被调用多少次),如果是很明确的知道只更新了list中的某一个项的数据(比如用户点击list某一项后更新该项的显示状态,或者 后台回调更新list某一项,等等),

  • Android截屏方案实现原理解析

    Android截屏的原理:获取具体需要截屏的区域的Bitmap,然后绘制在画布上,保存为图片后进行分享或者其它用途 在截屏功能中,有时需要截取全屏的内容,有时需要截取超过一屏的内容(比如:Listview,Scrollview,RecyclerView).下面介绍各种场景获取Bitmap的方法 普通截屏的实现 获取当前Window的DrawingCache的方式,即decorView的DrawingCache /** * shot the current screen ,with the sta

随机推荐