嘘~ 正在从服务器偷取页面 . . .

操作系统&计算机网络


操作系统

1. 基础

1.1 什么是操作系统

操作系统(Operating System,简称 OS)是管理计算机硬件与软件资源的程序,是计算机的基石。

操作系统本质上是一个运行在计算机上的软件程序 ,用于管理计算机硬件和软件资源。 举例:运行在你电脑上的所有应用程序都通过操作系统来调用系统内存以及磁盘等等硬件。

操作系统存在屏蔽了硬件层的复杂性。 操作系统就像是硬件使用的负责人,统筹着各种相关事项。

操作系统的内核(Kernel)是操作系统的核心部分,它负责系统的内存管理,硬件设备的管理,文件系统的管理以及应用程序的管理。 内核是连接应用程序和硬件的桥梁,决定着系统的性能和稳定性。

Kernel_Layout

1.2 系统调用

为了避免操作系统和关键数据被用户程序破坏,将处理器的执行状态分为内核态和用户态。

根据进程访问资源的特点,可以把进程在系统上的运行分为两个级别:

  1. 用户态(user mode):用户态运行的进程可以直接读取用户程序的数据;
  2. 系统态(kernel mode):可以简单的理解系统态运行的进程或程序几乎可以访问计算机的任何资源,不受限制。

内核态是操作系统管理程序执行时所处的状态,能够执行包含特权指令在内的一切指令,能够访问系统内所有的存储空间。用户态是用户程序执行时处理器所处的状态,不能执行特权指令,只能访问用户地址空间。

运行的用户程序中,凡是与系统态级别的资源有关的操作(如文件管理、进程控制、内存管理等),都必须通过系统调用方式(在用户态中需要调用系统态级别子功能)向操作系统提出服务请求,并由操作系统代为完成。

这些系统调用按功能大致可分为如下几类:

  • 设备管理。完成设备的请求或释放,以及设备启动等功能;
  • 文件管理。完成文件的读、写、创建及删除等功能;
  • 进程控制。完成进程的创建、撤销、阻塞及唤醒等功能;
  • 进程通信。完成进程之间的消息传递或信号传递等功能;
  • 内存管理。完成内存的分配、回收以及获取作业占用内存区大小及地址等功能。

2. 进程&线程

线程是进程划分成的更小的运行单位,一个进程在其执行的过程中可以产生多个线程。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。

2.1 进程状态

  • 创建状态(new) :进程正在被创建,尚未到就绪状态;
  • 就绪状态(ready) :进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行;
  • 运行状态(running) :进程正在处理器上上运行(单核 CPU 下任意时刻只有一个进程处于运行状态);
  • 阻塞状态(waiting) :又称为等待状态,进程正在等待某一事件而暂停运行如等待某资源为可用或等待 IO 操作完成。即使处理器空闲,该进程也不能运行;
  • 结束状态(terminated) :进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行。

进程状态切换

2.2 进程间通信方式

每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)

进程间通信模型

  1. 管道/匿名管道(Pipes) :用于具有亲缘关系的父子进程间或者兄弟进程之间的通信;
  2. 有名管道(Named Pipes) : 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循先进先出(first in first out)。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信;
  3. 信号(Signal) :信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;
  4. 消息队列(Message Queuing) :消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显式地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。比 FIFO 更有优势。消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺点
  5. 信号量(Semaphores) :信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件;
  6. 共享内存(Shared memory) :使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式;
  7. 套接字(Sockets) : 此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。

2.3 线程同步方式

