简单的 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 协议以及框架原理。