OSI 七层模型(是理想化网络模型)
- 应用层
- 表示层
- 会话层 实际中 5.6.7 归成应用层
- 传输层 TCP UDP 协议
- 网络层 路由器
- 数据链路层 交换机 网卡
- 物理层 物理设备 网线 光纤
实际五层
- 实际应用中会将会话层、表示层、应用层合并在一起为应用层
- 用户要传递的数据 装包 HTTP DNS
- 传输层 用户要传递很大的数据包 会对大的数据包进行拆分并标明
序号端口号用于传输到哪 TCP、UDP段 - 网络层 对数据再次进行包装 ip 地址 如果传输层的数据还是非常大 网络层会再次进行分包 IP 协议
包 - 数据链路层 再次包装成
帧 - 物理层 这两层涉及到的是硬件
五层模型(统称为 TCP/IP 协议 协议簇)
- 能称为协议的都在数据链路层之上,所以在网络层、传输层、应用层才有协议
- 协议的功能就是把数据按照某个规范去封装,再把数据进行传输(协议就是对数据的封装+传输)
- 网络层
- ip 协议 寻址
- ARP 协议 从 ip 地址获取 mac 地址(局域网下)
- 传输层:TCP、UDP
- 应用层: http 超文本传输协议、 DNS 域名解析成 ip 地址、 FTP 文件传输协议
DNS 协议
- DNS 服务器进行域名和对应 ip 地址转换的服务器
IP 协议
- 寻址 通过 ip 地址定位到最终设备
ARP协议
- 怎么通过ip地址找到mac地址的
- ARP协议:目的就是通过ip地址找到mac地址 (在局域网下)
- 会广播给每一个,对应到要找的IP地址会做出响应,将mac地址返回
- ARP会有缓存记录 有记录就不用广播 缓存的是 ip:mac
- 交换机缓存的是 mac:端口
TCP 协议
- 传输控制协议 特点是可靠、传输效率低
- TCP 提供全双工服务,即数据可在同一时间双向传播
- 数据是无序的在网络间传递,接收方需要有一种算法在接收到数据后恢复原有的顺序
- 三次握手
- 我能主动给你打电话吗
- 可以啊 那我也能给你打电话吗
- 可以 建立连接成功
- 四次挥手
- 我们分手吧
- 回复收到分手的信息
- 好吧 分手
- 行 那就到这
- 小结
- TCP 是双工的,所以握手需要三次,保证双方达成一致(建立连接浪费性能)
- 当断开连接时,发送(FIN)时另一方需要马上回复(ACK),但此时可能不能立即关闭(有未发送完的数据,还有一些准备断开的操作),所以等待确认可以关闭时再发送(FIN)
- 滑动窗口
- 窗口大小是以字节为单位
- 建立TCP连接时,接收方会告知自己的窗口可以接收多少字节的数据
- 滑动窗口的目的是实现丢包重传,并且将数据有效的发送给接收方,而且可以知道每次发送多少,从而做到流量控制
- 所以滑动窗口的核心是
控制流量 - TCP会做什么:传输的时候会做一个滑动窗口,不停地在协商滑动窗口的大小,如果值满了就停止传输,不停地发探测包,如果有了可用窗口大小,接着去传输数据。按照顺序,成功接收到一个数据之后滑动窗口会向后移动,并根据协商的结果修改窗口大小
- 慢启动、拥塞避免、快重传、快恢复
- 也是用来做流量控制的
- 慢开始 超时
- 快重传 不等超时 三次确认就立马重发
http
- 发展历程
- http/0.9 传输过程中没有请求头和请求体 内容采用ASCii字符流传输html
- http/1.0 加了请求头和响应头 实现多类型数据传输
- http/1.1
- 持久连接 一个TCP连接上可以传输多个http请求
- 管线化方式 每个域名最多维护6个TCP持久连接 有队头阻塞问题
- 引入客户端cookie机制,安全机制等
- http/2.0
- 采用多路复用机制 一个域名使用一个TCP长链接
- 头部压缩
- 服务端推送
- http/3.0 基于UDP
- 当访问网页,发生了哪些事情
- 客户端发送一个请求 发送的是一个域名 域名会发送给dns服务器
- dns解析出一个ip给到客户端
- http会把数据进行传输,http协议生成针对目标web服务器的http请求报文
- 交给TCP进行传输:为了方便通信,将http请求报文按序号分割成报文段,把每个报文段可靠的传给对方(丢包或者超时就重发)
- (TCP自身没有传输能力,如何传靠的是网络层)ip协议:寻址和路由 找到对方地址,通过mac地址一个个找到下一站,不停中转
- 从对方接收到TCP报文段, 对报文段按照原来的序号进行重组报文
- http解析传递过来的数据,对内容进行处理
- 处理完成之后再增加一些响应信息,同样利用TCP/IP通信协议向用户进行回传
HTTP是不保存状态的协议, 使用cookie来管理状态
为了防止每次请求都进行tcp链接的建立和断开, 采用保持链接的方式
keep-alive以前发送请求后需要等待并收到响应,才能发下一个,现在都是管线化方式
http应用
- http也是封装了一些信息和传输
curl -v www.baidu.com- http分为两部分 (发送请求 client)req -> (接收请求 server)res
- 请求分为三部分(三部分都可以传输数据)
- 请求行 通过方法 路径(传输的数据有限制 url大小限制)
- 如 GET / 版本号
- 请求头 放一些自定义信息 约定的信息 请求头不要过大
- 请求体 传输的数据
- 请求行 通过方法 路径(传输的数据有限制 url大小限制)
- 响应也分为三部分
- 响应行
- 如 版本号 200 ok
- 响应头
- 响应体
- 响应行
- restFul风格
- GET/POST/PUT/SELETE/OPTIONS
- OPTIONS请求代表的是跨域访问时可能会出现 预检请求 试探请求 如果对方确认后 可以发真实的请求
- OPTIONS请求只在复杂请求的状态下才能发送(get、post都是简单请求,如果增加了自定义header那么就是复杂请求)
- OPTIONS可以定义发送的时间间隔
- 常见的状态码
- 可以自己设定 但是浏览器和服务器之间是有一些约定的
- 1xx 服务器收到了信息 等到浏览器后续要做的事 websocket
- 2xx 成功
- 3xx 重定向 缓存
- 4xx 客户端出错 (浏览器参数、或者服务器无法解析客户端参数)
- 5xx 服务器错误
- 200 请求成功
- 204 请求成功 但是无响应内容
- 206部分内容 分段请求
- 301 302 永久重定向和临时重定向
- 304缓存
- 400 客户端请求错误
- 401 权限问题 当前用户没登录 无权限观看
- 403 登陆了 但是还是没有权限
- 404 找不到
- 405 服务器只支持get、post 但是发送了put请求 服务器就会响应找不到此方法
- 500 请求服务解析出错了 无法完成响应
- 502 服务期收到的内容无效
- 503 负载均衡挂了
const http = require("http");
// 下面两种写法是等价的
const server = http.createServer(function(req,res) {
console.log("request")
});
server.on('request', function(req,res) {
console.log("request")
});
let port = 3000;
server.listen(3000, function() {
console.log('server listen on 3000')
});
// 监听错误
server.on("error", function(err){
if(err.code === 'EADDRINUSE') { // 说明端口号被占用
server.listen(++port);// 不用再写回调 监听成功之后会走到上面对应的回调,打印server listen on 3000
}
})
- http模块对req、res的封装
const server = http.createServer(function(req, res) {
// req是一个可读流
// 请求行
console.log(req.method); //大写的
console.log(req.url);
console.log(req.httpVersion);
// 请求头
console.log(req.headers); // 统一node处理后全部都是小写
// 请求体
const arr = [];
req.on("data", function(data) {
console.log(data);
arr.push(data);
});
req.on("end", function(){
const data = Buffer.concat(arr).toString();
console.log("end", data);
})
})
const server = http.createServer(function(req, res) {
// res 是一个可写流
res.statusCode = 202;
res.statusMessage = "my 202";
res.setHeader("token", "ok");
res.write("1");
res.end("2");
});
实现静态服务
const http = require("http");
// url组成: 协议 ://(用户名:密码)域名:端口号/资源路径?查询参数#hash
const url = require("url");
// 参数加上true 将query转成对象形式
// const {pathname,query} = url.parse("http://username:password@www.zz.com:3000/xxx?a=1#hash", true);
// pathname => /xxx
const mime = require("mime");
const server = http.createServer((req,res) => {
const {pathname} = url.parse(req.url, true);
// 根据路径来读取文件 /public/index.html
const filePath = path.join(__dirname, pathname); // 获取绝对路径
fs.readFile(filePath, function(err, data) {
if(err) {
res.statusCode = 404;
return res.end('not found');
}
// 直接返回 浏览器可以正常展示 是因为在html中设置了 <meta charset="UTF-8">
// 如果浏览器不给编码 浏览器显示就会乱码
// 所以浏览器有可能不加编码 服务器在返回数据的时候就需要添加编码格式
res.setHeader("Content-Type", mime.getType(filePath) + ";charset=utf8");
res.end(data);
})
});
server.listen(3000);
- 写一个静态服务器
- 官方:npm i http-server -g 可以在本地启动一个服务
- 使用:命令行 hs 或者 http-server 默认会找public文件夹
// package.json
{
"name": "zhuhaha-server-listen",
"version":"1.0.0",
"description": "",
"main":"1.http.js",
"bin":{
"zsl": "./bin/www"
}
}
/*
这个包默认是无法使用的,要把它放到全局下进行测试
使用 npm link
会生成软链 一个叫做 zhuhaha-server-listen 一个叫 zsl
*/
www文件中:
#!/usr/bin/env node
重新 npm link --fore
- 服务器要支持: 使用
commander模块- --port 改端口号
- --directory 制定以哪个目录为基准
- --help 命令
- --version
- --useage
- 模版引擎的实现
- with + new Function
ejs.renderFile(path.resolve(__dirname, 'tmpl.html'), {arr:[1,2,3,4,5]}, {async: true}).then(data =>. console.log(data))
- 命令行配置的实现 -- commander模块
// www 对命令行做一些配置
const {program} = require("commander");
const version = require("../package.json").version;
const Server = require("../src/main.js");
const config = {
'port': {
option: '-p,--port <n>',
description: 'set server port',
default: 8080,
usage: 'zsl --port <n>'
},
'directory': {
option: '-d,--directory <n>',
description: 'set server directory',
default: process.cwd(),
usage: 'zsl -d D:'
}
}
program.version(version) // 版本
.name("zsl")
.usage("[options]")
const usages = [];
Object.entries(config).forEach(([key, value]) => {
usages.push(value.usage);
program.option(value.option, value.description, value.default) // 配置选项
})
program.on("--help", function() { // 使用示例
console.log("\nExamples:")
usages.forEach(usage => console.log(" " + usage))
})
program.parse(process.argv); // 解析命令行参数
let ops = program.opts(); // 得到参数对象
console.log(ops);
let server = new Server(ops);
server.start(); // 开启服务
- 模版引擎的实现
// 自实现模版引擎
async function renderFile(filePath, data) {
let tmplStr = await fs.readFile(filePath, 'utf8');
let myTemplate = `let str = ''\r\n`;
myTemplate += 'with(obj){'
myTemplate += 'str+=`'
tmplStr = tmplStr.replace(/<%=(.*?)%>/g, function() {
return '${' + arguments[1] + '}';
})
myTemplate += tmplStr.replace(/<%(.*)%>/g, function() {
return '`\r\n' + arguments[1] + '\r\nstr+=`'
})
myTemplate += '`\r\n return str \r\n}';
let fn = new Function('obj', myTemplate);
return fn(data);
}
async function render(tmplStr, data) {
let myTemplate = `let str = ''\r\n`;
myTemplate += 'with(obj){'
myTemplate += 'str+=`'
tmplStr = tmplStr.replace(/<%=(.*?)%>/g, function() {
return '${' + arguments[1] + '}';
})
myTemplate += tmplStr.replace(/<%(.*)%>/g, function() {
return '`\r\n' + arguments[1] + '\r\nstr+=`'
})
myTemplate += '`\r\n return str \r\n}';
let fn = new Function('obj', myTemplate);
return fn(data);
}
- 模版
// tmpl.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<%dirs.forEach(item =>%>
<a href="<%=item.url%>"><%=item.dir%></a>
<%)%>
</body>
</html>
- 搭建静态http服务
// src/main.js
const http = require("http");
const fs = require("fs").promises;
const path = require("path");
const url = require("url");
const mime = require("mime");
const os = require("os");
const chalk = require("chalk");
const zlib = require("zlib");
const crypto. = require('crypto');
const {createReadStream, readFileSync} = require("fs");
let address = Object.values(os.networkInterfaces()).flat().find(item => item.family == 'IPv4').address;
const template = readFileSync(path.resolve(__dirname, 'tmpl.html'), 'utf8');
module.exports = class MyServer{
constructor(opts={}) {
this.port = opts.port;
this.directory = opts.directory;
this.address = address;
this.template = template;
}
handleRequest = async (req, res) => {
console.log(this);
// 请求到来的时候 需要监控路径 看一下路径是否是文件 如果是文件 直接将文件返回 如果不是文件则读取文件中的目录
let {pathname} = url.parse(req.url);
pathname = decodeURIComponent(pathname); // 对路径中的中文进行转译 保证正常显示
let filePath = path.join(this.directory, pathname); // 文件绝对路径 在当前执行目录下进行查找
try{
let statObj = await fs.stat(filePath);
if(statObj.idDirectory()) {
const dirs = await fs.readdir(filePath);
// 通过模版引擎的方式来实现
let content = await render(template, {
dirs: dirs.map(dir => ({
url: path.join(pathname, dir),
dir
}))
});
res.setHeader("Content-Type", "text/html;charset=utf-8");
res.end(content);
}else{
// 文件
this.sendFile(req,res,statObj,filePath);
}
}catch(e) {
// 文件不存在则发生错误
this.sendError(res, e);
}
}
gzip(req,res){
// 在发送前要进行压缩处理
// 浏览器在请求头重会告知:Accept-Encoding:gzip,deflate,br
// node默认只支持gaip和deflate
// 服务器会在响应头告知:content-encoding:gzip 通过gzip压缩
let encoding = req.headers['accept-encoding'];
let zip;
if(encoding){
// 说明支持压缩
let ways = encoding.split(",");
for(let i = 0; i < ways.length; i++){
if(ways[i] === 'gzip'){
res.setHeader('content-encoding', 'gzip');
zip = zlib.createGzip();
break;
}else if(ways[i] === 'deflate'){
res.setHeader('content-encoding', 'deflate');
zip = zlib.createDeflate();
break;
}
}
}
return zip;
}
cache(req, res, statObj, filePath){
// 在发送文件之前可以要求此文件以后多少时间内不要再来访问了
// 只针对引用的资源 首次访问的资源不会被设置
// 即强制缓存 直接访问的页面是不能被缓存的
// res.setHeader('Cache-Control', 'max-age=10'); // s单位,10s内引用的其他资源不要再访问了,Cache-Control会比Expires优先级更高一些
// res.setHeader('Expires', new Date(Date.now()+10*1000).toGMTString()); // 设置一个到期时间戳,这个设置项是为低版本浏览器设置的
res.setHeader('cache-control', 'no-cache'); // 表示每次都来服务器询问 依旧会将文件放入缓存当中;no-store是每次都来询问服务器,还不缓存
// 协商缓存
// 有的文件可能10s后还是没有变 希望对比一下 如果文件没变 继续去缓存中找
// last-modified 服务器告诉浏览器 次文件最后修改时间是多少
// if-modified-since 浏览器下次访问的时候带过来的
console.log(req.headers['if-modified-since']); // 服务器设置了之后,才会打印出值
const ifModifiedSince = req.headers['if-modified-since'];
const ctime = statObj.ctime.toGMTString();
res.setHeader('last-modified', )
// if(ifModifiedSince !== ctime){
// return false;
// }
// 根据最后修改时间 可能会出现变化后但是内容没变 或者如果1s内多次变化也监控不到 因为缓存时间的单位是秒
// tag 根据内容来生成唯一的标识
// md5摘要 不可逆的 无法判断原来的值 特点是相同内容摘要出的结果是相同的,所以可以去撞库来反推原来的结果是什么
// 服务器提供 etag 浏览器提供 if-none-match
const ifNoneMatch = req.headers['if-none-match'];
// 真正开发的时候不会去读文件,一般会采用文件的某一部分或者计算文件的大小 总长度
const etag = crypto.createHash('md5').update(fs.readFileSync(filePath)).digest('base64');
res.setHeader('ETag', etag);
if(ifNoneMatch != etag){
return false;
}
return true;
}
sendFile(req,res,statObj,filePath) {
// 缓存
if(this.cache(req,res,statObj,filePath)) {
res.statusCode = 304;
return res.end();
}
res.setHeader("Content-Type", (mime.getType(filePath) || "text/plain") + ";charset=utf-8")
// 之前文件没有经过压缩 读取之后直接返回给浏览器
// createReadStream(filePath).pipe(res); // 异步方法
let zip = this.gzip(req,res);
if(zip){ // 支持压缩就压缩
createReadStream(filePath).pipe(zip).pipe(res);
}else{
createReadStream(filePath).pipe(res);
}
}
sendError(res, e) {
console.log('err', e);
res.statusCode = 404;
res.end("not found");
}
start() {
console.log('start ' + this.port + " " + this.directory);
const server = http.createServer(this.handleRequest);
server.listen(this.port, () => {
console.log(`${chalk.yellow('starting up http-server, seerving:')}` + this.directory);
consolee.log(` http://${address}:${chalk.green(this.port)}`);
consolee.log(` http://127.0.0.1:${chalk.green(this.port)}`)
})
}
}
/**
如果是文件 就通过流的方式将文件内容读取之后返回给浏览器
如果读取的是文件夹 就通过模版引擎将文件夹下面的文件或者子目录渲染出来,返回浏览器,当点击相应的子目录,显示相应的内容
*/
// 至此 静态服务就基本实现了
压缩
- gzip压缩 主要的压缩方式是替换 重复率越高压缩就越有效果
const zlib = require("zlib");
zlib.createGzip // 流的方式 读一点操作一点
zlib.gzip() // 非流的方式
zlib.gzip(fs.readFileSync(path.resolve(__dirname, '1.txt')), function(err,data){
fs.writeFileSync('1.txt.gz', data)
})
- 流的四种方式
- 可读流 on('data') on('end')
- 可写流 write end
- 双工流 能读能写
- 转化流 将数据进行转化如压缩 加密等
// 标准输入 读取用户的输入
process.stdin.on('data', function(chunk){ //能读
console.log(chunk); // 会监听到用户在命令行输入的内容,
})
process.stdin.on('data', function(chunk){
process.stdout.write(chunk); // 能写 相当于 console.log
})
// 这两串的简化写法就是:
process.stdin.pipe(process.stdout)
// 如果想实现输入小写输出转大写,就会用到转化流
const {Transform} = require('stream');
class MyTransform extends Transform{
_transfrom(chunk, encoding, clearBuffer){ // 参数和可写流一样
this.push(chunk.toString().toUpperCase());
clearBuffer();
}
}
let transform = new MyTransform();
process.stdin.pipe(transform).pipe(process.stdout);
- 在静态服务中 发送文件给浏览器之前先进行压缩
缓存
- 强制缓存
- Cache-Control: 'max-age=10'
- Expires 设置到期时间戳
- 协商缓存
- last-modified 服务器返给客户端的
- if-modified-since 客户端发请求时会带上的
- md5的特点
- 不可逆
- 相同的内容出来的结果相同
- 不同的内容 结果完全不同
- 摘要后的长度都是一致的
const r = crypto.createHash('md5').update('内容').digest('base64'); 创建md5摘要- 服务器提供 etag
- 浏览器提供 if-none-match