计算机网络

132 阅读38分钟

一、计算机网络简史

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的应用层、展示层、会话层简化成了应用层

  1. 应用层
  • 提供应用间通信能力(HTTP协议)
  1. 传输层
  • 提供主机到主机的通信能力(TCP/UDP协议)
  1. 网络层
  • 提供地址到地址的通信能力(form:12.3.41.1->to :9.1.2.12)
  1. 链路层
  • 提供设备到设备的通信能力(设备1->设备2)
  1. 物理层
  • 光电信号的传输

#1.2.2 TCP/IP的三次握手

#1.2.3 TCP/IP协议的消息顺序处理方法

  • 消息的绝对顺序用(SEQ,ACK)这一对元组描述

    • SEQ Sequence):这个消息发送前一共发送了多少字节
    • ACK (Acknowledge):这个消息发送前一共收到了多少字节

1.3 DNS与CDN

#1.3.1 DNS的基础知识

#统一资源定位符(URL)

  • 也被称作(网址),用于定位互联网上的资源

#DNS Query过程

#1.3.2 CDN 实现原理

cdn用于存变化不大的文件

#CDN云测工具

17ce

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
    • 数据传输加密(非对称+对称加密)

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 基础工具链

  1. Chrome
  • Google 开发的免费浏览器
  • Chrome 开发者拥有强大的调试能力
  1. 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
  1. fetch
  • 在网络上获取数据的标准接口

    • 提供对请求/返回对象(标准的Promise接口)
    • 提供自定义header能力
    • 提供跨域能力

浏览器上可以直接请求 fetch("/")

  1. postMan
  • 协作的API开发工具

经常用的场景是服务端工程师把一些复杂的请求参数配置好,通过postman可以分享出来给前端工程师

  1. 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

  1. 查询 GET /product
const express = require('express')
const app = express()

app.get('/product',(req,res)=>{
    res.send('ok')
})
app.listen(3000,()=>{
    console.log('启动成功');
})
  1. 新增 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
  1. 修改 PUT /product/:id
app.put('/product/:id',(req,res)=>{
    console.log(req.params.id)
    res.sendStatus(204)
})
  1. 删除 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
  1. 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,()=>{})
  1. 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

  1. 多路复用

http1.1是一个请求过去,一个请求返回来,然后在进行下一个请求;其实是在排队的。http2.0发现2个请求比较靠近,把2个请求打包成一个请求发过去;也就是说把几个请求打包成一个小块去请求,并行发送;即使一个阻塞了,另一个还能回来,可以并行的出去也可以并行的回来;假如第一个请求需要0.8秒,第二个第三个各需要0.5秒;那么http2.0可以在0.8秒内把这3个一起去请求,这样只需0.8秒,即使一个阻塞了也不影响其他的返回。

  1. 防止对头阻塞

http1.1如果第一个文件阻塞,第二个文件也就阻塞了。

http2.0的解决,把3个请求打包成一个小块发送过去,即使第一个阻塞了,后面2个也可以回来;相当于3个文件同时请求,就看谁先回来谁后回来,阻塞的可能就后回来,对带宽的利用是最高的;但没有解决TCP的对头阻塞,如果TCP发过去的一个分包发丢了,他会重新发一次;http2.0的解决了大文件的阻塞。

一个分包请求3个文件,即使第一个阻塞了,第二个也能返回

  1. 压缩头部
  • HPACK技术

例如:METHOD GET 用2表示,就是我用2去表示METHOD GET,这样不就小了,这样压缩比率非常大,可以参考下面表代表Header压缩后的字符。

  1. 服务端推送

我们现在的网站是先请求一个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缓存,主要是通过协议头,分别为协商缓存和强制缓存。

#缓存策略

当浏览器第一次加载资源时,服务端可以在响应头中返回缓存策略,浏览器会把这些缓存策略放在缓存中,当以后再次请求时浏览器通过expirescache-control判断是否命中缓存且没有过期,如果命中返回200状态码并从缓存中读取数据,如果没有命中强缓存,浏览器会发送一个请求并携带last-modifiede-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

#🍅 总结

  1. 发布新的静态资源的时候,如何更新缓存?

每次发布的文件名都不同,这是用的最多的一种策略,因为我们的静态资源在html加载,或者被js引用到了;最后把这些引用都改成新的;webpack都有这些功能。

三、网络安全防护

3.1 常见网络安全防护

#3.1.1 阻断服务攻击(DOS)

  • 阻断服务攻击,想办法目标网络资源用尽
  • 变种:分布式阻断服务攻击

影响:

  1. 宽带消耗性(消耗目标的带宽)
  2. 资源消耗型(消耗目标的计算资源)

解决方案:

  1. 防火墙
  2. 交换机(路由器)
  3. 流量清洗

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还没过期,黑客利用小广告之类让你点击,然后请求在程序中请求转账接口

解决方案:

  1. 验证referer字段
  2. 在请求地址添加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预检请求
  1. 用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

  1. 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进行代理

  1. 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这种简单请求不会受这种影响,putdelete属于复杂请求,我们添加上以下代码来允许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 实战一个单页面应用

  1. 服务端怎么做?
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)
  1. 前端怎么做?
<!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,如果没有设置失效时间,就会一直在。

image.png

#实战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.comdev.com的二级域名,受同源策略限制,我们通过设置Access-Control-Allow-Origin允许在api.dev.com跨域请求,虽然请求通了,但我们发现请求头里没有携带cookie,这是因为只有同源的情况下才会自动携带cookie。那怎么在不同源的情况下携带cookie呢?

可以在请求参数里加上{credentials:'include'},不过你又会发现下面报错了,它希望服务端加上Access-Control-Allow-Credentials

image.png

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个条件

  1. 必须是https协议
  2. 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) 

#其他浏览器存储

ccokieLocal storageSession Storage
容量4kb10mb5mb
作用域同源同源当前网页
过期时间手动永久当前网页关闭
位置浏览器/服务端浏览器浏览器