node原生处理前端传送的数据

简单的 node 服务

const http = require('http'); // 引入 http 模块
let server = http.createServer(() => { // 创建服务,生成实例
  console.log('请求来了')
});
server.listen('8000'); // 监听服务

启动 node 服务后,每次前端访问8000端口,后台都可以监控到。

接收浏览器的 GET 数据

在 createServer方法 的回调函数中,会有 request(请求对象) 和 response(响应对象) 两个参数供我们使用。

requerst 对象的属性有很多。

名称 含义
complete 前端请求是否发送完成
httpVersion http 协议版本
method http 请求方式
url 原始的请求路径
headers http 请求头
trailers http 请求尾
connection 当前 http 连接套接字,为net.Socket的实例
socket connection 属性别名
client client 属性别名

在前端页面中用一个表单来模拟 GET 请求

<form method="GET" action="http://localhost:8000/login">
  用户名: <input type="text" name="username" /><br />
  密码: <input  type="password" name="password" /><br />
  <input type="submit" value="提交" />
</form>

request 对象中我们比较常用的是 url 和 method。点击提交后,打印出对应的结果。

http.createServer((req, res) => {
  console.log(req.method) // GET
  console.log(req.url);   // /login?username=ang&password=123
}).listen('8000');

可以引入 url 模块对 req.url 进行解析

let { pathname, query } = url.parse(req.url, true);
console.log(pathname);  // /login
console.log(query);     // { username: 'ang', password: '123' }

接收浏览器的 POST 数据(不涉及文件上传)

post 方式传送数据和 get 是有区别的,当数据量较大时不能同时全部传过去,而是分块传送。

http.createServer((req, res) => {
  let body = []; // 接收 post 数据

  // 当有数据传送过来时,将其推入数组
  req.on('data', chuck => { // chuck 是二进制数据
    body.push(chuck);
  });

  // 所有的数据接收完成触发
  req.on('end', () => {
    // 将二进制数组拼接成一个 Buffer 对象
    let buffer = Buffer.concat(body);
    // 因为没有文件流,所以可以先将其转换成字符串
    console.log(buffer.toString()); // username=ang&password=123
    // 引入 querystring 模块解析数据
    let data = queryString.parse(buffer.toString());
    console.log(data); // { username: 'ang', password: '123' }
  });
}).listen('8000');

接收浏览器的文件上传数据(带有文件上传)

使用表单上传文件时需要将表单的编码方式更改为 multipart/form-data,在这里我们先使用一个 1.txt 的纯文本文件,这样我们依旧可以对二进制数据使用 toString方法 从而看到传送的内容。

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

1.txt 文件中的内容

1234
abcd

服务端的代码跟之前接受 post 数据的代码一样,将二进制数据转换成字符串后可以打印出:

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

ang
------WebKitFormBoundary24CM1eLiEK67ErB2
Content-Disposition: form-data; name="password"

123
------WebKitFormBoundary24CM1eLiEK67ErB2
Content-Disposition: form-data; name="f1"; filename="1.txt"
Content-Type: text/plain

1234
abcd

------WebKitFormBoundary24CM1eLiEK67ErB2--

可以明显地看到,传送过来的数据是一个包含分隔符、参数描述信息等内容的构造体,文件的数据与普通字段的数据多出了源文件的 filename 和文件形式 content-type。接下来我们就需要跟之前一样解析数据。

首先观察分隔符,分隔符的后面是一串随机的数字字母组合,每次上传文件都有所不同,要确定分隔符只能在 request 的数据中找,在 request 的 headers 中有一个 content-type 字段中有这个分隔符。

 'content-type':
   'multipart/form-data; boundary=----WebKitFormBoundary24CM1eLiEK67ErB2',
let boundary = `--${req.headers['content-type'].split('; ')[1].split('=')[1]}`;
conosle.log(boundary) // ------WebKitFormBoundary24CM1eLiEK67ErB2

除此之外,在 http 协议中的换行是 \r\n,通过这些我们可以对数据进行切分。但是为了保证文件数据,我们并不能对 toString 后的数据进行处理,而是要操作原始的 buffer 数据。

req.on('end', () => {
  let buffer = Buffer.concat(body);
  let arr1 = [];
  let n = 0;
  let len = boundary.length;
  while ((n = buffer.indexOf(boundary)) != -1) {
    arr1.push(buffer.slice(0, n));
    buffer = buffer.slice(n + len);
  }
  arr1.push(buffer); // arr1 是将 buffer 数据通过分隔符切分后得到的数组
  arr1.pop(); // 去除头部的空数据
  arr1.shift(); // 去除尾部的 -- 
  arr1.forEach(buffer => {
    buffer = buffer.slice(2, buffer.length - 2);  // 去除头尾的 /r/n
    let n = buffer.indexOf('\r\n\r\n');
    let info = buffer.slice(0, n).toString(); // info 是字段描述信息,可以转换成字符串查看
    let data = buffer.slice(n + 4); // data 是字段数据
    // 对文件和普通字段的数据做最后一步的区别处理
    if (info.indexOf('\r\n') != -1) {
      // 处理文件数据
      let arr2 = info.split('\r\n')[0].split('; ');
      let name = arr2[1].split('=')[1];
      let filename = arr2[2].split('=')[1];
      name = name.substring(1, name.length - 1); 
      filename = filename.substring(1, filename.length - 1);
      console.log(name, filename);
      // 引入 fs 模块,当前目录创建 upload 文件夹
      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);
      console.log(name, data.toString()); // 文本文件数据,可以转换成字符串查看
    }
  })
})

最后文件上传到了 upload 目录下,打印出的结果是:

username ang
password 123
f1 1.txt
上传成功

当然,上面的代码只是能让我们对原理上的解析有一个认识,真正的上传过程中碰到的问题还有很多,比如把所有的数据都放在一个数组中等接收完再处理,当文件过大时,服务器的内存开销也会过大,这就需要分段处理了。而且,对于这种比较基础的 node 模块,不可能所有的东西都用原生的自己来写,更多的是引用第三方库来处理,这里我们可以使用到 multiparty 这个包。这是专门解析有 content-type 的 http 请求 multipart/form-data。这个不是 node 的系统包,所以需要安装。

const http = require('http');
const multiparty = require('multiparty');

http.createServer((req, res) => {
  let form = new multiparty.Form({
    uploadDir: './upload' //文件上传的目录
  });
  form.parse(req); // 解析表单
  // 普通字段
  form.on('field', (name, value) => {
    console.log(name, value);
  })
  // 文件字段
  form.on('file', (name, file) => {
    console.log(name, file);
  });
  // 全部解析完成
  form.on('close', () => {
    console.log('表单解析完成');
  })
}).listen('8008');

最终的打印结果是

username ang
password 123
f1 { fieldName: 'f1',
  originalFilename: '1.txt',
  path: 'upload\\amaOyu-6u1_XlQUaxaNEhY0A.txt',
  headers:
   { 'content-disposition': 'form-data; name="f1"; filename="1.txt"',
     'content-type': 'text/plain' },
  size: 12 }
表单解析完成

当然,node 使用更多的是 express 和 koa 框架,但是分析原生的处理方式,能帮助我们了解 http 协议以及框架原理。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容