Node之HTTP模块(request对象解析、response对象解析、图片上传)

4/24/2021 NodeWeb

# 文章目录

# 一、什么是 Web 服务器?

当应用程序(客户端)需要某一个资源时,可以向一台服务器,通过 Http 请求获取到这个资源;提供资源的这个服务器,就是一个 Web 服务器。

在这里插入图片描述

目前有很多开源的 Web 服务器:Nginx、Apache(静态)、Apache Tomcat(静态、动态)、Node.js

# 1.1 web 服务器初体验

const http = require("http");

// 1.创建一个web服务器
const server = http.createServer((req, res) => {
  // res.write('heelo world)
  // res.close()  下面方法是上述两个方法的简写
  res.end("hello world");
});

// 2.启动服务器 并指定端口号和主机
// port:0-65535,
// hostname:主机 ['localhost','127.0.0.1','0.0.0.0']
server.listen(8080, "0.0.0.0", () => {
  console.log("server start");
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 1.2 另外一种创建方式

const http = require("http");

const server2 = new http.Server((req, res) => {
  res.end("server 2");
});
server2.listen(8002, () => {
  console.log("server2启动成功");
});
1
2
3
4
5
6
7
8

本质上是同一种,源码层面都是去 newServer这个类。

Node 源码:
在这里插入图片描述

# 1.3 listen 方法的参数详解

官网直达:http://nodejs.cn/api/net.html#net_server_listen_port_host_backlog_callback (opens new window)

listen 函数有三个参数:

  • 端口 port: 可以不传, 系统会默认分配端, 后续项目中我们会写入到环境变量中;
  • 如果 port 省略或是 0,系统会随意分配
  • 主机 host: 通常可以传入localhost、ip 地址127.0.0.1、或者 ip 地址0.0.0.0默认是 0.0.0.0
  • localhost:本质上是一个域名,通常情况下会被解析成 127.0.0.1;
  • 127.0.0.1:回环地址(Loop Back Address),表达的意思其实是我们主机自己发出去的包,直接被自己接收;
    • 正常的数据库包经常 应用层 - 传输层 - 网络层 - 数据链路层 - 物理层 ;
    • 回环地址,是在网络层直接就被获取到了,是不会经常数据链路层和物理层的;
    • 比如我们监听 127.0.0.1 时,在同一个网段下的主机中,通过 ip 地址是不能访问的;
  • 0.0.0.0:
    • 监听 IPV4 上所有的地址再根据端口找到不同的应用程序
    • 比如我们监听 0.0.0.0 时,在同一个网段下的主机中,通过 ip 地址是可以访问的;
  • 回调函数:服务器启动成功时的回调函数
const http = require("http");

const server = new http.Server((req, res) => {
  res.end("hello world!");
});

// port:不写动态生成,const result = server.address().port 可以拿到
// address:不写默认是 0.0.0.0
server.listen(8000, () => {
  console.log("服务器启动成功");

  // 拿到服务器信息
  const result = server.address();
  console.log("result: ", result);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 二、request 对象解析

# 2.1 基本使用

request 对象中常用的有三个属性,分别是:req.url req.method req.headers

const http = require("http");

const server = http.Server((req, res) => {
  // request对象中封装了客户端给我们服务器传递过来的所有信息
  console.log("req.url: ", req.url);
  console.log("req.method: ", req.method);
  console.log("req.headers: ", req.headers);

  res.end("hello world!");
});

server.listen(8000, () => {
  console.log("server start");
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14

output:

server start
req.url:  /login?username=sir&password=123
req.method:  POST
req.headers:  {
  'user-agent': 'PostmanRuntime/7.26.10',
  accept: '*/*',
  host: 'localhost:8000',
  'accept-encoding': 'gzip, deflate, br',
  connection: 'keep-alive',
  cookie: 'NMTID=00OtVDuIBEAhUMivkXAgeFb15wM0PUAAAF4kDtcOQ',
  'content-length': '0'
}

1
2
3
4
5
6
7
8
9
10
11
12
13

# 2.2 进阶使用

可以发现上述使用方法在处理我们的请求参数时不是很方便,node 为我们提供了一些内置库,帮助我们提取出 url 和 query 信息。

const http = require("http");
const url = require("url");
const qs = require("querystring");

const server = http.Server((req, res) => {
  // 1.基本使用
  // if(req.url === '/login'){
  //   res.end('欢饮回来')
  // }else if(req.url === '/users'){
  //   res.end('用户列表~')
  // }else{
  //   res.end('错误请求,请检查')
  // }

  console.log(req.url);

  // 2.进阶方式使用
  // 通过内置的库 url 解析出我们需要的 包含在url里的信息
  const ret = url.parse(req.url);
  console.log("ret: ", ret);

  const { pathname, query } = url.parse(req.url);
  if (pathname === "/login") {
    console.log("query: ", query);

    // 通过内置库 querystring 解析出query里我们需要的数据
    // query => username=sir&password=123
    const { username, password } = qs.parse(query);
    console.log("username: ", username);
    console.log("password: ", password);
  }

  res.end("登录成功!");
});

server.listen(8000, () => {
  console.log("server start");
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

output:

server start
/login?username=sir&password=123
ret:  Url {
  protocol: null,
  slashes: null,
  auth: null,
  host: null,
  port: null,
  hostname: null,
  hash: null,
  search: '?username=sir&password=123',
  query: 'username=sir&password=123',
  pathname: '/login',
  path: '/login?username=sir&password=123',
  href: '/login?username=sir&password=123'
}
query:  username=sir&password=123
username:  sir
password:  123
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 2.3 method 应用

const http = require("http");
const url = require("url");

const server = http.Server((req, res) => {
  const { pathname } = url.parse(req.url);

  if (pathname === "/login") {
    // 拿到body中的数据

    // 1. 拿到的是字符串(手动设置编码)
    req.setEncoding("utf-8");

    req.on("data", (data) => {
      console.log("data: ", data);
      console.log("typeof data: ", typeof data); // string
      // 2. 拿到的是 Buffer 调用 toString解码
      // console.log('data: ', data.toString());

      const { username, password } = JSON.parse(data);
      console.log("username: ", username);
      console.log("password: ", password);
    });

    console.log("req.method: ", req.method);
    res.end("hello server");
  }
});

server.listen(8000, () => {
  console.log("server start");
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

output:

server start
req.method:  POST
data:  {
    "username":"foo",
    "password":"8888"
}
typeof data:  string
username:  foo
password:  8888
1
2
3
4
5
6
7
8
9

# 2.4 headers 属性

在 request 对象的 header 中也包含很多有用的信息,客户端会默认传递过来一些信息:
在这里插入图片描述

词汇:accept 接受,encoding 编码,content 内容,keep-alive 持续服务

# 2.4.1 content-type

content-type 是这次请求携带的数据的类型:

  • application/json 表示是一个 json 类型;
  • text/plain 表示是文本类型;
  • application/xml 表示是 xml 类型;
  • multipart/form-data 表示是上传文件;
  • text/html 表示服务器返回的是 html 文档。

# 2.4.2 content-length

content-length:文件的大小和长度

# 2.4.3 keep-alive

keep-alive:

  • http 是基于 TCP 协议的,但是通常在进行一次请求和响应结束后会立刻中断;
  • 在 http1.0 中,如果想要继续保持连接:
    • 浏览器需要在请求头中添加 connection: keep-alive;
    • 服务器需要在响应头中添加 connection:keey-alive;
    • 当客户端再次放请求时,就会使用同一个连接,直至一方中断连接;
  • 在 http1.1 中,所有连接默认是 connection: keep-alive 的;
    • 不同的 Web 服务器会有不同的保持 keep-alive 的时间;
    • Node 中默认是 5s

# 2.4.4 accept-encoding

accept-encoding:告知服务器,客户端支持的文件压缩格式,比如 js 文件可以使用 gzip 编码,对应 .gz 文件;

# 2.4.5 accept

accept:告知服务器,客户端可接受文件的格式类型;

# 2.4.6 user-agent

user-agent:客户端相关的信息;
在这里插入图片描述

# 三、response 对象解析

# 3.1 设置响应体

如果我们希望给客户端响应结果数据,可以通过两种方式:

  • write 方法:这种方式是直接写出数据,但是并没有关闭流;
  • end 方法:这种方式是写出最后的数据,并且写出后会关闭流;
    在这里插入图片描述
    如果我们没有调用 end,客户端将会一直等待结果:
    所以客户端在发送网络请求时,都会设置超时时间。

# 3.2 设置响应状态码

设置状态码常见的有两种方式:

res.statusCode = 200;

res.writeHead(200);
1
2
3

在这里插入图片描述

# 3.3 设置响应头

  • res.setHeader("key","value")
  • res.writeHead({"key":"value"})

示例:

const http = require("http");

// 1.创建一个web服务器
const server = http.createServer((req, res) => {
  // 设置响应头
  // 1.setHeader
  // res.setHeader('Content-Type','text/plain;charset=utf8')
  // res.setHeader('Content-Type','text/html;charset=utf8')

  // 2.writeHead
  res.writeHead(200, {
    // "Content-Type": "application/json;charset=utf8",
    "Content-Type": "text/html;charset=utf8",
  });
  res.end("<h1>Hello,world!</h1>");
});

// 2.启动服务器 并指定端口号和主机
server.listen(8000, "0.0.0.0", () => {
  console.log("server start");
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 四、发送网络请求

# 4.1 http 发送 GET 请求

const http = require("http");

// http发送get请求
http.get("http://localhost:8000", (res) => {
  res.setEncoding("utf-8");
  res.on("data", (data) => {
    // 默认是 Buffer
    // console.log(data.toString());
    console.log(data);
  });

  res.on("end", () => {
    console.log("传输完成");
  });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 4.2 http 发送 POST 请求

const http = require("http");

// http发送post请求
const request = http.request(
  {
    method: "POST",
    hostname: "localhost",
    port: "8000",
  },
  (res) => {
    res.setEncoding("utf-8");
    res.on("data", (data) => {
      // 默认是 Buffer
      // console.log(data.toString());
      console.log(data);
    });
    res.on("end", () => {
      console.log("传输完成");
    });
  }
);
// 需要调用该方法表示 POST请求已经配置完成,可以发送了
request.end();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 五、使用原生 http 模块实现文件上传

准备:

  1. 准备 postman 发送一个 post 请求,请求体是一个form-data类型的表单数据,文件类型选择 file,上传一张图片。

注意: 不能直接把拿到的数据,写入到文件中,因为拿到的数据包含请求头,文件名称大小等数据,不是纯粹的图片二进制数据, 我需要我们人工拆解出真实的二进制图片数据。

下图中圈出来的才是图片的真实 二进制数据。
在这里插入图片描述

server.js

const http = require("http");
const url = require("url");
const fs = require("fs");
const qs = require("querystring");

const server = http.createServer((req, res) => {
  // 解析该url,解构出我们需要的数据
  const { pathname } = url.parse(req.url);

  if (pathname === "/upload") {
    if (req.method === "POST") {
      // 默认是utf8编码,必须要先设置编码  图片数据是二进制编码
      req.setEncoding("binary");

      let body = "";
      const boundary = req.headers["content-type"]
        .split(";")[1]
        .replace(" boundary=", "");
      console.log("boundary: ", boundary);

      req.on("data", (data) => {
        // data是Buffer 相加的时候会自动转化成字符串再相加
        body += data;
        // console.log(data);
      });

      req.on("end", () => {
        // console.log('body: ', body);

        // 处理数据
        // 1.获取image/png的位置
        const payload = qs.parse(body, "\r\n", ": ");
        console.log(payload["Content-Type"]);
        const type = payload["Content-Type"];

        // 2.开始再 image/png的位置进行截取
        const typeIndex = body.indexOf(type);
        const typeLength = type.length;
        let imageData = body.substring(typeIndex + typeLength);

        // 3.将中间的两个空格去掉
        imageData = imageData.replace(/^\s\s*/, "");

        // 4.将最后的 boundary去除掉
        imageData = imageData.substring(
          0,
          imageData.indexOf(`--${boundary}--`)
        );

        // 5.写入到文件
        fs.writeFile("./bar.png", imageData, { encoding: "binary" }, (err) => {
          console.log(err);
          console.log("文件上传成功!");
          res.end("文件上传成功!");
        });
      });
    } else {
      res.statusCode = "403";
      res.end("请求方法不正确");
    }
  } else {
    res.end("ok");
  }
});

server.listen(8000, () => {
  console.log("server start");
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

# 六、进阶 express

# 6.1 解析 query 和 params

假如现在我们将接收如下两个请求,该怎么拿到附带在请求中的数据呢?

// params方式
http://localhost:3000/products/123/why

// query方式
http://localhost:3000/login?username=why&pasword=123
1
2
3
4
5

其实在express中非常简单,req.params可以拿到第一种方式的请求数据,req.query可以拿到第二种。

示例:

const express = require("express");
const app = express();

// params方式
// 请求:http://localhost:3000/products/123/why
app.get("/products/:id/:name", (req, res, next) => {
  console.log("req.params: ", req.params);
  // req.params:  { id: '123', name: 'why' }
  res.send("商品的详情数据!");
});

// query方式
// 请求 http://localhost:3000/login?username=why&pasword=123
app.get("/login", (req, res, next) => {
  console.log("req.query: ", req.query);
  // req.query:  { username: 'why', pasword: '123' }

  console.log("登录成功!");
  res.end("登录成功!");
});
app.listen(3000, () => console.log(`Example app listening on 3000 port!`));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 6.2 十分好用的工具中间件推荐

本章主要讲解以下内容:自动解析 application/json,自动解析x-www-form-urlencoded,自动解析form-data

假如现在前端给我们发送了一个application/json类型的数据,如下:

{
  "username":"test",
  "password":"12346"
}
1
2
3
4

express中我们如何拿到该数据呢?

开始测试:
Postman:http://localhost:8080/login
在这里插入图片描述

普通做法:

const express = require("express");

const app = express();

app.post("/login", (req, res, next) => {
  req.on("data", (data) => {
    const result = JSON.parse(data.toString());
    console.log("result: ", result);
    // result:  { username: 'test', password: '12346' }
  });
  req.on("end", () => {
    res.end("welcome back");
  });
});

app.listen(8080, () => {
  console.log("server start");
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

进阶做法:

const express = require("express");

const app = express();

// 只解析 application/josn 成对象形式数据,保存到 req.body 中
app.use((req, res, next) => {
  if (req.headers["content-type"] === "application/json") {
    req.on("data", (data) => {
      const info = JSON.parse(data.toString());
      req.body = info;
    });
    req.on("end", () => {
      next();
    });
  } else {
    next();
  }
});

app.post("/login", (req, res, next) => {
  console.log("req.body: ", req.body);
  // req.body:  { username: 'test', password: '12346' }
  res.end("welcome back");
});

app.listen(8080, () => {
  console.log("server start");
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

可以发现在进阶做法中,我们注册了一个全局的中间件,并且它实现了只监测我们的 application/json数据的处理,解析出来的数据放到了req.body中。

这种方式十分的巧妙且灵活。

但是每次新项目都需要写这样写一短代码是不是有点麻烦呢?于是诞生了body-parser,它就是专门干这个的。

用法示例:

const express = require("express");

const app = express();

// 1. 公用中间件 用于处理解析出我们需要的数据
// 解析 application/josn 成对象形式数据
// app.use((req, res, next) => {
//   if (req.headers["content-type"] === 'application/json') {
//     req.on('data', data => {
//       const info = JSON.parse(data.toString())
//       req.body = info
//     })

//     req.on('end', () => {
//       next()
//     })
//   } else {
//     next()
//   }
// })

// 有一个库专门干这个:body-parser
// body-parser:express 3.x 内置于express框架
// body-parser:express 4.x 被分离出去
// body-parser类似功能:express 4.16.x 内置成函数

// 2. 当前express版本 4.17.1,直接使用内置的 body-parser 解析我们需要的数据
// 解析 application/json
app.use(express.json());
// 解析 x-www-form-urlencoded
// extended:true:那么对urlencoded进行解析时,它使用的是第三方库,qs
// extended:false:那么对urlencoded进行解析时,使用的是Node内置的querystring
app.use(express.urlencoded({ extended: true }));
//不加{ extended: true }抛出警告:不推荐使用body解析器

app.post("/login", (req, res, next) => {
  console.log("req.body: ", req.body);
  res.end("welcome back");
});
app.post("/products", (req, res, next) => {
  console.log("req.body: ", req.body);
  res.end("upload products info success");
});

app.listen(8080, () => {
  console.log("server start");
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

此外,还有一个库multer,专门解析form-data

用法示例:解析普通数据(非 file)

const express = require("express");
const multer = require("multer");

const app = express();
const upload = multer();

// 解析 application/json
app.use(express.json());
// 解析 x-www-form-urlencoded
app.use(express.urlencoded({ extended: true }));
// 解析 form-data

// upload.any() 会返回一个函数,直接把他注册为中间件,他会帮我们完成解析
app.post("/login", upload.any(), (req, res, next) => {
  console.log("用户登录成功");
  res.end("用户登录成功");
  console.log("req.body: ", req.body);
});

app.listen(8080, () => {
  console.log("form-data解析服务器启动成功~");
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

解析上传的文件:

const path = require("path");

const express = require("express");
const multer = require("multer");

const app = express();

// 解析 application/json
app.use(express.json());
// 解析 x-www-form-urlencoded
app.use(express.urlencoded({ extended: true }));

// 解析 form-data
// 构造文件上传的配置对象
const storage = multer.diskStorage({
  // destination 目的地
  destination: (req, file, cb) => {
    cb(null, "./uploads/");
  },
  filename: (req, file, cb) => {
    // file.originalname 能拿到原始文件的名称
    cb(null, Date.now() + path.extname(file.originalname));
  },
});

const upload = multer({
  // storage 表示自定义上传的文件信息,如文件名称、后缀名等
  storage,
});

// upload.any() 会返回一个函数,直接把他注册为中间件,他会帮我们解析
app.post("/login", upload.any(), (req, res, next) => {
  console.log("用户登录成功");
  res.end("用户登录成功");
  console.log("req.body: ", req.body);
});

// form-data 上传文件的中间价
// 该中间件:获取这次上传的文件,并且进行保存
// 单个文件upload.single('key') 多个:upload.array
app.post("/upload", upload.array("file"), (req, res, next) => {
  console.log("req.body: ", req.body);

  // 拿到保存的files
  console.log("req.files: ", req.files);
  res.end("文件上传成功!");
});

app.listen(8080, () => {
  console.log("form-data解析服务器启动成功~");
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
最后更新于: 2021年9月15日星期三晚上10点10分
Faster Than Light
Andreas Waldetoft / Mia Stegmar