线程同步是两个或多个共享关键资源的线程的并发执行。应该同步线程以避免关键的资源使用冲突。操作系统一般有下面三种线程同步的方式:

  • 互斥量(Mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制;
  • 信号量(Semaphore):它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量;
  • 事件(Event) :Wait/Notify,通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。

2.4 进程调度算法

  • 先到先服务(FCFS)调度算法 : 从就绪队列中选择一个最先进入该队列的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度;
  • 短作业优先(SJF)的调度算法 : 从就绪队列中选出一个估计运行时间最短的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度;
  • 时间片轮转调度算法 : 时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法,又称 RR(Round robin)调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间;
  • 多级反馈队列调度算法 :前面介绍的几种进程调度的算法都有一定的局限性。如短进程优先的调度算法,仅照顾了短进程而忽略了长进程 。多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业(进程)迅速完成。,因而它是目前被公认的一种较好的进程调度算法,UNIX 操作系统采取的便是这种调度算法;
  • 优先级调度 : 为每个流程分配优先级,首先执行具有最高优先级的进程,依此类推。具有相同优先级的进程以 FCFS 方式执行。可以根据内存要求,时间要求或任何其他资源要求来确定优先级。

2.5 死锁

死锁描述的是这样一种情况:多个进程/线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于进程/线程被无限期地阻塞,因此程序不可能正常终止。

2.5.1 产生死锁条件

  • 互斥:资源必须处于非共享模式,即一次只有一个进程可以使用。如果另一进程申请该资源,那么必须等待直到该资源被释放为止;

  • 占有并等待:一个进程至少应该占有一个资源,并等待另一资源,而该资源被其他进程所占有;

  • 非抢占:资源不能被抢占。只能在持有资源的进程完成任务后,该资源才会被释放;

  • 循环等待:有一组等待进程 {P0, P1,..., Pn}P0 等待的资源被 P1 占有,P1 等待的资源被 P2 占有,……,Pn-1 等待的资源被 Pn 占有,Pn 等待的资源被 P0 占有。

这四个条件是产生死锁的必要条件,也就是说只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生产生死锁。

2.5.2 解决死锁

解决死锁的方法可以从多个角度去分析,一般的情况下,有预防,避免,检测和解除四种

  • 预防 是采用某种策略,限制并发进程对资源的请求,从而使得死锁的必要条件在系统执行的任何时间上都不满足。
  • 避免则是系统在分配资源时,根据资源的使用情况提前做出预测,从而避免死锁的发生
  • 检测是指系统设有专门的机构,当死锁发生时,该机构能够检测死锁的发生,并精确地确定与死锁有关的进程和资源。
  • 解除 是与检测相配套的一种措施,用于将进程从死锁状态下解脱出来
预防

只要破坏四个必要条件中的任何一个就能够预防死锁的发生。

  • 破坏第一个条件互斥条件:使得资源是可以同时访问的,这是种简单的方法,磁盘就可以用这种方法管理,但是我们要知道,有很多资源往往是不能同时访问的 ,所以这种做法在大多数的场合是行不通的;
  • 破坏第三个条件非抢占 :也就是说可以采用 剥夺式调度算法,但剥夺式调度方法目前一般仅适用于主存资源处理器资源的分配,并不适用于所以的资源,会导致资源利用率下降

所以一般比较实用的预防死锁的方法,是通过考虑破坏第二个条件和第四个条件。

静态分配策略

静态分配策略可以破坏死锁产生的第二个条件(占有并等待)。所谓静态分配策略,就是指一个进程必须在执行前就申请到它所需要的全部资源,并且知道它所要的资源都得到满足之后才开始执行。进程要么占有所有的资源然后开始执行,要么不占有资源,不会出现占有一些资源等待一些资源的情况。

静态分配策略逻辑简单,实现也很容易,但这种策略严重地降低了资源利用率,因为在每个进程所占有的资源中,有些资源是在比较靠后的执行时间里采用的,甚至有些资源是在额外的情况下才是用的,这样就可能造成了一个进程占有了一些几乎不用的资源而使其他需要该资源的进程产生等待的情况。

层次分配策略

层次分配策略破坏了产生死锁的第四个条件(循环等待)。在层次分配策略下,所有的资源被分成了多个层次,一个进程得到某一次的一个资源后,它只能再申请较高一层的资源;当一个进程要释放某层的一个资源时,必须先释放所占用的较高层的资源,按这种策略,是不可能出现循环等待链的,因为那样的话,就出现了已经申请了较高层的资源,反而去申请了较低层的资源,不符合层次分配策略。

避免

将系统的状态分为 安全状态不安全状态 ,每当在未申请者分配资源前先测试系统状态,若把系统资源分配给申请者会产生死锁,则拒绝分配,否则接受申请,并为它分配资源。

如果操作系统能够保证所有的进程在有限的时间内得到需要的全部资源,则称系统处于安全状态,否则说系统是不安全的。很显然,系统处于安全状态则不会发生死锁,系统若处于不安全状态则可能发生死锁。

通过银行家算法改善解决了资源使用率低的问题,但是它要不断地检测每个进程对各类资源的占用和申请情况,以及做 安全性检查 ,需要花费较多的时间。

当一个进程申请使用资源的时候,银行家算法通过先 试探 分配给该进程资源,然后通过安全性算法判断分配后的系统是否处于安全状态,若不安全则试探分配作废,让该进程继续等待。

银行家算法图示

检测

对资源的分配加以限制可以预防和避免死锁的发生,但是都不利于各进程对系统资源的充分共享。解决死锁问题的另一条途径是死锁检测和解除。这种方法对资源的分配不加以任何限制,也不采取死锁避免措施,但系统定时地运行一个“死锁检测”的程序,判断系统内是否出现死锁,如果检测到系统发生了死锁,再采取措施去解除它。

操作系统中的每一刻时刻的系统状态都可以用进程-资源分配图来表示,进程-资源分配图是描述进程和资源申请及分配关系的一种有向图,可用于检测系统是否处于死锁状态

进程-资源分配图

  • 如果进程-资源分配图中无环路,则此时系统没有发生死锁;
  • 如果进程-资源分配图中有环路,且每个资源类仅有一个资源,则系统中已经发生了死锁;
  • 如果进程-资源分配图中有环路,且涉及到的资源类有多个资源,此时系统未必会发生死锁。如果能在进程-资源分配图中找出一个既不阻塞又非独立的进程,该进程能够在有限的时间内归还占有的资源,也就是把边给消除掉了,重复此过程,直到能在有限的时间内消除所有的边 ,则不会发生死锁,否则会发生死锁。

当死锁检测程序检测到存在死锁发生时,应设法让其解除,让系统从死锁状态中恢复过来,常用的解除死锁的方法有以下四种:

  1. 立即结束所有进程的执行,重新启动操作系统:这种方法简单,但以前所在的工作全部作废,损失很大;
  2. 撤销涉及死锁的所有进程,解除死锁后继续运行:这种方法能彻底打破死锁的循环等待条件,但将付出很大代价,例如有些进程可能已经计算了很长时间,由于被撤销而使产生的部分结果也被消除了,再重新执行时还要再次进行计算;
  3. 逐个撤销涉及死锁的进程,回收其资源直至死锁解除
  4. 抢占资源:从涉及死锁的一个或几个进程中抢占资源,把夺得的资源再分配给涉及死锁的进程直至死锁解除。

3. 内存管理

操作系统的内存管理主要负责内存的分配与回收(malloc 函数:申请内存,free 函数:释放内存),另外地址转换也就是将逻辑地址转换成相应的物理地址等功能也是操作系统内存管理做的事情。

3.1 内存管理机制

连续分配管理方式非连续分配管理方式这两种。连续分配管理方式是指为一个用户程序分配一个连续的内存空间,常见的如 块式管理 。同样地,非连续分配管理方式允许一个程序使用的内存分布在离散或者说不相邻的内存中,常见的如页式管理段式管理

  1. 块式管理 : 远古时代的计算机操作系统的内存管理方式。将内存分为几个固定大小的块,每个块中只包含一个进程。如果程序运行需要内存的话,操作系统就分配给它一块,如果程序运行只需要很小的空间的话,分配的这块内存很大一部分几乎被浪费了。这些在每个块中未被利用的空间,我们称之为碎片;
  2. 页式管理 :把主存分为大小相等且固定的一页一页的形式,页较小,相比于块式管理的划分粒度更小,提高了内存利用率,减少了碎片。页式管理通过页表对应逻辑地址和物理地址;
  3. 段式管理 : 页式管理虽然提高了内存利用率,但是页式管理其中的页并无任何实际意义。 段式管理把主存分为一段段的,段是有实际意义的,每个段定义了一组逻辑信息,例如,有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。 段式管理通过段表对应逻辑地址和物理地址。

段页式管理机制结合了段式管理和页式管理的优点。简单来说段页式管理机制就是把主存先分成若干段,每个段又分成若干页,也就是说 段页式管理机制 中段与段之间以及段的内部的都是离散的。

3.2 块表&多级页表

编程一般只有可能和逻辑地址打交道,比如在 C 语言中,指针里面存储的数值就可以理解成为内存里的一个地址,这个地址也就是我们说的逻辑地址,逻辑地址由操作系统决定。物理地址指的是真实物理内存中地址,更具体一点来说就是内存地址寄存器中的地址。物理地址是内存单元真正的地址。

在分页内存管理中,很重要的两点:

  1. 虚拟地址到物理地址的转换要快;
  2. 解决虚拟地址空间大,页表也会很大的问题。

3.2.1 块表

为了提高虚拟地址到物理地址的转换速度,操作系统在 页表方案 基础之上引入了 快表 来加速虚拟地址到物理地址的转换。我们可以把快表理解为一种特殊的高速缓冲存储器(Cache),其中的内容是页表的一部分或者全部内容。作为页表的 Cache,它的作用与页表相似,但是提高了访问速率。由于采用页表做地址转换,读写内存数据时 CPU 要访问两次主存。有了快表,有时只要访问一次高速缓冲存储器,一次主存,这样可加速查找并提高指令执行速度。

  1. 根据虚拟地址中的页号查快表;
  2. 如果该页在快表中,直接从快表中读取相应的物理地址;
  3. 如果该页不在快表中,就访问内存中的页表,再从页表中得到物理地址,同时将页表中的该映射表项添加到快表中;
  4. 当快表填满后,又要登记新页时,就按照一定的淘汰策略淘汰掉快表中的一个页。

3.2.2 多级页表

引入多级页表的主要目的是为了避免把全部页表一直放在内存中占用过多空间,特别是那些根本就不需要的页表就不需要保留在内存中。多级页表属于时间换空间的典型场景。

3.3 分页机制&分段机制

共同点

  • 分页机制和分段机制都是为了提高内存利用率,减少内存碎片;
  • 页和段都是离散存储的,所以两者都是离散分配内存的方式。但是,每个页和段中的内存是连续的。

区别

  • 页的大小是固定的,由操作系统决定;而段的大小不固定,取决于我们当前运行的程序;
  • 分页仅仅是为了满足操作系统内存管理的需求,而段是逻辑信息的单位,在程序中可以体现为代码段,数据段,能够更好满足用户的需要。

3.4 CPU 寻址

现代处理器使用的是一种称为 虚拟寻址(Virtual Addressing) 的寻址方式。使用虚拟寻址,CPU 需要将虚拟地址翻译成物理地址,这样才能访问到真实的物理内存。 实际上完成虚拟地址转换为物理地址转换的硬件是 CPU 中含有一个被称为 内存管理单元(Memory Management Unit, MMU) 的硬件。

页表是被缓存在内存中的,尽管内存的速度相对于硬盘来说已经非常快了,但与 CPU 还是有所差距。为了防止每次地址翻译操作都需要去访问内存,CPU 使用了高速缓存与 TLB 来缓存 PTE。

通过 MMU 寻找真实物理地址

3.4.1 页表

MMU需要借助存放在内存中的页表来动态翻译虚拟地址,该页表由操作系统管理。

操作系统通过将虚拟内存分割为大小固定的块来作为硬盘和内存之间的传输单位,这个块被称为虚拟页(Virtual Page, VP),每个虚拟页的大小为P=2^p字节。物理内存也会按照这种方法分割为物理页(Physical Page, PP),大小也为P字节。

页表是一个元素为页表条目(Page Table Entry, PTE)的集合,每个虚拟页在页表中一个固定偏移量的位置上都有一个PTE。下面是 PTE 仅含有一个有效位标记的页表结构,该有效位代表这个虚拟页是否被缓存在物理内存中。

img

由于CPU每次进行地址翻译的时候都需要经过PTE,所以如果想控制内存系统的访问,可以在PTE上添加一些额外的许可位(例如读写权限、内核权限等),这样只要有指令违反了这些许可条件,CPU就会触发一个一般保护故障,将控制权传递给内核中的异常处理程序。一般这种异常被称为“段错误”(Segmentation Fault)。

如果虚拟页已经缓存到 PTE 中,就是页命中;反之,就是缺页的情况。

3.4.2 虚拟地址作用

如果直接把物理地址暴露出来的话会带来严重问题,比如可能对操作系统造成伤害以及给同时运行多个程序造成困难。

没有虚拟地址空间的时候,程序直接访问和操作的都是物理内存

  1. 用户程序可以访问任意内存,寻址内存的每个字节,这样就很容易(有意或者无意)破坏操作系统,造成操作系统崩溃;
  2. 想要同时运行多个程序特别困难,比如你想同时运行一个微信和一个 QQ 音乐都不行。为什么呢?举个简单的例子:微信在运行的时候给内存地址 1xxx 赋值后,QQ 音乐也同样给内存地址 1xxx 赋值,那么 QQ 音乐对内存的赋值就会覆盖微信之前所赋的值,这就造成了微信这个程序就会崩溃。

通过虚拟地址访问内存有以下优势:

  • 程序可以使用一系列相邻的虚拟地址来访问物理内存中不相邻的大内存缓冲区;
  • 程序可以使用一系列虚拟地址来访问大于可用物理内存的内存缓冲区。当物理内存的供应量变小时,内存管理器会将物理内存页(通常大小为 4KB)保存到磁盘文件。数据或代码页会根据需要在物理内存与磁盘之间移动;
  • 不同进程使用的虚拟地址彼此隔离。一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存。

4. 虚拟内存

虚拟内存 使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。与没有使用虚拟内存技术的系统相比,使用这种技术的系统使得大型程序的编写变得更容易,对真正的物理内存(例如 RAM)的使用也更有效率。

4.1 局部性原理

局部性原理是虚拟内存技术的基础,正是因为程序运行具有局部性原理,才可以只装入部分程序到内存就开始运行。

局部性原理表现在以下两个方面:

  1. 时间局部性 :如果程序中的某条指令一旦执行,不久以后该指令可能再次执行;如果某数据被访问过,不久以后该数据可能再次被访问。产生时间局部性的典型原因,是由于在程序中存在着大量的循环操作;
  2. 空间局部性 :一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问,即程序在一段时间内所访问的地址,可能集中在一定的范围之内,这是因为指令通常是顺序存放、顺序执行的,数据也一般是以向量、数组、表等形式簇聚存储的。

时间局部性是通过将近来使用的指令和数据保存到高速缓存存储器中,并使用高速缓存的层次结构实现。空间局部性通常是使用较大的高速缓存,并将预取机制集成到高速缓存控制逻辑中实现。虚拟内存技术实际上就是建立了 “内存一外存”的两级存储器的结构,利用局部性原理实现髙速缓存。

4.2 技术实现

虚拟内存的实现有以下三种方式:

  1. 请求分页存储管理 :建立在分页管理之上,为了支持虚拟存储器功能而增加了请求调页功能和页面置换功能。请求分页是目前最常用的一种实现虚拟存储器的方法。请求分页存储管理系统中,在作业开始运行之前,仅装入当前要执行的部分段即可运行。假如在作业运行的过程中发现要访问的页面不在内存,则由处理器通知操作系统按照对应的页面置换算法将相应的页面调入到主存,同时操作系统也可以将暂时不用的页面置换到外存中;
  2. 请求分段存储管理 :建立在分段存储管理之上,增加了请求调段功能、分段置换功能。请求分段储存管理方式就如同请求分页储存管理方式一样,在作业开始运行之前,仅装入当前要执行的部分段即可运行;在执行过程中,可使用请求调入中断动态装入要访问但又不在内存的程序段;当内存空间已满,而又需要装入新的段时,根据置换功能适当调出某个段,以便腾出空间而装入新的段;
  3. 请求段页式存储管理

请求分页存储管理建立在分页管理之上。他们的根本区别是是否将程序全部所需的全部地址空间都装入主存。请求分页存储管理不要求将作业全部地址空间同时装入主存。基于这一点,请求分页存储管理可以提供虚存,而分页存储管理却不能提供虚存。

一般都需要:

  1. 一定容量的内存和外存:在载入程序的时候,只需要将程序的一部分装入内存,而将其他部分留在外存;
  2. 缺页中断:如果需执行的指令或访问的数据尚未在内存(称为缺页或缺段),则由处理器通知操作系统将相应的页面或段调入到内存,然后继续执行程序;
  3. 虚拟地址空间 :逻辑地址到物理地址的变换。

4.3 页面置换算法

地址映射过程中,若在页面中发现所要访问的页面不在内存中,则发生缺页中断 。

缺页中断 就是要访问的不在主存,需要操作系统将其调入主存后再进行访问。 在这个时候,被内存映射的文件实际上成了一个分页交换文件。

当发生缺页中断时,如果当前内存中并没有空闲的页面,操作系统就必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。用来选择淘汰哪一页的规则叫做页面置换算法,可以把页面置换算法看成是淘汰页面的规则。

  • OPT 页面置换算法(最佳页面置换算法):最佳(Optimal, OPT)置换算法所选择的被淘汰页面将是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率。但由于人们目前无法预知进程在内存下的若千页面中哪个是未来最长时间内不再被访问的,因而该算法无法实现。一般作为衡量其他置换算法的方法;
  • FIFO(First In First Out) 页面置换算法(先进先出页面置换算法): 总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面进行淘汰;
  • LRU (Least Recently Used)页面置换算法(最近最久未使用页面置换算法):LRU 算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 T,当须淘汰一个页面时,选择现有页面中其 T 值最大的,即最近最久未使用的页面予以淘汰;
  • LFU (Least Frequently Used)页面置换算法(最少使用页面置换算法): 该置换算法选择在之前时期使用最少的页面作为淘汰页。

计算机网络

1. HTTP

HTTP(HyperText Transfer Protocol),超文本传输协议。

  • “协议”:确立了一种计算机之间交流通信的规范;
  • “传输”:在计算机世界里专门用来在两点之间传输数据的约定和规范;
  • “超文本”:文字、图片、音频和视频等的混合体,最关键的是含有“超链接”,能够从一个“超文本”跳跃到另一个“超文本”,形成复杂的非线性、网状的结构关系。

HTTP 1.0 规定了请求头和请求尾,响应头和响应尾(get post),每一个请求都是一个单独的连接,做不到连接的复用。

1.1 分层

网络分层的原则:每一层独立于其它层完成自己的工作,而不需要相互依赖,上下层之间通 过标准结构来互相通信,简单易用又具有拓展性。复杂的系统需要分层,因为每一层都需要专注于一类事情。我们的网络分层的原因也是一 样,每一层只专注于做一类事情。

  1. 各层之间相互独立:各层之间相互独立,各层之间不需要关心其他层是如何实现的,只需要知道自己如何调用下层提供好的功能就可以了(可以简单理解为接口调用)。这个和我们对开发时系统进行分层是一个道理;
  2. 提高了整体灵活性:每一层都可以使用最适合的技术来实现,你只需要保证你提供的功能以及暴露的接口的规则没有改变就行了。这个和我们平时开发系统的时候要求的高内 聚、低耦合的原则也是可以对应上的;
  3. 大问题化小:分层可以将复杂的网络间题分解为许多比较小的、界线比较清晰简单的小问题来处理和解决。这样使得复杂的计算机网络系统变得易于设计,实现和标准化。这个和我们平时开发的时候,一般会将系统功能分解,然后将复杂的问题分解为容易理解的更小的问题是相对应的,这些较小的问题具有更好的边界(目标和接口)定义。

1.1.1 四层

最初始的 TCP/IP 协议总共有四层,这是实践中出现的“四层”分层。

“四层”结构

  • 第一层叫“链接层”(link layer),负责在以太网、WiFi 这样的底层网络上发送原始数据包,工作在网卡这个层次,使用 MAC 地址来标记网络上的设备,所以有时候也叫 MAC 层。
  • 第二层叫“网际层”或者“网络互连层”(internet layer),IP 协议就处在这一层。因为 IP 协议定义了“IP 地址”的概念,所以就可以在“链接层”的基础上,用 IP 地址取代 MAC 地址,把许许多多的局域网、广域网连接成一个虚拟的巨大网络,在这个网络里找设备时只要把 IP 地址再“翻译”成 MAC 地址就可以了。
  • 第三层叫“传输层”(transport layer),这个层次协议的职责是保证数据在 IP 地址标记的两点之间“可靠”地传输,是 TCP 协议工作的层次,另外还有它的一个“小伙伴” UDP。
  • 第四层叫“应用层”(application layer),由于下面的三层把基础打得非常好,所以在这一层就“百花齐放”了,有各种面向具体应用的协议。例如 Telnet、SSH、FTP、SMTP 等等,当然还有我们的 HTTP。

1.1.2 七层

在之后,OSI 组织进行理论上划分,分为七层。

“七层”结构

  1. 物理层,主要解决两台物理机之间的通信,通过二进制比特流的传输来实现,二进制数据表现为电流电压上的强弱,到达目的地再转化为二进制机器码。网卡、集线器工作在这一层;
  2. 数据链路层,在不可靠的物理介质上提供可靠的传输,接收来自物理层的位流形式的数据,并封装成帧,传送到上一层;同样,也将来自上层的数据帧,拆装为位流形式的数据转发到物理层。这一层在物理层提供的比特流的基础上,通过差错控制、流量控制方法,使有差错的物理线路变为无差错的数据链路。提供物理地址寻址功能。交换机工作在这一层;
  3. 网络层,将网络地址翻译成对应的物理地址,并决定如何将数据从发送方路由到接收方,通过路由选择算法为分组通过通信子网选择最佳路径。路由器工作在这一层;
  4. 传输层,提供了进程间的逻辑通信,传输层向高层用户屏蔽了下面网络层的核心细节,使应用程序看起来像是在两个传输层实体之间有一条端到端的逻辑通信信道;
  5. 会话层,建立会话:身份验证,权限鉴定等;
    保持会话:对该会话进行维护,在会话维持期间两者可以随时使用这条会话传输局;
    断开会话:当应用程序或应用层规定的超时时间到期后,OSI会话层才会释放这条会话。
  6. 表示层,对数据格式进行编译,对收到或发出的数据根据应用层的特征进行处理,如处理为文字、图片、音频、视频、文档等,还可以对压缩文件进行解压缩、对加密文件进行解密等;
  7. 应用层,提供应用层协议,如 HTTP 协议,FTP 协议等等,方便应用程序之间进行通信

1.1.3 五层

但七层的划分只是基于理论,在生产中,设备厂家将原先的七层映射成了五层。

最常见的“五层”划分

  1. 物理层,TCP/IP 里无对应;
  2. 数据链路层,对应 TCP/IP 的链接层;
  3. 网络层,对应 TCP/IP 的网际层;
  4. 传输层,对应 TCP/IP 的传输层;
  5. 统一对应到 TCP/IP 的应用层。

1.2 常见状态码

HTTP 协议是基于 TCP 协议的,发送 HTTP 请求之前首先要建立 TCP 连接也就是要经历 3 次握手。目前使用的 HTTP 协议大部分都是 1.1。在 1.1 的协议里面,默认是开启了 Keep-Alive 的,这样的话建立的连接就可以在多次请求中被复用了。

五大类 HTTP 状态码

Code Description
200 请求被成功处理
301 永久性重定向 比如建设一个网站后,将网站的 url 变换了,
重新申请一个域名,但是希望之前的用户访问之前 url 仍然可以访问到,
就可以做一个重定向新的 url 下面
302 临时性重定向 比如用户在未登录时访问个人中心页面,这时可以临时重定向到登录的 url
304 Not Modified 当客户端拥有可能过期的缓存时,
会携带缓存的标识 etag、时间等信息询问服务器缓存是否仍可复用,
而 304 是告诉客户端可以 复用缓存。
400 请求出错 由于语法格式有误,服务器无法理解此请求。不作修改,客户程序就无法重复此请求
401 未授权 (未授权) 请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应
403 没有访问权限 系统中某些页面只有在某些权限下才能访问,
当用户去访问了一个本身没有访问权限的 URL,回报 403 错误
404 没有对应资源 一般是自己输入了一个 URL,这个 URL并不合法。
404 找不到,Web 服务器找不到您所请求的文件或脚本。
请检查 URL 以确保路径正确。
405 不允许此方法 405 不允许此方法,对于请求所标识的资源,
不允许使用请求行中所指定的方法。
(GET/POST/PUT/DELETE 请求用混淆了,更正即可)
请确保为所请求的资源设置了正确的 MIME 类型
406 不可接受 根据此请求中所发送的“接受”标题,
此请求所标识的资源只能生成内容特征为“不可接受”的响应实体
407 需要代理身份验证 407 需要代理身份验证,在可为此请求提供服务之前,
必须验证此代理服务器。请登录到代理服务器,然后重试
412 前提条件失败 在服务器上测试前提条件时,
部分请求标题字段中所给定的前提条件估计为 FALSE。
客户机将前提条件放置在当前资源 metainformation(标题字段数据)中,
以防止所请求的方法被误用到其他资源
414 Request-URI 太长 414 Request-URI 太长,Request-URL 太长,服务器拒绝服务此请求。
仅在下列条件下才有可能发生此条件:
1. 客户机错误地将 POST 请求转换为具有较长的查询信息的 GET 请求;
2. 客户机遇到了重定向问题(例如,指向自身的后缀的重定向前缀);
3. 服务器正遭受试图利用某些服务中的安全性漏洞的客户干扰。
(将固定长度的缓冲区用于读取或执行 Request-URI)
500 服务器错误 比如服务器某一个函数代码出错了,有没有捕获异常,这时候会报 500 错误。
500 服务器的内部错误,Web 服务器不能执行此请求。请稍后重试此请求。
501 未实现 501 未实现,Web 服务器不支持实现此请求所需的功能。请检查 URL 中的错误
502 网关出错 Bad Gateway 网关出错,当用作网关或代理时,
服务器将从试图实现此请求时所访问的 upstream 服务器中接收无效的响应
503 服务器停机 系统正在维护或者服务器暂停的时候,回报500错误
504 Gateway Timeout 代理服务器无法及时的从上游获得响应

1.3 报文

网络中,数据以报文(message)的形式被发送出去。拿 TCP 报文来举例,它在实际要传输的数据之前附加了一个 20 字节的头部数据,存储 TCP 协议必须的额外信息,例如发送方的端口号、接收方的端口号、包序号、标志位等等。

TCP 头 + 实际数据

HTTP 协议的请求报文和响应报文的结构基本相同,由三大部分组成:

  1. 起始行(start line):描述请求或响应的基本信息;
  2. 头部字段集合(header):使用 key-value 形式更详细地说明报文;
  3. 消息正文(entity):实际传输的数据,它不一定是纯文本,可以是图片、视频等二进制数据。

这其中前两部分起始行和头部字段经常又合称为“请求头”或“响应头”,消息正文又称为“实体”,但与“header”对应,很多时候就直接称为“body”。

HTTP 协议规定报文必须有 header,但可以没有 body,而且在 header 之后必须要有一个“空行”,也就是“CRLF”,十六进制的“0D0A”。

所以,一个完整的 HTTP 报文就像是下图的这个样子,注意在 header 和 body 之间有一个“空行”。

完整 HTTP 报文

简单抓包之后,浏览显示的内容

1.3.1 请求行

请求报文里的起始行,就是请求行,由三部分构成:

  1. 请求方法:是一个动词,如 GET/POST,表示对资源的操作;
  2. 请求目标:通常是一个 URI,标记了请求方法要操作的资源;
  3. 版本号:表示报文使用的 HTTP 协议版本。

这三个部分通常使用空格(space)来分隔,最后要用 CRLF 换行表示结束

通过 Wireshark 抓包的数据来举例:

GET / HTTP/1.1

“GET”是请求方法,“/”是请求目标,“HTTP/1.1”是版本号。

1.3.2 请求方法

HTTP 1.0 定义了三种请求方法: GET、POST 和 HEAD 方法。

HTTP 1.1 新增了六种请求方法:OPTIONS、PUT、PATCH、DELETE、TRACE 和 CONNECT 方法。

方法 描述
GET 请求指定的页面信息,并返回实体主体
HEAD 类似于 GET 请求,只不过返回的响应中没有具体的内容,用于获取报头
POST 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)
数据被包含在请求体中。POST 请求可能会导致新的资源的建立和/或已有资源的修改
PUT 从客户端向服务器传送的数据取代指定的文档的内容
DELETE 请求服务器删除指定的页面
CONNECT HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器
OPTIONS 允许客户端查看服务器的性能
TRACE 回显服务器收到的请求,主要用于测试或诊断
PATCH 是对 PUT 方法的补充,用来对已知资源进行局部更新

