OSI七层模型
Open System Interconnection
适用于所有网络
- 分工带来效能
- 将复杂的流程分解为几个功能相对单一的子进程
- 整个流程更加清晰,复杂问题简单化
- 更容易发现问题并针对性的解决问题
- 应用层(Application)提供网络与用户应用软件之间的接口服务(HTTP)
- 表示层(Presentation)提供格式化的表示和转换数据服务,如加密和压缩
- 会话层(Session)提供包括访问验证和会话管理在内的建立和维护应用之间通信的机制;这三层也可归为
应用层
- 传输层(Transimission)提供建立、维护和取消传输连接功能,负责可靠的传输数据(TCP)
- 网络层(NetWork)处理网络间路由,确保数据及时传送(路由器)
- 数据链路层(DataLink)负责无错传输数据,确认帧,发错重传等(交换机)
- 物理层(Physics)提供机械、电气、功能和过程特性(网卡,网线,双绞线,同轴电缆,中继器)
封装过程
TCP/IP参考网络模型
- TCP/IP是传输控制协议/网络互联协议的简称
- 早期的TCP/IP模型是一个四层结构,从下往上依次是网络接口层,互联网层;传输层和应用层
- 后来在使用过程中,借鉴OSI七层参考模型,将网络接口层划分为物理层和数据链路层,形成五层结构
协议的概念和作用
- 为了让计算机能够通信,计算机需要定义通信规则,这些规则就是协议
- 规则是多种,协议也有多种
- 协议就是数据封装格式+传输
常用协议
- TCP/IP协议被称为传输控制协议/互联网协议,又称为网络通讯协议
- 是由网络层的IP协议和传输层的TCP协议组成,是一个很大的协议集合
- 物理层和数据链路层没有定义任何特定协议,支持所有的标准和专用的协议
网络接口层
网络接口层是TCP/IP模型的最底层,负责接收从上一层交来的数据报并将数据报通过底层的物理网络发送出去,比较常见的就是设备的驱动程序,此层没有特定的协议
网络接口层又分为物理层和数据链路层
-
物理层
- 计算机在传输数据的时候传递的都是0和1的数字,而物理层关心的是用什么信号来表示0和1,是否可以双向通信,最初的连接如何建立以及完成连接如何终止,物理层是为数据传输提供可靠的环境
- 尽可能的屏蔽掉物理设备和传输媒介,使数据链路层不考虑这些差异,只考虑本层的协议和服务
- 为用户提供在一条物理传输媒体上提供传送和接收比特流的能力
-
需要解决物理连接,维护和释放的问题
-
数字信号的编码
- 数字新的编码:用何种物理信号来表示0和1
-
非归零编码
优点:编/译码简单
缺点:内部不含时钟信号,收发端同步困难
用途:计算机内部,或低速数据通信
因为时钟信号无法确定,即时钟频率无法确定,例如一直发的都是1/0,是一条直线,怎么分割信号呢?无解
- 曼彻斯特编码
- 优点
- 内部自含时钟,收发端同步容易
- 抗干扰能力强
- 缺点
- 编译码较复杂
- 占用更多的信道带宽,在同样的比特率的情况下,要比非归零编码多占用一倍信道带宽
- 用途:802.3局域网(以太网)
数据链路层
- 数据链路层是OSI参考模型的第二层,介乎于物理层和网络层之间
- 数据链路层在物理层提供的服务的基础上向网络层提供服务,其最基本的服务是将源自网络层来的数据可靠的传输到相邻节点的目标机网络层
- 如何将数据组合成数据块,在数据链路层中称这种数据块为帧,帧是数据链路层的传输单位
- 如何控制帧在物理信道上的传输,包括如何处理传输差错,如何调节发送速率以使与接收方相匹配
- 以及在两个网络实体之间提供数据链路通路的建立,维持和释放的管理
以太网
- 以太网是一种计算机局域网技术。IEEE组织的IEEE 802.3标准制定了以太网的技术标准,它规定了包含物理层的连线,电子信号和介质访问层协议的内容
- 以太网的标准拓扑结构为总线型拓扑
- 以太网仍然使用总现拓扑和CSMA/CD的总线技术
- 以太网实现了网络上无线电系统多个节点发送信息的想法,每个节点必须获取电缆或者信道才能传输信息
- 每个节点都有全球唯一的48位地址也就是制造商分配给网卡的MAC地址,以保证以太网上所有节点能互相鉴别
总线拓扑
- 总线型拓扑是采用单根传输作用共用的传输介质,将网络中的所有计算机通过相应的硬件接口和电缆直接连接到这根共享的总线上
- 使用总线型拓扑结构需解决的是确保端用户使用媒体发送数据时不同出现冲突
- 总线型网络采用
载波监听多路访问/冲突检测协议
(CSMA/CD)作为控制策略
载波监听多路访问
- 是一种允许多个设备在同一信道发送信号的协议,其中的设备监听其他设备是否忙碌,只有在线路空闲时候才发送
- 在这种访问方式下,网络中的所有用户共享传输介质,信息通过广播传送到所有端口,网络中的工作站对接收到的信息进行确认,若是发给自己的就接收否则不理
- 从发送端情况看,当一个工作站有数据要发送时候,它首先监听信道并检测网络上是否有其他的工作站正在发送DATA,如果检测到信道忙,工作站将继续WAIT若发现信道空闲,则开始发送数据,信息发送出去后,发送端还要继续对发送的消息进行确认,以了解接收端是否已经正确的接收到数据,如果收到则发送结束,否则再此发送
- 核心思想
- 先听后讲,信道空闲则发送,信道忙则等待
- 边听边讲 发送信息时不断检测信道是否碰撞
- 碰撞即停
- 退避重传 二进制指数退避重传
- 多次碰撞 放弃发送,最多16次
冲突检测
- 冲突检测即发送站点在发送数据时要边发送边监听信道,若监听到信道有干扰信号,则表示产生了冲突,于是就要立刻停止发送消息,计算出退避等待时间,然后使用CSMA方法继续尝试发送
- 计算退避等待时间采用的时
二进制指数退避算法
演化
上面总线形式数据传输有很多弊端,后期演化成交换机交换数据,交换机是二层设备在物理链路层上面工作,工作方式是通过MAC地址定向传输数据,避免了总线形式的时候所有机器都可以接收到数据的问题。
MAC地址
- 在通信过程中是用内置的网卡内的地址来标识计算机身份的
- 每个网卡都有一个全球唯一的地址来标识自己,不会重复
-
MAC地址48位的二进制组成,通常分为6段,用16进制表示
以太网帧格式
- 在以太网链路上的数据包称为以太帧,以太帧起始部分由前导码和帧开始符z盛
- 后面紧跟着一个以太网报头,以MAC地址说明目的地址和源地址
- 帧的中部是该帧负载的包含其他协议报头的数据包(例如IP协议)
- 以太帧由一个32位冗余校验码结尾。它用于检验数据传输是否出现损坏
路由器IP寻址网络层,交换机mac寻址数据链路层
ARP协议
- 地址解析协议,即ARP,是根据IP地址获取物理地址(MAC)的一个TCP/IP协议
- 主机发送信息时将包含目标IP地址的ARP请求广播到网络上的所有主机,并接收返回消息,以此确定目标的物理地址;收到返回消息后将该IP地址和物理地址存入本机ARP缓存中并保留一定时间,下次请求时直接查询ARP缓存以节约资源
- 地址解析协议是建立在网络中各个主机互相信任的基础上的,网络上的主机可以自主发送ARP应答消息,其他主机收到应答报文时不会检测该报文的真实性就会将其记入本机ARP缓存
- 由此攻击者就可以向某一主机发送伪ARP应答报文,使其发送的信息无法达到预期的主机或到达错误的主机,构成ARP欺骗
ARP协议报文
ARP地址解析过程
主机A和B在同一个网段,主机A要向主机B发送信息
- 主机A首先查看自己的ARP表,确定其中是否包含有主机B对应的ARP表项。如果找到了对应的MAC地址,则主机A直接利用ARP表中的MAC地址,对IP数据包进行帧封装,并将数据包发送给主机B。
- 如果主机A在ARP表中找不到对应的MAC地址,则将该缓存该数据报文,然后以广播方式发送一个ARP请求报文。ARP请求报文中的发送端IP地址和发送端MAC地址为主机A的IP地址和MAC地址,目标IP地址和目标MAC地址为主机B的IP地址和全0的MAC地址。由于ARP请求报文以广播方式发送,该网段上的所有主机都可以接收到该请求,但只有被请求的主机(即主机B)会对该请求进行处理。
- 主机B比较自己的IP地址和ARP请求报文中的目标IP地址,当两者相同时进行如下处理:将ARP请求报文中的发送端(即主机A)的IP地址和MAC地址存入自己的ARP表中。之后以单播的方式发送ARP响应报文给主机A,其中包含自己的MAC地址。
- 主机A收到ARP响应报文后,将主机B的MAC地址加入到自己的ARP表中以用于后续报文的转发,同时将IP数据包进行封装后发送出去
互联网层(网络层)
位于传输层和网络接口层之间,用于把数据从源主机经过若干个中间节点传送到目标主机,并向传输层提供最基础的数据传输服务,主要提供路由和选址的工作
选址
交换机是靠MAC来寻址的,而因为MAC地址是无层次的,所以要靠IP地址来确认计算机的位置,这就是选址
路由
在能够选择的多条道路之间选择一条最短的路劲就是路由的工作
在大网络情况下,起始是多个路由器跳转,路由器之间有路由算法,即其实也不知道真实IP主机在哪里,但是可以知道转发给哪个路由可以到达目标IP
IP
在网络中,每台计算机都有一个唯一的地址,方便别人找到它,这个地址称为IP地址
-
IP头部
-
IP地址格式
- IP地址是一个网络编码,用来确定网络中的一个节点
-
IP地址是由32位二进制(32bit)组成
-
IP地址组成
- 网络部分(NETWORK)
- 主机部分(HOST)
- IP地址的分类
- IP地址的网络部分是由Internet地址分配机构来统一分配的,这样可以保证IP的唯一性
- IP地址中全为1的IP即255.255.255.255,它称为限制广播地址,如果将其作为数据包的目标地址可以理解为发送到所有网络的所有主机
- IP地址中全为0的IP即0.0.0.0,它表示启动时的IP地址,其含义就是尚未分配时的IP地址
- 127是用来进行本机测试的,除了127.255.255.255外,其他的127开头的地址都代表本机
- 如果只有最后一位是255则表明是某网段的广播地址(不能作用主机地址),此处区别于255.255.255.255
- 如果只有最后一位是0也代表未分配地址,该IP不能作为主机地址
- 公有地址和私有地址
分类 | 范围 |
---|---|
A类私有IP | 10.0.0.0 ~10.255.255.255 |
B类私有IP | 172.16.0.0~172.31.255.255 |
C类私有IP | 192.168.0.0~192.168.255.255 |
其他范围的IP均为公有IP地址
- 子网掩码
- 子网掩码又叫子网络遮罩,它是一种用来指明一个IP地址的那些位标识的是主机所在的子网,以及哪些位标识的是主机位的掩码
- 子网掩码不能单独存在,它必须结合IP地址一起使用
- 子网掩码只有一个作用,就是将某个IP地址划分成网络地址和主机地址部分
- 子网掩码也是32个二进制
- 对应IP的网络部分用1表示
- 对应IP的主机部分用0表示
- IP地址和子网掩码做逻辑与运算的到网络地址
- 0和任何数相与都是0
- 1和任何树相与都等于任何数本身
- A B C三类地址都有自己默认的子网掩码
- A类255.0.0.0
- B类255.255.0.0
- C类255.255.255.0
说白了,网络地址相同才是同一个网段才能通信,否则即使IP地址很像也不行
传输层
- 位于应用层和网络接口层之间
- 是面向连接的,可靠的进程到进程通信的协议
- TCP提供全双工通信,即数据可在同一时间双向传播
- TCP将若干个字节构成一个分组,此分组称为报文段
- 对可靠性要求高的上层协议,实现可靠性的保证,如果数据丢失、损坏的情况下如何保证可靠性,网络层只管传递数据,成功与否并不关心
- TCP是在不可靠协议(互联网层)基础上维护可靠传输
- 提供一种端到端的连接
单工
单向传输,广播,电视
半双工
在同一个时间点内,只能单向通信
全双工
在任意时间点双方都可以收发数据 例如:电话
协议分类
- TCP
- 传输控制协议
- 可靠的‘面向连接的协议
- 传输效率低
- UDP
- 用户数据报协议
- 不可靠的,无连接的服务
- 传输效率高
TCP
- 将数据进行分段打包传输
- 对每个数据包编号进行控制顺序
- 运输中丢失、重发和丢弃处理
- 流量控制避免拥塞
TCP数据包封装
-
源端口号和目标端口号,计算机通过端口号识别访问哪个服务,比如HTTP服务或FTP服务,发送方端口号是进行随机端口,目标端口号决定了接收方是哪个程序
32位序列号
TCP用序列号对数据包进行标记,以便达到目的地后重新重装,假设当前的序列号为s,发送数据长度为l,则下次发送数据时的序列号为S+l。在建立连接时通常由计算机生成一个随机数作为序列号的初始值
确认应答号
它等于下一次应该接收到的数据的序列号,假设发送端的序列号为s,发送数据的长度为l,那么接收端返回的确认应答号也是s+l.发送端接收到这个确认应答后,可以认为这个位置以前所有的数据都已被正常接收,也就代表着,随机数据包前后顺序不一定,但是后面包的确认信息肯定是全面包都收到后才发送给发送端的
首部长度
TCP首部的长度,单位为4字节。如果没有可选字段,那么这里的值就是5。表示TCP首部的长度为20字节。
控制位
- 控制位TCP的连接、传输和断开都受这六个控制位的指挥
- PSH缓存区将满,立刻传输数据
- REST 连接断了重新连接
- URG 紧急信号
- 紧急指针
尽在URG控制位为1时有效。表示紧急数据的末尾在TCP数据部分中的位置。通常在暂时中断通信时候使用(例如Crtl+C)
- SYN
同步序号位TCP建立连接时候要将这个值设置为1
- ACK
为1表示确认号
- FIN
发送端完成位,提出断开连接的乙方把FIN置为1表示要断开连接
窗口值
- 窗口值:说明本地可接收数据段的数目,这个值的大小是可变的。当网络通常时将这个窗口值变大加快传输速度,当网络不稳定时候减少这个值可以保证网络数据的可靠传输。它是在TCP传输中进行流量控制的
- 窗口大小:用于表示从应答号开始能够接受多少个8位字节。如果窗口大小为0,可以发送窗口探测
-
窗口探测:例如接收方窗口已经堆满了数据,没来的及处理,则接收方通过ACK返回窗口大小为0,则发送方知道0了则不发送了,但是可能后期接收方处理了,窗口有了,需要发送方每隔一段时间去接收方问窗口空了没,这就是窗口探测
差错控制
- 校验和用来做差错控制,TCP校验和的计算包括TCP首部,数据和其他填充字节,在发送TCP数据段时候,由发送端计算校验和,当到达目的地时候又进行一次校验和计算。如果两次校验和一致说明数据是正确的,否则将认为数据被破坏,接收端将丢弃数据
握手和断开 - TCP是面向连接的协议,它在源点和终点之间建立虚拟连接,而不是物理连接
- 在数据通信之前,发送端与接收端要先建立连接,等数据发送结束后,双方在断开连接
-
TCP连接的每一方都是由一个IP地址和一个端口组成
之所以四次挥手中间两次不能合并是因为,服务端响应必须立刻给客户端,否则客户端会重新传消息。之后确定不确定断开在专门发FIN消息
滑动窗口
- 滑动窗口是一种流量控制技术
- 早期的网络通信中,通信双方不会考虑网络的拥挤情况直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包,谁也发不了数据,所以就有了滑动窗口机制来解决这个问题
- TCP中采用滑动窗口来进行传输控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据,发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据
- 当滑动窗口为0时,发送方一般不能再发送数据报,但有两种情况除外,一种情况是可以发送紧急数据,例如:允许用户终止再远端机上的运行进程。另一种情况是发送方可以发送一个1字节的数据报来通知接收方重新声明它希望接收的下一字节及发送方的滑动窗口大小。
窗口机制
- 滑动窗口协议的基本原理就是在任意时刻,发送方都维持了一个连续的允许发送的帧的序号,称为发送窗口;同时,接收方也维持一个连续的允许接收的帧的序号,称为接收窗口
- 发送窗口和接收窗口的序号的上下界不一定要一样,甚至大小也可以不同
- 不同的滑动窗口协议窗口大小一般不同
- 发送窗口内的序列号代表了那些已经被发送,但是还没有被确认的帧,或者是哪些可以被发送的帧
拥塞控制
- TCP拥塞控制是传输控制协议,避免网络拥塞的算法,是互联网上主要的一个拥塞控制措施
- TCP使用多种拥塞控制策略来避免雪崩式拥塞,TCP会为每条连接维护一个"拥塞窗口"来限制端对端间传输的未确认分组总数量
- 这类似TCP流量控制机制中使用的滑动窗口,是由发送方控制的
- TCP在一个连接初始化或超时后使用一种"慢启动"机制来增加拥塞窗口的大小。它的起始值一般为最大分段大小的两倍,虽然命名为慢启动,初始值也向当地,但其增长极快;当每个分段的到确认时,拥塞窗口会增加一个MSS,使的再每次往返内拥塞窗口能高效的双倍增长
- 再流量控制中,接收方通过TCP的"窗口"值来告知发送方,由发送方通过对拥塞窗口和接收窗口的大小比较,来确定任何时刻内需要传输的数据量
- 和式增加,积式减少是一种反馈控制算法,其包含了对拥塞窗口线性增加,和当发生拥塞时对窗口积式减少。多个使用AIMD控制的TCP流量最终会收敛到对线路的等量竞争使用
- 未确认的数据包刚好等于带宽等于延迟
-
当发现丢包的时候立刻减半
UDP
- UDP是一个面向无连接,不保证可靠性的传输层协议,也就是说发送端不关心发送的数据是否达到目标主机,数据是否出错等,收到的数据的主机也不会告诉发送方是否收到了数据,它的可靠性由上层协议来保障
- 首部结构简单,再数据传输时能实现最小的开销,如果进程想发送很短的报文而对可靠性要求不高可以使用
DNS
DNS服务器进行域名与之对应的IP地址转换的服务器
- IP地址不易记忆
- 早期使用Hosts文件解析域名
- 主要名称重复
- 主机维护困难
- DNS(域名系统)
- 分布式
-
层次性
其中本地DNS服务器可能是路由器等
DHCP(应用层)
- 保证任何IP地址在同一时刻只能由一台DHCP客户机所使用(DHCP服务器再小网络环境一般集成再路由器中)
- DHCP应当可以给用户分配永久固定的IP地址
- DHCP应当可以同用其他方法获的IP地址的主机共存(如手工设置IP地址的主机)
- DHCP服务器应当向现有的BOOTP客户端提供服务
工作流程
- 主机发送DHCPDISCOVER广播包再网络上寻找DHCP服务器
- DHCP服务器向主机发送DHCPOFFER单播数据包,包含IP地址、MAC地址、域名信息以及地址租期
- 主机发送DHCPREQUEST广播包,正式向服务器请求分配已提供的IP地址
- DHCP服务器向主机发送DHCPPACK单播包,确认主机的请求
应用层
应用层常见协议
- HTTP超文本传输协议
- FTP文件传输协议
- SMTP(发送邮件)和POP3(接收邮件)
案例
数据->传输层(包)->网络层(段Segment)->数据链路层(帧)
- 在应用层要把各式各样的数据如字母、数字、汉字、图片等转换成二进制
- 再TCP传输层中,上层的数据被分割成小的数据段,并为每个分段后的数据封装TCP报文头部
- 再TCP头部有一个关键的字段信息端口号,它用于标识上层的协议或应用程序,确保上层数据的正常通信
- 计算机可以多进程并发运行,例如在发邮件的同时也可以通过浏览器浏览网页,这两种应用通过端口号进行区分
- 在网络层,上层数据被封装上层的报文头部(IP头部),上层的数据是包括TCO头部的。IP地址包括的最关键字段信息就是IP地址,用于标识网络的逻辑地址
- 数据链路层,上层数据包装成一个MAC头部,内部有最关键的是MAC地址。MAC地址就是固化在硬件设备内部的全球唯一的物理地址
-
在物理层,无论在之前哪一层封装的报文头和还是上层数据都是由二进制组成,物理层将这些二进制数据比特流转换成电信号在网络中传输
接收数据的过程是逆向的解包操作
真实网络环境
- 发送方和接收方可能会有多个硬件中转设备
- 中间可能会增加交换机和路由器
- 数据在传输过程中不断的进行封装和解封装的过程,每层设备只能处理那一层的数据
- 交换机数据数据链路层
-
路由器属于网络层
HTTP缓存
该部分完全复制公众号,连接
浏览器缓存几个阶段
- 强缓存策略
浏览器端发起请求之后不会直接向服务器请求数据,直接先到达强缓存阶段,如果强缓存命中直接返回,如果没有命中进入下一阶段协商缓存策略。
- 协商缓存策略
协商缓存是当强缓存没有命中的情况或者按下 F5 键刷新页面会触发,它每次都会携带标识与服务器进行校验,符合则返回 304 标识,表示资源没有更新,如果协商缓存也失效了,进入下一个阶段获取最新数据,并返回且状态码为 200。
- 存储策略
当强缓存->协商缓存都未命中,请求会直接到达服务器,获取最新资源设置缓存策略,进行返回。
强缓存
强缓存的实现分为 Expires、Cache-Control 两个。
- Expires
Expires 属于 HTTP 1.0 时期的产物,在响应中进行设置,示例如下:
response.writeHead(200, {
'Content-Type': 'text/javascript',
'Expires': new Date('2020-03-25 11:19:00'),
});
设置成功运行 node expires.js 在 Response Headers 里可以看到如下信息:
Expires: Wed Mar 25 2020 11:19:00 GMT+0800 (GMT+08:00)
刷新两次页面,可以看到第二次 size 一栏返回了 memory cache 此时 Expires 缓存命中。
Expires 是参考的本地时间,如果修改本地时间,可能就会造成缓存失效。
- Cache-Control
Cache-Control 属于 HTTP 1.1 时代的产物,可以再请求头或者响应头中设置,它的取值包含如下选项:
- 可缓存性
- public:http 经过的任何地方都可以进行缓存(代理服务器也可缓存)
- private:只有发起请求的这个浏览器才可以进行缓存,如果设置了代理缓存,那么代理缓存是不会生效的
- no-cache:任何一个节点都不可以缓存(绕过强缓存,但是还会经过协商缓存)
- 到期
- max-age=:设置缓存到多少秒过期
- s-maxage=:会代替 max-age,只有在代理服务器(nginx 代理服务器)才会生效
- max-stale=:是发起请求方主动带起的一个头,是代表即便缓存过期,但是在 max-stale 这个时间内还可以使用过期的缓存,而不需要向服务器请求新的内容
- 重新验证
- must-revalidate:如果 max-age 设置的内容过期,必须要向服务器请求重新获取数据验证内容是否过期
- proxy-revalidate:主要用在缓存服务器,指定缓存服务器在过期后重新从原服务器获取,不能从本地获取
- 其它
- no-store:本地和代理服务器都不可以存储这个缓存,永远都要从服务器拿 body 新的内容使用(强缓存、协商缓存都不会经过)
- no-transform:主要用于 proxy 服务器,告诉代理服务器不要随意改动返回的内容
Cache-Control 示例
先思考两个问题?
1. 在页面中引入静态资源文件,为什么静态资源文件改变后,再次发起请求还是之前的内容,没有变化呢?
2. 在使用webpack等一些打包工具时,为什么要加上一串hash码?
- cache-control.html
<html>
<head>
<meta charset="utf-8" />
<title>cache-control</title>
</head>
<body>
<script src="/script.js"></script>
</body>
</html>
- cache-control.js
浏览器输入 http://localhost:3010/ 加载 cache-control.html 文件,该文件会请求 http://localhost:3010/script.js 如果 url 等于 /script.js 设置 cache-control 的 max-age 进行浏览器缓存。
const http = require('http');
const fs = require('fs');
const port = 3010;
http.createServer((request, response) => {
console.log('request url: ', request.url);
if (request.url === '/') {
const html = fs.readFileSync('./example/cache/cache-control.html', 'utf-8');
response.writeHead(200, {
'Content-Type': 'text/html',
});
response.end(html);
} else if (request.url === '/script.js') {
response.writeHead(200, {
'Content-Type': 'text/javascript',
'Cache-Control': 'max-age=200'
});
response.end("console.log('script load')");
}
}).listen(port);
console.log('server listening on port ', port);
- 第一次运行
浏览器运行结果,没有什么问题,正常响应
- 修改 cache-control.js 返回值
response.writeHead(200, {
'Content-Type': 'text/javascript',
'Cache-Control': 'max-age=200'
});
response.end("console.log('script load !!!')");
- 中断上次程序,第二次运行
浏览器运行结果
第二次运行,从 memory cahce 读取,浏览器控制台并没有打印修改过的内容
控制台运营结果
只请求了 / 并没有请求 /script.js
源码参考:github.com/Q-Angelo/http-protocol/blob/master/example/cache/cache-control.js
以上结果浏览器并没有返回给我们服务端修改的结果,这是为什么呢?
先回答第一个问题
在页面中引入静态资源文件,为什么静态资源文件改变后,再次发起请求还是之前的内容,没有变化呢?
是因为我们请求的 url /script.js
没有变,那么浏览器就不会经过服务端的验证,会直接从客户端缓存去读,就会导致一个问题,我们的js静态资源更新之后,不会立即更新到我们的客户端,这也是前端开发中常见的一个问题,我们是希望浏览器去缓存我们的静态资源文件(js、css、img等)我们也不希望服务端内容更新了之后客户端还是请求的缓存的资源,
回答第二个问题
在使用webpack等一些打包工具时,为什么要加上一串hash码?
解决办法也就是我们在做 js 构建流程时,把打包完成的 js 文件名上根据它内容 hash 值加上一串 hash 码,这样你的 js 文件或者 css 文件等内容不变,这样生成的 hash 码就不会变,反映到页面上就是你的 url 没有变,如果你的文件内容有变化那么嵌入到页面的文件 url 就会发生变化,这样就可以达到一个更新缓存的目的,这也是目前前端来说比较常见的一个静态资源方案。
Expires 与 Cache-Control 对比
- HTTP 协议对比:Expires 属于 HTTP 1.0 时代的产物,Cache-Control 属于 HTTP 1.1 时代的产物
- 优先级对比:如果同时使用 Cache-Control 的 max-age 与 Expires,则 max-age 优先级会更高,会忽略掉 Expires
- 缓存单位:Expires 与 Cache-Control 两者的缓存单位都是以时间为维度,如果我要根据文件的内容变化来判断缓存是否失效怎么办呢?就需要用到下面的协商缓存了。
协商缓存
如果强缓存未命中或用户按下 F5 强制刷新后进入协商缓存,服务器则根据浏览器请求时的标识进行判断,如果协商缓存生效返回 304 否则返回 200。协商缓存的实现也是基于两点 Last-Modified、ETag 这个需要在 HTTP Headers 中设置。
Last-Modified/If-Modified-Since
Last-Modified 是在服务端设置进行响应,If-Modified-Since 是在浏览器端根据服务端上次在 Response Headers 中设置的 Last-Modified 取其值,如果存在请求时设置其 Request Headers 值 If-Modified-Since 传到服务器,服务器也是拿到这个值进行比对,下面为核心实现。
if (request.url === '/script.js') {
const filePath = path.join(__dirname, request.url); // 拼接当前脚本文件地址
const stat = fs.statSync(filePath); // 获取当前脚本状态
const mtime = stat.mtime.toGMTString() // 文件的最后修改时间
const requestMtime = request.headers['if-modified-since']; // 来自浏览器传递的值
console.log(stat);
console.log(mtime, requestMtime);
// 走协商缓存
if (mtime === requestMtime) {
response.statusCode = 304;
response.end();
return;
}
// 协商缓存失效,重新读取数据设置 Last-Modified 响应头
console.log('协商缓存 Last-Modified 失效');
response.writeHead(200, {
'Content-Type': 'text/javascript',
'Last-Modified': mtime,
});
const readStream = fs.createReadStream(filePath);
readStream.pipe(response);
}
执行 node last-modified.js 启动程序,浏览器执行 http://localhost:3010/打开页面,我多次调用发现第一次是从服务器拿的数据且状态为 200,之后每次都是 memory cache 为什么不是 304 呢?
显然是强缓存生效了,你可能会想我没有设置强缓存哦😯
这是因为浏览器默认启用了一个启发式缓存,这在设置了 Last-Modified 响应头且没有设置 Cache-Control: max-age/s-maxage 或 Expires时会触发,它的一个缓存时间是用 Date - Last-Modified 的值的 10% 作为缓存时间。
现在我们要达到 304的效果,不走强缓存直接走协商缓存,修改我们的响应,设置 Cache-Control=max-age=0 修改如下:
response.writeHead(200, {
'Content-Type': 'text/javascript',
'Last-Modified': mtime,
'Cache-Control': 'max-age=0', // 修改地方
});
再次运行我们的程序,控制台执行 node last-modified-max-age.js 再次重新打开页面查看效果,第二次直接走的协商缓存且 Request Headers 携带了 If-Modified-Since: Wed, 25 Mar 2020 12:31:58 GMT
ETag 和 If-None-Match
Last-Modified 是以文件的修改时间来判断,Etag 是根据文件的内容是否修改来判断,如果 Etag 有修改重新获取新的资源返回,如果未修改返回 304 通知客户端使用本地缓存。
Etag 的判断主要也是在服务端通过一种 Hash 算法实现,核心实现如下:
if (request.url === '/script.js') {
const filePath = path.join(__dirname, request.url); // 拼接当前脚本文件地址
const buffer = fs.readFileSync(filePath); // 获取当前脚本状态
const fileMd5 = md5(buffer); // 文件的 md5 值
const noneMatch = request.headers['if-none-match']; // 来自浏览器端传递的值
if (noneMatch === fileMd5) {
response.statusCode = 304;
response.end();
return;
}
console.log('Etag 缓存失效');
response.writeHead(200, {
'Content-Type': 'text/javascript',
'Cache-Control': 'max-age=0',
'ETag': fileMd5,
});
const readStream = fs.createReadStream(filePath);
readStream.pipe(response);
}
node etag.js 运行我们程序,打开我们的页面多次访问,第二次会看到浏览器会携带一个 If-None-Match 的 Header 头传递到服务端进行校验,当前协商缓存命中了所以响应状态为 304.
Last-Modified 与 Etag 对比
- 精确度:Last-Modified 以时间(秒)为单位,如果出现 1 秒内文件多次修改,在 Last-Modified 缓存策略下也不会失效,Etag 是对内容进行 Hash 比较,只要内容变动 Etag 就会发生变化,精确度更高。
- 分布式部署问题:分布式部署必然涉及到负载均衡,造成的一种现象是 Last-Modified 的时间可能还不太一致,而 Etag 只要保证每台机器的 Hash 算法是一致的就可保证一致性。
- 性能消耗:Etag 需要读取文件做 Hash 计算,相比 Last-Modified 性能上是有损耗的。
- 优先级:如果 Last-Modified/Etag 同时设置,Etag 的优先级会更高些。
- 相同点:校验通过返回 304 通知客户端使用本地缓存,校验不通过重新获取最新资源,设置 Last-Modified/Etag 响应头,返回状态码 200 。
疑问?
POST 可以缓存吗?
GET 是一个幂等操作,通常用于缓存,POST 是一个非幂等的操作,每次创建新的资源,也不会自动处理 POST 请求进行缓存,参考 rfc2616-sec9.html#sec9.1
附录
- 不同层中的称谓:
- 数据帧(Frame):是一种信息单位,它的起始点和目的点都是数据链路层
- 数据包(Packet):也是一种信息单位,它的起始和目的地都是网络层
- 段(Segment): 通常是指起始点和目的地都是传输层的信息单元
- 消息(message): 是指起始点和目的地都在网络层以上(经常在应用层)的信息单元