Coder Social home page Coder Social logo

interview_notes's People

Contributors

douc1998 avatar

Stargazers

 avatar

Watchers

 avatar  avatar

interview_notes's Issues

页面的回流和重绘

回流(重排)

当我们使用 JS 对 DOM 的修改引发了 DOM 的增删、几何尺寸变化、位置变化或浏览器窗口改变等情况,浏览器就需要重新计算元素的几何属性(因为一个元素改变很可能也引起其他元素的改变,因此也要计算其他元素),然后将计算的结果绘制出来。这个过程就叫回流,也叫重排。

常见引起回流的属性和方法

  • 添加或删除可见的 DOM 元素。
  • 元素尺寸改变,如边距、边框、内容、宽高等。
  • 内容变化,比如用户在 input 中输入文字。
  • 浏览器窗口尺寸改变(resize)
  • 计算 offsetWidth 和 offsetHeight 属性
  • 设置 style属性的值

重绘

当我们修改了 DOM 元素的样式(比如修改了背景颜色),但是并没有改变到它的几何属性、增删、位置变化等等,只是改变了样式。浏览器不需要重新计算元素的几何属性,直接为该元素绘制新的样式。这个过程叫重绘

常见引起重绘属性和方法

image

如何减少回流和重绘

回流一定会引起重绘,重绘不一定会引发回流。回流的操作成本比重绘高得多,因此我们除了在不得以的情况下,尽量避免回流。可以参考以下建议:

  • 使用 transform 替代 top
  • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
  • 不要把节点的属性值放在一个循环里当成循环里的变量。
for(let i = 0; i < 1000; i++) {
    // 获取 offsetTop 会导致回流,因为需要去获取正确的值
    console.log(document.querySelector('.test').style.offsetTop)
}
  • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
  • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
  • CSS 选择符从右往左匹配查找,避免节点层级过多
  • 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点。比如对于 video 标签来说,浏览器会自动将该节点变为图层。

参考

你不知道的浏览器页面渲染机制

浏览器加载不同资源时的行为

浏览器加载不同资源时的行为

  • 浏览器解析遇到 CSS 样式资源时,CSS 会异步下载,不会阻塞浏览器构建 DOM 树,但是会阻塞渲染。因为,在构建渲染树之前,会先等 CSS 下载并解析完毕才进行。(因为渲染会涉及到 CSS 样式,如果没有解析完 CSS 资源就渲染,会出问题)。

  • 浏览器解析遇到 JS 脚本资源时,需要等待 JS 脚本资源下载并执行完毕之后,才会继续解析 HTML,因为 JS 会修改 DOM 元素。。(deferasync 标识的脚本除外)

  • CSS 加载会阻塞后面的 JS 语句的执行。H5 标准中规定,浏览器在执行 Script 脚本前,必须保证当前的外联 CSS 已经解析完成。因为 JS 可能会去获取或变更 DOM 得 CSS 样式。如果 CSS 没有解析好,那加载的结果就是有问题的。

  • 解析遇到 Img 图片,直接异步下载,不会影响其他解析。

为什么 CSS 要放在头部

外联 CSS 无论放在哪里都不会阻塞 HTML 的解析,但是会影响 HTML 渲染。如果 CSS 放在头部,那么就可以和 HTML 并行解析,当两者都解析完毕时,构建渲染树进行渲染。

然而,如果 CSS 放在尾部,就会导致一系列的阻塞问题:

  • 如 JS 需要其外联的 CSS 解析完毕才能执行,而 JS 又会阻塞 HTML 解析,从而出现 CSS 解析阻塞 HTML 解析的情况。
  • CSS 放在底部也会阻塞渲染,因为构建渲染树需要解析 CSSOM 树和 DOM 树。CSS 放在底部就不能尽早的执行,从而导致网页加载卡顿。当浏览器在 CSS 加载解析完毕之后,重新计算样式绘制,会造成回流重绘、页面闪动等现象。

为什么 Script 要放在尾部

因为当浏览器解析 Script 时,就会立即下载并执行,中断 HTML 的解析过程。如果下载解析外部脚本时间太长,就会导致页面长时间未响应。

HTTP 状态码

HTTP 状态码

HTTP 应答状态码:

状态码 类别 描述
1xx Informational(信息性状态码) 请求正在被处理
2xx Success(成功状态码) 请求处理成功
3xx Redirection(重定向状态码) 需要进行重定向
4xx Client Error(客户端状态码) 服务器无法处理请求
5xx Server Error(服务端状态码) 服务器处理请求时出错

HTTP 常见应答状态码:

状态码 描述
200 客户端请求成功
204 请求处理成功,但响应体为空,即没有资源返回。一般用于只需从客户端往服务器发送信息。
206 表示客户端进行了范围请求,而服务器成功执行了这部分的 GET 请求,实现断点续传或同时分片下载。响应报文中包含由 Content-Range 指定范围的实体内容。
301 (永久重定向)请求的资源已被永久分配了新的 URI,以后应该永久使用资源现在所指的 URI 进行访问。
302 (临时重定向)请求的资源已被分配了新的 URI,希望用户(本次)能使用新的 URI 访问。
303 303 状态码和 302 状态码有着相同的功能,但 303 状态码明确表示客户端应当采用 GET 方法获取资源。
304 客户端发送附带条件的请求时,服务器端允许请求访问资源,但请求资源未修改,可以使用缓存的资源,不用在服务器取,则返回 304。该相应不包含响应的主体部分(即可直接使用缓存)
400 请求报文中存在语法错误
401 发送的请求需要 HTTP 认证
403 请求的资源禁止被访问。
404 服务器上无法找到请求的资源。
405 客户端请求的方法虽然能被服务器识别,但是服务器禁止使用该方法。
500 服务器内部错误。
503 服务器正忙,处于超负荷或维护状态,无法请求。

参考:
具有代表性的 HTTP 状态码

3-19 笔试算法题

/**
 * 米哈游笔试;连通块
 * 一个 n x m 的矩阵中有 R G B 字母,每个格子里的值可能是三者之一。一个 + 号形状可以视为四连通。
 * 也就是一个格子上/下/左/右边的格子和它里面字母一样,认为是一个连通块。
 * 但是米小游是一个色盲,G 和 B 分不清,所以可以把含有 G 和 B 的格子都忽略掉,去寻找矩阵里 R 字母组成的连通块个数。
 * 返回的结果是:矩阵中正确的连通块个数 - 米小游看到的连通块个数。
 * 举例:
 * R R G G B B
 * R G B G R R
 * 正确的连通块个数是:6(RR\RR\GG\GG\BB\RR)
 * 米小游看到的连通块个数是: 3(RR\RR\RR)
 * 返回的答案是: 6 - 3 = 3
 */

function getNum(matrix, n, m) {
    // 记录每个位置的状态
    const state = Array(n).fill().map(_ => Array(m).fill({
        top: false,
        right: false,
        bottom: false,
        left: false
    }))
    // 记录连通块个数
    let num = 0;
    // 遍历二维矩阵
    for (let r = 0; r < n; r++) {
        for (let c = 0; c < m; c++) {
            // 上,判断有没有算过
            if (r - 1 >= 0 && matrix[r][c] === matrix[r - 1][c] && !state[r - 1][c].bottom) {
                num++;
            }
            // 右,不需要判断有没有算过,但是要设置 right 值,防止右边的数再算一次
            if (c + 1 < m && matrix[r][c] === matrix[r][c + 1]) {
                num++;
                state[r][c].right = true;
            }
            // 下,不需要判断有没有算过,但是要设置 bottom 值,防止下边的数再算一次
            if (r + 1 < n && matrix[r][c] === matrix[r + 1][c]) {
                num++;
                state[r][c].bottom = true;
            }
            // 左,判断有没有算过
            if (c - 1 >= 0 && matrix[r][c] === matrix[r][c - 1] && !state[r][c - 1].right) {
                num++;
            }
        }
    }
    return num;
}

const matrix = [['R', 'G', 'G', 'G', 'B', 'B'],
['R', 'G', 'B', 'G', 'R', 'R']]

let rightMatrix = Array(matrix.length).fill().map(_ => Array(matrix[0].length));
let wrongMatrix = Array(matrix.length).fill().map(_ => Array(matrix[0].length));

for(let i = 0; i < matrix.length; i++){
    for(let j = 0; j < matrix[0].length; j++){
        rightMatrix[i][j] = matrix[i][j];
        // 把 G B 设置为随机数,大概率是不相等的,也可以设置为其他不等的符号。
        wrongMatrix[i][j] = matrix[i][j] === 'R' ? matrix[i][j] : Math.random()
    }
}
let rightNum = getNum(rightMatrix, matrix.length, matrix[0].length);
let wrongNum = getNum(wrongMatrix, matrix.length, matrix[0].length);

console.log(`正确的连通块个数为:${rightNum}\n米小游看到的连通块个数为:${wrongNum}\n答案为:${rightNum - wrongNum}`);

3-30 阿里面试手撕题

// 手撕 LRU (最久未使用)缓存替换策略

// capcacity 表示缓存容量,使用 map 来存储缓存的 key - value
var LRUCache = function(capacity) {
    this.map = new Map();
    this.capacity = capacity;
};

