开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 20 天,点击查看活动详情
1. 创建服务器
什么是 Web 服务器?
- 当应用程序(客户端)需要某个资源时,可以向一台服务器,通过
HTTP请求获取到这个资源 - 提供资源的这个服务器,就是一个
Web服务器
目前有很多开源的 Web 服务器:Nginx、Apache(静态)、Apache Tomcat(静态、动态)、Node.js
在 Node 中,提供 web 服务器的资源返回给浏览器,主要是通过 http 模块。
我们先简单对它做一个使用:
const http = require("http");
const HTTP_PORT = 9000;
const server = http.createServer((req, res) => {
res.end("Hello World");
});
server.listen(HTTP_PORT, () => {
console.log(`服务器在${HTTP_PORT}启动`);
});
此时我们在浏览器中输入 localhost:9000,就会出现 Hello World:
解释上面这段代码:
-
通过
http模块的createServer方法创建了一个服务器对象,它的底层其实是直接使用new Server创建对象的。那么当然,我们也可以自己来创建这个对象:
const server = new http.Server((req, res) => { res.end("Hello World"); }); server.listen(HTTP_PORT, () => { console.log(`服务器在${HTTP_PORT}启动`); }); -
创建
Server时会传入一个回调函数,这个回调函数在被调用时会传入两个参数:req:request请求对象,包含请求相关的信息;res:response响应对象,包含我们要发送给客户端的信息;
-
Server通过listen方法来开启服务器,并且在某一个主机的端口上监听网络请求也就是当我们通过
ip:port的方式发送到我们监听的Web服务器上时,我们就可以对其进行相关的处理;listen函数有三个参数:-
端口
port:可以不传,系统会默认分配端 -
主机
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地址是可以访问的;
- 监听
-
-
回调函数:服务器启动成功时的回调函数;
-
2. Request 请求
2.1 Request 对象
在向服务器发送请求时,我们会携带很多信息,比如:
- 本次请求的
URL,服务器需要根据不同的URL进行不同的处理; - 本次请求的请求方式,比如
GET、POST请求传入的参数和处理的方式是不同的; - 本次请求的
headers中也会携带一些信息,比如客户端信息、接收数据的格式、支持的编码格式等; - 等等...
这些信息,Node 会帮助我们封装到一个 request 的对象中,我们可以直接来处理这个 request 对象:
const server = new http.Server((req, res) => {
// request 对象
console.log(req)
console.log(req.method)
console.log(req.headers)
res.end("Hello World");
});
2.2 URL 的处理
客户端在发送请求时,会请求不同的数据,那么会传入不同的请求地址:
-
比如
http://localhost:9000/login; -
比如
http://localhost:9000/products;
服务器端需要根据不同的请求地址,作出不同的响应:
const server = new http.Server((req, res) => {
const url = req.url;
if (url === "/login") {
res.end("welcome Back~");
} else if (url === "/products") {
res.end("products");
} else {
res.end("error message");
}
});
2.3 URL 的解析
如果用户发送的地址中还携带一些额外的参数,例如以下情况
-
http://localhost:9000/login?name=why&password=123; -
这个时候,
url的值是/login?name=why&password=123;
这个时候我们可以使用内置模块 url 处理
const parseInfo = url.parse(req.url);
console.log(parseInfo)
此时的 parseInfo 为:
接下来我们就可以直接用 qs 或者 URLSearchParams 处理 query 拿到参数了
const queryObj = new URLSearchParams(parseInfo.query);
console.log(queryObj.get("name"));
console.log(queryObj.get("password"));
2.4 请求头
在 request 对象的 header 中也包含很多有用的信息,客户端会默认传递过来一些信息:
content-type是这次请求携带的数据的类型:application/x-www-form-urlencoded:表示数据被编码成以'&'分隔的键-值对,同时以'='分隔键和值application/json:表示是一个json类型;text/plain:表示是文本类型application/xml:表示是xml类型;multipart/form-data:表示是上传文件;
content-length:文件的大小长度keep-alive:http是基于TCP协议的,但是通常在进行一次请求和响应结束后会立刻中断;- 在
http1.0中,如果想要继续保持连接:- 浏览器需要在请求头中添加
connection: keep-alive; - 服务器需要在响应头中添加
connection: keep-alive; - 当客户端再次放请求时,就会使用同一个连接,直接一方中断连接;
- 浏览器需要在请求头中添加
- 在
http1.1中,所有连接默认是connection: keep-alive的;- 不同的
Web服务器会有不同的保持keep-alive的时间; Node中默认是5s中;
- 不同的
accept-encoding:告知服务器,客户端支持的文件压缩格式,比如js文件可以使用gzip编码,对应.gz文件;accept:告知服务器,客户端可接受文件的格式类型;user-agent:客户端相关的信息;
3. Response 响应
3.1 返回响应结果
如果我们希望给客户端响应的结果数据,可以通过两种方式:
write方法:这种方式是直接写出数据,但是并没有关闭流;end方法:这种方式是写出最后的数据,并且写出后会关闭流;
// 响应数据的两个方式
res.write('Hello World)
res.write("Hello Response")
res.edn("message end")
如果我们没有调用 end 和 close,客户端将会一直等待结果:
- 所以客户端在发送网络请求时,都会设置超时时间。
3.2 返回状态码
Http状态码(Http Status Code)是用来表示 Http 响应状态的数字代码:
Http状态码非常多,可以根据不同的情况,给客户端返回不同的状态码;MDN响应码解析地址:developer.mozilla.org/zh-CN/docs/…
| 常见HTTP状态码 | 状态描述 | 信息说明 |
|---|---|---|
| 200 | OK | 客户端请求成功 |
| 201 | Created | POST请求,创建新的资源 |
| 301 | Moved Permanently | 请求资源的URL已经修改,响应中会给出新的URL |
| 400 | Bad Request | 客户端的错误,服务器无法或者不进行处理 |
| 401 | Unauthorized | 未授权的错误,必须携带请求的身份信息 |
| 403 | Forbidden | 客户端没有权限访问,被拒接 |
| 404 | Not Found | 服务器找不到请求的资源 |
| 500 | Internal Server Error | 服务器遇到了不知道如何处理的情况。 |
| 503 | Service Unavailable | 服务器不可用,可能处于维护或者重载状态,暂时无法访问 |
// 1.
res.statusCode = 400
// 2.
res.writeHead(200)
3.3 响应头文件
返回头部信息,主要有两种方式:
-
res.setHeader:一次写入一个头部信息; -
res.writeHead:同时写入header和status;
res.setHeader('Context-Type', 'application/json;charset=utf8')
res.writeHead(200, {
"Content-Type": "application/json;charset=utf8"
})
Header 设置 Content-Type 有什么作用呢?
- 默认客户端接收到的是字符串,客户端会按照自己默认的方式进行处理;
4. 文件上传
其实对于后端,手动处理上传的文件是很复杂的,正常情况下我们都会借助于一些插件做处理, 下面仅做对于图片上传的一个简单的演示。
const http = require("http");
const qs = require("querystring");
const fs = require("fs");
const HTTP_PORT = 9000;
// 1. 创建 sever 服务器
const server = new http.Server((req, res) => {
// 文件设置为二进制
req.setEncoding("binary");
// 获取 content-type 中的 boundary的值
let boundary = req.headers["content-type"]
.split("; ")[1]
.replace("boundary=", "");
const fileSize = req.headers["content-length"];
let curSize = 0;
let body = "";
req.on("data", (data) => {
curSize += data.length;
res.write(`文件上传进度:${(curSize / fileSize) * 100}%\n`);
body += data;
});
req.on("end", () => {
// 切割数据
const payload = qs.parse(body, "\r\n", ":");
// 获取最后的类型(image/png)
const fileType = payload["Content-Type"].substring(1);
// 获取要截取的长度
const fileTypePosition = body.indexOf(fileType) + fileType.length;
let binaryData = body.substring(fileTypePosition);
binaryData = binaryData.replace(/^\s\s*/, "");
const finalData = binaryData.substring(
0,
binaryData.indexOf("--" + boundary + "--")
);
fs.writeFile("./foo.png", finalData, "binary", (err) => {
console.log(err);
res.end("文件上传完成~");
});
});
});
// 2. 开启 server 服务器
server.listen(HTTP_PORT, () => {
console.log(`服务器在${HTTP_PORT}启动`);
});
使用 postman 测试,并查看图片能显示。