可扩展的请求方法

GET 的含义是请求从服务器获取资源,这个资源既可以是静态的文本、页面、图片、视频,也可以是由 PHP、Java 动态生成的页面或者其他格式的数据。

HEAD 方法与 GET 方法类似,也是请求从服务器获取资源,服务器的处理机制也是一样的,但服务器不会返回请求的实体数据,只会传回响应头,也就是资源的“元信息”。

只要向服务器发送数据,用的大多数都是 POST。

PUT 的作用与 POST 类似,也可以向服务器提交数据。不同在于,通常 POST 表示的是“新建”“create”的含义,而 PUT 则是“修改”“update”的含义。

其他方法

DELETE 方法指示服务器删除资源,因为这个动作危险性太大,所以通常服务器不会执行真正的删除操作(QQ 对于消息的撤回和删除也是采取假删),而是对资源做一个删除标记。当然,更多的时候服务器就直接不处理 DELETE 请求。

CONNECT 是一个比较特殊的方法,要求服务器为客户端和另一台远程服务器建立一条特殊的连接隧道,这时 Web 服务器在中间充当了代理的角色。

OPTIONS 方法要求服务器列出可对资源实行的操作方法,在响应头的 Allow 字段里返回。它的功能很有限,用处也不大,有的服务器(例如 Nginx)干脆就没有实现对它的支持。

TRACE 方法多用于对 HTTP 链路的测试或诊断,可以显示出请求 - 响应的传输路径。它的本意是好的,但存在漏洞,会泄漏网站的信息,所以 Web 服务器通常也是禁止使用。

安全&幂等

在 HTTP 协议里,所谓的“安全”是指请求方法不会“破坏”服务器上的资源,即不会对服务器上的资源造成实质的修改。

只有 GET 和 HEAD 方法是“安全”的,因为它们是“只读”操作,只要服务器不故意曲解请求方法的处理方式,无论 GET 和 HEAD 操作多少次,服务器上的数据都是“安全的”。

所谓的“幂等”实际上是一个数学用语,被借用到了 HTTP 协议里,意思是多次执行相同的操作,结果也都是相同的,即多次“幂”后结果“相等”。

GET 和 HEAD 既是安全的也是幂等的,DELETE 可以多次删除同一个资源,效果都是“资源不存在”,所以也是幂等的。按照 RFC 里的语义,POST 是“新增或提交数据”,多次提交数据会创建多个资源,所以不是幂等的;而 PUT 是“替换或更新数据”,多次更新一个资源,资源还是会第一次更新的状态,所以是幂等的。

GET 和 POST

网上一般笼统的回答:

  • GET 在浏览器回退时是无害的,而 POST 会再次提交请求。GET 方式是幂等的,因为是“只读”操作,不会对服务器资源产生影响。GET 产生的 URL 地址可以被 Bookmark,而 POST 不可以。GET 请求会被浏览器主动 cache,而 POST 不会,除非手动设置;
  • GET 请求只能进行 URL 编码,而 POST 支持多种编码方式。GET 参数通过 URL 传递,POST 放在 Request body 中。所以对参数的数据类型,GET 只接受 ASCII 字符,而 POST 没有限制。GET 比 POST 更不安全,因为参数直接暴露在 URL 上,所以不能用来传递敏感信息。GET 请求在 URL 中传送的参数是有长度限制的,而 POST 没有(浏览器不会对 Body 中的参数进行限制);
  • GET 请求参数会被完整保留在浏览器历史记录里,而 POST 中的参数不会被保留;
参数位置

无论 GET 还是 POST,用的都是同一个传输层协议,所以在传输上没有区别。当携带参数的时候,GET 请求是放在 URL 中,POST 则放在 body 中。

GET 方法简约版报文:

GET /index.html?name=qiming.c&age=22 HTTP/1.1
Host: localhost

POST 方法简约版报文:

POST /index.html HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded

name=qiming.c&age=22

注意:这里只是约定,并不属于 HTTP 规范,相反的,可以在 POST 请求中 URL 中写入参数,或者 GET 请求中的 body 携带参数。

参数长度

HTTP 协议没有 BodyURL 的长度限制,对 URL 限制的大多是浏览器和服务器的原因。

IEURL 长度的限制是 2083 字节(2K + 35)。对于其他浏览器,如 Netscape、FireFox 等,理论上没有长度限制,其限制取决于操作系统的支持。这里限制的是整个 URL 长度,而不仅仅是参数值的长度。服务器处理长 URL 要消耗比较多的资源,为了性能和安全考虑,会给 URL 长度加限制。

安全

POSTGET 安全,因为数据在地址栏上不可见。

然而,从传输的角度来说,他们都是不安全的,因为 HTTP 在网络上是明文传输的,只要在网络节点上捉包,就能完整地获取数据报文。只有使用 HTTPS 才能加密安全。

数据包

对于 GET 方式的请求,浏览器会把 http headerdata 一并发送出去,服务器响应 200(返回数据)。