// 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1
// 思路:每 get 一次就重新插入到 map 尾部,表示是最近使用过的
LRUCache.prototype.get = function(key) {
    if(this.map.has(key)){
        let value = this.map.get(key);
        this.map.delete(key); // 删除后,再 set ,相当于更新到 map 最后一位
        this.map.set(key, value);
        return value;
    } else {
        return -1
    }
};

// 如果关键字 key 已经存在,则变更其数据值 value ;
// 如果不存在,则向缓存中插入该组 key-value 。
// 如果插入操作导致关键字数量超过 capacity ,则应逐出 最久未使用 的 key。
LRUCache.prototype.put = function(key, value) {
    // 如果已有,那就要更新,即要先删了再进行后面的 set
    if(this.map.has(key)){
        this.map.delete(key);
    }
    this.map.set(key, value);
    // put 后判断是否超载,如果超载,就要删除最久未使用的 key-value, 就是第一个
    if(this.map.size > this.capacity){
        this.map.delete(Array.from(this.map.keys())[0]);
        // this.map.delete(this.map.keys().next().value); // 或者直接用 next 来调用迭代器 iterator
    }
};

如何解决 UDP 不可靠(丢包)问题

如何解决 UDP 不可靠(丢包)问题

1、使用前向纠错。前向纠错技术可以在发送方添加一些冗余数据,使接收方能够在接收到部分数据包时也能恢复丢失的数据。

2、重传机制。当出现丢包时,重新传输对应的包数据。

3、流量控制。根据网络情况限制发送端的发送速率,可以减少网络拥塞导致的丢包问题。

4、使用确认机制。尽管UDP本身没有确认机制,但你可以在应用层添加确认机制。例如,在发送数据时,要求接收方发送确认消息,以确保数据的完整性和正确性。

5、增加接收缓冲区大小。通过增加接收缓冲区的大小,可以减少丢包的可能性。

6、数据包重排列。给数据包添加 index 属性,接收端将接收到的数据包按照 index 排序,保证数据包按照正确顺序提交给应用层处理。

8-21 高德面试手撕题

const data = [
    {'name': 'jack', 'age': 24, 'home': '杭州'}, 
    {'name': 'mike', 'age': 17, 'home': '上海'}, 
    {'name': 'lucy', 'age': 22, 'home': '杭州'}, 
    {'name': 'john', 'age': 25, 'home': '上海'}
]

query(data).select(item => item.age >= 18).orderBy('age').groupBy('home').execute();
// 实现当传入 data 之后,根据一些筛选、排序、组合的方法,输出以下结果
// [
//     [
//         {'name': 'lucy', 'age': 22, 'home': '杭州'}, 
//         {'name': 'jack', 'age': 24, 'home': '杭州'}
//     ],
//     [
//         {'name': 'john', 'age': 25, 'home': '上海'}
//     ]
// ]


// 因为没有使用到 new,因此不能使用 类 或者 构造函数+原型 的方法,只能使用 闭包+返回对象
// 因为是链式调用,因此每次函数执行返回都应该是 this,指向 query返回的对象
function query(data){
    let thisData = data; // 存数据
    return {
        select: function(fn){ // 通过 filter 筛选数据
            thisData = thisData.filter(fn);
            return this;
        },

        orderBy: function(key){ // sort 排序数据
            thisData.sort((a, b) => a[key] - b[key]);
            return this;
        },

        groupBy: function(key){ // 组织数据
            let newData = [];
            let myObj = {};
            for(const item of thisData){
                if(item[key] in myObj){
                    myObj[item[key]].push(item);
                }else{
                    myObj[item[key]] = [item];
                }
            }

            for(const key in myObj){
                newData.push(myObj[key]);
            }

            thisData = newData;
            return this;
        },

        execute: function(){ // 输出数据
            console.log(thisData);
        }
    }
}

HTTP 请求方法

HTTP 请求方法

请求方法 描述
GET 请求资源
POST 浏览器向服务器提交数据,一般会造成服务器的资源修改
HEAD 类似 GET,但仅要求服务器返回头部信息
PUT 上传资源用于更新
PATCH 对 PUT 补充,对已知资源部分更新
DELETE 删除某个资源
TRACE 追踪请求/响应路径,用于测试或诊断
CONNECT 将连接改为管道方式,用于代理服务器
OPTION 查询服务器支持的请求方法,常用来跨域请求

3-18 面试算法题

/**
 * 百度面试算法题
 * 实现字符串的全排列:
 * 'abcd' -> [abcd, abdc, acbd, acdb ...]
 */

function getResult(str){
  // 先切分字符串
  const arr = str.split('');
  // 回溯,其实也可以添加一个参数,用来存储没被加入的数字。我这里是用时间复杂度换空间复杂度
  function trackBack(res, path){
    // 判断结束条件
    if(path.length === arr.length){
      res.push([...path].join(''));
      return;
    }
    // 单层逻辑
    for(let i = 0; i < arr.length; i++){
      if(path.includes(arr[i])) continue; // 全排列不允许有重复
      path.push(arr[i]);
      trackBack(res, path); // 递归
      path.pop(); // 回溯
    }
  }
  let res = [], path = [];
  trackBack(res, path);
  return res;
}

// Test
console.log(getResult('abcd'));
// [
//   'abcd', 'abdc', 'acbd',
//   'acdb', 'adbc', 'adcb',
//   'bacd', 'badc', 'bcad',
//   'bcda', 'bdac', 'bdca',
//   'cabd', 'cadb', 'cbad',
//   'cbda', 'cdab', 'cdba',
//   'dabc', 'dacb', 'dbac',
//   'dbca', 'dcab', 'dcba'
// ]

静态链接和动态链接

静态链接和动态链接

编译链接过程不仅存在于我们前端了解的 webpack 中,在任何需要编译打包程序的过程中,都需要使用到。这里简单介绍两种链接方式:静态链接动态链接

静态链接

在我们的实际开发中,不可能将所有代码放在一个源文件中,所以会出现多个源文件。然而多个源文件之间不是独立的,它们之间会存在多种依赖关系,如:在源文件 A 中需要调用源文件 B 中定义的函数,但是在编译过程中每个源文件都是独立编译的,因此为了满足前面说的依赖关系,则需要将这些源文件产生的目标文件进行链接,从而形成一个可以执行的程序。这个链接的过程就是静态链接

简而言之,静态链接就是在形成可执行程序之前对具有依赖关系的源文件进行链接,静态库也可以被认为是一系列存在关系的文件的集合。

优点

  1. 在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。

缺点

  1. 浪费空间,因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,如多个程序中都调用了printf() 函数,则这多个程序中都含有该函数所在的源文件副本。

  2. 更新比较困难。因为每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。

动态链接

动态链接就是为了解决静态链接浪费时间、更新困难等问题而存在的。动态链接的基本**是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。

举个例子,假设程序 A 依赖某个源文件 C,程序 B 也依赖源文件 C。系统首先加载程序 A,当系统发现程序 A 中用到了文件 C 的函数,那么系统接着加载文件 C,如果程序 A 和 文件 C 还依赖于其他目标文件,则依次全部加载到内存中。当程序 B 运行时,系统发现它依赖于文件 C,但是此时 C 已经存在于内存中,这个时候就不再进行重新加载,而是将内存中已经存在的 C 映射到 B 的虚拟地址空间中,从而进行链接(这个链接过程和静态链接类似)形成可执行程序。

优点

  1. 节省存储空间。即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多个副本,而是这多个程序在执行时共享同一份副本。

  2. 更新便捷。更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。

缺点

  1. 性能损失。链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以运行速度变慢。

参考

用户线程和内核线程的区别

用户线程和内核线程的区别

线程的实现可以分两类:用户级线程内核级线程(或混合式线程)。

用户线程

用户级线程(也称为用户态线程)是指:不需要内核支持而在用户程序中实现的线程,它的内核切换是由用户态程序自己控制内核的切换,不需要内核的干涉。但是它不能像内核级线程一样更好的运用多核CPU。

优点

  1. 线程的调度不需要内核直接参与,控制简单。

  2. 可以在不支持线程的操作系统中实现。

  3. 同一进程中只能同时有一个线程在运行,如果有一个线程使用了系统调用而阻塞,那么整个进程都会被挂起,可以节约更多的系统资源。

缺点

  1. 一个用户级线程的阻塞将会引起整个进程的阻塞。

  2. 用户级线程不能利用系统的多重处理,仅有一个用户级线程可以被执行。

内核线程

内核级线程(又称内核态线程)切换由内核控制,当线程进行切换的时候,由用户态转化为内核态。切换完毕要从内核态返回用户态。可以很好的运用多核CPU,例如电脑的四核八线程,双核四线程等。

优点

  1. 当有多个处理机时,一个进程的多个线程可以同时执行。

  2. 由于内核级线程只有很小的数据结构和堆栈,切换速度快,当然它本身也可以用多线程技术实现,提高系统的运行速率。

缺点

  1. 线程在用户态的运行,而线程的调度和管理在内核实现,在控制权从一个线程传送到另一个线程需要用户态到内核态再到用户态的模式切换,比较占用系统资源。(就是必须要受到内核的监控)

两者的关联和区别

相同点:

  1. 内核线程和用户线程都是线程的一种,都可以执行任务。

