Web服务器数据交互:
- 静态资源响应,返回文件
- 处理数据交互请求(GET、POST)
- 解析上传的文件数据
- 流式操作和gz压缩
- 数据库交互(后续再讲)
1. 返回文件
常用于response返回数据的方法:setHead, write, writeHead
基于请求文件返回服务器端对应文件,简单实现如下:
fs.readFile(`www${req.url}`, (err, data)=> {
if (err){
res.writeHead('404'); //设置状态码
res.write('error');
} else {
res.write(data);
}
res.end();
})
从浏览器请求文件时,后端返回了对应的html文件。
2. 处理数据交互请求
get
新建一个简单的get请求的表单html,提交到我们本地的node服务器。
<form action="http://localhost:8080/aaa" method="get">
用户:<input type="text" name="user"><br>
密码:<input type="password" name="pass"><br>
<input type="submit" value="提交">
</form>
get传递的数据在url中,通过node的url模块,可以将url解析成对象数据。
let obj = url.parse(req.url);
解析后的结果:
post
和get一样,新建一个简单的post请求的表单html。
<form action="http://localhost:8080/aaa" method="post">
…………
post传输的数据在body里面。post的数据传递,一个大数据包切成多个小包发送:
- 大数据包不切块发送,其他所有网络交互被阻塞,需要等待大数据包传输完成才能进行。
- 传输失败的时候只需要重传失败的一小段,速度快。
因此,我们服务器端可以监听收到的每小段数据包,以及发送结束的消息。
let str = '';
// 接收到一个一个分段的数据
req.on('data', data => {
str += data;
});
// 结束
req.on('end', () => {
let post = querystring.parse(str);
console.log(str, post);
});
解析后的结果:
url.parse 可以解析整个url
querystring 解析数据部分(a=**&b=**)
3. 文件数据处理
POST表单的3种enctype:
-
text/plain
纯文本内容 -
application/x-www-form-urlencoded
默认,url编码方式 xx=xx&&x=x(上面讲了如何解析) -
multipart/form-data
上传文件内容
解析multipart/form-data文件上传数据
3.1 编写一个使用post上传文件的form表单
<form action="http://localhost:8080/upload" method="post" enctype="multipart/form-data">
用户:<input type="text" name="user"><br/>
密码:<input type="password" name="password"><br/>
文件:<input type="file" name="file"><br/>
<input type="submit" value="提交">
</form>
得到简单的表单
3.2 接收buffer数据
上一篇文章中讲post到Node服务端的数据,使用了str来存储,如果上传二进制文件,使用str接收就会破坏二进制数据导致文件损坏。因此使用Buffer
处理是比较合理的方式。
let arr =[];
req.on('data', data => {
arr.push(data);
});
req.on('end', () => {
const result = Buffer.concat(arr);
res.end();
});
3.3 分析上传的内容
为了方便分析上传的内容,提交一个文本文件上传,将buffer转换为字符串,打印出来的结果如下:
用------WebKitFormBoundaryT8xL9avSAHMZDI98
作为一个分隔符号,换行使用\r\n
表示,简化后如下图:
<分隔符>\r\n数据描述\r\n\r\n数据值\r\n
<分隔符>\r\n数据描述\r\n\r\n数据值\r\n
<分隔符>\r\n数据描述1\r\n数据描述2\r\n\r\n<文件内容>\r\n
<分隔符>--
一步步将数据解析开
-
用“<分隔符>”切开数据
[ null, \r\n数据描述\r\n\r\n数据值\r\n, \r\n数据描述\r\n\r\n数据值\r\n, \r\n数据描述1\r\n数据描述2\r\n\r\n<文件内容>\r\n, -- ]
“<分隔符>”怎么得到?分析请求的headers,发现“<分隔符>”就是content-type中的boundary(每次的boundary都重新生成),如下:
-
丢弃数组的头尾,和丢弃每项头尾的
\r\n
[ 数据描述\r\n\r\n数据值, 数据描述\r\n\r\n数据值, 数据描述1\r\n数据描述2\r\n\r\n<文件内容>, ]
-
使用第一次出现的
\r\n\r\n
切分(如果文件中有空行,文件里也有\r\n\r\n
,所以用第一个)[ [数据描述, 数据值], [数据描述, 数据值], [数据描述1\r\n数据描述2, <文件内容>], ]
-
判断描述里有没有
\r\n
,就可以区分文件。没有 —— 普通数据:`[数据描述, 数据值]` 有 —— 文件数据:`[数据描述1\r\n数据描述2, <文件内容>]`
-
分析“数据描述”,可以看出其中name的值是需要的数据:
Content-Disposition: form-data; name="user" Content-Disposition: form-data; name="password" Content-Disposition: form-data; name="file"; filename="text.txt" Content-Type: text/plain
3.4 代码化实现
上述分析过程改写成代码:
let uploadArr = [];
// 接收到一个一个分段的数据
req.on('data', data => {
uploadArr.push(data);
});
// 数据接收结束
req.on('end', () => {
let bf = Buffer.concat(uploadArr);
// 1. 找到分隔符,用分隔符切开数据
let boundary = '--' + req.headers['content-type'].split(/;\s*/)[1].split('=')[1];
let arr = bf.split(boundary);
// 2. 丢弃数组的头尾,和丢弃每项头尾的`\r\n`
arr.shift();
arr.pop();
arr = arr.map(ele => ele.slice(2, ele.length - 2));
// 3. 使用第一次出现的`\r\n\r\n`切分
arr = arr.map(ele => {
let firstSpace = ele.indexOf('\r\n\r\n');
return [ele.slice(0, firstSpace), ele.slice(firstSpace + 4)];
});
// 4. 判断描述里有没有`\r\n`,就可以区分文件,取出需要的数据
let normalData = {};
let fileData = {};
arr = arr.map(ele => {
let description = ele[0].toString();
let content = ele[1];
if (description.indexOf('\r\n') >= 0) {
// 5. 文件数据,获得name和文件内容
fileData['data'] = { data: content };
description.split('\r\n')[0].split(/;\s*/).forEach(attr => {
let [name, val] = attr.toString().split('=');
if (name == 'name' || name == 'filename') {
fileData[name] = val.replace(/"/igm, '');
}
});
} else {
// 5. 普通数据,获得name
let name = description.split(/;\s*/)[1].split('=')[1].replace(/"/igm, '');
normalData[name] = content.toString();
}
});
console.log(normalData);
console.log(fileData);
res.writeHead(200);
});
提取出来提交的表单数据如下,正是需要的结果:
3.5. 保存文件到服务器
// 写入文件,content是上传的文件buffer
fs.writeFile(`upload/${filename}`, content, (err) => {
if (err){
console.log('文件写入失败', err);
res.writeHead(404);
res.end();
} else {
console.log('文件写入成功');
res.writeHead(200);
res.end();
}
});
运行后,分别提交了一个文本文件和图片,都能正常的保存和使用,到这里,post方式multipart/form-data类型的数据上传处理就完成了。
4. 流式操作和gz压缩
4.1 流式操作
readFile和writefile
先把所有数据全部读到内存中,然后回调,极其占用内存,资源利用非常不充分。流式操作读一部分发一部分,不会等待所有数据完成后再操作。
- 读取流 fs.createReadStream、request
- 写入流 fs.createWriteStream、response
- 读写流 压缩、加密
流式返回服务器端文件
请求文件返回服务器端对应文件,使用流的方式操作:
let rs= fs.createReadStream(`www${req.url}`);
rs.pipe(res);
rs.on('error', err=>{
console.log('读取失败');
res.writeHead(404);
res.write('Not found');
res.end();
});
4.2 gz压缩
没有压缩之前,请求1.html静态资源,返回的文件大小为:
在实际使用过程中,会有大量的静态资源,压缩能够降低带宽的消耗降低成本,和节省加载时间。
需要注意:要设置Content-Encoding通知浏览器文件内容是压缩过的
let rs= fs.createReadStream(`www${req.url}`);
// 创建zlib压缩对象
let gz = zlib.createGzip();
// 设置header通知浏览器文件内容是压缩过的
res.setHeader('Content-Encoding', 'gzip');
rs.pipe(gz).pipe(res);
压缩后请求相同资源,文件变小。
5. 数据库交互
数据库分类:
关系型数据库 —— MySQL、Oracle、SQL Server
文件型数据库 —— sqlite
文档型数据库 —— MongoDB
5.1 关系型数据库
常见常用,数据之间是有关系的
5.2 文件型数据库
特点是简单,轻量级、小巧
5.3 文档型数据库
存储异构数据,处理速度快,适用于频繁写入的数据。
NoSQL 没有复杂的关系,对性能有极高的要求。
例如redis、memcached、hypertable、bigtable
数据库操作放在后面结合数据库一起写。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。