对于 POST,浏览器先发送header,服务器响应 100 continue,浏览器再发送 data,服务器响应 200 ok。但并不是所有浏览器都会在 POST 中发送两次包,Firefox 就只发送一次。

REST API

REST API 全称为表述性状态转移(Representational State Transfer,REST)即利用 HTTP中get、post、put、delete 以及其他的 HTTP 方法构成 REST 中数据资源的增删改查操作:

  • Create : POST;
  • Read : GET;
  • Update : PUT/PATCH;
  • Delete: DELETE。

1.3.3 状态行

响应报文里的起始行,在这里它不叫“响应行”,而是叫“状态行”(status line),意思是服务器响应的状态。同样也是由三部分构成:

  1. 版本号:表示报文使用的 HTTP 协议版本;
  2. 状态码:一个三位数,用代码的形式表示处理的结果,比如 200 是成功,500 是服务器错误;
  3. 原因:作为数字状态码补充,是更详细的解释文字,帮助人理解原因(可以简单理解为 code 和 message)。

状态行组成

按抓包成功和失败两个举例:

HTTP/1.1 200 OK
HTTP/1.1 404 Not Found

1.3.4 头部字段

请求行/状态行 + 头部字段集合就构成了 HTTP 报文里完整的请求头或响应头。

头部字段是 key-value 的形式,key 和 value 之间用“:”分隔,最后用 CRLF 换行表示字段结束。比如在“Host:127.0.0.1”这一行里 key 就是“Host”,value 就是“127.0.0.1”。

使用头字段需要注意下面几点:

  1. 字段名不区分大小写,例如“Host”也可以写成“host”,但首字母大写的可读性更好;
  2. 字段名里不允许出现空格,可以使用连字符“-”,但不能使用下划线“_”。
    例如,“test-name”是合法的字段名,而“test name”、“test_name”是不正确的字段名;
  3. 字段名后面必须紧接着“:”,不能有空格,而“:”后的字段值前可以有多个空格;
  4. 字段的顺序是没有意义的,可以任意排列不影响语义;
  5. 字段原则上不能重复,除非这个字段本身的语义允许,例如 Set-Cookie。
常用头部字段

HTTP 协议规定了非常多的头部字段,实现各种各样的功能,但基本上可以分为四大类:

  1. 通用字段:在请求头和响应头里都可以出现;
  2. 请求字段:仅能出现在请求头里,进一步说明请求信息或者额外的附加条件;
  3. 响应字段:仅能出现在响应头里,补充说明响应报文的信息;
  4. 实体字段:它实际上属于通用字段,但专门描述 body 的额外信息。
  • Host:属于请求字段,只能出现在请求头里,它同时也是唯一一个 HTTP/1.1 规范里要求必须出现的字段,也就是说,如果请求头里没有 Host,那这就是一个错误的报文。
    Host 字段告诉服务器这个请求应该由哪个主机来处理,当一台计算机上托管了多个虚拟主机的时候,服务器端就需要用 Host 字段来选择,有点像是一个简单的“路由重定向”;
  • User-Agent:请求字段,只出现在请求头里。它使用一个字符串来描述发起 HTTP 请求的客户端,服务器可以依据它来返回最合适此浏览器显示的页面;
  • Date:是一个通用字段,但通常出现在响应头里,表示 HTTP 报文创建的时间,客户端可以使用这个时间再搭配其他字段决定缓存策略;
  • Server:响应字段,只能出现在响应头里。它告诉客户端当前正在提供 Web 服务的软件名称和版本号。

1.4 HTTP/1.1 特性

1.4.1 特点

HTTP 特点总结

灵活可扩展

HTTP 只规定了报文的基本格式,比如用空格分隔单词,用换行分隔字段,“header+body”等,报文里的各个组成部分都没有做严格的语法语义限制,可以由开发者任意定制。

HTTP 协议就随着互联网的发展一同成长起来了。在这个过程中,HTTP 协议逐渐增加了请求方法、版本号、状态码、头字段等特性。而 body 也不再限于文本形式的 TXT 或 HTML,而是能够传输图片、音频视频等任意数据,这些都是源于它的“灵活可扩展”的特点。

可靠传输

因为 HTTP 协议是基于 TCP/IP 的,而 TCP 本身是一个“可靠”的传输协议,所以 HTTP 自然也就继承了这个特性,能够在请求方和应答方之间“可靠”地传输数据。具体做法与 TCP/UDP 差不多,都是对实际传输的数据(entity)做了一层包装,加上一个头,然后调用 Socket API,通过 TCP/IP 协议栈发送或者接收。

应用层协议

HTTP 凭借着可携带任意头字段和实体数据的报文结构,以及连接控制、缓存代理等方便易用的特性,一出现就“技压群雄”,迅速成为了应用层里的“明星”协议。只要不太苛求性能,HTTP 几乎可以传递一切东西,满足各种需求,称得上是一个“万能”的协议。

请求-应答

请求 - 应答模式是 HTTP 协议最根本的通信模型,请求方先发起连接和请求,是主动的,而应答方只有在收到请求后才能答复,是被动的,如果没有请求时不会有任何动作。

传统的 C/S(Client/Server)系统架构,请求方作为客户端、应答方作为服务器,随着互联网的发展,用轻量级的浏览器代替笨重的客户端应用,就出现了 B/S(Browser/Server)架构。

请求 - 应答”模式则加剧了 HTTP 的性能问题,这就是著名的“队头阻塞”(Head-of-line blocking),当顺序发送的请求序列中的一个请求因为某种原因被阻塞时,在后面排队的所有请求也一并被阻塞,会导致客户端迟迟收不到数据。

后面的请求无法发送

无状态

TCP 协议是有状态的,一开始处于 CLOSED 状态,连接成功后是 ESTABLISHED 状态,断开连接后是 FIN-WAIT 状态,最后又是 CLOSED 状态。这些“状态”就需要 TCP 在内部用一些数据结构去维护,可以简单地想象成是个标志量,标记当前所处的状态,例如 0 是 CLOSED,2 是 ESTABLISHED 等等。

在整个 HTTP 协议里没有规定任何的“状态”,客户端和服务器永远是处在一种“无知”的状态。建立连接前两者互不知情,每次收发的报文也都是互相独立的,没有任何的联系。收发报文也不会对客户端或服务器产生任何影响,连接后也不会要求保存任何信息。

1.4.2 优缺点

明文

HTTP 协议里有一把优缺点一体的“双刃剑”,就是明文传输

“明文”意思就是协议里的报文(准确地说是 header 部分)不使用二进制数据,而是用简单可阅读的文本形式。它的优点显而易见,不需要借助任何外部工具,用浏览器、Wireshark 或者 tcpdump 抓包后,直接用肉眼就可以很容易地查看或者修改,为我们的开发调试工作带来极大的便利。

缺点也是一样显而易见,HTTP 报文的所有信息都会暴露在“光天化日之下”,在漫长的传输链路的每一个环节上都毫无隐私可言。

不安全

HTTP 没有提供有效的手段来确认通信双方的真实身份。虽然协议里有一个基本的认证机制,但因为刚才所说的明文传输缺点,这个机制几乎可以说是“纸糊的”,非常容易被攻破。

为了解决 HTTP 不安全的缺点,所以就出现了 HTTPS。

1.4.3 HTTP 连接

  1. 对网址进行 DNS 域名解析,得到对应的 IP 地址;
  2. 根据这个 IP,找到对应的服务器,发起 TCP 的三次握手;
  3. 建立 TCP 连接后发起 HTTP 请求;
  4. 服务器响应 HTTP 请求,浏览器得到 html 代码;
  5. 浏览器解析 html 代码,并请求 html 代码中的资源(如 js、css、图片等)(先得到 html 代码,才能去找这些资源);
  6. 浏览器对页面进行渲染呈现给用户;
  7. 服务器关闭关闭 TCP 连接。

1.4.4 长连接 & 短连接

长连接:长连接,指在一个连接上可以连续发送多个数据包,在连接保持期间,如果没有数据包发送,需要双方发链路检测包。

短连接:短连接(short connnection)是相对于长连接而言的概念,指的是在数据传送过程中,只在需要发送数据时,才去建立一个连接,数据发送完成后,则断开此连接,即每次连接只完成一项业务的发送。

设置长连接时,字段中的 Connection 会设置 keep-alive。

连接的长短是通过协议来规定和实现的。而轮询的长短,是服务器通过编程的方式手动挂起请求来实现的。

1.5 转发 & 重定向

转发是服务器行为。服务器直接向目标地址访问 URL,将相应内容读取之后发给浏览器,用户浏览器地址栏 URL 不变,转发页面和转发到的页面可以共享 request 里面的数据。

客户浏览器发送 HTTP 请求,Web 服务器接受此请求,调用内部的一个方法在容器内部完成请求处理和转发动作,将目标资源发送给客户;在这里,转发的路径必须是同一个 Web 容器下的 URL,其不能转向到其他的 Web 路径上去,中间传递的是自己的容器内的 request。在客户浏览器路径栏显示的仍然是其第一次访问的路径,也就是说客户是感觉不到服务器做了转发的。转发行为是浏览器只做了一次访问请求。

重定向是利用服务器返回的状态码来实现的,如果服务器返回 301 或者 302,浏览器收到新的消息后自动跳转到新的网址重新请求资源。用户的地址栏 url 会发生改变,而且不能共享数据。

这要用到两个字段:响应头字段 Set-Cookie 和请求头字段 Cookie。Cookie 和 Session 并不是对立的关系,更多的是相辅相成,术业有专攻。

1.6.1 区别

Cookie 实际上是一小段的文本信息。客户端请求服务器,如果服务器需要记录该用户的状态,就使用 response 向客户端浏览器颁发一个 Cookie。客户端浏览器会把 Cookie 保存起来。当浏览器再次请求该网站时,浏览器就会把请求地址和 Cookie 一同给服务器。服务器检查该 Cookie,从而判断用户的状态。服务器还可以根据需要修改 Cookie 的内容。

Session 是另一种记录客户状态的机制。不同的是 Cookie 保存在客户端浏览器中,而 Session 保存在服务器上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上,这就是 Session。客户端浏览器再次访问时只需要从该 Session 中查找该客户的状态就可以了。

如果说 Cookie 机制是通过检查客户身上的“通信证”,那么 Session 机制就是通过检查服务器上的“客户明细表”来确认客户身份。对于 Cookie 而言,将重要账号密码放在其中进行身份验证是危险的,因为这是明文传输。

在浏览器禁用 Cookie 的情况下,一般会直接把 Session ID 写入 URL 中。

Cache 是一种缓存数据的方式,它可以将频繁访问的数据存储在内存中,以提高访问速度。Cache 可以存储一些不经常更新的数据,例如静态文件、数据库查询结果等。 Cache 的优点是访问速度快,可以大大减少对数据库和其他数据源的访问次数,缺点是需要开发人员进行维护,以避免缓存数据的过期和失效。同时,缓存数据的大小也需要控制,避免占用过多的内存资源。

1.6.2 安全性设置

属性“HttpOnly”会告诉浏览器,此 Cookie 只能通过浏览器 HTTP 协议传输,禁止其他方式访问,浏览器的 JS 引擎就会禁用 document.Cookie 等一切相关的 API,脚本攻击也就无从谈起了(防止 XSS 攻击)。

另一个属性“SameSite”可以防范“跨站请求伪造”(XSRF)攻击,设置成“SameSite=Strict”可以严格限定 Cookie 不能随着跳转链接跨站发送,而“SameSite=Lax”则略宽松一点,允许 GET/HEAD 等安全方法,但禁止 POST 跨站发送。

还有一个属性叫“Secure”,表示这个 Cookie 仅能用 HTTPS 协议加密传输,明文的 HTTP 协议会禁止发送。但 Cookie 本身不是加密的,浏览器里还是以明文的形式存在。

1.6.3 大量 session

如果有几千个 session,怎么提高效率?

当一个应用程序有成千上万个会话时,会影响应用程序的性能。为了提高效率,可以采取以下措施:

  • Session 持久化:将 session 信息存储在持久化存储中,如数据库、文件系统或 NoSQL 存储中,这样可以避免将所有 session 信息存储在内存中,从而减少内存的使用量;
  • Session 复制:将 session 信息从一台服务器复制到另一台服务器上,这样可以实现负载均衡,并将会话信息在多个服务器之间共享;
  • Session 失效策略:设置合理的 session 失效策略,例如根据用户活动时间、最大不活动时间等来决定 session 的失效时间,可以减少无用的 session 信息;
  • 集群:使用集群环境来分散请求和负载,这样可以使应用程序在多个服务器上运行,从而提高应用程序的性能和可扩展性。