不同点:

  1. 内核线程是由操作系统内核创建、管理、切换、调度的,而用户线程是由应用程序创建、管理、切换、调度的。

  2. 内核线程运行在内核态,可以访问操作系统的所有资源,而用户线程运行在用户态,只能访问应用程序的资源。

  3. 内核线程可以执行任何操作系统提供的服务,如文件系统、网络等,而用户线程只能执行应用程序提供的服务。

  4. 用户级线程执行系统调用指令时将导致其所属进程被中断,而内核支持线程执行系统调用指令时,只导致该线程被中断。

  5. 在只有用户级线程的系统内,CPU调度还是以进程为单位,处于运行状态的进程中的多个线程,由用户程序控制线程的轮换运行,用户线程无法并发,同一进程中只能同时有一个线程在运行。而内核线程的调度则以线程为单位,由 OS 的线程调度程序负责线程的调度。

参考

网页安全-XSS/CSRF攻击

Web 网页安全

浏览器网页维护安全的主要方法就是:同源策略限制

同源策略

同源策略指的是我们访问站点的:协议域名端口号 一致才叫同源,有一个不一样,都会被认为是跨源(跨域)。

浏览器默认同源站点之间是可以互相访问资源和操作 DOM 的,而不同元源之间想要互相访问资源或者操作 DOM 的话,就需要添加一些安全策略的限制。如下:

  1. DOM 层面:不同源站点之间不能互相访问和操作 DOM。
  2. 数据层面:不能获取不同源站点的 Cookie、LocalStorage、indexDB 等数据。
  3. 网络层面:不能通过 XMLHttpRequest 向不同源站点发送请求。(后来基于 CORS 可以实现但是也会存在一定的限制)

当然,同源策略也不是绝对隔离不同源的站点,比如 linkimgscript 标签都没有跨域限制,这也导致了一些安全问题。如 XSS 攻击和 CSRF 攻击

XSS(跨站脚本攻击)

XSS 叫跨站脚本攻击,原本应该叫 CSS,但是为了和层叠样式区别开,叫 XSS。XSS 攻击是一种代码注入攻击,通过恶意注入 JS 脚本在浏览器运行,然后调取用户信息。

image

造成 XSS 攻击本质上还是因为网站没有过滤恶意代码。当恶意代码混入正常代码中一起执行时,浏览器没有办法分辨哪些是可信的,然后导致恶意代码也被执行。引起的危害有:

  • 页面数据或用户信息被窃取。如 CookieLocalStorageDOM 等。
  • 修改 DOM。比如伪造登陆窗口或在页面生成浮窗广告。
  • 监听用户行为。如添加 addEventListener 来监听键盘事件,获取用户密码。
  • 流量被劫持向其他网站。用户刚进入一个网站,就直接跳转到另外一个网站上。

XSS 攻击有三种类型:存储型反射型DOM 型

  1. 存储型
    存储型 XSS 攻击主要是将恶意脚本存储到服务端,当读取到该恶意脚本时,浏览器就会识别为一段 JS 代码来执行。比如在评论区,有人写了一段恶意脚本并提交,恶意脚本就会被服务器存储到数据库。当别人访问时,加载这段评论,浏览器就把它识别为 JS 代码来执行。

  2. 反射型
    通过 URL 参数注入恶意脚本,经服务器解析并响应后,凭借在 HTML 中传回浏览器,然后浏览器解析时就会执行恶意脚本。比如打开包含恶意脚本的链接,打开后会向服务器发送请求,服务器会获取 URL 中的数据然后凭借在 HTML 上返回,然后执行。它和存储型的区别在于不会存储在服务器中。

  3. 基于 DOM 型
    通过一定的手段在网页向服务端请求资源时,劫持并修改页面的数据,插入恶意代码。

解决方法

  • 在服务端对 script 标签进行转义或过滤,再传回给浏览器,这样浏览器在 HTML 解析时就不会把 script 标签当作 JS 代码执行了。
  • 利用 http-only。当 Cookie 设置 http-only 后,会禁止 JavaScript 来访问 Cookie。
  • 充分利用 CSP。内容安全策略(CSP)是一个额外的安全层,会限制加载其他域下的资源文件、禁止向第三方提交数据。

CSRF(跨站请求伪造)

CSRF 是跨站请求伪造攻击,顾名思义,就是第三方利用用户的登录信息伪造成用户发起跨域请求。比如邮箱里的乱七八糟链接,打开链接的时候邮箱处于登陆状态,第三方就可以利用这个登陆状态,伪造带有正确 Cookie 的 http 请求,绕过后台验证,冒充用户进行一些操作。

image

发起 CSRF 攻击有三个必要条件:

  • 目标网站有 CSRF 漏洞
  • 用户登录过目标网站,并且浏览器保存了登陆状态。
  • 用户主动打开第三方站点。

CSRF 本质上是利用进行 HTTP 同源请求时会携带 Cookie 信息这一特点,实现冒充用户

解决方法

  • Cookie Hashing(服务器生成 Cookie 传递给客户端时会生成一个随机数存在其中)。最简单有效方式,因为攻击者理论上无法获取第三方的Cookie,所以伪造 Cookie 失败,无法通过用户验证。
  • 携带 token。客户端向服务器请求 token(令牌),服务器返回 token 给客户端,并且客户端在之后的所有请求中都要带上 token 以作身份验证。
  • Origin 和 Referer。服务器验证 Referer 是否从第三方网站发出来的,阻止第三方网站请求接口。但是这两者可以通过 ajax 自定义请求头的方式被伪造。

参考

什么是 XSS 攻击
理解前端常见的 CORS 和 CSRF

HTTP 缓存

浏览器缓存

为了提高用户的体验和浏览器请求数据的效率,降低服务器的开销,浏览器会对请求资源进行缓存。

  • 浏览器每次发起请求前,都会先在浏览器缓存中查找该请求的结果和缓存标识。如果没有缓存或缓存失效,才会去进一步请求服务器。
  • 浏览器每次拿到返回的请求结果都把结果和缓存标识存入浏览器缓存中,以供之后直接使用。

对于缓存,浏览器主要有两种缓存策略强缓存协商缓存

强缓存

强缓存通过设置两种 HTTP Header 实现,分别是:ExpiresCache-Control。强缓存表示在缓存期间不需要发送请求,直接使用即可,返回的状态码为 200。

  • ExpiresHTTP 1.0 的产物。值表示的是服务端的时间,并且 Expires 受限于本地时间,如果修改了本地时间导致本地时间和服务器时间不一致,可能会造成缓存失效。
  • Cache-ControlHTTP 1.1 的产物,优先级高于 Expires。该属性具有 max-age 值,表示资源会在请求后多少秒过期。

Cache-Control 的值也可以设置为 no-storeno-cache,两个指令在请求和响应中都可以使用。

  • no-store 表示不进行任何缓存,告知服务器或缓存服务器,我请求或响应的内容里有机密信息。
  • no-cache 如果在请求头中使用,表示强制使用协商缓存,;如果在响应头中被返回时,表示缓存服务器不能对该资源进行缓存,客户端可以缓存资源,但是每次使用之前都必须向服务器确认其有效性。

协商缓存

如果缓存过期了或者 Cache-Control 设置为了 no-cache,客户端就需要向服务端发起请求验证资源是否有更新。在服务器发送请求时,服务器会根据这个请求头的 If-Modified-SinceIf-None-Match 来判断是否命中协商缓存。如果命中,则返回 304 状态码并更新浏览器缓存有效期。

协商缓存的标识为:Last-ModifiedETag

  • Last-Modified 标识本地文件最后修改时间。发送请求时,会将当前的 Last-Modified 值作为 If-Modified-Since 字段的内容,放在请求头中发送给服务器,去询问服务器在这个时间后资源是否有更新。有更新的话服务端就返回新的资源,没有的话返回 304 状态码。

  • ETag 类似于文件指纹。客户端请求时会将当前的 ETag 作为 If-None-Match 字段的内容,并放在请求头中发送给服务器。服务器接收到 If-None-Match 后会跟服务器上该资源的 ETag 进行对比。如果有变动的话,就把新的资源返回,没有的话返回 304 状态码。

字段 Header 类型 HTTP 版本 缓存类型
Last-Modified Response 头 1.0 协商缓存
If-Modified-Since Request 头 1.0 协商缓存
ETag Response 头 1.1 协商缓存
If-None-Match Resquest 头 1.1 协商缓存

ETag 的出现是为了弥补 Last-Modified 的缺点:

  • 如果文件编辑了,但是内容没变,缓存标识就会失效。
  • Last-Modified 记录的时间单位为秒,如果文件在 1s 内被修改多次,就无法记录。

浏览器的缓存过程

  1. 浏览器第一次加载资源时,服务器返回状态码 200,浏览器把资源文件从服务器上请求下载下来,并把 response header 及该请求的返回时间一起缓存下来。
  2. 下一次加载资源时,先比较当前时间和上一次返回 200 时的时间差,如果没超过 Cache-Controlmax-age,则缓存没有过期,命中强缓存,不发请求直接读取本地缓存。如果浏览器不支持 HTTP 1.1,则用 expires 判断是否过期。如果时间过期,浏览器向服务器发送 header 带有 If-None-MatchIf-Modified-Since 的请求。
  3. 服务器收到请求后,有优先根据 ETag 的值判断被请求的文件有没有修改,没有修改则命中协商缓存,返回 304。反之,直接返回新资源,带上新的 ETag并返回 200。
  4. 如果服务器收到的请求没有 ETag 值,则将 If-Modified-Since 和被请求文件的最后修改时间做比对,相同则命中协商缓存;反之返回新的 Last-Modified 和文件,并返回 200。

