Node.js(二、请求响应原理及HTTP协议【详细】)
1、服务器端基础概念
1.1、IP地址
互联网中设备的唯一标识。IP是Internet Protocol Address的缩写,代表互联网协议地址。
1.2、域名
由于IP地址难遇记忆,所以产生了域名的概念,所谓域名就是平时上网所使用的网址。
虽然在地址栏中输入的是网址,但是最终还是会将域名转换为ip才能访问到指定的网站服务器。如:https://www.lgg.com => https://127.0.0/。
1.3、端口
端口是计算机与外界通讯交流的出口,用来区分服务器电脑中提供不同的服务。
1.4、URL
统一资源定位符,又叫URL(Uniform Resource Locator),是专为标识Internet网上资源位置而设的一种编址方式,我们平时所说的网页地址指的即是URL。
URL的组成:
传输协议://服务器IP或域名:端口/资源所在位置标识。(如:https://www.lgg.com/newsList/lgg.html)
http:超文本传输协议,提供了一种发布和接受HTML页面的方法。
2、创建web服务器
⚠️注意:整个代码拷贝后不能够正常运行,需要按需求拆分处理。
// 引用系统模块
const http = require('http');
// get请求参数获取需要借助系统模块url,用来处理url地址
const url = require('url');
// post请求参数获取需要导入系统模块querystring,用于将HTTP参数转换为对象格式
const queryString = require('querystring');
// 创建web服务器
const app = http.createServer();
//当客户端发送请求的时候
app.on('request', (req, res) => {
// 1、获取请求方式req.method
if(req.method == 'POST') {
res.end('我是POST请求。')
} else if(req.method == ''GET) {
res.end('我是GET请求。')
}
// 2、获取请求地址req.url
if(req.url == '/index' || req.url == '/') {
res.end('我是首页。')
} else {
res.end('未找到内容,请重新输入。')
}
// 3、响应res.end(`<h1>hello,lgg!</h1>`)
// 4、获取请求报文信息req.headers['accept']
// 5、设置响应报文
/*格式:
res.writeHead('响应状态码', {'内容类型'})
响应状态码:200请求成功、300重定向、400客户端错误、500服务器错误等。
内容类型(content-type):'text/html'、'text/css'、'application/javascript'、'image/jpeg'、'application/json'等。
附:'content-type': 'text/html;charset=utf8'; 可解决中文乱码问题。 */
res.writeHead(200, {
'content-type': 'text/html;charset=utf8'; // 可解决中文乱码问题。
})
// 6、get请求参数获取。
/*GET请求参数被放置在浏览器地址栏中(POST是在请求体即报文中传输的),例如:http://localhost:3000/?age=25。
参数的获取需要借助系统模块url,来处理url地址:
(1)、第一个参数是要解析的URL地址。
(2)、第二个参数是将查询参数解析成对象形式:url.parse(res.url, true).query
*/
let value = url.parse(res.url, true)
// url为http://localhost:3000/?age=25
console.log(res.url) // /?age=25
console.log(value) /*
Url {
protocol: null,
slashes: null,
auth: null,
host: null,
port: null,
hostname: null,
hash: null,
search: '?age=25',
query: [Object: null prototype] { age: '25' },
pathname: '/',
path: '/?age=25',
href: '/?age=25'
}
*/
console.log(value.query) // [Object: null prototype] { age: '25' }
});
// 7、post请求处理请求参数:导入系统模块querystring,用于将HTTP参数转换为对象格式。
/*post参数是通过事件的方式接受的。
参数被放置在请求体中(即报文中)进行传输。
获取post参数需要使用data事件和end事件。
使用querystring系统模块将参数转换为对象格式。
data当请求参数传递的时候触发data事件。
end当参数传递完成的时候触发end事件。*/
let postParams = '';
// 监听参数传输事件
req.on('data', params => {
postParams += params;
})
// 监听参数传输完毕事件
req.on('end', () => {
// 把字符串转换为对象
let { user, password } = queryString.parse(postParams);
})
res.end('ok')
// 监听端口
app.listen(3000)
3、HTTP协议
3.1、HTTP协议的概念
超文本传输协议(英文:HyperText Transfer Protocol,缩写:HTTP)规定了如何从网站服务器传输超文本到本地浏览器,它基于客户端服务器架构工作,是客户端(用户)和服务器端(网站)请求和应答的标准。
3.2、报文
在HTTP请求和响应过程中传递的数据块就叫报文,包括要传送的数据和一些附加信息,并且要遵守规定好的格式。
3.2.1、请求报文
1、请求方式(Request Method)
- GET 请求数据
- POST 发送数据
2、请求地址(Request URL)
app.on('request', (req, res) => {
req.headers // 获取请求报文
req.url // 获取请求地址
req.method // 获取请求方法
})
3.2.2、响应报文
1、HTTP状态码
200请求成功、300重定向、400客户端错误、500服务器端错误。
2、设置响应报文
app.on('request', (req, res) => {
res.writeHead(200, {
'Content-Type': '内容类型'
})
})
3、内容类型(Content-Type)
* text/html
* text/css
* application/javascript
* image/jpeg
* application/json
* ···
* 附:text/html;charset=utf8;可解决中文乱码问题。
4、HTTP请求与响应处理
4.1、GET请求参数
- 参数被放置在地址栏中,如:http://localhost:3000/?name=lgg
- 参数获取需要借助系统模块url。
const http = require('http');
const url = require('url');
const app = http.createServer();
app.on('request', (req, res) => {
// true代表将参数解析为对象格式
let { query } = url.parse(req.url, true);
});
app.listen(3000);
4.2、POST请求参数
- 参数被放置在请求体中进行传输。
- 获取POST参数需要使用data和end事件。
- 需要使用系统模块querystring将参数转换为对象格式。
···
const querystring = require('querystring');
···
app.on('request', (req, res) => {
let postParams = '';
app.on('data', params => {
postParams += params;
})
app.on('end', () => {
let { '参数', pathName } = querystring.parse(postParams )
res.end(pathName )
})
})
4.3、路由
路由是指客户端请求地址与服务器端程序代码的对应关系,
4.4、静态资源
服务器端不需要处理,可以直接响应给客户端的资源就是静态资源,如:css、js、图片等文件。(在服务器端专门创建一个文件夹,用来放置静态资源,当客户端请求某个文件是直接响应给客户端,这就是静态资源访问功能)
const http = require('http');
const url = require('url');
const path = require('path');
const fs = require('fs');
// 需要第三方模块mime中的getType方法
const mime = require('mime');
const app = http.createServer();
app.on('request', (req, res) => {
let { pathname } = url.parse(req.url);
pathname = pathName = '/'? 'default.html' : pathname;
// 将用户的请求路径转换为实际的服务器硬盘路径(比如用户请求http://localhost:3000/default.html)
let realPath = path.join(__dirname, 'public', pathname);
let type = mime.getType(realPath);
fs.readFile('realPath ', (err, result) => {
if(err != null) {
res.writeHead(404, {
'Content-Type': 'text/html;charset=utf8'
})
res.end('请求失败!');
return;
}
res.writeHead(200, {
'Content-Type': type
})
res.end(result)
})
})
app.listen(3000)
4.5、动态资源
相同的请求地址(可以传送不同的参数)得到不同的响应资源,这种资源就是动态资源。如:
http://localhost:3000/news?id=1、
http://localhost:3000/news?id=2
4.6、前端请求途径
4.6.1、GET方式
- 浏览器地址栏
- link标签的href属性
- script标签的src属性
- img的src属性
- form表单提交
<link rel="stylesheet" href="base.css">
<script src="index.js"></script>
<img src="login.png">
<form action="example.cn" method='GET'></form>
4.6.2、POST方式
- form表单提交
<form action="example.cn" method='POST'>
5、Node.js异步编程
5.1、同步API,异步API
同步API: 只有当前API执行完后,才继续执行下一个API。如:console.log(···)
异步API: 当前API的执行不会阻塞后续代码的执行。如:定时器等。
5.2、同步API,异步API的区别:
5.2.1、获取返回值
同步API可以从返回值中拿到API执行结果,但是异步API不可以。
// 同步
function sumNum(n1, n2) {
return n1 + n2;
}
const num = sumNum(1, 2);
console.log(num); // 3
// 异步
function getMsg() {
setTimeout(function() {
return { msg: 'Hello world!'}
}, 2000);
// 因为此处默认有return undefined;
}
const msg = getMsg();
console.log(msg) // undefined
1、回调函数
自己定义函数让别人去调用。
// getData函数定义
function getData(callback) {}
// getData(() => {})
2、使用回调函数获取异步API执行结果
function getMsg(callback) {
setTimeout(callback({msg: 'Hello world!'}), 2000)
}
getMsg(function(data) {
console.log(data); // {msg: 'Hello world!'}
})
5.2.2、代码执行顺序
同步API从上到下依次执行,前面代码会阻塞后面代码的执行。
异步API不会等待API执行完成后再向下执行代码。
console.log('begin');
setTimeout(() => { console.log('2s后执行的代码')}, 2000)
setTimeout(() => { console.log('0s后执行的代码')}, 0)
console.log('after')
// 执行顺序为:begin、after、‘2s后执行的代码’、‘0s后执行的代码’
分析:
- 附:Node JS在执行代码时,不是从上到下依次执行,而是先把所有的同步API执行完毕,再去执行异步API
5.2.3、Node.js中的异步API
fs.readFile('./lgg.html', (err, result) => {})
var app = http.createServer();
app.on('request', (req, res) => {});
- 事件处理函数(req、res)=> {}我们没有主动调用过它们,事件处理函数都是在事件发生的时候系统去调用的,系统在传递的时候分别给我们传递了两个对象,分别是请求对象和响应对象。所以我们在定义这个函数的时候才可以通过形参的方式去接受这两个参数。所以事件处理函数就是回调函数,事件监听的API就是异步API。
- 如果异步API后面代码的执行依赖当前异步API的执行结果,但实际上后续代码在执行的时候异步API还没有返回结果。该如何?
需求:依次读取A文件、B文件、C文件。
1、依次进行嵌套,但是有回调地狱问题。
方案一:
const fs = require('fs');
fs.readFile('./lgg1.txt', 'utf8', (err, result1) => {
console.log(result1)
fs.readFile('./lgg2.txt', 'utf8', (err, result2) => {
console.log(result2)
fs.readFile('./lgg3.txt', 'utf8', (err, result3) => {
console.log(result3)
})
})
});
2、Promise
Promise出现的目的是解决Node.js异步编程中回调地狱问题。(是一个构造函数,需要先用new去创建。promise就是在我们原先的异步函数外面包裹一层,当异步API有返回结果时它给我们提供了两个方法resolve:把结果传递到外面、reject)
- 在成功时调用resolve方法,实际上就是调用.then中的result => console.log(result)方法。
- 在失败时调用reject方法,实际上就是调用.catch中的error => console.log(error)方法。
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
if(true) {
resolve({msg: 'lgg'})
} else {
reject('err')
}
}, 2000)
})
promise
.then(result => console.log(result)); // {msg: 'lgg'}
.catch(error => console.log(error)) // err
方案二:
// 只要new一个promise里面的代码就会执行,所以要放到函数内
function lgg1() {
return new Promise((resolve, reject) => {
fs.writeFile('./lgg1.html', 'utf8', (err1, result1) => {
if(err1 != null) {
resolve(result1)
} else {
reject(err1)
}
})
})
}
function lgg2() {
return new Promise((resolve, reject) => {
fs.writeFile('./lgg2.html', 'utf8', (err2, result2) => {
if(err2 != null) {
resolve(result2)
} else {
reject(err2)
}
})
})
}
function lgg3() {
return new Promise((resolve, reject) => {
fs.writeFile('./lgg3.html', 'utf8', (err3, result3) => {
if(err3 != null) {
resolve(result3)
} else {
reject(err3)
}
})
})
}
// lgg1函数必须return才能用.then
lgg1().then((result1) => {
console.log(result1);
return lgg2()
})
.then((result2) => {
console.log(result2);
return lgg3()
})
.then((result3) => {
console.log(result3);
})
3、异步函数
异步函数是异步编程语法终极解决方案,它可以让我们将异步代码写成同步的形式,让代码不再有回调函数嵌套,使代码变得更清晰了。
3.1、async关键字
1、普通函数定义前加async关键字,普通函数变成异步函数
async function fn() {}
2、异步函数默认返回promise对象,而不是undefined。
async function fn() {}
console.log(fn()) // Promise { undefined }
3、在异步函数内部使用return关键字进行结果返回,结果会被包裹在promise对象中,return关键字代替了resolve方法。
async function fn() {
return 'lgg';
}
console.log(fn()) // Promise { 'lgg' }
fn().then((data) => {
console.log(data) // 'lgg'
})
// 与promise相比,简洁之处
function fn() {
return new Promise() // 省略了这一步(new一个promise再返回)
}
在异步函数内部return能替代resolve方法,但是如果发生错误呢?怎样才能把错误信息返回到外面呢?
4、在异步函数内部使用throw关键字抛出程序异常
之前用reject,现在可以用throw关键字替代,还用catch获取。
async function fn() {
throw '发生了错误' // 这个执行了下面就不再执行了
return 123; // 如果return在throw代码前面,则throw不再执行
}
fn().then((data) => {
console.log(data) // (如果return在throw代码前面)只输出123,.catch代码不再执行。
})
.catch((err) => {
console.log(err) // 发生了一些错误
})
5、调用异步函数再链式调用then方法获取异步函数执行结果。
6、调用异步函数再链式调用catch方法获取异步函数执行的错误信息。
3.2、await关键字
1、await关键字只能出现在异步函数中。
2、await promise, await后面只能写promise对象,写其他类型的API是不可以的。
3、await关键字可以暂停异步函数向下执行,直到promise返回结果。
async function lgg1() {
return 'lgg1'
}
async function lgg2() {
return 'lgg2'
}
async function lgg3() {
return 'lgg3'
}
async function lgg() {
let lgg1 = await lgg1();
let lgg2 = await lgg2();
let lgg3 = await lgg3();
console.log(lgg1, lgg2, lgg3); // lgg1 lgg2 lgg3
}
方案三:
readFile方法是通过返回值的方式来获取文件的读取结果的,也就是不返回promise对象,所以不能够加await关键字。也就是说异步函数在nodejs中没法用,但nodejs提供了一个方法可以对现有的异步API进行包装,让方法返回promise对象,以支持异步函数语法
const fs = require('fs');
const promisify = require('util').promisify;
const readFile = promisify(fs.readFile);
async function lgg() {
let lgg1 = await readFile('./lgg1.html', 'utf8')
let lgg2 = await readFile('./lgg1.html', 'utf8')
let lgg3 = await readFile('./lgg1.html', 'utf8')
console.log(lgg1, lgg2, lgg3); // lgg1 lgg2 lgg3
}
lgg();