1.7 HTTPS

一般 http 中存在如下问题:

  • 请求信息明文传输,容易被窃听截取;
  • 数据的完整性未校验,容易被篡改;
  • 没有验证对方身份,存在冒充危险。

HTTPS 通过信息加密、校验机制、身份证书,解决 HTTP 出现的上述问题。

HTTPS 把 HTTP 下层的传输协议由 TCP/IP 换成了 SSL/TLS,由“HTTP over TCP/IP”变成了“HTTP over SSL/TLS”,让 HTTP 运行在了安全的 SSL/TLS 协议上(通过将服务器的公钥放在 CA 证书中,这样就能证明公钥的可信度)。

HTTP 和 HTTPS:

  1. 端口:HTTP 的 URL 由 “http://” 起始且默认使用端口 80,而 HTTPS 的 URL 由 “https://” 起始且默认使用端口 443。
  2. 安全性和资源消耗:HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。 HTTPS 是运行在 SSL 之上的 HTTP 协议,SSL 运行在 TCP 之上。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。所以说,HTTP 安全性没有 HTTPS 高,但是 HTTPS 比 HTTP 耗费更多服务器资源。

SSL 全称为 Secure Sockets Layer 即安全套接层,其继任为 TLS(TLSTransport Layer Security)传输层安全协议,均用于在传输层为数据通讯提供安全支持。可以将 HTTPS 协议简单理解为 HTTP 协议+TLS/SSL。

HTTPS 的缺点

  • HTTPS 协议多次握手,导致页面的加载时间延长近 50%;
  • HTTPS 连接缓存不如 HTTP 高效,会增加数据开销和功耗;
  • 申请 SSL 证书需要钱,功能越强大的证书费用越高。
  • SSL 涉及到的安全算法会消耗 CPU 资源,对服务器资源消耗较大。

1.7.1 HTTPS 连接过程

  1. 浏览器将支持的加密算法信息发给服务器;
  2. 服务器选择一套浏览器支持的加密算法,以证书的形式回发给浏览器;
  3. 客户端(SSL/TLS)解析证书验证证书合法性,生成对称加密的密钥,我们将该密钥称之为 client key,即客户端密钥,用服务器的公钥对客户端密钥进行非对称加密;
  4. 客户端会发起 HTTPS 中的第二个 HTTP 请求,将加密之后的客户端对称密钥发送给服务器;
  5. 服务器接收到客户端发来的密文之后,会用自己的私钥对其进行非对称解密,解密之后的明文就是客户端密钥,然后用客户端密钥对数据进行对称加密,这样数据就变成了密文;
  6. 服务器将加密后的密文发送给客户端;
  7. 客户端收到服务器发送来的密文,用客户端密钥对其进行对称解密,得到服务器发送的数据。这样 HTTPS 中的第二个 HTTP 请求结束,整个 HTTPS 传输完成。

HTTPS 数据传输流程

1.7.2 数字证书和 CA 机构

服务端的证书都是由 CA (Certificate Authority,证书认证机构)签名的,一个数字证书通常包含了:

  • 公钥;
  • 持有者信息;
  • 证书认证机构(CA)的信息;
  • CA 对这份文件的数字签名及使用的算法;
  • 证书有效期;
  • 还有一些其他额外信息。

数字证书签发和验证流程

CA 签发证书的过程,如上图左边部分:

  • 首先 CA 把持有者的公钥、用途、颁发者、有效时间等信息打成一个包,然后对这些信息进行 Hash 计算,得到一个 Hash 值;
  • 然后 CA 会使用自己的私钥将该 Hash 值加密,生成 Certificate Signature,也就是 CA 对证书做了签名;
  • 最后将 Certificate Signature 添加在文件证书上,形成数字证书。

客户端校验服务端的数字证书的过程,如上图右边部分:

  • 首先客户端会使用同样的 Hash 算法获取该证书的 Hash 值 H1;
  • 通常浏览器和操作系统中集成了 CA 的公钥信息,浏览器收到证书后可以使用 CA 的公钥解密 Certificate Signature 内容,得到一个 Hash 值 H2 ;
  • 最后比较 H1 和 H2,如果值相同,则为可信赖的证书,否则则认为证书不可信。

1.7.3 TLS 握手

HTTPS 是应用层协议,需要先完成 TCP 连接建立,然后走 TLS 握手过程后,才能建立通信安全的连接。

RSA 握手过程

多个记录可以组合成一个 TCP 包发送,所以通常经过「四个消息」就可以完成 TLS 握手,也就是需要 2个 RTT 的时延,然后就可以在安全的通信环境里发送 HTTP 报文,实现 HTTPS 协议。

RSA 握手过程

传统的 TLS 握手基本都是使用 RSA 算法来实现密钥交换的,在将 TLS 证书部署服务端时,证书文件其实就是服务端的公钥,会在 TLS 握手阶段传递给客户端,而服务端的私钥则一直留在服务端。

在 RSA 密钥协商算法中,客户端会生成随机密钥,并使用服务端的公钥加密后再传给服务端。根据非对称加密算法,公钥加密的消息仅能通过私钥解密,这样服务端解密后,双方就得到了相同的密钥,再用它加密应用消息。

RSA 握手流程

第一次握手

客户端首先会发一个「Client Hello」消息,消息里面有客户端使用的 TLS 版本号、支持的密码套件列表,以及生成的随机数(Client Random),这个随机数会被服务端保留,它是生成对称加密密钥的材料之一。

第二次握手

当服务端收到客户端的「Client Hello」消息后,会确认 TLS 版本号是否支持,和从密码套件列表中选择一个密码套件,以及生成随机数(Server Random)。接着,返回「Server Hello」消息,消息里面有服务器确认的 TLS 版本号,也给出了随机数(Server Random),然后从客户端的密码套件列表选择了一个合适的密码套件(基本的形式是「密钥交换算法 + 签名算法 + 对称加密算法 + 摘要算法」)。

Server Hello 消息

然后,服务端为了证明自己的身份,会发送「Server Certificate」给客户端,这个消息里含有数字证书。随后,服务端发了「Server Hello Done」消息,本次握手结束。

第三次握手

客户端验证完证书后,认为可信则继续往下走。接着,客户端就会生成一个新的随机数 (*pre-master*),用服务器的 RSA 公钥加密该随机数,通过「Client Key Exchange」消息传给服务端。服务端收到后,用 RSA 私钥解密,得到客户端发来的随机数 (pre-master)。

至此,客户端和服务端双方都共享了三个随机数,分别是 Client Random、Server Random、pre-master。于是,双方根据已经得到的三个随机数,生成会话密钥(Master Secret),它是对称密钥,用于对后续的 HTTP 请求/响应的数据加解密。

生成完「会话密钥」后,然后客户端发一个「Change Cipher Spec」(「Change Cipher Spec」之前传输的 TLS 握手数据都是明文,之后都是对称密钥加密的密文),告诉服务端开始使用加密方式发送消息。然后,客户端再发一个「Encrypted Handshake Message(Finishd)」消息,把之前所有发送的数据做个摘要,再用会话密钥(master secret)加密一下,让服务器做个验证,验证加密通信「是否可用」和「之前握手信息是否有被中途篡改过」。

第四次握手

服务器也是同样的操作,发「Change Cipher Spec」和「Encrypted Handshake Message」消息。如果双方都验证加密和解密没问题,那么握手正式完成。

RSA 算法缺陷

因为客户端传递随机数(用于生成对称加密密钥的条件之一)给服务端时使用的是公钥加密的,服务端收到后,会用私钥解密得到随机数。所以一旦服务端的私钥泄漏了,过去被第三方截获的所有 TLS 通讯密文都会被破解。

为了解决这个问题,后面就出现了 ECDHE 密钥协商算。

1.7.4 ECDHE 握手

HTTPS 常用的密钥交换算法有两种,分别是 RSA 和 ECDHE 算法。其中,RSA 是比较传统的密钥交换算法,它不具备前向安全的性质,因此现在很少服务器使用的。而 ECDHE 算法具有前向安全,所以被广泛使用。

DH 算法

当模数 p 是一个很大的质数,即使知道底数 a 和真数 b ,在现有的计算机的计算水平是几乎无法算出离散对数的,这就是 DH 算法的数学基础。

离散对数概念

DHE 算法

根据私钥生成的方式,DH 算法分为两种实现:

  • static DH 算法,这个是已经被废弃了;
  • DHE 算法,现在常用的。

static DH 算法里有一方的私钥是静态的,也就说每次密钥协商的时候有一方的私钥都是一样的,一般是服务器方固定,即 a 不变,客户端的私钥则是随机生成的。static DH 算法不具备前向安全性

DHE 算法,E 全称是 ephemeral(临时性的)。每个通信过程的私钥都是没有任何关系的,都是独立的,这样就保证了「前向安全」

ECDHE 算法

DHE 算法由于计算性能不佳,因为需要做大量的乘法,为了提升 DHE 算法的性能,所以就出现了现在广泛用于密钥交换算法 —— ECDHE 算法。ECDHE 算法是在 DHE 算法的基础上利用了 ECC 椭圆曲线特性,可以用更少的计算量计算出公钥,以及最终的会话密钥。

这个过程中,双方的私钥都是随机、临时生成的,都是不公开的,即使根据公开的信息(椭圆曲线、公钥、基点 G)也是很难计算出椭圆曲线上的离散对数(私钥)。

握手过程

ECDHE 握手过程

使用了 ECDHE,在 TLS 第四次握手前,客户端就已经发送了加密的 HTTP 数据ECDHE 相比 RSA 握手过程省去了一个消息往返的时间。在还没连接完全建立前,就发送了应用数据,这样便提高了传输的效率。

1.8 HTTP 演变

由于 HTTP 的可扩展性,它自己在不断演变,为了解决各种性能或者工程上的问题。

影响一个 HTTP 网络请求的因素主要有两个:带宽和延迟

带宽随着光纤等技术的发展,不再成为网络速度的瓶颈。目前需要对延迟进行优化。

延迟主要有下面三个方面:

  • 浏览器阻塞(HOL blocking):浏览器会因为一些原因阻塞请求。浏览器对于同一个域名,同时只能有 4个连接(这个根据浏览器内核不同可能会有所差异),超过浏览器最大连接数限制,后续请求就会被阻塞。
  • DNS 查询(DNS Lookup):浏览器需要知道目标服务器的 IP 才能建立连接。将域名解析为 IP 的这个系统就是 DNS。这个通常可以利用 DNS 缓存结果来达到减少这个时间的目的。
  • 建立连接(Initial connection):HTTP 是基于 TCP 协议的,浏览器最快也要在第三次握手时才能捎带 HTTP 请求报文,达到真正的建立连接,但是这些连接无法复用会导致每次请求都经历三次握手和慢启动。三次握手在高延迟的场景下影响较明显,慢启动则对文件类大请求影响较大。

1.8.1 HTTP 1.1

长连接: 在 HTTP 1.0 中,默认使用的是短连接。HTTP 1.1 起,默认使用长连接Connection:keep-alive。HTTP 1.1 的持续连接,有非流水线方式和流水线方式 。流水线方式,是客户在收到 HTTP 的响应报文之前,就能接着发送新的请求报文;与之相对应的非流水线方式,是客户在收到前一个响应后才能发起下一个请求;

流水线的设计一定程度解决了队头阻塞的问题

pipelining 也存在不少缺陷:

  • 只有幂等的请求比如 GET、HEAD 才能使用 pipelining ,非幂等请求比如 POST 则不能使用,因为请求之间可能存在先后依赖关系;
  • 其实队头阻塞问题并没有完全解决,因为服务器返回的响应还是要依次返回,也就是返回的请求时 FIFO - 先发先回;
  • 绝大多数 HTTP 代理服务器不支持 pipelining;
  • 和不支持 pipelining 的老服务器协商有问题。。

错误响应码:在 HTTP 1.1 中,新增了 24个错误状态响应码,如 409(Conflict):表示请求的资源与资源的当前状态发生冲突;410(Gone):表示服务器上的某个资源被永久性的删除;

缓存处理:HTTP 1.0 中,主要使用 header 头里的 If-Modified-SinceExpires 来做为缓存判断的标准;HTTP 1.1 则引入了更多的缓存控制策略,如 Entity tagIf-Unmodified-SinceIf-MatchIf-None-Match 等;

带宽优化及网络连接的使用:HTTP 1.0 中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象传送了过来,并且不支持断点续传功能;HTTP 1.1 中,则在请求头引入了 range 头域,它允许只请求资源的某个部分,即返回码是 206(Partial Content),这样方便开发者自由的选择,以便于充分利用带宽和连接。