参考:

浏览器原理

3-26 阿里笔试第二题

/**
 * 阿里 3.26 笔试第二题
 * n 个学生围成一圈报数,编号为 1 到 n,学生们从 1 开始依次报数,报到素数的学生出列,剩下的学生继续报。
 * 一直报数报到只剩一个学生时,停止报数,求解这个学生的编号。
 * ======================
 * 解题思路:模拟队列
 */

// 判读素数
function isPrime(num){
    if(num <= 1){
        return false;
    }
    for(let i = 2; i <= Math.sqrt(num); i++){
        if(num % i === 0){
            return false
        }
    }
    return true;
}


function getStudent(n){
    let students = new Array(n).fill(true);
    let num = 1; // 报数号码
    let loc = 0; // 当前报数学生的编号
    while(true){
        if(isPrime(num)){ // 判断素数
            students[loc] = false;
        }
        // 判断是否还剩一个学生
        if(students.filter(item => !!item).length === 1){
            return students.findIndex(item => !!item) + 1
        }
        // 寻找为 true 的下一个学生
        loc = (loc + 1) % n;
        while(!students[loc]){
            loc = (loc + 1) % n;
        }
        num++;
    }
}

console.log(getStudent(4)); // 4
console.log(getStudent(6)); // 4
console.log(getStudent(9)); // 1

3-12 笔试第三题

/**
 * 题目:支持同时加载 N 张图片,且支持配置整体资源加载超时时间
 * 要求-1: x 秒内资源没有全部响应完成,控制台输出 resource load cost over xxx ms
 * 要求-2: x 秒内资源全部响应完成,控制台输出加载失败的资源的错误信息,成功的不输出。
 * 
 * @param {*} imgUrls 
 * @param {*} timeout 
 */
// preloadImage
async function preloadImage(imgUrls, timeout){
    // 使用 race 限制超时,使用 all 并行请求
    let res = await Promise.race([Promise.all(imgUrls.map(item =>
        fetch(item).then(response => {
            // fetch 除了网络请求问题,一般都会得到 resolve 状态的期约,主要根据 response.ok 决定是否请求成功
            if(response.ok) return;
            else return `${item} load fail !`
        })
    )), new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(`resource load cost over ${timeout} ms`)
        }, timeout)
    })]);

    // 如果返回的不是数组,说明是 timeout,反之输出数组内容。
    if(Array.isArray(res)){
        res.forEach(item => {
            if(item) console.log(item);
        })
    }else{
        console.log(res);
    }
}

// Test:user1.json 不存在,user2.json 存在,user3.json 不存在
const imgUrls = ['./myData/users1.json', './myData/users2.json', './myData/users.json'];
preloadImage(imgUrls, 1000);
/**
 * ./myData/users1.json load fail !
 * ./myData/users2.json load fail !
 */

Cookie 和 Session

Cookie 和 Session

Session 和 Cookie 主要用来识别登录者身份的,默认通过 SessionID 唯一编号进行验证。Session 是在服务器端保存的一个数据结构,用来跟踪用户的状态,也可以保存用户相关的一些数据,可以保存在内存、缓存、数据库等存储结构中。Cookie 是客户端保存用户信息的一种机制。

什么是 Cookie

当客户端第一次发出请求,请求服务器时,如果服务器需要记录该用户状态,就会使用 response 向客户端浏览器响应回一个Cookie。客户端会把 Cookie 保存起来,保存在浏览器的内存中。

Cookie 是服务器发送到浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一用户,如保持用户的登录状态。Cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。

Cookie 主要用于以下三个方面:

  • 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
  • 个性化设置(如用户自定义设置、主题等)
  • 浏览器行为跟踪(如跟踪分析用户行为等)

什么是 Session

每个用户访问服务器都会建立一个 Session,当用户与服务器建立连接的同时,服务器会自动为其分配一个SessionId,用于辨别用户。

Session 代表着服务器和客户端一次会话的过程。Session 对象(key-value形式)存储特定用户会话的状态(实际上和 Cookie 类似,都是为了存储用户相关的信息)。这样,当用户多次请求服务器时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当客户端关闭会话,或者 Session 超时失效时会话结束。

Session 与 Cookie 的区别

  • 作用范围不同。Cookie 保存在客户端(浏览器),Session 保存在服务器端。
  • 存取方式的不同。Cookie 只能保存 ASCII,Session 可以存任意数据类型,一般情况下我们可以在 Session 中保持一些常用变量信息,比如说 UserId 等。
  • 有效期不同。Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般失效时间较短,客户端关闭或者 Session 超时都会失效。
  • 隐私策略不同,Cookie 存储在客户端,比较容易遭到不法获取,早期有人将用户的登录名和密码存储在 Cookie 中导致信息被窃取;Session 存储在服务端,安全性相对 Cookie 要好一些。
  • 存储大小不同。单个 Cookie 保存的数据不能超过 4K,Session 可存储数据远高于 Cookie。

Session 和 Cookie 的联系

Session 和 Cookie 是相互合作实现的,而它们两个之间的桥梁便是 SessionId。

image

用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session ,请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器,浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名。

当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。

cookie 、session 和 token 的区别

cookie 、session 和 token 的区别

在先前的一篇 issue 中提到了 cookie 和 session 的区别。这一次又出现了 token。不过无论是 cookie 还是 token,也都是一些熟悉字眼儿了,包括在平时生活中也有所了解。

我们知道 cookiesession 是相辅相成的,所以实际上我认为谈论它俩的区别有点牵强。但是讨论 tokencookie-session 的区别还是很有意义的。

这里就不介绍什么是 cookiesession 了,可以跳转到上面的链接去了解一下。下面主要讲解 token

token

token 的全名叫 JSON Web Token,缩写为 JWT

首先我们已经知道 cookiesession 一个存储在客户端,一个存储在服务端,并且通过验证内部的 sessionId 来实现用户的身份验证。之所以需要这种验证,主要还是因为 HTTP 的无状态特点,以至于用户每个页面切换时,都需要进行登陆,就很麻烦了。但是 cookiesession 可以帮助我们实现这种身份验证。

那么既然有了 cookiesession,为什么还需要 token 呢?

