一、计算机网络简史
1.1 OSI七层模型
#1.1.1 OSI七层模型
- 开放式系统互联网模型
- 世界范围内的网络标准概念模型
- OSI的努力让互联网协议组件走向标准化
#应用层
提供高级API
- 定义了网络主机提供的方法和接口(业务协议、高级协议)
- 往往直接对应用户行为
- 例如:HTTP、FTP、SMTP等
#展示层
- 也被称做语法层
- 将Application layer中的数据转化为传输格式,保留语义(如:序列化、加密解密、字符串编码解码等)
- 确保数据发送取出后可以被接受者理解
#会话层
- 提供管理会话的方法(Open/Close/ReOpen/检查状态等)
- 提供对底层连接的断断续续的隐藏;甚至对多种底层流的隐藏(提供数据同步点)
#传输层
提供主机到主机(host-to-host)的数据通信能力
- 建立连接保证数据封包发送、接受到的顺序一致
- 提供可靠性(发送者知道数据有没有被完整送达)
- 提供流控制(发送者和接受者同步速率)
- 提供多路复用(多种信号复用一个信道)
#网络层
提供数据在逻辑单元(例如IP地址)之间的传递能力
- 路由:决定数据的下一站在哪里
- 寻址:为数据封包增加头信息(地址等)
#数据链路层
提供数据在设备和设备之间的传输能力
- 流控制 :发送者接受者之间同步数据是收发速度和数据量
- 错误控制:检测数据有没有出错,并重发出错的数据
#物理层
定义底层一个位(bit)的数据如何让变成物理信号
- 将数据链路层发生的数据传递行为转化成为物理设备识别的信号
- 封装了大量底层物理设备的能力
#1.2 TCP/IP 协议和互联网协议群
今天使用最多的协议群
#1.2.1 TCP/IP协议群简介
互联网协议群
- 类似OSI模型,一种网络协议的概念模型
它对OSI七层模型做了简化,把OSI的应用层、展示层、会话层简化成了应用层
- 应用层
- 提供应用间通信能力(HTTP协议)
- 传输层
- 提供主机到主机的通信能力(TCP/UDP协议)
- 网络层
- 提供地址到地址的通信能力(form:12.3.41.1->to :9.1.2.12)
- 链路层
- 提供设备到设备的通信能力(设备1->设备2)
- 物理层
- 光电信号的传输
#1.2.2 TCP/IP的三次握手
#1.2.3 TCP/IP协议的消息顺序处理方法
-
消息的绝对顺序用(SEQ,ACK)这一对元组描述
SEQSequence):这个消息发送前一共发送了多少字节ACK(Acknowledge):这个消息发送前一共收到了多少字节
1.3 DNS与CDN
#1.3.1 DNS的基础知识
#统一资源定位符(URL)
- 也被称作(网址),用于定位互联网上的资源
#DNS Query过程
#1.3.2 CDN 实现原理
cdn用于存变化不大的文件
#CDN云测工具
1.4 HTTP入门和基础工具链
#蒂姆.伯纳斯-李
-
英国著名科学家(1955-)
- 万维网(1990年HTTP协议)
- 创办MIT人工智能实验室
#1.4.1 HTTP协议
-
超文本传输协议
超是一个形容词,形容这个文本很厉害,这个协议是挂在tcp上实现的,开始设计就是客户端和服务端通信的,它提供了一种标准就是中间可以传文本, 因为传二进制太复杂了,传文本大家都看的懂,容易推广就容易普及。
-
处理客户端和服务端之间的通信
-
http请求/http返回
-
网页/json/xml/提交表单
#纯文本+无状态(stateless)
-
应用层协议(下面可以是TCP/IP)
-
信息纯文本传输
-
无状态
- 每次请求独立
- 请求间不影响
-
浏览器提供了手段维护状态(Cookie,session,*Storage等)
#HTTP历史
- 1991 HTTP 0.9
- 1996 HTTP 1.0
- 1999 HTTP 1.1
- 2015 HTTP 2.0
#设计的基础因素
-
带宽
- 基础网络(线路、设备等)
-
延迟
- 浏览器
- DNS查询
- 建立连接(TCP三次握手)
#设计考虑因素-缓存与带宽优化
-
缓存
- (http1.0)提供缓存机制如IF-Modified-Since等基础缓存控制策略
- (http1.1)提供E-Tag等高级缓存策略
-
带宽优化
- (http 1.1)利用range头获取文件的某个部分
- (http 1.1)利用长连接让多个请求在一个TCP连接上排队 如果每一个请求都建立一个TCP,这样是非常浪费带宽的,如果所有请求都复用一个tcp,那么这样省去了一些tcp握手的开销。
- (http 2.0)利用多路复用技术同时传输多个请求 进一步节省带宽
#设计考虑因素-压缩/安全性
-
压缩
- 主流web服务器如nginx/express等都提供gzip压缩功能
- (http2.0)采用二进制传输,头部使用HPACK算法压缩
-
HTTPS
- 在HTTP和TCP/IP之间增加
TSL/SSL层 数据传输加密(非对称+对称加密)
- 在HTTP和TCP/IP之间增加
1.4.2 HTTPS
-
安全超文本传输协议(Hyper Text Transfer Protocol Secure) -
数据加密传输防止各种攻击手段(信息泄露、篡改等)
-
SSL/TSL(Secure Socket Layer/Transport Layer Secure)- SSL-安全套接层
- TSL-传输层安全性协议
- 需要在客户端安装证书
#1.4.3 Node.js实战http请求
#🍅 Header和Body(实战)
-
http协议是一个文本传输协议,传输内容是人类可读的文本,大体分成2部分:
- 请求头(header)/返回头
- 消息体Body
-
观察node实现http的基础协议
const net = require('net')
const response = `
HTTP/1.1 200 ok
Data: Tue,30 Jun 2020 01:00:00 GMT
Content-Type: text/plain
Connection: Closed
// 上面是描述,下面设计文本内容
Hello word
`
const server= net.createServer(socket=>{
socket.end(response)
})
server.listen(80,(err)=>{
console.log('err',err);
})
我们请求一下http://localhost/
从上图可以看出respoons header是我们添加进去的,request header是浏览器帮我们添加进去的
1.4.3 基础工具链
- Chrome
- Google 开发的免费浏览器
- Chrome 开发者拥有强大的调试能力
- cURL
-
传输一个URL(和服务端交互的工具)
- url:网址(Uniform Resource Locator)
-
支持多种协议(HTTP/HTTPS/FTP/FTPS/SCP/SFTP/DICT/TELNET)
curl www.baidu.com # 返回百度首页的内容,跟浏览器是一样的,会少一些响应头
curl -I www.baidu.com # 返回协议头部
HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform
Connection: keep-alive
Content-Length: 277
Content-Type: text/html
Date: Sun, 14 Feb 2021 09:12:18 GMT
Etag: "575e1f59-115"
Last-Modified: Mon, 13 Jun 2016 02:50:01 GMT
Pragma: no-cache
Server: bfe/1.0.8.18
- fetch
-
在网络上获取数据的标准接口
- 提供对请求/返回对象(标准的Promise接口)
- 提供自定义header能力
- 提供跨域能力
浏览器上可以直接请求 fetch("/")
postMan
- 协作的API开发工具
经常用的场景是服务端工程师把一些复杂的请求参数配置好,通过postman可以分享出来给前端工程师
- whistle
whistle是一个抓包的工具,也叫网络调试工具,它能看到这些分包,可以在http协议上修改一些参数。
-
跨平台网络调试工具
- 需要SwitchOmega插件
- node.js 开发
- 支持抓包 、重放、替换、修改等
npm i whistle -g # 下载,mac电脑:sudo npm i whistle -g
whistle start # 启动
http://localhost:8899/ # 浏览器查看
SwitchOmega # 谷歌代理插件,可以配置127.0.0.1:8899的服务上
#🍅 谷歌代理插件SwitchOmega,配置代理服务器127.0.0.1:8899的服务
#🍅 示例:以代理百度为例
在想要代理的网址上选择proxy
查看代理信息
#小结
简单比效率更重要(java/HTTP)等
1.5 HTTP协议详情讲解
#1.5.1 HTTP协议内容和方法
#🍅 HTTP常见的请求头
#🍅 HTTP常见的返回头
HTTP/1.1 201 Created # 协议版本、状态码
x-Powered-By: Express # X-Powered-By
Date:Sun,19 Jul 2020 14:01:51 GMT # 日期
Connection: keep-alive # Connection
Content-length: 0 # content-length
#🍅 基本方法
-
GET- 从服务器获取资源
-
HEAD
- HEAD和GET类似,只是服务器的响应中只返回头部(没有实体部分);在不获取资源情况下了解资源的状况
-
POST- 在服务器创建资源
-
PUT
- 在服务器修改资源(幂等性),同一个url多次请求只修改一次
-
DELETE
- 在服务器删除资源(幂等性),同一个url多次请求只删除一次
-
OPTIONS
-
跨域时复杂请求
- 使用了 put/delete/connnect/trace/patch
- 人为设置了一些header字段
- content-type的值不属于:application/x-www-form-urlencoded、multipart/form-data、text/plain
-
-
TRACE 用于显示调试信息
- 多数网站不支持,会泄露一些调试信息或者只有内部的时候才支持,它能帮助你追述整个http的链路,假如http发出去,它可能接收第一个不是网关; 它可能是代理服务器,再通过负载均衡才到达你真实的服务器,这里面有一定的路径,trace可以帮助我们请求http协议的网址时协议调试信息
-
CONNECT
- 代理
-
PATCH
- 对资源进行部分更新(极少用)
#1.5.2 常见HTTP状态码
#🍅 状态码
-
1.xx :提供信息
- 100 continue 主要用来提供信息的,这个100还有一点历史原因,现在带宽都比较大;有时候传输数据比较大 客户端询问一下服务端,服务端如果发送100continue,客户端继续向服务端传送,现在已经用的很少了。
- 101 切换协议(switch Protocol) 我们浏览器生态不光有http协议、websocket协议、还有一些视频流的一些协议;这些协议和http之间是怎么切换的,客户端请求服务端的时候,如果服务端需要切换协议,会返回101,告诉客户端切换协议
HTTP/1.1 101 Switching Protocols Upgrade:websocket Connection: Upgrade -
2XX:成功
200-ok- 201- Created 已创建
- 202- Accepted 已接收
- 203- Non-Authoritative information 非权威内容
- 204- No Content 那样内容
- 205- Reset Content 重置内容
- 206- Partial Content 服务器下发了部分内容(range header) 注:多数服务端开发已经不遵循状态码
-
3XX:重定向
- 300- Multiple Choices 用户请求了多个选项的资源(返回选项列表)
- 301- Moved Permanently 永久转移 (如果以前是post、delete等方法,如果在303的情况都会变成get)
- 302- Found 资源被找到(以前是临时转移,现在被拆成了2个状态码303和307)
- 303- See Other 可以使用GET方法在另一个URL找到资源(不管以前使用什么方法跳转过来的,最终303都给跳转一个get方法)
- 304- Not Modified 没有修改(这个资源没有再请求一遍,现在很多web服务器,它在下发资源给浏览器的时候,它会把资源内容做一次计算,计算出一个唯一的哈希值,哈希值会作为target传下来,浏览器会对比这个target,target变化会再去请求一次,web服务器知道哪些资源有变化,它有些算法在里面,计算出有没有变化,没有变化会返回一个304,并不会返回真实的资源。)
- 305- Use Proxy需要代理
- 307- Temporary Redirect 临时重定向
- 308- Permanent Redirect 永久重定向 (如果是post请求,收到308后还是308请求)
收到服务端状态码,我们该怎么做?
这不是程序所决定的,它不是这个逻辑,因为整体设计是一个协议一个标准;在请求的时候我们能拿到状态码,即使返回了301我们都可以不用去跳转,反正你就不遵守协议,这无所谓,但是设计浏览器的人,他们都遵循这些协议,这些协议是一个标准,正常来说也要遵循这些协议;即使权利交到你手里 ,如果总是违反协议做事情,大家都乱了,大家要重新商议协议,沟通成本会增加。
🍅 面试解惑:301 vs 308
-
共同点
- 资源被永久移动到新的地址
-
差异
- 客户端收到308请求后,之前是什么method,那么之后也会沿用这个method(POST/GET/PUT)到新地 址
- 客户端收到301请求后,通常用户会向新地址发起GET请求
历史原因:最早的浏览器设计不是像今天这样用ajax请求去动态创建网页,过去网页是网页设计师写出来的,那时候主要的请求都是GET,最早定义规范的人,他也强调过301怎么去做,定义协议的人其实考虑到这个问题了,但是大家都没有遵守这个协议,一开始本来你给我POST,我给你POST ,只是网址变化一下,但都大家不理它,导致http1.1之后还有增加308这些头。
🍅 面试解惑:302/303/307
-
共同点
- 资源临时放到新地址(请不要缓存)
-
差异
- 302是http1.0提出的,最早叫做Moved Temporarily;很多浏览器实现的时候没有遵循标准,把所有请求都重定向到GET
- 1999年标准委员会增加了303和307,并将302重新定义为Found
- 303告诉客户端使用GET方法重定向资源
- 307告诉客户端使用原请求method重定向资源
-
4XX:客户端错误
400- Bad Request 请求格式错误- 401- Unauthorized 没有授权
- 402- Payment Required 请先付费
403- Forbidden 禁止访问404- Not Found 没有找到- 405- Mehtod Not Allowed 方法不被允许
- 406- Not Acceptable 服务端可以提供的内容和客户端期待不一样
注:多数服务端开发已经不遵循状态码
-
5XX:服务端错误
500 - internal Server Error(内部服务器错误)- 501 - Not implemented(没有实现)
- 502 - Bad Gateway(网管错误)
503 - Servive Unavailable(服务不可用)504 - Gateway Timeout (网关超时)- 505 - HTTP Version Not Supported(版本不支持)
注:多数服务端开发已经不遵循状态码
1.5.3 常见HTTP头
#🍅 Content-Length
-
发送给接收者的Body内容长度(字节)
- 一个byte是8bit
- Utf-8编码的自渎1-4字节
#🍅 User-Agent
-
帮助区分客户端特性的字符串
- 操作系统
- 浏览器
- 制造商(手机类型等)
- 内核类型
- 版本号......
#🍅 Content-Type
-
帮助区分资源的媒体类型(Media Type/MIME Type)
- text/html
- text/css
- application/json
- image/jpeg
- ......
#🍅 Origin
-
描述请求来源地址
- scheme://host:port
- 不含路径
- 可以是null
#🍅 Accept
-
建议服务端返回何种媒体类型(MIME Type)
- */*代表所有类型(默认)
- 多个类型用逗号隔开例如:text/html,application/json
-
Accept-Encoding:建议服务端发送哪种编码(压缩算法)
- deflate,gzip;q=1.0,*;q=0.5
-
Accept-Language:建议服务端传递哪种语言
- Accept-Language:fr-CH,fr;q=0.9,en;q=0.8,de;q=0.7,*;q=0.5
#🍅 Referer
-
告诉服务端打开当前页面的上一张页面的URL;如果是ajax请求,那么就告诉服务端发送请求的URL的是什么
- 非浏览器环境有时候不发送Referer(或者虚拟Referer,通常是爬虫)
- 常常用于用户行为分析
#🍅 Connection
-
决定连接是否在当前事务完成后关闭
- Http1.0 默认是close
- Http1.1后默认是keep-alive
1.6 全栈角度看HTTP协议
#1.6.1 解析Body和2xx状态码
#🍅 实战-method和解析body
- 查询 GET /product
const express = require('express')
const app = express()
app.get('/product',(req,res)=>{
res.send('ok')
})
app.listen(3000,()=>{
console.log('启动成功');
})
- 新增 POST /product
const express = require('express')
const app = express()
app.post('/product',(req,res)=>{
const contentType = req.headers['content-type']
let requestText=""
// http请求基于tcp,tcp会把传的数据,分成一个个分包;并不是一次传过来的;req不仅是一个请求对象,也继承了流的性质
// 流代表了未来的数据,当数据来的时候把数据给装起来
req.on('data',(buffer)=>{
// utf-8 用1到4个字节描述一个字符,世界上有太多字符了,像a、b、c这样的一个字节就描述完了
console.log('buffer',buffer.length)
requestText += buffer.toString('utf-8')
})
req.on('end', ()=>{
console.log('contentType',contentType)
switch(contentType) {
case "application/json" :
// console.log('requestText',JSON.parse(requestText))
res.set('content-type','application/json')
res.status(201).send(JSON.stringify({success:'ok'}))
break
}
})
})
app.listen(3000,()=>{
console.log('启动成功');
})
在浏览器上请求一下fetch("/product",{method:"POST",headers:{'content-type':'application/json'},body:JSON.stringify({name:''.padStart(100000,'A')})}).then(res=>{console.log(res)})
在控制台中可以看出,打印buffer.length出现了2次,因为传值不是一下传过来的,因为tcp是分包传输。
➜ 1.6 git:(master) ✗ nodemon ./index.js
[nodemon] 2.0.4
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node ./index.js`
启动成功
buffer 64773
buffer 35238
contentType application/json
- 修改 PUT /product/:id
app.put('/product/:id',(req,res)=>{
console.log(req.params.id)
res.sendStatus(204)
})
- 删除 DELETE /product/:id
app.delete('/product/:id',(req,res)=>{
console.log(req.params.id)
res.sendStatus(204)
})
1.6.2 跳转Header和3xx状态码
#🍅 实战-重定向观察
-
观察下列重定向行为的区别
- 301、302、303、307
- 301
当访问http://localhost:3000/301时候,浏览器会跳转到http://localhost:3000/def;跳转是浏览器的行为。
const express = require('express')
const app = express()
// 遵守规范有利于网站的seo,不能随便用301,浏览器会有缓存
app.get('/301',(req,res)=>{
res.redirect(301,'/def')
})
app.get('/def',(req,res)=>{
res.send('THIS IS DEF(get)')
})
app.listen(3000,()=>{})
- 302、303、307
const express = require('express')
const app = express()
// 302、303 post请求会重定向到get请求
app.post('/302',(req,res)=>{
res.redirect(303,'/def')
})
app.post('/303',(req,res)=>{
res.redirect(303,'/def')
})
app.get('/def',(req,res)=>{
res.send('THIS IS DEF(get)')
})
// 307 post请求,会重定向到post请求
app.post('/307',(req,res)=>{
res.redirect(307,'/def')
})
app.post('/def',(req,res)=>{
res.send('THIS IS DEF(307post)')
})
app.listen(3000,()=>{})
.6.3 实战-错误处理
-
为下列场景返回不同的错误码
- 用户没有登录
- 服务器报错
- 内容没有找到
- 不支持POST请求
const express = require('express')
const app = express()
// 用户没有登录
app.post('/test',(req,res)=>{
res.sendStatus(404)
})
// 500服务端抛错,会自定帮你做
app.post('/test',(req,res)=>{
throw "Error"
})
// 502网管错误,一般不自己指定
app.post('/test',(req,res)=>{
res.sendStatus(502)
})
// 找不到资源,一般找不到资源框架会帮去返回404
app.post('/test',(req,res)=>{
res.sendStatus(404)
})
app.listen(3000,()=>{})
1.7 加密和HTTP证书
#1.7.1 对称加密和非对称加密
#🍅 明文传输
如果2个人对话,本来就是私密的,当然不想让三个人看见;如果小区有交换机,小区的工人人员就可以看报文,可能就会动起了坏心思。
#🍅 加密
给Alice传的发出内容进行加密,Bob这边在进行解密;加密解密也没回绝对安全;这样给破解人增加了难度。
#🍅 什么是加密
将明文信息变成不可读的密文内容,只有拥有解密方法的对象才能够将密闻还原成加密前的内容
下面的例子就是隔3个拿一个字符,从而达到给明文加密
#🍅 加密方法/解密方法
- 计算机中,加密方法和解密方法,可以描述为一段程序,我们称作加密/解密算法
- 加密有时候会对暗号,比如上一个例子中每次跳过3个字符,[3]就是一个暗号,我们称作密钥
#🍅 对称加密/非对称加密
- 加密和解密的暗号(秘钥)相同,我们称为对称加密
- 加密和解密的暗号(秘匙)不同,我们称为非对称加密
#🍅 非对称加密(秘钥对)
- 创建者创建一个秘钥对(分成公钥和私钥)
- 公钥加密必须私钥解密
- 私钥加密必须公钥解密
- 创建者保留私钥,公钥向外界公开
1.7.2 信任体系
#🍅 证书体系
#🍅 算法如何验证证书就是Alibaba
Alibaba向第三方证书机构申请证书,第三方机构用根证书的私钥给Ailibaba证书签名;然后可以用根证书的公钥进行验证签名是不是alibaba的证书。
#1.7.3 算法种类介绍
-
DES (Data Encryption Standard)
- 1970 IMB提出的对称加密算法
- 可暴力破解
-
AES (Advanced Encryption Standard)
- 2001年美国国家标准于技术研究院发布的对称加密算法
- 可旁道攻击
-
RSA(Rivest-Shamir-Adleman)
- 1997年发布的非对称加密算法
#🍅 对称 VS 非对称
- 非对称加密安全性更好
- 对称加密计算速度更快
- 通常混合使用(利用非对称加密协商密钥,然后进行对称加密,https也是混合使用,不是一种加密用到底的)
#1.7.4 HTTPS工作原理
假如有30ms网络延迟,假如用https发送一条数据,服务端收到到确认成功,最少使用多少时间?
需要190ms,服务端收到后还会发送还会发送一个ack
1.8 UDP vs TCP,HTTP vs HTTPS
#1.8.1 UDP
-
比TCP节省网络资源和迅速
- 不需要建立连接(延迟更低)
- 封包体积更小 (传输入速度快)
- 不关心数据顺序(不需要序号和ACK,传输快速)
- 不保证数据不丢失
#思考
没有虚拟连接、不校验数据、不保证顺序、没有收到不重复---是不是意味着不安全、不可靠?
-
UDP自由度跟高
- 需要用户程序在应用层定义类似的机制
- TCP面向流(API接收流)、UDP面向消息(API接收数据包)
-
场景不同
- 模糊:文件&文本、多媒体
- TCP:远程控制
- UDP:DNS查询
#1.8.2 HTTP2.0 目标
多个请求多路复用防止对头阻塞压缩HTTP头部服务端推送
#🍅 HTTP 1.1 排队问题
HTTP 1.1多个文件共用一个TCP,这样可以减少tcp握手,这样3个文件就不用握手9次了,不过这样请求文件需要排队,请求和返回都需要排队, 如果第一个文件响应慢,会阻塞后面的文件,这样就产生了对头的等待问题。
有的网站可能会有很多文件,浏览器处于对机器性能的考虑,它不可能让你无限制的发请求建连接,因为建立连接需要占用资源,浏览器不想把用户的网络资源都占用了,所以浏览器最多会建立6个tcp连接;如果有上百个文件可能都需要排队,http2.0正在解决这个问题。
#🍅 HTTP 2.0
- 多路复用
http1.1是一个请求过去,一个请求返回来,然后在进行下一个请求;其实是在排队的。http2.0发现2个请求比较靠近,把2个请求打包成一个请求发过去;也就是说把几个请求打包成一个小块去请求,并行发送;即使一个阻塞了,另一个还能回来,可以并行的出去也可以并行的回来;假如第一个请求需要0.8秒,第二个第三个各需要0.5秒;那么http2.0可以在0.8秒内把这3个一起去请求,这样只需0.8秒,即使一个阻塞了也不影响其他的返回。
- 防止对头阻塞
http1.1如果第一个文件阻塞,第二个文件也就阻塞了。
http2.0的解决,把3个请求打包成一个小块发送过去,即使第一个阻塞了,后面2个也可以回来;相当于3个文件同时请求,就看谁先回来谁后回来,阻塞的可能就后回来,对带宽的利用是最高的;但没有解决TCP的对头阻塞,如果TCP发过去的一个分包发丢了,他会重新发一次;http2.0的解决了大文件的阻塞。
一个分包请求3个文件,即使第一个阻塞了,第二个也能返回
- 压缩头部
- HPACK技术
例如:METHOD GET 用2表示,就是我用2去表示METHOD GET,这样不就小了,这样压缩比率非常大,可以参考下面表代表Header压缩后的字符。
- 服务端推送
我们现在的网站是先请求一个html,然后加载这个html,如果这个html有js文件和css文件,在去请求js和css;有没有可能html、js、css一起推送过来,这样是不是快很多。
当也有一个问题,现在页面主要是js渲染,那么js的先后顺序影响了页面的渲染;那么服务端推送也就存在问题了。
1.8.3 HTTP3.0
http3.0现在还属于一个实验阶段,上面是一个体系图,理解了TCP和UDP的关系,就会理解为什么有个3.0。
从网络层(ip)->链接层->物理层这里是基本不变的,传输层是TCP或UDP,在这个TCP之上构建了HTTP1.1/HTTTP2.0的体系,0.9、1.0大家已经不用了,现在基本都是2.0。
3.0直接把TCP换成UDP了,换成UDP后就出问题了,可靠性和安全性上出了问题,那么就自己做一层,这个叫QUIC,就是快的意思;谷歌的作品,这个谷歌说给http提高了很大的一部分空间,因为TCP到UDP本来就有很大的性能差距。
http1.1/http2.0中间还有一个加密层,还要对数据的压缩,QUIC自己层承担了这部分工作,优化做的非常足,所以速度很快;3.0不再改协议了,也没有这么多的概念,它是整个层重写了,是基于UDP的,因为要保证可靠性,它是基于文件传输做的重写;文本传输做的重写,这就是3.0。
#总结
- UDP把自由度给了用户,使用的人少,TCP自由度低,用的人多。
- HTTP2.0/HTTP3.0都兼容HTTP1.1
二、网络请求实战
2.5 缓存、清理缓存和HTTP缓存
#2.5.1 缓存
-
缓存将被用到的数据,让数据访问更快
- 命中:在缓存中找到了请求的数据
- 不命中/穿透:缓存中没有需要的数据
- 命中率:命中次数/总次数
- 缓存大小:缓存中一共可以存多少数据
- 清空策略:如果缓存空间不够数据如何让被替换
#清空策略(FIFO)
这是一种清空策略,它是先进先出;假如我们现在有100条数据,存储的方向是从一个链表的尾部插入,从一个链表的头部取出;假设我们是这样的一个算法。 当我们现有的数据小于100的时候,我们可以一直存入;当现有数据等于100的时候,我们每存入一个就需要取出一个,从头部存入,从尾部取出;这样就会产生一个结果, 我们最早存入的数据最早取出,这样就构成了先进先出。
#LRU-Least Frequently used(使用频率最低的)
当缓存不够时,优先清除使用频率最低的
#LRU-Least recently used(最近使用的)
假如最近一年前的记录,最近使用了,会更新成最近的
2.5.2 http缓存
http缓存,主要是通过协议头,分别为协商缓存和强制缓存。
#缓存策略
当浏览器第一次加载资源时,服务端可以在响应头中返回缓存策略,浏览器会把这些缓存策略放在缓存中,当以后再次请求时浏览器通过expires和cache-control判断是否命中缓存且没有过期,如果命中返回200状态码并从缓存中读取数据,如果没有命中强缓存,浏览器会发送一个请求并携带last-modified、e-tag咨询服务端资源是否过期,如果服务端返回304状态码,说明没过期让浏览器从缓存中读取,如果过期了浏览器则会重新请求资源。
#🍅 Cache-Control 重要
这个头定义所有缓存都要遵守的行为,不管是协商缓存还是强制缓存
- 可缓存性(要不要缓存)
大体有2种情况,一种是客户端要不要缓存,还有一种是中间方要不要缓存;因为我们一个请求从发出到响应可能还经历了其他服务器,可能还经过中间的代理服务器,这里定义了中间的服务器它们需不需缓存。
如果允许中间方缓存就定义成public。
如果不允许中间方缓存,只允许端缓存我们就定义成private。
如果需要所有人都不缓存那就定义成no-store。
如果你希望要不要缓存是一个协商的行为,那么你可以用no-cache,就是每次请求前先问一下服务端是不是最新的,如果不是最新的就不用再次请求。
这几个值可以组合。
- 可缓存性
| 值 | 含义 |
|---|---|
| public | 允许所有方缓存 |
| private | 只允许浏览器缓存 |
| no-cache | 每次必须先询问服务器资源是都已经更新 |
| no-store | 禁止缓存 |
- 缓存期限(要不要缓存)
| 值 | 含义 |
|---|---|
| max-age | 秒(存储周期) |
| s-maxage | 秒(共享缓存如代理等,存储周期) |
#Cache-Control 常用用法
不同文件,cache-control不同的用法:
第一个文件:react、vue、jquery这些库我们会把期限设置成最大,原因是这种更新周期是非常慢的,如果要更新这个文件我们可以更改文件名的hash值,这样文件名变了又可以请求新的文件了。如果我们什么都不写,就是允许公有方,像cdn、代理服务器它们都可以缓存这个文件。
第二个文件:这种自己的库,会设置成private,就是我们不允许中间方缓存,这里面可能有我们敏感的算法;造成不必要的麻烦,如果变化不大的情况下,max-age也设置成很大的值。
第三个文件:变化情况不大的情况下,max-age可以设置成很大的值。
第四个文件:图片的名字也没有带hash,可能存在一个更新问题,所以我们把它的时间设置成一天。
#🍅 强制缓存 重要
强制使用缓存,不去服务器对比;(缓存生效不再发送请求)
Cache-control:max-age=600 // 600秒
Expires:<最后期限> // 写一个具体日期,到什么时候失效,现在基本不用了
案例:
const express= require('express')
const app = express()
app.get('/x',(req,res)=>{
+ res.set("Cache-Control","max-age=600")
res.send('x3')
})
app.listen(3000)
max-age=0 等于0的情况和no-cache的情况一样,每次都向服务器询问
协商缓存1 重要
协商使用缓存,每次需要向服务器请求对比,缓存生效下不传回body
服务端先返回一个Last-Modified告诉客户端这个资源最后修改时间,然后客户端每次发请求都回带上if-Modified-Since;如果资源变化了,服务就会知道,就会返回新的资源和新的Last-Modified
返回:Last-Modified:<昨天>
请求:if-Modified-Since:<昨天>
1
2\
const express= require('express')
const app = express()
app.set('etag',false) // 先把etag关掉,因为它也是一种缓存,现在我们只用一种,好演示一些
app.get('/x',(req,res)=>{
res.set("Last-Modified","Sun Feb 28 2021 23:24:47 GMT+0800") // 如果资源变了需要更新这个时间
res.send('x3')
})
app.listen(3000)
#🍅 协商缓存2 重要
这个是用的最多的协商缓存,服务端在返回任何数据的时候,就会带上一个E-Tag;这个不用我们配置
返回:E-Tag:1234
请求: if-None-Match:1234
#🍅 总结
- 发布新的静态资源的时候,如何更新缓存?
每次发布的文件名都不同,这是用的最多的一种策略,因为我们的静态资源在html加载,或者被js引用到了;最后把这些引用都改成新的;webpack都有这些功能。
三、网络安全防护
3.1 常见网络安全防护
#3.1.1 阻断服务攻击(DOS)
- 阻断服务攻击,想办法目标网络资源用尽
- 变种:分布式阻断服务攻击
影响:
- 宽带消耗性(消耗目标的带宽)
- 资源消耗型(消耗目标的计算资源)
解决方案:
- 防火墙
- 交换机(路由器)
- 流量清洗
3.1.2 跨站脚本攻击(xss)
- 原理:将跨站脚本注入到被攻击的网页上,用户打开网页会执行跨站脚本。
解决方案: 1. 输入过滤(转义) 2. 输出过滤(转义)
#3.1.3 SQL注入
‘;update user set money=99999 where id=10025’
select *from user where user_name=';update user set money=99999 where id=10025'
解决方案: 输入过滤(转义) 数据库安全策略
3.1.4 跨站请求伪造(csrf)
假如你刚登录银行网站不久,cookie还没过期,黑客利用小广告之类让你点击,然后请求在程序中请求转账接口
解决方案:
验证referer字段在请求地址添加token并验证
#3.1.5 HTTPS 中间人攻击
黑客在电脑上安装伪造的证书,拦截客户端的请求
3.2 同源策略
#3.2.1 定义
-
禁止一个源(origin)的脚本&文档和另一个源的脚本&文档交互
两个URL的protocol,port和host相同,那么同源- 思考:如果两个源产生过多交换有什么问题?
思考:
- 为什么不禁用不同源的js?
因为有时候需要把js放到cdn上,那么可能就不同源了,所以行不通。
- 应不应该允许不同源的js修改dom?
允许
- 应不应该允许网站提交数据到不同源的的服务器?
不允许
- 应不应该允许网站提交cookie到不同源的服务器?
不允许
3.2.2 跨域的N种方法
1.JSONP
- 利用不限制跨越脚本执行的特点
// 服务端数据(data.js)
jsonp("example",{
a:1,
b:2
})
// index.html
function jsonp () {
console.log(topic,data)
}
// 加载跨域数据脚本
var script = document.createElement('script')
script.setAttribute('src','data.js')
document.getElementdByTagName('head')[0].appendChild(script)
思考
- jsonp可以用来提交数据吗?
可以在url上,但只能get请求;服务端可以通过判断返回不同的脚本
- 尝试为fetch函数扩展jsonp功能
fetch(<jsonp-url>,{method: 'jsonp'})
.then(data=>{
console.log(data)
})
#2. 跨域资源共用 重要
- 跨域资源共用(Cross-Origin Resource Sharing)使用额外HTTP头允许指定的源和另一个源进行交互 服务端设置 Access-control-Allow-Origin:a.com
get、post我们称之为简单请求,简单请求在同源策略中会简单的处理,如果b.com返回了这个头Access-control-Allow-Origin:https://a.com,那么我们认为 这个请求是可以通过的。
预检
但是还有复杂一点的请求,我们需要先发OPTIONS请求,a.com想请求b.com它需要发一个自定义的Headers:X-ABC和content-type,这个时候就不是简单请求了, a.com要给b.com 发一个options请求,它其实在问b.com我用post行不行,还想在Headers中带X-ABC和content-type;并不是所有的headers都发这个OPTIONS请求,因为X-ABC是自定义的,所以需要发;b.com看到OPTIONS请求,先不会返回数据,先检查自己的策略,看看能不能支持这次请求,如果支持就返回200。
OPTIONS请求返回以下报文
HTTP/2.0 20 OK
Access-Control-Allow-Origin:https://a.com
Access-Control-Allow-Methods:POST,GET,OPTIONS
Access-Control-Allow-Headers:X-ABC,Content-Type
Access-Control-Max-Age:86400 // 告诉浏览器这个策略生效时间为一个小时,在一个小时之内发送类似的请求,不用在问服务端了,相当于缓存了
浏览器收到了OPTIONS的返回,会在发一次,这一次才是真正的请求数据,这次headers会带上X-ABC、contentType。
整体的过程cors将请求分为2种,简单请求和复杂请求,需不需要发送OPTIONS浏览器说的算,浏览器判断是简单请求还是复杂请求,cors是非常广泛的跨域手段 这里的缺点是OPTIONS请求也是一次请求,消耗带宽,真正的请求也会延迟。
#3.反向代理 重要
因为跨越是浏览器的限制,所以可以用同源的服务器去代理请求,代理服务使链路变的更长。
.2.3 实战-CORS(fetch+node.js)
- 观察node.js在服务端的实现CORS跨域
- 观察浏览器器fetch的使用方法
- 观察OPTIONS预检请求
- 用express起2个服务
const express = require('express');
const app1 = express();
app1.get('/',function(req,res){
res.send('hello')
})
app1.listen(3000)
const app2 = express()
app2.get('/api',function(req,res){
res.send('go')
})
app2.post('/api',function(req,res){
res.send('go')
})
app2.listen(3001)
启动node服务 nodeman cors.js
- 用
whislte做代理
npm i whistle -g # 下载,mac电脑:sudo npm i whistle -g
whistle start # 启动
http://localhost:8899/ # 浏览器查看
SwitchOmega # 谷歌代理插件,可以配置127.0.0.1:8899的服务上
配置whislte进行代理的域名
谷歌代理插件SwitchOmega,配置代理服务器127.0.0.1:8899的服务
SwitchOmega 选择 proxy进行代理
- 在
dev.com网站上请求dev1.com/api
从上面看到在dev.com网站上请求dev1.com/api有跨域的报错信息,告诉我们可以用CORS加请求头,以下是解决方法。
const express = require('express');
const app1 = express();
app1.get('/',function(req,res){
res.send('hello')
})
app1.listen(3000)
const app2 = express()
app2.get('/api',function(req,res){
+ res.set('Access-Control-Allow-Origin','http://www.dev.com')
res.send('go')
})
app2.post('/api',function(req,res){
res.send('go')
})
app2.put('/api',function(req,res){
res.set('Access-Control-Allow-Origin','http://www.dev.com')
res.send('go')
})
app2.listen(3001)
在浏览器控制面板输入fetch('www.dev1.com/api',{metho… 再次请求并加content-type字段,如果我们用post请求并在headers里加字段content-type:'application/json',因为这是复杂请求,浏览器会先发送一个options请求,我们需要设置响应的headers允许添加某个字段。
app1.listen(3000)
const app2 = express()
app2.get('/api',function(req,res){
res.set('Access-Control-Allow-Origin','http://www.dev.com')
res.send('go')
})
+ app2.options('/api',function(req,res){
+ res.set('Access-Control-Allow-Origin','http://www.dev.com')
+ res.set('Access-Control-Allow-Headers','content-type')
+ res.sendStatus(200)
+ })
app2.post('/api',function(req,res){
+ res.set('Access-Control-Allow-Origin','http://www.dev.com')
res.send('go')
})
app2.put('/api',function(req,res){
res.set('Access-Control-Allow-Origin','http://www.dev.com')
res.send('go')
})
app2.listen(3001)
用put请求看看,报错信息中可以看出是要在响应前加上允许PUT的请求
GET/POST/HEAD这种简单请求不会受这种影响,put、delete属于复杂请求,我们添加上以下代码来允许PUT请求
app2.options('/api',function(req,res){
res.set('Access-Control-Allow-Origin','http://www.dev.com')
res.set('Access-Control-Allow-Headers','content-type')
+ res.set('Access-Control-Allow-Methods','PUT')
res.sendStatus(200)
})
我们在请求的时候在headers添加自定义字段 token,需要添加以下代码允许自定义
app2.options('/api',function(req,res){
res.set('Access-Control-Allow-Origin','http://www.dev.com')
+ res.set('Access-Control-Allow-Headers','content-type,token')
res.set('Access-Control-Allow-Methods','PUT')
res.sendStatus(200)
})
子域名下请求父域名、父域名下请求子域名、子域名下请求子域名 都属于跨域,服务端通常通过判断是不是同一个一级域名,然后在origin里加上通过的域名
fetch('www.dev1.com/api',{metho…) 这种加上no-cors,会显示请求成功了,但是拿不到数据,这种请求属于透明请求。
浏览器状态同步和路由
4.1 前端路由和服务端路由
#4.1.1 前端路由和History API
#History API
提供操作控制浏览器会话历史,维护会话栈(Session stack)的能力
#history.go()
切换我们的会话栈,但并不改变我们的会话栈
#back & forward
back相当于go(-1),forward相当于go(1)
#pushState(state,title,url)
新增一个状态(State)到会话栈(session Stack)
- state:状态数据(自定义),可以通过history.state获取
- title:预留字段,多数浏览器不使用
- url:新状态的url
假如我们现在打开baidu.com网站,我们在控制台里输入以下内容
history.pushState(null,null,'/test')
// 发现页面并没有再次请求资源,我们就是利用这个能力去做单页面应用的
history.pushState({name:"demo1"},null,'/test')
// 第一个参数可以传递参数,可以用history.state拿到,可以用于区分页面
// history.pushState({name:"demo1"},null,'/test?name=demo1')跟query传递参数是一样的
有些垃圾网站当你打开后,你会发现按返回,一直返回不了,它们就是在你的会话栈中pushState很多次,它们push的url也不会导致页面变化,所以给人错觉进入这样的网站像中病毒一样,返回不了。
#pushState(state,title,url)
替换会话栈(session Stack)中当前的状态
- state:状态数据(自定义),可以通过history.state获取
- title:预留字段,多数浏览器不使用
- url:新状态的url
history.pushState(null,null,'/test1')
history.pushState(null,'/test2')
// 可以替换test1
history.replaceState(null,'/test3')
4.1.2 实战服务端路由
#观察node.js实现服务端路由
源码地址:/Senior-FrontEnd/examples/computerNetwork/4.1
做的功能是当访问http://localhost:8080/details返回的是details.html,当访问http://localhost:8080/list返回的是list.html
const app = require('express')()
const path = require('path')
const fs = require('fs')
// __dirname 当前文件相对目录
const pageDir = path.resolve(__dirname,'page')
const htmlFile = fs.readFileSync(pageDir)
function displayHtmlFile(name) {
return (req,res)=>{
const filePath= path.resolve(pageDir,name+".html")
res.sendFile(filePath)
}
}
htmlFile.forEach(file=>{
const [name,ext] = file.split('.')
app.get('/',name,displayHtmlFile(name))
})
app.listen(3000)
#观察用Cluster启动后多个实例进行负载均衡
const cluster = require('cluster');
// 想知道机器上有多少cpu核心
const numCPUs = require('os').cpus().length
const express = require('express')
//cluster.isMaster 判断主进程还是从进程
if(cluster.isMaster) {
console.log(`Master ${process.pid} is running`);
// Fork workers
for(let i=0;i<numCPUs;i++){
cluster.fork() // 创建worker,又启动了几次次当前文件,不过进不了主进程了,上面又isMaster判断了
}
cluster.on('exit',(worker,code,signal)=>{
console.log(`worker${worker.process.pid} died`)
})
}else {
// Worker can share any TCP connection
// in this case it is an HTTP server
// 把所有的进程都监听8888
const app = new express()
app.listen(8888)
console.log(`Worker${process.pid} started`)
}
#4.1.3 实战一个单页面应用
- 服务端怎么做?
const app = require('express')()
const path = require('path')
const htmlFile = path.resolve(__dirname, "page/spa.html")
// 请求products 或者product/123 都访问spa.html
app.get(//product(s|/\d+)/,(req,res)=>{
res.sendFile(htmlFile)
})
app.listen(3000)
- 前端怎么做?
<!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">
<style>
a {
color : skyblue;
cursor: pointer
}
</style>
<title>Document</title>
</head>
<body>
<h2>单页面应用示例</h2>
<div id="content"></div>
<ul>
<li><a onclick="route('/products')">列表</a></li>
<li><a onclick="route('/product/123')">详情</a></li>
</ul>
<script>
// 更新列表的函数
function pageList (){
const html = `
<ul>
<li>Apple</li>
<li>TicTok</li>
<li>Alibaba</li>
</ul>
`
document.getElementById('content').innerHTML = html
}
// 更新详情的函数
function pageDetail() {
document.getElementById('content').innerHTML = "DETAIL"
}
// 实现页面切换并加入记录
function route (page) {
history.pushState(null,null,page)
matchRoute(pages,window.location.href)
}
const pages = [
{
match: //products/,
route: pageList
},
{
match : //product/\d+/,
route: pageDetail
}
]
// 监听浏览器前进回退按钮,实现页面渲染
window.onpopstate= function() {
matchRoute(pages,window.location.href)
}
// 匹配当前页面
function matchRoute(pages,href) {
const page = pages.find(page=>page.match.test(href))
page.route()
}
matchRoute(pages,window.location.href)
</script>
</body>
</html>
4.2 Session&Cookie&Storage和单点登录
#4.2.1 Session和Storage
#登录场景(理解Session和Cookie)
- Session 代表一次会话
- SessionID 是这一次会话的唯一标识
- Cookie 是浏览器用于存储少量数据的存储手段
#实战Session/Cookie-1
- 观察浏览器发出请求,服务端返回cookie
- 观察Set-Cookie在跨域情况下会发生什么
const express = require('express')
const app1 = express()
app1.set('etag',false)
app1.get('/',(req,res)=>{
res.setHeader('Set-Cookie','abc=123')
res.send('ok')
})
app1.listen(3000)
nodemon ./cookie.js
为了后面演示方便我们用whistle做代理服务; whistle怎么用过?可以参考:跳转地址 基础工具链第5个类型
从上图可以看出,我们配置了3个域名,分别代理到本地服务
我们在chrome浏览器上访问http://www.dev.com/,就可以访问本地启动的3000端口的服务了,可以在Cookies里看见我们刚刚设置的cookie:apc=123; cookie也是受同源策略限制的,只有同域名下才能访问我们设置的cookie,如果没有设置失效时间,就会一直在。
#实战Session/Cookie-2 重要
我们在dev.com下请求api.dev.com看看会发生什么?
const express = require('express')
const app1 = express()
+ const app2 = express()
app1.set('etag',false)
app1.get('/',(req,res)=>{
res.setHeader('Set-Cookie','abc=123')
res.send('ok')
})
+ app2.get('/',(req,res)=>{
+ res.setHeader('Set-Cookie','apc=123')
+ res.setHeader(
+ "Access-Control-Allow-Origin",
+ "http://www.dev.com"
+)
+ res.send('ok')
+ })
app1.listen(3000)
+ app2.listen(3001)
api.dev.com是dev.com的二级域名,受同源策略限制,我们通过设置Access-Control-Allow-Origin允许在api.dev.com跨域请求,虽然请求通了,但我们发现请求头里没有携带cookie,这是因为只有同源的情况下才会自动携带cookie。那怎么在不同源的情况下携带cookie呢?
可以在请求参数里加上{credentials:'include'},不过你又会发现下面报错了,它希望服务端加上Access-Control-Allow-Credentials
const express = require('express')
const app1 = express()
const app2 = express()
app1.set('etag',false)
app1.get('/',(req,res)=>{
res.setHeader('Set-Cookie','abc=123')
res.send('ok')
})
app2.get('/',(req,res)=>{
res.setHeader('Set-Cookie','apc=123')
res.setHeader(
"Access-Control-Allow-Origin",
"http://www.dev.com"
)
+ res.setHeader(
+ "Access-Control-Allow-Credentials",
+ "true"
+ )
res.send('ok')
})
app1.listen(3000)
app2.listen(3001)
总结
cookie也受同源策略限制,同源才会自动携带cookie,不同源需要添加Credentials
#实战Session/Cookie-3 重要
我们在dev.com下请求api.com看看会发生什么?
因为2个域名属于不同的主域名,如果想让请求api.com时候携带dev.com下的cookie需要满足以下2个条件
- 必须是https协议
- SameSite=None;Secure
const express = require('express')
const app1 = express()
const app2 = express()
app1.set('etag',false)
app1.get('/',(req,res)=>{
+ res.setHeader('Set-Cookie','abc=123;SameSite=None;Secure')
res.send('ok')
})
app2.get('/',(req,res)=>{
res.setHeader('Set-Cookie','apc=123')
res.setHeader(
"Access-Control-Allow-Origin",
"http://www.dev.com"
)
res.setHeader(
"Access-Control-Allow-Credentials",
"true"
)
res.send('ok')
})
app1.listen(3000)
app2.listen(3001)
#其他浏览器存储
| ccokie | Local storage | Session Storage | |
|---|---|---|---|
| 容量 | 4kb | 10mb | 5mb |
| 作用域 | 同源 | 同源 | 当前网页 |
| 过期时间 | 手动 | 永久 | 当前网页关闭 |
| 位置 | 浏览器/服务端 | 浏览器 | 浏览器 |