1.8.2 HTTP/2

HTTP 2.0 自从设计到诞生以来,发生了很多变化,是基于 HTTPS 的。

改进
二进制格式

HTTP 1.x 的诞生使用的是明文协议,它的格式主要由三部分构成:请求行(request line)、请求头(header) 和报文体(body),要识别这三部分必须要做协议解析,而协议解析是基于文本的,基于文本的解析存在多样性的缺陷,而二进制格式只能识别 0 和 1,比较固定就,基于这种考量,HTTP 2.0 决定采用二进制格式,实现方便而且健壮性强。

HTTP 1.x 和 HTTP 2.0 报文格式

实际上 HTTP 2.0 并没有改变 HTTP 1.x 的语义,它只是在 HTTP 1.x 的基础上封装了一层。

HTTP 1.x 中的请求行、请求头被 HTTP 2.0 封装成为了 HEADERS Frame,而 HTTP 1.x 中的报文体被 HTTP 2.0 封装成为了 Data Frame。

HTTP 2.0 封装报文

多路复用

多路复用允许同时通过单一的 HTTP/2 连接发起多重的请求-响应消息,实现多流并行而并不依赖多个 TCP 连接,HTTP/2 把 HTTP 协议通信的基本单位缩小为一个一个的帧,这些帧对应着逻辑流中的消息,并行地在同一个 TCP 连接上双向交换消息。

HTTP/2 基于二进制分帧层,HTTP/2 可以在共享 TCP 连接的基础上同时发送请求和响应。HTTP 消息被分解为独立的帧,而不破坏消息本身的语义交错发出去,在另一端根据流标识符和首部将他们重新组装起来。通过多路复用技术,可以避免 HTTP 旧版本的消息头阻塞问题,极大提高传输性能。

HTTP 1.1 和 HTTP 2.0 对比

针对不同的 HTTP 请求用独一无二的 Stream ID 来区分,接收端可以通过 Stream ID 有序组装成 HTTP 消息,不同 Stream 的帧是可以乱序发送的,因此可以并发不同的 Stream ,也就是 HTTP/2 可以并行交错地发送请求和响应

多个 Stream 服用在一条 TCP 连接

头部压缩

HTTP 每次请求或响应都会携带首部信息用于描述资源属性。HTTP/1.1 使用文本的形式传输消息头,消息头中携带 Cookie 每次都需要重复传输几百到几千的字节,这十分占用资源。

HTTP/2 使用 HPACK 算法来压缩头字段,这种压缩格式对传输的头字段进行编码,减少了头字段的大小。同时,在两端维护了索引表,用于记录出现过的头字段,后面在传输过程中就可以传输已经记录过的头字段的索引号,对端收到数据后就可以通过索引号找到对应的值。

优先级排序

将 HTTP 消息分解为很多独立的帧之后就可以复用多个数据流中的帧,客户端和服务器交错发送和传输这些帧的顺序就成为关键的性能决定因素。HTTP/2 允许每个数据流都有一个关联的权重和依赖关系,数据流依赖关系和权重的组合明确表达了资源优先级,这是一种用于提升浏览性能的关键功能。HTTP/2 协议还允许客户端随时更新这些优先级,可以根据用户互动和其他信号更改依赖关系和重新分配权重,这进一步优化了浏览器性能。

队头阻塞

HTTP/2 是基于 TCP 协议来传输数据的,TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且连续的,这样内核才会将缓冲区里的数据返回给 HTTP 应用,那么当「前 1 个字节数据」没有到达时,后收到的字节数据只能存放在内核缓冲区里,只有等到这 1 个字节数据到达时,HTTP/2 应用层才能从内核中拿到数据,这就是 HTTP/2 队头阻塞问题。

Segment 丢失问题

一旦发生了丢包现象,就会触发 TCP 的重传机制,这样在一个 TCP 连接中的所有的 HTTP 请求都必须等待这个丢了的包被重传回来

服务器主动推送资源

HTTP/1.1 不支持服务器主动推送资源给客户端,都是由客户端向服务器发起请求后,才能获取到服务器响应的资源。在 HTTP/2 中,客户端在访问 HTML 时,服务器可以直接主动推送 CSS 文件,减少了消息传递的次数。

客户端发起的请求,必须使用的是奇数号 Stream,服务器主动的推送,使用的是偶数号 Stream。服务器在推送资源时,会通过 PUSH_PROMISE 帧传输 HTTP 头部,并通过帧中的 Promised Stream ID 字段告知客户端,接下来会在哪个偶数号 Stream 中发送包体。

1.8.3 HTTP/3

  • HTTP/1.1 中的管道( pipeline)虽然解决了请求的队头阻塞,但是没有解决响应的队头阻塞,因为服务端需要按顺序响应收到的请求,如果服务端处理某个请求消耗的时间比较长,那么只能等响应完这个请求后, 才能处理下一个请求,这属于 HTTP 层队头阻塞;
  • HTTP/2 虽然通过多个请求复用一个 TCP 连接解决了 HTTP 的队头阻塞 ,但是一旦发生丢包,就会阻塞住所有的 HTTP 请求,这属于 TCP 层队头阻塞。

HTTP/3 把 HTTP 下层的 TCP 协议改成了 UDP。基于 UDP 的 QUIC 协议 可以实现类似 TCP 的可靠性传输。

HTTP/1 ~ HTTP/3

QUIC 有以下 3 个特点。

  • 无队头阻塞;
  • 更快的连接建立;
  • 连接迁移。
无队头阻塞

QUIC 协议也有类似 HTTP/2 Stream 与多路复用的概念,也是可以在同一条连接上并发传输多个 Stream,Stream 可以认为就是一条 HTTP 请求。

QUIC 有自己的一套机制可以保证传输的可靠性的。当某个流发生丢包时,只会阻塞这个流,其他流不会受到影响,因此不存在队头阻塞问题。这与 HTTP/2 不同,HTTP/2 只要某个流中的数据包丢失了,其他流也会因此受影响。

更快的连接建立

对于 HTTP/1 和 HTTP/2 协议,TCP 和 TLS 是分层的,分别属于内核实现的传输层、openssl 库实现的表示层,因此它们难以合并在一起,需要分批次来握手,先 TCP 握手,再 TLS 握手。

QUIC 内部包含了 TLS,它在自己的帧会携带 TLS 里的“记录”,再加上 QUIC 使用的是 TLS/1.3,因此仅需 1 个 RTT 就可以「同时」完成建立连接与密钥协商。

TCP HTTPS(TLS/1.3) 和 QUIC HTTPS

在第二次连接的时候,应用数据包可以和 QUIC 握手信息(连接信息 + TLS 信息)一起发送,达到 0-RTT 的效果。

HTTP/3 重新连接

连接迁移

当移动设备的网络从 4G 切换到 WIFI 时,意味着 IP 地址变化了,那么就必须要断开连接,然后重新建立连接

TCP 四元组

QUIC 协议没有用四元组的方式来“绑定”连接,而是通过连接 ID来标记通信的两个端点,客户端和服务器可以各自选择一组 ID 来标记自己,因此即使移动设备的网络变化后,导致 IP 地址变化了,只要仍保有上下文信息(比如连接 ID、TLS 密钥等),就可以“无缝”地复用原连接,消除重连的成本,没有丝毫卡顿感,达到了连接迁移的功能。

QUIC 是新协议,对于很多网络设备,根本不知道什么是 QUIC,只会当做 UDP,这样会出现新的问题,因为有的网络设备是会丢掉 UDP 包的,而 QUIC 是基于UDP 实现的,那么如果网络设备无法识别这个是 QUIC 包,那么就会当作 UDP包,然后被丢弃。

1.9 缓存技术

避免发送 HTTP 请求的方法就是通过缓存技术,HTTP 缓存有两种实现方式,分别是强制缓存和协商缓存

1.9.1 强制缓存

强缓存指的是只要浏览器判断缓存没有过期,则直接使用浏览器的本地缓存,决定是否使用缓存的主动性在于浏览器这边。

强缓存是利用下面这两个 HTTP 响应头部(Response Header)字段实现的,它们都用来表示资源在客户端缓存的有效期:

  • Cache-Control, 是一个相对时间;
  • Expires,是一个绝对时间;

如果 HTTP 响应头部同时有 Cache-Control 和 Expires 字段的话,Cache-Control的优先级高于 Expires

Cache-control 选项更多一些,设置更加精细,所以建议使用 Cache-Control 来实现强缓存。具体的实现流程如下:

  • 当浏览器第一次请求访问服务器资源时,服务器会在返回这个资源的同时,在 Response 头部加上 Cache-Control,Cache-Control 中设置了过期时间大小;
  • 浏览器再次请求访问服务器中的该资源时,会先通过请求资源的时间与 Cache-Control 中设置的过期时间大小,来计算出该资源是否过期,如果没有,则使用该缓存,否则重新请求服务器;
  • 服务器再次收到请求后,会再次更新 Response 头部的 Cache-Control。

1.9.2 协商缓存

协商缓存就是与服务端协商之后,通过协商结果来判断是否使用本地缓存

协商缓存过程

协商缓存可以基于两种头部来实现。

第一种:请求头部中的 If-Modified-Since 字段与响应头部中的 Last-Modified 字段实现,这两个字段的意思是:

  • 响应头部中的 Last-Modified:标示这个响应资源的最后修改时间;
  • 请求头部中的 If-Modified-Since:当资源过期,发现响应头中具有 Last-Modified 声明,则再次发起请求的时候带上 Last-Modified 的时间,服务器收到请求后发现有 If-Modified-Since 则与被请求资源的最后修改时间进行对比(Last-Modified),如果最后修改时间较大,说明资源又被改过,则返回最新资源,HTTP 200 OK;如果最后修改时间较小,说明资源无新修改,响应 HTTP 304 走缓存。

第二种:请求头部中的 If-None-Match 字段与响应头部中的 ETag 字段,这两个字段的意思是:

  • 响应头部中 Etag:唯一标识响应资源;
  • 请求头部中的 If-None-Match:当资源过期时,浏览器发现响应头里有 Etag,则再次向服务器发起请求时,会将请求头If-None-Match 值设置为 Etag 的值。服务器收到请求后进行比对,如果资源没有变化返回 304,如果资源变化了返回 200。

第一种实现方式是基于时间实现的,第二种实现方式是基于一个唯一标识实现的,相对来说后者可以更加准确地判断文件内容是否被修改,避免由于时间篡改导致的不可靠问题。

如果在第一次请求资源的时候,服务端返回的 HTTP 响应头部同时有 Etag 和 Last-Modified 字段,那么客户端再下一次请求的时候,如果带上了 ETag 和 Last-Modified 字段信息给服务端,这时 Etag 的优先级更高,也就是服务端先会判断 Etag 是否变化了,如果 Etag 有变化就不用在判断 Last-Modified 了,如果 Etag 没有变化,然后再看 Last-Modified。

协商缓存这两个字段都需要配合强制缓存中 Cache-control 字段来使用,只有在未能命中强制缓存的时候,才能发起带有协商缓存字段的请求

ETag 主要能解决 Last-Modified 几个比较难以解决的问题:

  1. 在没有修改文件内容情况下文件的最后修改时间可能也会改变,这会导致客户端认为这文件被改动了,从而重新请求;
  2. 可能有些文件是在秒级以内修改的,If-Modified-Since 能检查到的粒度是秒级的,使用 Etag 就能够保证这种需求下客户端在 1 秒内能刷新多次;
  3. 有些服务器不能精确获取文件的最后修改时间。

1.9.3 工作流程

当使用 ETag 字段实现的协商缓存的过程:

  • 当浏览器第一次请求访问服务器资源时,服务器会在返回这个资源的同时,在 Response 头部加上 ETag 唯一标识,这个唯一标识的值是根据当前请求的资源生成的;
  • 当浏览器再次请求访问服务器中的该资源时,首先会先检查强制缓存是否过期:
    • 如果没有过期,则直接使用本地缓存;
    • 如果缓存过期了,会在 Request 头部加上 If-None-Match 字段,该字段的值就是 ETag 唯一标识;
  • 服务器再次收到请求后,会根据请求中的 If-None-Match 值与当前请求的资源生成的唯一标识进行比较:
    • 如果值相等,则返回 304 Not Modified,不会返回资源
    • 如果不相等,则返回 200 状态码和返回资源,并在 Response 头部加上新的 ETag 唯一标识;
  • 如果浏览器收到 304 的请求响应状态码,则会从本地缓存中加载资源,否则更新资源。

强制缓存和协商缓存的工作流程

2. TCP/IP