这里我们先不谈 session,因为它是存储在服务端的。cookietoken 是存储在客户端的,我们就先谈一谈 cookie 的不足之处。

  • cookie 是小文本文件,大小只有 4KB,很显然它存储的内容是十分有限的,一般也就是 key-value 形式保存一下我们的身份信息。
  • 如果遇到跨域问题,cookie 是无法发送的。(除非我们通过设置客户端和服务端的 header 实现,包括 withCredentialsAccess-Control-Allow-CredntialsAccess-Control-Allow-Origin
  • cookie 不能存储敏感信息,因为 cookie 能够被客户端篡改。

现在我们再谈一谈 session 的不足之处。

  • session 会存储在服务端,每一个用户都会对应一个自己的用户信息,如果用户量非常大,这会增加服务器的负载。
  • 如果存在多个服务器,如负载均衡时,每个服务器的状态表必须同步,否则就无法实现身份验证。

既然上面提出了一些 cookiesession 的不足之处,下面我们就谈一谈 token


token 实际上就类似于 HTTPS签名方式帮助服务器实现校验。token 由三部分构成:头部负载签名

  • 头部:存储 token 的类型和签名算法(如类型是 JWT,加密算法是 HS256)
  • 负载:存储要存储的信息,如用户账号、姓名、昵称等。
  • 签名:签名就是由指定的加密算法,将转义后的头部和负载,加上密钥一起加密得到的。

最后把这三部分连接起来就可以得到一个 token 了。

而使用 token 认证的流程如下:

image

  1. 客户端使用用户名和密码请求登陆。
  2. 服务端收到请求后,去验证用户名和密码。
  3. 验证成功后,服务端签发一个 token,发送给客户端。
  4. 客户端收到 token 后会把它存储起来,比如放在 cookie 中获 localStorage 中。
  5. 客户端每次发送请求时都需要带着服务端签发的 token(把 token 放在 HTTPheader 中)。
  6. 服务端收到请求后,需要验证请求里带有的 token(通过解密算法解密),验证成功则返回对应的数据。

从上面对 token 的介绍和认证流程,我们知道 token 是一种服务端无状态的认证方式,服务端不需要存储 token 数据,而是存在每个客户端中。服务端用解析 token 的计算时间换取 session 的存储空间,从而减轻服务器的存储压力和频繁查询数据库的性能压力。

token 具有加密签名,而 cookiesession 是没有的。token 能存储的数据较多,而 cookie 仅有 4 KB。此外,token支持跨域认证的,而 cookie 是不允许的。

因此,tokencookiesession 的区别总结如下:

  • token 大小相较于 cookie 更大,能够存储更多信息。
  • token 支持跨域认证,而 cookie 不可以,需要客户端和服务端一起设置一些 header 才行。
  • token 具有加密签名,相比 cookie 更加安全。
  • token 是无状态的,存储在客户端本地,无需像 session 一样存储在服务器中增加内存压力。
  • token 利用解密的计算时间换取 session 在服务端的存储空间,并且不需要频繁的查询数据库。

行内元素和块级元素

行内元素和块级元素

HTML的标签大多数都是行内元素和块级元素,那么行内元素和块级元素和有哪些区别呢?哪些是行内元素,哪些是块级元素呢?还有什么是行内块元素

MDN 中对行内元素块级元素的说明。

行内元素

行内元素不可以设置宽高,其宽高会根据内容自适应(表现在高度根据字体大小决定,宽度根据内容长度决定)。不过,行内元素可以与其他行内元素位于同一行。但是,行内元素内不可以包含块级元素。

行内元素有以下特点:

  • 不会独占一行,相邻的行内元素会排列在同一行里,直到一行排不下才会自动换行。
  • 高宽无效,对外边距(margin)和内边距(padding)仅设置左右方向有效,上下无效;
  • 设置行高有效,等同于给父级元素设置行高。
  • 行内元素中不能放块级元素,a 链接里面不能再放链接。

行内元素有:

 <a>     // 标签可定义锚 
 <abbr>     // 表示一个缩写形式 
 <acronym>     // 定义只取首字母缩写 
 <b>     // 字体加粗 
 <bdo>     // 可覆盖默认的文本方向 
 <big>     // 大号字体加粗 
 <br>     // 换行 
 <cite>     // 引用进行定义 
 <code>    // 定义计算机代码文本
 <dfn>     // 定义一个定义项目
 <em>     // 定义为强调的内容
 <i>     // 斜体文本效果
 <kbd>     // 定义键盘文本
 <label>     // 标签为 input 元素定义标注(标记)
 <q>     // 定义短的引用
 <samp>     // 定义样本文本
 <select> // 创建单选或多选菜单
 <small>     // 呈现小号字体效果
 <span>     // 组合文档中的行内元素
 <strong> // 加粗
 <sub>     // 定义下标文本
 <sup>     // 定义上标文本
 <tt>     // 打字机或者等宽的文本效果
 <var>    // 定义变量

块级元素

块级元素可以自己设置宽高,并且每一个 “块” 都会独占一行。块级元素可以用作容器,其内部可以具有行内元素。

块级元素有以下特性:

  • 每个块级元素都独占一行
  • 高度,宽度,行高,外边距(margin)以及内边距(padding)都可以控制;
  • 元素的宽度如果不设置的话,默认为父元素的宽度(父元素宽度100%);
  • 多个块状元素标签写在一起,默认排列方式为从上至下

块级元素包括:

 <address>  // 定义地址 
 <caption>  // 定义表格标题 
 <dd>      // 定义列表中定义条目 
 <div>     // 定义文档中的分区或节 
 <dl>    // 定义列表 
 <dt>     // 定义列表中的项目 
 <fieldset>  // 定义一个框架集 
 <form>  // 创建 HTML 表单 
 <footer> // 页脚
 <h1>    // 定义最大的标题
 <h2>    // 定义副标题
 <h3>     // 定义标题
 <h4>     // 定义标题
 <h5>     // 定义标题
 <h6>     // 定义最小的标题
 <hr>     // 创建一条水平线
 <header> // 页头
 <legend>    // 元素为 fieldset 元素定义标题
 <li>     // 标签定义列表项目
 <noframes>    // 为那些不支持框架的浏览器显示文本,于 frameset 元素内部
 <noscript>    // 定义在脚本未被执行时的替代内容
 <ol>     // 定义有序列表
 <ul>    // 定义无序列表
 <p>     // 标签定义段落
 <pre>     // 定义预格式化的文本
 <section> // 段落
 <table>     // 标签定义 HTML 表格
 <tbody>     // 标签表格主体(正文)
 <td>    // 表格中的标准单元格
 <tfoot>     // 定义表格的页脚(脚注或表注)
 <th>    // 定义表头单元格
 <thead>    // 标签定义表格的表头
 <tr>     // 定义表格中的行

行内块元素

行内块级元素,它既具有块级元素的特点,也有行内元素的特点,它可以自由设置元素宽度和高度,也可以在一行中放置多个行内块级元素。

行内块元素有以下特性:

  • 高度、行高、外边距以及内边距都可以控制;
  • 默认宽度就是它本身内容的宽度,不独占一行,但是之间会有空白缝隙,设置它上一级的 font-size 为 0,才会消除间隙;

行内块元素包括:

<button>  // 按钮
<input>   // 输入框
<textarea>  // 多行纯文本编辑控件
<select> // 选项菜单
<img> // 图片

参考

CSS中 块级元素、行内元素、行内块元素区别

HTTP 1.1 和 2.0 区别

HTTP 1.1

HTTP 1.1 的新特性有:

  1. 默认持久连接
    只要客户端和服务端任意一端没有明确提出断开 TCP 连接,就会一直保持连接,并且在这个过程中可以发送多次 HTTP 请求。HTTP 1.0 默认使用短连接,而 HTTP 1.1 默认使用长连接。

  2. 管线化
    客户端可以同时发送多个 HTTP 请求,不用等待响应。

  3. 断点续传
    利用 HTTP 消息头(Range 和 Content-Range)使用分块传输编码,将实体主体进行分块传输。

  4. 缓存策略
    在HTTP1.0中主要使用header里的 Expires, Last-Modified / If-Modified-Since来做为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略例如:cache-control, ETag / If-None-Match等更多可供选择的缓存头来控制缓存策略。

HTTP 2.0

HTTP 2.0 的新特性有:

  1. 二进制格式传输
    HTTP 1.x 的解析都是基于文本,而 HTTP 2.0 采用二进制格式,实现了效率更高的传输。

  2. 并发传输
    HTTP 1.1 基于请求-响应模型,同一个连接中, HTTP 完成一个事务(请求与响应),才能处理下一个事务。在发出请求等待响应的过程中是没办法做其他事情的,这会造成队头阻塞问题 。
    HTTP2 通过 Stream(流)设计,多个 Stream 复用一条 TCP 连接,达到了并发的效果。(实际上就是一个请求对应了一个 ID,一个 TCP 连接可以有多个请求,接收方根据请求的 ID 将请求归属到不同的服务端请求中)

  3. 压缩头部
    HTTP 1.1 报文的 Header 部分含有很多固定字段,且很多字段值是重复的。因此 HTTP 2.0 采用 HPACK 算法(包括静态字典、动态字典、哈夫曼编码)对头部进行压缩,降低头部占用空间大小,提高传输效率。

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

  5. 永久性
    HTTP 2.0 中只要客户端和某个服务器完成连接,将具有永久性,之后如果再次需要和该服务器进行连接时,可以直接复用,不需要再次建立连接。

GET 和 POST 请求的区别

GET 请求的特点

  1. GET 请求效率更高。
  2. GET 请求用来请求资源,不修改服务器上的资源。
  3. GET 请求的结果会被保存在历史记录中。
  4. GET 会被浏览器主动缓存,如果下一次传输的数据相同,浏览器就会直接返回缓存中的内容。
  5. GET 的参数都写在 URL 中,是可见的,不能传输敏感信息。
  6. GET 请求的URL 具有长度限制,这个并不是 HTTP 协议的限制,而是浏览器,不同浏览器限制长度不同。
  7. GET 只产生一个 TCP 数据包,浏览器会把请求头和请求数据一并发送出去,服务器响应状态码 200 为请求成功。

POST 请求的特点

  1. POST 请求效率相对更低。
  2. POST 请求多用于上传信息给服务器,更新服务器资源。
  3. POST 不会被浏览器主动缓存,除非手动设置。而且每次 POST 请求都是新的,多次提交就会多次修改服务器上的资源。
  4. POST 请求的结果不会被保存在历史记录中。
  5. POST 的参数都写在 Request body 中,可以传输敏感信息。
  6. POST 请求没有 URL 长度限制。
  7. POST 会产生两个 TCP 数据包,浏览器会先将请求头发送给服务器,待服务器响应 100 continue,浏览器再发送请求数据,服务器响应 200 请求成功,返回数据。

Http 和 Https 的区别

HTTP

HTTP 即超文本传输协议,主要用于 Web 上传输超媒体文本的底层协议,经常在浏览器和服务器之间传递数据。通信就是以纯文本的形式进行。

特点

无连接、无状态、灵活易于扩展、简单快速。

  • 无连接:每一次请求都要连接一次,请求结束就会断掉,不会保持连接

  • 无状态:每一次请求都是独立的,服务器不会去记忆 HTTP 的状态,所以不需要额外的资源来记录状态信息,从而减少了网络开销。这既是优点也是缺点。

  • 灵活易于扩展:HTTP 协议中的各种请求方法、URI / URL、状态码、头字段等每个组成都是可以由开发人员自定义和扩充的。

  • 简单快速:基本报文的格式为 header + body,头部信息也是键值对的文本形式,易于理解。当发送请求访问某个资源时,只需传送请求方法和 URL 就可以了,使用简单。正由于http协议简单,使得http服务器的程序规模小,因而通信速度很快

缺点

无状态、明文传输、不安全

  • 无状态:服务器不会记忆 HTTP 的状态,所以不需要申请额外的资源,能够减轻服务器的负担。但是也就无法区分多个请求发起者身份是不是同一个客户端的,意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。

  • 明文传输:传输的报文 (header部分) 是肉眼可见的,直接将信息暴露给了外界。

  • 不安全:明文传输可能被窃听不安全,缺少身份认证可能遭遇伪装,缺少报文完整性验证可能遭到篡改。

HTTPS

HTTPS 在 HTTP 的基础上添加了 SSL / TLS 安全传输协议,使得报文能够加密传输

HTTPS 在 TCP 三次握手之后,还需要进行 SSL /TLS 的握手过程,才可进入加密报文传输。

特点

信息加密、校验机制、身份证书

  • 信息加密:浏览器和服务器之间交互的信息无法被窃取。

  • 校验机制:无法篡改通信的内容,因为一旦被篡改就不能正常显示。

  • 身份证书:提供当前报文完整性的证明。

优点

  1. 在数据传输过程中,使用密钥加密,安全性更高。

  2. 能够认证用户和服务器,确保数据发送到正确的用户和服务器。

缺点

  1. 在 TCP 三次握手后,还需要进行 SSL/TLS 握手,导致握手阶段延迟提高。
  2. 需要进行加解密计算,占用 CPU 资源,从而导致计算成本提高。
  3. 需要购买 SSL 证书,功能越强大,证书价格越贵,导致部署成本升高。

HTTP 和 HTTPS 区别

  1. HTTPS 是 HTTP 协议的安全版本,HTTP 协议的数据传输是明文的,是不安全的,HTTPS 使用了 SSL/TLS 协议进行了加密处理,相对更安全。

  2. HTTP 和 HTTPS 使用连接方式不同,默认端口也不一样,HTTP是 80,HTTPS 是443。

  3. HTTPS 由于需要设计加密以及多次握手,性能方面不如 HTTP

参考

HTTP和HTTPS详解

TCP 和 UDP 区别

TCP / IP 网络模型

计算机与网络设备要相互通信,双方就必须基于相同的方法。比如,如何探测到通信目标、由哪一边先发起通信、使用哪种语言进行通信、怎样结束通信等规则都需要事先确定。不同的硬件、操作系统之间的通信,所有的这一切都需要一种规则。而我们就把这种规则称为协议(protocol)。

TCP/IP 网络模型一系列网络协议的总称,不同协议各司其职。该网络模型可以划分为四层:链路层、网络层、传输层和应用层。(也可以根据 OSI 模型,划分为七层,主要是对链路层和应用层进行了功能细分)

  • 链路层:负责封装和解封装 IP 报文,发送和接受 ARP / RARP 报文等。
  • 网络层:负责路由以及把分组报文发送给目标网络或主机。
  • 传输层:负责对报文进行分组和重组,并以 TCP 或 UDP 协议格式封装报文。
  • 应用层:负责向用户提供应用程序,比如HTTP、FTP、Telnet、DNS、SMTP等。

TCP 和 UDP 都是传输层协议。

TCP 协议

TCP 协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。#3 在这篇 issue 中,讲解了 TCP 的三次握手和四次挥手过程,涉及了 TCP 建立连接和结束连接的详细过程。

TCP 协议的特点如下:

  1. 面向连接
    面向连接,是指发送数据之前必须在两端建立连接。建立连接的方法是“三次握手”,这样能建立可靠的连接。

  2. 一对一传输
    每条 TCP 传输连接只能有两个端点,进行一对一的数据传输,而不支持一对多、多对一、多对多等方式。

  3. 面向字节流
    TCP 传输是在不保留报文边界的情况下以字节流方式进行传输,而不是一个一个报文的形式进行传输。

  4. 可靠传输
    在三次握手的过程中,TCP 头部包含了客户端和服务端的随机序列号以及确认应答码信息,每次连接这些信息都是不一样的。因此,两端可以通过这些信息互相进行确认,确保传递的可靠性。

  5. 提供拥塞控制
    TCP 具有慢开始和避免拥塞的特点。当刚建立 TCP 连接时,TCP 连接会一点点提速,试探网络的承受能力,以免打乱网络通信的秩序,慢慢地翻倍提速。如果遇到网络拥塞,TCP 会降低传输速度,缓解拥塞。

UDP

UDP协议全称是用户数据报协议,在网络中它与TCP协议一样用于处理数据包,是一种无连接的协议。在OSI模型中,在第四层——传输层,处于IP协议的上一层。UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。

它有以下几个特点:

  1. 面向无连接
    首先 UDP 是不需要和 TCP一样在发送数据前进行三次握手建立连接的,想发数据就可以开始发送了。并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作。

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

  1. 有单播,多播,广播的功能
    UDP 不仅支持一对一的传输方式,还支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。

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

  3. 不可靠性
    无连接的特性允许数据想发就发,而不需要进行验证,也不会关心对方是否已经收到数据了。再者,UDP 协议一直以恒定的速度发送数据,如果遇到网络拥塞,可能会出现丢包现象,但是它也不会去确认数据的完整性。

  4. 头部开销小
    UDP 的头部开销小,只需要 8 字节,比 TCP 至少 20 字节小得多,因此传输更高效。对于一些实时性要求高的场景(如视频、电话会议),UDP 协议更适合。

TCP 和 UDP 的区别

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

HTTP 3.0

HTTP 3.0

HTTP 3.0 是基于 QUIC 协议的新版本 HTTP 协议,相比之前的 HTTP 1.1 和 HTTP 2.0 有以下几个变动:

  • 传输协议改变:HTTP 3.0 基于 QUIC 协议,而 HTTP 1.1和 HTTP 2.0 是基于 TCP 协议。QUIC 协议是一个基于 UDP 协议的安全传输协议。HTTP3.0 使用 QUIC 协议,可以更快地建立连接和更好地管理网络拥塞。QUIC 协议通过减少握手次数、降低延迟和网络拥塞控制等机制来提高网络性能。

  • 多路复用改进:HTTP 3.0 通过 QUIC 协议实现多路复用,该方法能够将数据流动态地分割成多个数据包,这些数据包可以并行地发送和接收,避免了头阻塞的问题。此外,不需要像 HTTP 2.0 那样预先将请求和响应打包成帧,而是将它们作为数据流动态地切分为多个数据包,这使得它更加灵活,可以适应不同的网络条件。减少了延迟和头阻塞等问题。

  • 加密改进:HTTP3.0 使用 TLS 1.3 来加密数据传输,而 TLS 1.3 相比 TLS 1.2 有更好的性能和安全性。

  • 服务器推送改进:HTTP 3.0 将服务器推送机制优化,基于流的传输方式可以在一个连接上同时进行多个流,从而使得服务器推送更加高效和灵活。此外,还支持取消服务器推送,避免浪费网络资源。

  • 首部压缩改进:HTTP 3.0 将采用 QPACK 首部压缩算法,相比于 HTTP 2.0 的 HPACK 算法,更加高效。

3-23 腾讯音乐笔试题1

/**
 * 腾讯音乐笔试题第二题
 * 题目:一个由字母组成的字符串的权值定义为:字母种类个数 * 字符串长度,如 value(abac) = 3 * 4 = 12
 * 输入一个字符串 str,和正整数 k ,请把该字符串按顺序分为 k 个子串,找出 k 个子串的权值最大值 maxValue。
 * 一共有很多种切分的方法,求解所有切分方法里的 min(maxValue1, maxValue2, ... ),即最大值的最小值。
 * 
 * 思路:递归遍历 + 回溯
 * 剪枝:下面方法没有剪枝。思路是:可以给递归加一个curMaxValue 参数,实时计算当前 path 数组里的最大权值,如果已经大于 minMaxValue 了,就没必要继续了
 */

// 计算字符串的权值
const getValue = (str) => {
    let words = new Set(str);
    return words.size * str.length;
}
// 计算数组里 k 个子串的权值最大值
const getMaxValue = (arr) => {
    let maxValue = -1;
    for (const item of arr) {
        let curValue = getValue(item);
        // console.log(item, curValue);
        if (maxValue < curValue) maxValue = curValue;
    }
    return maxValue;
}
// 计算最终结果
const getMinMaxValue = (str, k) => {
    let minMaxValue = Infinity;
    // 递归 + 回溯
    const trackBack = (str, path, startIndex) => {
        // 判断结束条件
        if (path.length === k) {
            let temp = [...path];
            let maxValue = getMaxValue(temp);
            if (minMaxValue > maxValue) minMaxValue = maxValue;
            return;
        }

        // 单层逻辑:
        for (let i = startIndex; i < str.length + path.length - k + 1; i++) {
            path.push(str.slice(startIndex, i + 1));
            if (path.length === k - 1) { // 如果已经分割了 k - 1 个了,最后一个就是剩余子串
                path.push(str.slice(i + 1));
                trackBack(str, path, str.length);
                // 回溯,要连续取两个出来
                path.pop();
                path.pop();
            } else {
                trackBack(str, path, i + 1); // 下一个子串从第 i + 1个 位置开始切割
                path.pop();
            }

        }
    }
    trackBack(str, [], 0);
    return minMaxValue;
}

console.log(getMinMaxValue('ababbbb', 2)); // 6
console.log(getMinMaxValue('ababbbb', 3)); // 4
console.log(getMinMaxValue('abcbbdef', 3)); // 9  

8-22 快手面试手撕题

// sum(1, 2, 3, 4)(5)(6).sumOf() 输出 21
// sum 方法可以传入任意多数值,执行 sumOf 方法将输出结果。

function sum(...params){
    // 闭包存一个计数器
    let res = 0;
    params.forEach(item => {
        res += item;
    })

    // 因为 sum 可以多次调用,因此需要返回一个函数,这个函数能够继续把传入的参数加到 res 上。
    const add = (...args) => {
        args.forEach(item => {
            res += item;
        })
        return add;
    }

    // 返回的函数有一个 sumOf 的属性,输出对应的结果(函数也是一个对象,因此也可以有属性)
    add.sumOf = () => {
        console.log(res);
    }

    return add;
}

sum(1, 2, 3, 4)(5)(6).sumOf();  // 21

3-13 笔试第三题

/**
 * 题目:
 * 1、输入一个 num
 * 2、用 r、e、d 三个字母(不限个数)组合成一个字符串
 * 3、要求该字符串能够组成的回文子串数量等于 num
 * eg:num = 3,该字符串可以是:red -> [r, e, d] 或 rr -> [r, r, rr]
 */


// 判断是不是回文串
function isPalindrome(s, l, r) {
    for (let i = l, j = r; i < j; i++, j--) {
        if (s[i] !== s[j]) return false;
    }
    return true;
}

// 查找回文子串
function partition(s) {
    const res = [], path = [], len = s.length;
    function backtracking(startIndex){
        // 终止条件:startIndex 前面的字符串都是分好了的回文串
        if(startIndex >= len){
            // 为了统计数量,把 path 元素全都提出来放进 res
            res.push(...path);
            return;
        }
        for(let i = startIndex; i < len; i++){
            // 如果这一段是回文串,就插入 path 结果中,从下一个字母继续开始判断
            if(isPalindrome(s, startIndex, i)){
                path.push(s.slice(startIndex, i + 1));
                backtracking(i + 1);
                path.pop();
            }else{
                // 不是回文串,i 就再往后移进行判断
                continue;
            }
        }
    }
    backtracking(0);
    return res;
}

// 获取结果
function getStr(num){
    let res = ''; // 保存结果
    let str = 'r'; // 初始字符串
    let otherWords = ['r', 'e', 'd'];
    function backtracking(s){
        // 得到 s 字符串的所有回文子串
        let arr = partition(s);
        // 递归终止条件 1: 如果已经找到了一个符合要求的结果就不需要继续递归了。
        if(res) return;
        // 递归终止条件 2: 如果当前字符串 s 生成的回文子串的个数达到了 num,说明找到了一个符合要求的解。
        if(arr.length === num){
            res = s;
            return;
        }
        // 剪枝: 如果数量超过了 num,之后只会越来越多,没必要迭代
        if(arr.length > num){
            return;
        }
        for(let i = 0; i < otherWords.length; i++){
            str += otherWords[i];
            // 递归查找
            backtracking(str);
            // 回溯
            str = str.slice(0, str.length - 1);
        }
    }
    backtracking(str);
    return res;
}

// Test
console.log(getStr(50)); // rrerrderde
console.log(getStr(100)); // rrrredredredre
console.log(getStr(500)); // rrrrrederderderd

在浏览器地址栏中键入 URL 后会发生什么

在浏览器地址栏中键入 URL 后会发生什么

  1. 在浏览器的地址栏中,输入要跳转页面的 URL,按下回车键。
  2. 浏览器依次在浏览器缓存、系统缓存、路由器缓存中去寻找匹配的 URL,如果有,就会在屏幕中直接显示出页面内容。
  3. 如果没有在缓存中找到匹配的 URL,则需要进行域名解析(即 DNS 解析),以获取相应的 IP 地址。
  4. 获取到 IP 地址后,浏览器向服务器发起 TCP 连接,进行 TCP 三次握手
  5. 握手成功后,浏览器会像服务器发送 HTTP 请求,来请求服务器端的数据包。
  6. 服务器处理浏览器发来的请求,接着将数据返回给浏览器。
  7. 浏览器收到 HTTP 响应后,可以发起结束连接,进行 TCP 四次挥手
  8. 浏览器解析文件,如:解析 HTML 代码、生成 DOM 树、解析 CSS 样式、处理 JS 交互等。
  9. 解析完毕后,进行浏览器的布局和渲染

浏览器渲染:Step 8 - Step 9 详细步骤:

  1. 浏览器解析 HTML 代码,生成 DOM 树。
  2. 解析 CSS 样式,生成 CSS 样式树。
  3. 如果在解析 HTML 过程中遇到了 JS 代码,会先暂停 HTML 的解析(因为 JS 可能会改变 DOM),等该部分 JS 代码处理完毕后,继续解析 HTML。
  4. 合并渲染树。HMTL 和 CSS 解析完毕后,将 DOM 树和 CSS 树合并为渲染树。
  5. 计算元素布局。生成渲染树后,需要计算元素的大小及位置,包括字体大小、换行位置等信息。
  6. 绘制。渲染主线程会为每个层单独产生绘制指令集,用于描述这一层的内容该如何画出来。

----- 至此,渲染主线程任务都完成了 -----

  1. 分块。合成线程对每个图层进行分块,将其划分为更多的小区域,以交给不同线程去执行。
  2. 光栅化。分块完成后,进入光栅化阶段。合成线程会将块信息交给 GPU 进程,将每块信息处理成位图。
  3. Draw(画)。将位图信息绘制到屏幕上。包括旋转、缩放等变形。(因为是和合成线程上进行的,不会影响到主线程,这也是 transform 效率高的原因)

image

为什么 transform 效率高?

因为 transform 既不会影响布局也不会影响绘制指令,它影响的只是渲染流程的最后一个 draw 阶段。

由于 draw 阶段在合成线程中,所以 transform 的变化几乎不会影响渲染主线程。反之,渲染主线程无论如何忙碌,也不会影响 transform 的变化。


更新 2023 - 08 - 15

被京东的面试官问到了这个问题,但是他当时拓展问到该如何更对整个过程进行优化?

answer:

  • 在 html 文件编写角度,css 放在文件头部,script 放在尾部。
  • 在渲染角度,可以采用 ssr 提高首屏渲染速度,可以分块打包,动态加载文件,避免不必要的依赖模块加载。
  • 在操作角度,尽可能减少回流(重排),可以用重绘的一些方法来实现替代。

但是,上面答的似乎都没答到面试官想要的。他说了一点:

  • 从网络角度如何优化?可以使用 cdn,这样可以让各地的人加载网页都很快。

参考资料:
在浏览器地址栏键入URL,按下回车之后会经历了那些事
图解浏览器渲染页面详细过程

3-20 算法题满二叉子树

/**
 * 阿里 3.15 笔试第一题
 * 求解满二叉子树的个数
 */

// 树结点
function treeNode(val) {
    this.val = val;
    this.left = null;
    this.right = null;
}

// 记录满二叉子树的个数
let count = 0;

// 深度遍历
function deepTraversal(root) {
    // 叶子结点
    if (!root.left && !root.right) {
        root.val = true;
        return;
    }
    // 当前逻辑:如果
    let left = false, right = false; // left right 初始化为 false,这样如果没有左结点和右结点的话就一定不是满二叉树
    root.left && (function () { deepTraversal(root.left); left = root.left.val })();
    root.right && (function () { deepTraversal(root.right); right = root.right.val })();
    root.val = left && right;
}

// 寻找满二叉子树的个数
function getAllFullBinaryTrees(root) {
    // 递归结束条件
    if (!root) return;
    if (root.val) {
        count++;
    }
    getAllFullBinaryTrees(root.left);
    getAllFullBinaryTrees(root.right);
}

// Test
let root = new treeNode('root');
let l = new treeNode('l');
let r = new treeNode('r');
let ll = new treeNode('ll');
let lr = new treeNode('lr');
let rl = new treeNode('rl');
let rr = new treeNode('rr');
root.left = l;
root.right = r;
l.left = ll;
l.right = lr;
r.left = rl;
r.right = rr;

deepTraversal(root);
getAllFullBinaryTrees(root);
console.log(count); // 7

TCP 三次握手和四次挥手

TCP 三次握手和四次挥手

TCP

TCP是一种面向连接的、可靠安全的、基于字节流的传输层协议。TCP中文名称是传输控制协议,属于传输层协议。HTTP 是应用层协议,它是建立在 TCP / IP 协议基础上进行的。

三次握手

image

  1. 第一次:浏览器随机初始化序列号 x,放进 TCP 首部序列号段,即 seq = x,并把 SYN 设置为 1。然后把 SYN 码发送给服务器,请求和服务器建立连接,浏览器进入 SYN-SENT 状态。

  2. 第二次:服务器接收到 SYN 码后,把自己的序列号 y 放进 TCP 首部序列号段,即 seq = y,并把确认应答号 ack 设置为 x + 1。把 SYN 和 ACK 设置为 1,发送给浏览器,告诉浏览器已建立连接。服务器进入 SYN-RECV 状态。

  3. 第三次:浏览器接收 ACK 码,需确认 ACK 码是否正确。如果正确,浏览器会再向服务器发送一个数据包,数据包中将 ACK 置为 1,并将确认应答号 ack 设置为 y+1,表示收到了来自服务器的 SYN。

此后,浏览器和服务器都进入 ESTABLISHED 建立连接状态。

四次挥手

以浏览器先发出结束请求为例。

image

  1. 第一次:浏览器发送 FIN 码给服务器,告诉服务器,我要传给你的数据已经完成。浏览器进入 FIN_WAIT_1 状态。

  2. 第二次:服务器接收到 FIN 码,然后发送 ACK 码给浏览器,告诉浏览器已经收到消息。服务器发送完之后,就进入 CLOSE_WAIT 状态。浏览器进入 FIN_WAIT_2 状态。

  3. 第三次:虽然浏览器给服务器传输的数据完成了,但是服务器给浏览器的数据可能尚未完成。因此,当服务器数据传输成功后,发送FIN 码告诉浏览器自己的数据传输完毕。此时服务器进入 LAST_ACK 状态。

  4. 第四次:浏览器接收到 FIN 码之后,同样会发送 ACK 码给服务器,告诉服务器,我已接收到,你可以断开连接。此时,浏览器进入 TIME_WAIT 状态。

服务器收到 ACK 码后,进入 CLOSE 状态。在 2 MSL 后(MSL 是报文最大生存时间,但是消息传递是一来一回的,因此需要 2 倍),浏览器也会自动进入 CLOSE 状态。

为什么需要三次握手和四次挥手

为什么需要三次握手:

  1. 三次握手才可以阻止重复历史链接的初始化(即请求超时导致的历史脏连接)。
  • 当旧的 SYN 报文先到达服务器时,服务器会返回一个 ACK + SYN。
  • 浏览器接收到报文后可以根据自己的上下文信息,判断这是不是历史连接,如果是就会发送 RST 报文给服务器,表示中止这次连接。
  • 如果只是两次握手,是无法判断当前连接是否是历史连接。
  1. 三次握手才可以确认发送端和接受端都能具备信息的发送和接受能力。
  • 如果是两次握手,发送端可以确定自己发送的信息能对方能收到,也能确定对方发的包自己能收到,但接收端只能确定对方发的包自己能收到,无法确定自己发的包对方能收到。
  1. 三次握手才可以同步双方的初始序列号
  • 浏览器发送第一个报文,是携带了浏览器初始序列号的 SYN 报文。
  • 服务器发送第二个报文,是携带了服务器初始序列号的 ACK + SYN 的应答报文,表示收到。
  • 浏览器发送第三个报文,是携带了服务器的 ACK 应答报文。
  • 这样一来一回,才能够确保双方的初始序列号被可靠的同步。
  1. 三次握手才能避免资源浪费
  • 两次握手无法确保浏览器和服务器之间的消息同步。如果存在网络阻塞,浏览器和服务器没有办法及时收到对方的回应,浏览器可能会重新发送 SYN 报文,而每次发送,服务器都会建立一个新连接。

为什么需要四次挥手:

四次挥手实际上就是浏览器和服务器之间的两次消息发送与应答。当浏览器第一次发送 FIN 报文之后,只是代表着浏览器不再发送数据给服务端,但此时浏览器还是有接收数据的能力的。而服务器虽然收到 FIN 报文的时候,但可能还有数据要传输给客户端,所以只能先回复 ACK给客户端,等自己传输完毕之后再告诉浏览器传输结束。

参考资料:
让你真实的看见 TCP 三次握手和四次挥手到底是什么样!
TCP三次握手和四次挥手

3-26 阿里笔试第一题

/**
 * 阿里 3.26 笔试第一题
 * 环形数组,切两刀且成两个数组,让两个数组的元素和相等则算一种方案,求问一共多少种方案
 * ======================
 * 解题思路:前缀和 + Map 存储
 */

// 不使用 Map 存储,双 for 循环会超时
function getAllResult(arr){
    let len = arr.length;
    let count = 0;
    let preSum = [arr[0]];
    // 计算前缀和并存储
    for(let i = 1; i < len; i++){
        preSum.push(preSum[i - 1] + arr[i]);
    }

    // 不论是 i 还是 j,在位置 index 切,都是切在 index 对应数的后面,也就是会算上 index 这个数
    // i 不用切在第 0 个数前面,因为 j 切到最后一个数后面和这种情况是等效的。
    for(let i = 0; i < len - 1; i++){
        for(let j = i; j < len; j++){
            let sum1 = preSum[j] - preSum[i];
            let sum2 = preSum[len - 1] - preSum[j] + preSum[i];
            if(sum1 === sum2){
                console.log(i, j)
                count++;
            }
        }
    }
    return count;
}

let arr = [1, 2, -1, 2];
console.log(getAllResult(arr)); // 2

// 使用 Map 存储:前缀和 - 对应前缀和元素所在位置的集合
// 降低时间复杂度
function getAllResult(arr){
    let len = arr.length;
    let count = 0;
    let preSum = [arr[0]];
    // 计算前缀和并存储
    for(let i = 1; i < len; i++){
        preSum[i] = preSum[i - 1] + arr[i];
    }

    let sum = preSum[len - 1];
    if(sum % 2 === 1) return count; // 如果数组所有元素总和不是偶数,那肯定不存在分两个数组元素和相等

    // 用于记录前缀和有几种,对应的元素编号是多少
    let preSumMap = {};
    for(let i = 0; i < len; i++){
        if(preSum[i] in preSumMap){
            preSumMap[preSum[i]].push(i);
        }else{
            preSumMap[preSum[i]] = [i];
        }
    }

    // 遍历前缀和数组
    for(let i = 0; i < len; i++){
        let pre = preSum[i] - sum / 2; // 找出当前切割位置需要的另一个切割位置的前缀和值
        if(pre in preSumMap){
            // 如果另一个切割位置在当前位置的前面,就符合,因为我们是用 preSum[i] - sum / 2
            for(let j = 0; j < preSumMap[pre].length; j++){
                if(j < i){
                    count++
                }
            }
        }
    }
    return count;
}

console.log(getAllResult(arr)); // 2

基本分页和请求分页存储的区别

基本分页和请求分页存储的区别

当我们需要存储大量数据时,我们通常需要将数据分成多个部分,然后将这些部分存储在磁盘或内存中。其中,基本分页存储和请求分页存储是两种常见的数据分割和存储方式。

基本分页存储

基本分页存储是将数据划分为固定大小的页,并按顺序存储在磁盘或内存中。例如,一个文件可以被分割为多个固定大小的页,每一页包含一定数量的字节。当需要访问特定的数据时,系统会根据数据所在的页号和页内偏移量来定位数据。这种存储方式通常用于传统的文件系统和关系型数据库管理系统(RDBMS)中。

优点

基本分页存储的优点是:简单、高效、易于管理和维护

缺点

  1. 浪费内存。作业装入内存后,便一直驻留在内存中,直至作业运行结束。尽管运行中的进程会因 I/O 而长期等待,或有的程序模块在运行过一次后就不再需要了,但它们都仍将继续占用宝贵的内存资源。

  2. 无法进行分布式存储和处理

请求分页存储

请求分页存储中的数据仍然按页分割,但不一定按顺序存储。每个页都有一个唯一的标识符,并且存储在分布式系统的多个节点上。在进程开始运行之前,仅装入当前要执行的部分页面即可运行。在执行过程中,当需要某一个页面时,再请求从外部调入。如需要请求访问但又不在内存的页面,可以通过缺页中断机构,请求OS将所缺之页调入内存。当内存空间已满,而又需要装入新的页面时,者根据置换功能适当调出某个页面,以便腾出空间而装入新的页面。

为实现请求分页,需要一定的硬件支持,包括:页表机制缺页中断机制地址变换机制


1、页表机制包括几个属性:

  • 状态位:用于指示该页是否已调入内存,供程序访问时参考。
  • 访问字段:用于记录本页在一段时间内被访问的次数,或记录本页近期已有多长时间未被访问,供选择换出页面时参考。
  • 改动位:表示该页在调入内存后是否被改动过。因为内存中的每一页都在外存上保留一份副本,因此,若未被改动,在置换该页时就不需再将该页写回到外存上,以降低系统的开销和启动磁盘的次数;若已改动,则必须将该页重写到外存上,以保证外存中所保留的始终是最新副本 。改动位供置换页面时参考。
  • 外存地址:用于指出该页在外存上的地址,一般是物理块号,供调入该页时参考。

2、 缺页中断机制

缺页中断是指当一个程序访问虚拟内存中的某一页时,该页并未在物理内存中,需要将该页从磁盘或其他存储介质中读入到物理内存中。此时操作系统就会触发缺页中断机制,将控制权交给操作系统内核,内核根据页表信息将缺失的页面读入物理内存,然后将控制权返回给程序,使程序能够继续执行。

3、 地址变换机制

地址变换机制指将虚拟地址转换成物理地址的过程。在现代操作系统中,为了保护程序的内存空间和实现虚拟内存的功能,程序访问的地址一般都是虚拟地址,而不是直接访问物理内存。

当一个程序发起内存访问请求时,其请求的地址是虚拟地址,操作系统会通过地址变换机制将其转换成物理地址,然后才能真正的访问内存。地址变换机制一般包括以下几个步骤:

  • 地址解析:将虚拟地址中的页号和页内偏移量解析出来。

  • 查找页表:通过页表查找到对应的页表项,获取该页表项中的物理页号。

  • 地址重定位:将物理页号和页内偏移量组合成物理地址,完成地址重定位。

  • 访问物理内存:使用物理地址访问物理内存中的数据。

在操作系统中,地址变换机制是由硬件和软件共同实现的。硬件主要负责地址解析和地址重定位的部分,而软件则负责页表的管理和维护。通过地址变换机制,操作系统可以实现多道程序之间的内存隔离和虚拟内存的实现,从而提高了系统的稳定性和可用性。


优点

  1. 支持大规模数据处理。
  2. 高可用性。

缺点

  1. 需要更多的管理和维护工作
  2. 需要更高的网络和存储成本

总结

总的来说,基本分页存储适用于单机环境,适合小规模的数据存储和处理;而请求分页存储适用于分布式环境,适合大规模数据存储和处理。在实际应用中,我们需要根据具体的需求和场景选择合适的存储方式。

参考:

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.