【Web全栈课程7】Node服务器和浏览器数据交互

小叶子

Web服务器数据交互:

  1. 静态资源响应,返回文件
  2. 处理数据交互请求(GET、POST)
  3. 解析上传的文件数据
  4. 流式操作和gz压缩
  5. 数据库交互(后续再讲)

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文件。
image.png

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);

解析后的结果:
image.png

post

和get一样,新建一个简单的post请求的表单html。

<form  action="http://localhost:8080/aaa"  method="post">
…………

post传输的数据在body里面。post的数据传递,一个大数据包切成多个小包发送:

  1. 大数据包不切块发送,其他所有网络交互被阻塞,需要等待大数据包传输完成才能进行。
  2. 传输失败的时候只需要重传失败的一小段,速度快。

因此,我们服务器端可以监听收到的每小段数据包,以及发送结束的消息。

let str = '';
// 接收到一个一个分段的数据
req.on('data', data  => {
    str += data;
});
// 结束
req.on('end', () => {
    let post = querystring.parse(str);
    console.log(str, post);
}); 

解析后的结果:
image.png

url.parse 可以解析整个url
querystring 解析数据部分(a=**&b=**)

3. 文件数据处理

POST表单的3种enctype:

  1. text/plain 纯文本内容
  2. application/x-www-form-urlencoded 默认,url编码方式 xx=xx&&x=x(上面讲了如何解析)
  3. 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>

得到简单的表单
image.png

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转换为字符串,打印出来的结果如下:
image.png
------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
<分隔符>--

一步步将数据解析开

  1. 用“<分隔符>”切开数据

    [
        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都重新生成),如下:
    image.png

  2. 丢弃数组的头尾,和丢弃每项头尾的\r\n

    [
        数据描述\r\n\r\n数据值,
        数据描述\r\n\r\n数据值,
        数据描述1\r\n数据描述2\r\n\r\n<文件内容>,
    ]
  3. 使用第一次出现的\r\n\r\n切分(如果文件中有空行,文件里也有\r\n\r\n,所以用第一个)

    [
        [数据描述, 数据值],
        [数据描述, 数据值],
        [数据描述1\r\n数据描述2, <文件内容>],
    ]
  4. 判断描述里有没有\r\n,就可以区分文件。

    没有 —— 普通数据:`[数据描述, 数据值]`
    有 —— 文件数据:`[数据描述1\r\n数据描述2, <文件内容>]`
  5. 分析“数据描述”,可以看出其中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);
});

提取出来提交的表单数据如下,正是需要的结果:
image.png

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类型的数据上传处理就完成了。
image.png

4. 流式操作和gz压缩

4.1 流式操作

readFile和writefile
先把所有数据全部读到内存中,然后回调,极其占用内存,资源利用非常不充分。流式操作读一部分发一部分,不会等待所有数据完成后再操作。

  1. 读取流 fs.createReadStream、request
  2. 写入流 fs.createWriteStream、response
  3. 读写流 压缩、加密

流式返回服务器端文件
请求文件返回服务器端对应文件,使用流的方式操作:

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静态资源,返回的文件大小为:
image.png
在实际使用过程中,会有大量的静态资源,压缩能够降低带宽的消耗降低成本,和节省加载时间。
需要注意:要设置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);

压缩后请求相同资源,文件变小。
image.png

5. 数据库交互

数据库分类:
关系型数据库 —— MySQL、Oracle、SQL Server
文件型数据库 —— sqlite
文档型数据库 —— MongoDB

5.1 关系型数据库

常见常用,数据之间是有关系的

5.2 文件型数据库

特点是简单,轻量级、小巧

5.3 文档型数据库

存储异构数据,处理速度快,适用于频繁写入的数据。

NoSQL 没有复杂的关系,对性能有极高的要求。
例如redis、memcached、hypertable、bigtable

数据库操作放在后面结合数据库一起写。

阅读 545

一只萌萌的程序媛妹子,踏入前端领域不久,希望大家多指导,欢迎大家多多指出问题

63 声望
11 粉丝
0 条评论

一只萌萌的程序媛妹子,踏入前端领域不久,希望大家多指导,欢迎大家多多指出问题

63 声望
11 粉丝
宣传栏