传输控制协议(Transmission Control Protocol,TCP)是一种面向连接的、可靠的、基于字节流的传输层通信协议。在简化的计算机网络 OSI 模型模型中,它完成第四层传输层所指定的功能。

TCP 头格式

2.1 TCP 建立连接

为什么 TCP 可靠?

TCP 有三次握手建立连接,四次挥手关闭连接的机制。TCP给每个数据包一个序号,保证了按序接收和重组。除此之外还有滑动窗口和拥塞控制算法。最最关键的是还保留超时重传的机制。对于每份报文也存在校验,保证每份报文可靠性。

TCP 使用超时重传来实现可靠传输:如果一个已经发送的报文段在超时时间内没有收到确认,那么就重传这个报文段。

  1. 应用数据被分割成 TCP 认为最适合发送的数据块;
  2. TCP 给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层;
  3. 校验和:TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段;
  4. TCP 的接收端会丢弃重复的数据;
  5. 流量控制:TCP 连接的每一方都有固定大小的缓冲空间,TCP 的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低 发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议。(TCP 利用滑动窗口实现流量控制);
  6. 拥塞控制:当网络拥塞时,减少数据的发送;
  7. ARQ 协议:也是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组;
  8. 超时重传:当 TCP 发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。

2.1.1 三次握手

在 TCP 连接中,SYN 为同步标志位,ACK 为确认标志位,seq 为序列号,ack 为确认号。

  1. A 向 B 发送连接请求报文,SYN = 1,ACK = 0,选择一个初始的序号 seq = x(TCP 规定,SYN 报文段(SYN=1 的报文段)不能携带数据,但需要消耗掉一个序号。如果 SYN 报文段携带数据,那么在网络传输中可能会丢失或乱序,导致数据无法正确接收);
  2. B 收到连接请求报文,如果同意建立连接,则向 A 发送连接确认报文,SYN = 1,ACK = 1,确认号为 ack = x + 1,同时也选择一个初始的序号 seq = y(不能携带数据,但是同样要消耗一个序号);
  3. A 收到 B 的连接确认报文后,还要向 B 发出确认,确认号为 ack = y + 1,序号为 seq = x + 1。B 收到 A 的确认后,连接建立(TCP 规定,ACK 报文段可以携带数据,但是如果不携带数据则不消耗序号)。

TCP 建立连接过程

在 TCP 三次握手过程中,第三次握手时客户端发送的 ACK 报文可以携带数据。也就是说,在客户端发送 ACK 报文的同时,也可以发送数据。这是因为在第二次握手时,服务器已经向客户端确认了它的接收能力和发送能力。因此,在第三次握手时,客户端可以开始传输数据。

为什么需要最后确认

为什么需要三次握手,不能是两次或者是四次?

如果不存在最后一次握手就进行连接,可能会发生各种情况(简单来说,就是客户端无法确认服务端是否收到请求,也无法确认服务端是否同意连接请求):

  • 服务端发送的确认的信息客户端根本没有收到,从而建立了无效连接;
  • 客户端发送的报文等待时间过长,但是没有失效,重新发送了一次。这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。

TCP 还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为 2 小时,若 2 小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔 75 秒发送一次。若一连发送 10 个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。

2.1.2 四次挥手

  1. A 发送连接释放报文,FIN = 1。设置一个序列号 seq = u;
  2. B 收到之后发出确认,它发回一个 ACK 确认报文,确认序号为收到的序号加 1,ack = u + 1,设置序列号为 seq = v。此时 TCP 属于半关闭状态,B 能向 A 发送数据但是 A 不能向 B 发送数据;
  3. 当 B 不再需要连接时,发送连接释放报文,FIN = 1,ack = u + 1,设置 seq = w;
  4. A 收到后发出 ACK 确认报文,并将确认序号设置为收到序号加 1,seq = w + 1,进入 TIME-WAIT 状态,等待 2MSL(最大报文存活时间)后释放连接。

四次挥手过程

CLOSE-WAIT 状态问题

客户端发送了 FIN 连接释放报文之后,服务器收到了这个报文,就进入了 CLOSE-WAIT 状态。这个状态是为了让服务器端发送还未传送完毕的数据,传送完毕之后,服务器会发送 FIN 连接释放报文。

TIME-WAIT 状态问题

客户端接收到服务器端的 FIN 报文后进入此状态,此时并不是直接进入 CLOSED 状态,还需要等待一个时间计时器设置的时间 2MSL。这么做有两个理由:

  • 确保最后一个确认报文能够到达。如果 B 没收到 A 发送来的确认报文,那么就会重新发送连接释放请求报文,A 等待一段时间就是为了处理这种情况的发生;

  • 等待一段时间是为了让本连接持续时间内所产生的所有报文都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文。

为什么挥手需要四次

因为握手的时候并没有数据传输,所以服务端的 SYN 和 ACK 报文可以一起发送,但是挥手的时候有数据在传输,所以 ACK 和 FIN 报文不能同时发送,需要分两步,所以会比握手多一步。

这样做的目的是为了确保对方收到了最后一个 ACK 报文。如果对方没有收到这个 ACK 报文,它会重传 FIN 报文。此时,主动关闭连接的一方可以再次发送 ACK 应答报文。服务器收到客户端的 FIN 报文时,内核会马上回一个 ACK 应答报文,但是服务端应用程序可能还有数据要发送,所以并不能马上发送 FIN 报文,而是将发送 FIN 报文的控制权交给服务端应用程序

  • 如果服务端应用程序有数据要发送的话,就发完数据后,才调用关闭连接的函数;
  • 如果服务端应用程序没有数据要发送的话,可以直接调用关闭连接的函数。
为什么要等待等待 2MSL

因为保证服务端接收到了 ACK 报文,因为网络是复杂了,很有可能 ACK 报文丢失了,如果服务端没接收到 ACK 报文的话,会重新发送 FIN 报文,只有当客户端等待了 2MSL 都没有收到重发的 FIN 报文时就表示服务端是正常收到了 ACK 报文,那么这个时候客户端就可以关闭了。

等待 2 倍 MSL 的时间还有另一个目的:处理延迟的重复报文。这主要是为了避免前后两个使用相同四元组(源 IP 地址、目标 IP 地址、源端口号、目标端口号)的连接中,前一个连接的报文干扰后一个连接。

2.2 TCP 粘包

TCP 作为面向字节流的协议,不存在“粘包问题”。TCP 粘包问题是指在基于 TCP 协议的通信中,由于 TCP 是面向字节流的,没有消息边界的概念,导致接收方无法准确地区分出发送方发送的多个数据包,从而将多个数据包合并为一个或者将一个数据包拆分为多个(比如客户端调用了两次 send,服务器端一个 recv 就把信息都读出来了)。

TCP 粘包问题的原因有以下几个方面:

  • 发送方的缓冲区:发送方可能会将多个数据包放入缓冲区,等到缓冲区满了或者超时了再一次性发送,这样就会造成粘包;
  • 接收方的缓冲区:接收方可能会将收到的数据放入缓冲区,等到有足够的数据再交给应用层处理,这样就会造成粘包;
  • 网络传输过程:网络传输过程中可能会发生丢包、重传、乱序等现象,导致接收方收到的数据和发送方发送的数据不一致,这样也会造成粘包。

Nagle 算法(或者 TCP_CORK)是发送方造成粘包的一个原因。Nagle 算法(或者 TCP_CORK)的目的是为了减少网络中小分组的数量,提高网络效率。Nagle 算法(或者 TCP_CORK)的原理是:当发送方有数据要发送时,如果该数据小于 MSS(最大报文段长度),并且发送缓冲区中没有未确认的数据,那么就立即发送该数据;否则,就将该数据放入发送缓冲区,等到下一个 ACK 到来或者缓冲区满了再发送。

一般有三种方式分包的方式:

  • 固定长度:发送方和接收方约定每个数据包的长度都是固定的,这样接收方就可以根据长度来划分数据包;
  • 分隔符:发送方和接收方约定一个特殊的字符或者字符串作为每个数据包的分隔符,这样接收方就可以根据分隔符来划分数据包;
  • 消息头:发送方和接收方约定每个数据包都有一个消息头,消息头中包含了该数据包的长度、类型等信息,这样接收方就可以根据消息头来划分数据包。

2.3 UDP

UDP 不需要和 TCP 一样在发送数据前进行三次握手建立连接,面向无连接。

  • 在发送端,应用层将数据传递给传输层的 UDP 协议,UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了;
  • 在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作。

UDP 同样支持一对多,多对多,多对一的方式。

发送方的 UDP 对应用程序交下来的报文,在添加首部后就向下交付 IP 层。UDP 对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文。

2.3.1 应用场景

TCP 应用场景:效率要求相对低,但对准确性要求相对高的场景。因为传输中需要对数据确认、重发、排序等操作,相比之下效率没有 UDP 高。举几个例子:文件传输、接受邮件、远程登录。

UDP 应用场景:效率要求相对高,对准确性要求相对低的场景。举几个例子:QQ 聊天、在线视频、网络语音电话、广播通信(广播、多播)

UDP 面向数据报无连接的,数据报发出去,就不保留数据备份了。仅仅在 IP 数据报头部加入校验和复用。UDP 没有服务器和客户端的概念。UDP 报文过长的话是交给 IP 切成小段,如果某段报废报文就废了。

2.3.2 UDP 快的原因

  1. 不需要建立连接;
  2. 对于收到的数据,不用给出确认;
  3. 没有超时重发机制;
  4. 没有流量控制和拥塞控制。

2.3.3 对比 TCP

UDP TCP
是否连接 无连接 面向连接
是否可靠 不可靠传输,不使用流量控制和拥塞控制 可靠传输,使用流量控制和拥塞控制
连接对象个数 支持一对一,一对多,多对一和多对多交互通信 只能是一对一通信
传输方式 面向报文 面向字节流
首部开销 首部开销小,仅 8 字节 首部最小 20 字节,最大 60 字节
适用场景 适用于实时应用(IP 电话、视频会议、直播等) 适用于要求可靠传输的应用,例如文件传输

2.4 流量控制

TCP 流量控制是指发送方根据接收方的可用缓冲区大小来调整发送速率,以避免发送方过快地发送数据导致接收方无法处理的情况。TCP 流量控制使用了滑动窗口机制。

滑动窗口机制是 TCP 的一种流量控制方法,该机制允许发送方在停止并等待确认前连续发送多个分组,而不必每发送一个分组就停下来等待确认,从而增加数据传输的速率提高应用的吞吐量。

TCP 在进行数据传输的时候都是先将数据放在数据缓冲区中的,TCP 维护了两个缓冲区,发送方缓冲区和接收方缓冲区。

  • 发送方缓冲区:发送方缓冲区用于存储已经准备就绪数据和发送了但是没有被确认的数据;
  • 接收方缓冲区:接收方缓冲区用于存储已经被接收但是还没有被用户进程消费的数据。

发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。接收窗口只会对窗口内最后一个按序到达的字节进行确认。如果发送窗口内的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向滑动接收窗口。

TCP 包的四种状态

TCP 的包可以分为四种状态:

已发送并且已经确认的包、已发送但是没有确认的包、未发送但是可以发送的包、不允许被发送的包。

2.4.1 零窗口

TCP 零窗口是指接收方通知发送方其可接收的数据量(窗口大小)为零,即接收方暂时无法接收数据的情况。这可能是因为接收方的缓冲区已满,或者接收方的应用程序没有及时读取数据。

为了避免这种情况导致的死锁,TCP 使用了持续计时器,每当发送方收到一个零窗口的应答后就启动该计时器,时间一到便主动发送报文询问接收方的窗口大小。如果接收方仍然返回零窗口,则重置该计时器继续等待;如果窗口不为零,则表示应答报文丢失了,此时重传数据。

2.5 拥塞控制

如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。TCP 主要通过四个算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。

发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量,还需要设置一个慢开始门限 ssthresh 状态变量。

  1. 开始与拥塞避免:
    发送的最初执行慢开始(cwnd < ssthresh),令拥塞窗口大小为 1,发送方只能发送 1 个报文段;当收到确认后,将拥塞窗口大小加倍(指数增长)。设置一个慢开始门限,当拥塞窗口的大小大于慢开始门限时(cwnd > ssthresh),进入拥塞避免,每个轮次只将拥塞窗口 + 1。如果出现了超时,则令 慢开始门限 = 拥塞窗口大小 / 2,然后重新执行慢开始。
  2. 快重传与快恢复:
    在接收方,要求每次接收到报文段都应该对最后一个已收到的有序报文段进行确认。在发送方如果收到三个重复确认,那么可以知道下一个报文段丢失,此时执行快重传, 立即重传下一个报文段,进入快恢复阶段。在这个阶段,发送方将阈值设置为当前拥塞窗口的一半,并将拥塞窗口设置为阈值加上三个分组大小(表示已经收到三个重复确认)。然后每收到一个确认报文,就将拥塞窗口加一。当收到新的确认报文时(表示丢失的分组已经被确认),就退出快恢复阶段,并进入拥塞避免阶段。

3. DNS

DNS 协议是基于 UDP 的应用层协议,它的功能是根据用户输入的域名,解析出该域名对应的 IP 地址,从而给客户端进行访问。。平时上网中 URL 输入的经常是域名而非 IP,因为字母组成的域名更加符合直觉。

3.1 结构

DNS 服务器一般分三种,根 DNS 服务器,顶级 DNS 服务器,权威 DNS 服务器。

DNS 服务器层次结构

除此之外,还有 IPS 提供的本地 DNS 服务。常用的 DNS 结果一般会被缓存起来,如果缓存中没有,本地服务器才会将请求发送到根域名 DNS 服务器。

3.2 解析过程

  1. 客户机发出查询请求,在本地计算机缓存查找(在 Windows 中,会显示 Non-authoritative answer),若没有找到,就会将请求发送给 DNS 服务器;
  2. 本地 DNS 服务器会在自己的区域里面查找,找到即根据此记录进行解析,若没有找到,就会在本地的缓存里面查找;
  3. 本地服务器没有找到客户机查询的信息,就会将此请求发送到根域名 DNS 服务器;
  4. 根域名服务器解析客户机请求的根域部分,它把包含的下一级的 DNS 服务器的地址返回到客户机的 DNS 服务器地址;
  5. 客户机的 DNS 服务器根据返回的信息接着访问下一级的 DNS 服务器;
  6. 这样递归的方法一级一级接近查询的目标,最后在有目标域名的服务器上面得到相应的 IP 信息;
  7. 客户机的本地的 DNS 服务器会将查询结果返回给客户机,客户机根据得到的 IP 信息访问目标主机,完成解析过程。

3.3 递归/迭代查询

主机向本地域名服务器一般递归查询;

本地域名服务器向根域名服务器使用迭代查询。

第一个阶段:主机向本地域名服务器的请求使用一般使用递归查询

在这个阶段,主机向本地域名服务器发起请求,如果本地域名服务器不知道请求方想要的东西,那么本地域名服务器会向别的根域名服务器发起请求。如果按照这种方式不断持续进行下去的话,那么每次请求发起方都会改变。这个过程就有点像我们编程里面的递归,在递归的多次执行中,每次的执行环境都在发生变化。

第二阶段:本地域名服务器向根域名服务器请求使用迭代查询

在这个阶段,本地域名服务器向根域名服务器发起请求,如果根域名服务器不知道请求方想要的东西,那根域名服务器会给请求方指定一个域名服务器让其去查询。这个过程请求方始终是本地域名服务器。

不严谨的总结一下,他们的差别在于:

迭代查询是在同一个主体的基础上进行的,而递归查询每次都在变更查询主体。

迭代查询和递归查询

3.4 浏览器输入网址之后

  1. 浏览器会检查缓存中是否有该网址对应的 IP 地址,如果没有,就会通过 DNS(域名系统)查询 IP 地址;
  2. 浏览器会向服务器发送一个 HTTP 请求,请求获取网页的内容;
  3. 服务器会根据请求的内容,返回一个 HTTP 响应,包含网页的 HTML 代码和其他资源,如图片,样式表,脚本等;
  4. 浏览器会解析 HTML 代码,并请求其他资源,然后渲染网页并显示给用户。

4. 网络安全

4.1 加密算法

4.1.1 对称加密

“对称加密”很好理解,就是指加密和解密时使用的密钥都是同一个,是“对称”的。只要保证了密钥的安全,那整个通信过程就可以说具有了机密性。

TLS 里有非常多的对称加密算法可供选择,比如 RC4、DES、3DES、AES、ChaCha20 等,但前三种算法都被认为是不安全的,通常都禁止使用,目前常用的只有 AES 和 ChaCha20。

AES 的意思是“高级加密标准”(Advanced Encryption Standard),密钥长度可以是 128、192 或 256。它是 DES 算法的替代者,安全强度很高,性能也很好,而且有的硬件还会做特殊优化,所以非常流行,是应用最广泛的对称加密算法。

ChaCha20 是 Google 设计的另一种加密算法,密钥长度固定为 256 位,纯软件运行性能要超过 AES,曾经在移动客户端上比较流行,但 ARMv8 之后也加入了 AES 硬件优化,所以现在不再具有明显的优势,但仍然算得上是一个不错算法。

4.1.2 非对称加密

非对称加密算法的设计要比对称算法难得多,在 TLS 里只有很少的几种,比如 DH、DSA、RSA、ECC 等。

RSA 可能是其中最著名的一个,几乎可以说是非对称加密的代名词,它的安全性基于“整数分解”的数学难题,使用两个超大素数的乘积作为生成密钥的材料,想要从公钥推算出私钥是非常困难的。

4.1.3 混合加密

虽然非对称加密更加安全,但是解密速度慢了几百倍,现在 TLS 使用的是混合加密方式。

在通信刚开始的时候使用非对称算法,比如 RSA、ECDHE,首先解决密钥交换的问题。

然后用随机数产生对称算法使用的“会话密钥”(session key),再用公钥加密。因为会话密钥很短,通常只有 16 字节或 32 字节,所以慢一点也无所谓。

对方拿到密文后用私钥解密,取出会话密钥。这样,双方就实现了对称密钥的安全交换,后续就不再使用非对称加密,全都使用对称加密。

混合加密方式

HTTPS 采用混合的加密机制。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。

4.1.4 摘要算法

实现完整性的手段主要是摘要算法(Digest Algorithm),也就是常说的散列函数、哈希函数(Hash Function)。

摘要算法不具有机密性,如果明文传输,那么黑客可以修改消息后把摘要也一起改了,网站还是鉴别不出完整性。真正的完整性必须要建立在机密性之上,在混合加密系统里用会话密钥加密消息和摘要,这样黑客无法得知明文,也就没有办法动手脚了。

目前 TLS 推荐使用的是 SHA-1 的后继者:SHA-2。

4.1.5 数字签名

这里还有一个“公钥的信任”问题。因为谁都可以发布公钥,我们还缺少防止黑客伪造公钥的手段。

CA(Certificate Authority,证书认证机构),由它来给各个公钥签名,用自身的信誉来保证公钥无法伪造,是可信的。小一点的 CA 可以让大 CA 签名认证,但链条的最后,也就是 Root CA,就只能自己证明自己了,这个就叫“自签名证书”(Self-Signed Certificate)或者“根证书”(Root Certificate)。

证书链确保根证书的绝对安全性,将根证书隔离地越严格越好,不然根证书如果失守了,那么整个信任链都会有问题。

CA 信任链

4.2 Dos 攻击

拒绝服务(Deny of Service,DoS)攻击是一种网络攻击,恶意行为者通过中断设备的正常功能,使其目标用户无法使用计算机或其他设备。DoS 攻击通常通过请求压垮或淹没目标计算机,直到其无法处理正常流量,从而对其他用户造成拒绝服务。DoS 攻击的特征是使用一台计算机来发起攻击。

分布式拒绝服务 (DDoS) 攻击是一种 DoS 攻击,它来自许多分布式来源,例如僵尸网络 DDoS 攻击。

4.2.1 SYN 攻击

SYN 攻击即利用 TCP 协议缺陷,通过发送大量的半连接请求,占用半连接队列,耗费 CPU 和内存资源。SYN 洪水攻击利用 TCP连接的握手过程发动攻击。正常情况下,TCP 连接将完成三次握手以建立连接。

SYN Flood DDoS 攻击动画

这样服务端就会迟迟收不到客户端发来的 ACK 包,从而被阻塞。

攻击方式
  1. 直接攻击:不伪造 IP 地址的 SYN 洪水攻击称为直接攻击。在此类攻击中,攻击者完全不屏蔽其 IP 地址。由于攻击者使用具有真实 IP 地址的单一源设备发起攻击,因此很容易发现并清理攻击者。为使目标机器呈现半开状态,黑客将阻止个人机器对服务器的 SYN-ACK 数据包做出响应。为此,通常采用以下两种方式实现:部署防火墙规则,阻止除 SYN 数据包以外的各类传出数据包;或者,对传入的所有 SYN-ACK 数据包进行过滤,防止其到达恶意用户机器。实际上,这种方法很少使用(即便使用过也不多见),因为此类攻击相当容易缓解 – 只需阻止每个恶意系统的 IP 地址。哪怕攻击者使用僵尸网络(如 Mirai 僵尸网络),通常也不会刻意屏蔽受感染设备的 IP。
  2. 欺骗攻击:恶意用户还可以伪造其发送的各个 SYN 数据包的 IP 地址,以便阻止缓解措施并加大身份暴露难度。虽然数据包可能经过伪装,但还是可以通过这些数据包追根溯源。此类检测工作很难开展,但并非不可实现;特别是,如果 Internet 服务提供商 (ISP) 愿意提供帮助,则更容易实现。
  3. 分布式攻击(DDoS):如果使用僵尸网络发起攻击,则追溯攻击源头的可能性很低。随着混淆级别的攀升,攻击者可能还会命令每台分布式设备伪造其发送数据包的 IP 地址。哪怕攻击者使用僵尸网络(如 Mirai 僵尸网络),通常也不会刻意屏蔽受感染设备的 IP。
缓解方式

一些简单的处理优化方式包括:

  1. 缩短 SYN Timeout 时间;
  2. 记录 IP,若连续受到某个 IP 的重复 SYN 报文,从这个 IP 地址来的包会被一概丢弃。
扩展积压工作队列

目标设备安装的每个操作系统都允许具有一定数量的半开连接。若要响应大量 SYN 数据包,一种方法是增加操作系统允许的最大半开连接数目。为成功扩展最大积压工作,系统必须额外预留内存资源以处理各类新请求。如果系统没有足够的内存,无法应对增加的积压工作队列规模,将对系统性能产生负面影响,但仍然好过拒绝服务。

回收最先创建的 TCP 半开连接

在填充积压工作后覆盖最先创建的半开连接。这项策略要求完全建立合法连接的时间低于恶意 SYN 数据包填充积压工作的时间。当攻击量增加或积压工作规模小于实际需求时,这项特定的防御措施将不奏效。

此策略要求服务器创建 Cookie。为避免在填充积压工作时断开连接,服务器使用 SYN-ACK 数据包响应每一项连接请求,而后从积压工作中删除 SYN 请求,同时从内存中删除请求,保证端口保持打开状态并做好重新建立连接的准备。如果连接是合法请求并且已将最后一个 ACK 数据包从客户端机器发回服务器,服务器将重建(存在一些限制)SYN 积压工作队列条目。虽然这项缓解措施势必会丢失一些 TCP 连接信息,但好过因此导致对合法用户发起拒绝服务攻击。

参考文章

  1. Java Guide 操作系统常见面试题
  2. 重学操作系统
  3. 一句话+一张图说清楚——银行家算法
  4. 虚拟内存的实现方式
  5. 虚拟内存的那点事儿
  6. 【计网】网络分层模型:七层、四层、五层
  7. JAVA 开发岗|2020 春招、2021 秋招面试高频问题总结
  8. 十分钟搞懂 HTTP 和 HTTPS 协议?
  9. HTTP 请求方法
  10. 说一下 GET 和 POST 的区别?
  11. 长连接 & 短连接
  12. HTTP 的长连接和短连接
  13. 面试题之请求转发和重定向的区别
  14. 什么是 Cookie?Session 和 Cookie 有什么区别?
  15. HTTP 1.0 和 HTTP 1.1 的主要区别
  16. 再过五分钟,你就懂 HTTP 2.0 了
  17. 什么是 HTTP/2?HTTP/2 和 HTTP/1.1 区别是什么?
  18. 两张动图-彻底明白 TCP 的三次握手与四次挥手
  19. 大白话告诉你 TCP 为什么需要三次握手四次挥手
  20. 怎么解决 TCP 网络传输「粘包」问题?
  21. 一次完整的 HTTP 请求过程
  22. 一文搞懂 TCP 与 UDP 的区别
  23. TCP 的滑动窗口机制
  24. DNS 解析的过程是什么
  25. 深入理解 DNS 解析过程
  26. 怎么更好的理解 DNS 的迭代查询与递归查询
  27. 什么是拒绝服务(DoS)攻击?
  28. SYN 洪水 DDoS 攻击

文章作者: 陈鑫扬
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 陈鑫扬 !
评论
  目录