执鸢者

执鸢者 查看完整档案

衡水编辑华中科技大学  |  机械科学与工程学院 编辑  |  填写所在公司/组织填写个人主网站
编辑

摸摸头,编程使我快乐。
公众号:执鸢者
欢迎关注公众号,并加群,一起加油

个人动态

执鸢者 发布了文章 · 10月24日

前端更应懂产品

关注公众号“执鸢者”,回复“红宝书”获取“javaScript高级程序第四版(pdf)”并进入专业交流群.
个人观点:前端工程师是最接近用户的研发工程师,我的观点就是“前端更应懂产品”。有人肯定会质疑我的观点,产品不是产品经理的事情,我们敲代码的懂这个有什么用?我们程序员懂技术不就行了吗?的确,技术对我们来说很重要,但是我认为产品对程序员(特别是前端工程师)也很重要。一方面毕竟产品是技术变现的途径,没有变现怎么来维护公司正常运营,给我们提供学习技术的土壤;另一方面学习产品知识能够为我们的职业生涯续命——(某位前辈说的,我也很认同)。本着这种想法,自己一直想了解一些产品的知识,可是没有合适的契机。近期一位“同学”给我推荐了“腾讯8分钟产品课”,收获良多,算是自己入坑产品的第一门课程。(纯属个人观点,不喜勿喷
本文将利用八张图从用户、定位、需求、时机、匠心、危机、合作、商业化八个方面对整门课进行了总结,并加入一些自己的理解,与诸君共同交流、学习、进步,欢迎诸位留言交流。

一、用户

开发一款产品目的是服务于用户,说白了用户就是我们的金主“爸爸”,所以开发一款产品前应该做的就是定义我们的用户群体并从用户的角度去考虑问题,这样才能让我们的产品拥有更大的市场——归结一句就是从用户思维考虑问题。

二、定位

每一个产品开发初期都存在一个定位,后期的业务拓展都将服务该定位。就拿我们团队来说,核心是为媒体服务(为媒体投放广告),后期开展的工作都是围绕该核心进行,例如开发了很多智能工具,其目的是为了媒体进行最优投放。

三、需求

每一个产品都是为了解决人们生活中的痛点问题,所以我们应该有一双善于发现的眼睛,发现用户的心理诉求,让用户变的更方便、更舒适、更“懒”。基于我们发现的需求,按照需求的紧急程度依次完成。

四、时机

说白了就是要了解外部环境。就拿前端工程师找工作来说,目前市场上需要的是技术栈是React、Vue,你面试的时候再谈jQuery是不是就有点与外部环境脱节了,如果你是后者,显然没有了解目前的技术环境。

五、匠心

每一位工程师对待自己的产品都应该有工匠精神,不断进行打磨、不断进行优化、不断做的更加傻瓜好用,这样才能做到业界领先。

六、危机

生于忧患死于安乐,时刻保持危机感,不断去了解我们产品当前的状况,基于内外部环境变换去优化产品,才能让产品的生命周期更长。(此处有个疑问,短视频行业的未来是不是AR、VR、全息投影?)

七、合作

合作的前提条件是利益,找到共赢点才能更好的合作。

八、商业化

不管是谁,开发一款产品的目的肯定是为了赚钱,考虑好该产品的盈利点并获得利益才能正向的促进该产品的“进化”——(产品赚钱如果不损害用户体验该多好,感觉微信朋友圈的广告就很好,不仔细看根本看不出是广告)

欢迎大家关注公众号(回复“书籍”获取大量前端学习资料,回复“前端视频”获取大量前端教学视频)
1.如果觉得这篇文章还不错,来个分享、点赞、吧,让更多的人也看到
2.关注公众号执鸢者,领取学习资料,定期为你推送原创深度好文

image
查看原文

赞 2 收藏 2 评论 0

执鸢者 发布了文章 · 10月17日

三步法解析Express源码

关注公众号“执鸢者”,获取大量教学视频及私人总结面筋并进入专业交流群.
image
在抖音上有幸看到一个程序员讲述如何阅读源代码,主要分为三步:领悟思想、把握设计、体会细节。

  1. 领悟思想:只需体会作者设计框架的初衷和目的
  2. 把握设计:只需体会代码的接口和抽象类以及宏观的设计
  3. 体会细节:是基于顶层的抽象接口设计,逐渐展开代码的画卷

基于上述三步法,迫不及待的拿Express开刀了。本次源码解析有什么不到位的地方各位读者可以在下面留言,我们一起交流。

一、领悟思想

在Express中文网上,介绍Express是基于Node.js平台,快速、开放、极简的Web开发框架。在这句话里面可以得到解读出以下几点含义:

  1. Express是基于Node.js平台,并且具备快速、极简的特点,说明其初衷就是为了通过扩展Node的功能来提高开发效率。
  2. 开放的特点说明该框架不会对开发者过多的限制,可以自由的发挥想象进行功能的扩展。
  3. Express是Web开发框架,说明作者的定位就是为了更加方便的帮助我们处理HTTP的请求和响应。

二、把握设计

理解了作者设计的思想,下面从源码目录、核心设计原理及抽象接口三个层面来对Express进行整体的把握。

2.1 源码目录

如下所示是Express的源码目录,相比较来说还是比较简单的。

├─application.js---创建Express应用后可直接调用的api均在此处(核心)<br/>
├─express.js---入口文件,创建一个Express应用<br/>
├─request.js---丰富了http中request实例上的功能<br/>
├─response.js---丰富了http中response实例上的功能<br/>
├─utils.js---工具函数<br/>
├─view.js---与模板渲染相关的内容<br/>
├─router---与路由相关的内容(核心)<br/>
| ├─index.js<br/>
| ├─layer.js<br/>
| └route.js<br/>
├─middleware---与中间件相关的内容<br/>
| ├─init.js---会将新增加在request和response新增加的功能挂载到原始请求的request和response的原型上<br/>
| └query.js---将请求url中的query部分添加到request的query属性上<br/>

2.2 抽象接口

对源码的目录结构有了一定了解,下面利用UML类图对该系统各个模块的依赖关系进一步了解,为后续源码分析打好基础。

2.3 设计原理

这一部分是整个Express框架的核心,下图是整个框架的运行流程,一看是不是很懵逼,为了搞清楚这一部分,需要明确四个概念:Application、Router、Layer、Route。

为了明确上述四个概念,先引入一段代码
const express = require('./express');
const res = require('./response');
const app = express();
app.get('/test1', (req, res, next) => {
    console.log('one');
    next();
}, (req, res) => {
    console.log('two');
    res.end('two');
})
app.get('/test2', (req, res, next) => {
    console.log('three');
    next();
}, (req, res) => {
    console.log('four');
    res.end('four');
})
app.listen(3000);
  1. Application
    表示一个Express应用,通过express()即可进行创建。
  2. Router<
    路由系统,用于调度整个系统的运行,在上述代码中该路由系统包含app.get('/test1',……)和app.get('/test2',……)两大部分
  3. Layer
    代表一层,对于上述代码中app.get('/test1',……)和app.get('/test2',……)都可以成为一个Layer
  4. Route
    一个Layer中会有多个处理函数的情况,这多个处理函数构成了Route,而Route中的每一个函数又成为Route中的Layer。对于上述代码中,app.get('/test1',……)中的两个函数构成一个Route,每个函数又是Route中的Layer。

了解完上述概念后,结合该幅图,就大概能对整个流程有了直观感受。首先启动服务,然后客户端发起了http://localhost:3000/test2的请求,该过程应该如何运行呢?

  1. 启动服务时会依次执行程序,将该路由系统中的路径、请求方法、处理函数进行存储(这些信息根据一定结构存储在Router、Layer和Route中)
  2. 对相应的地址进行监听,等待请求到达。
  3. 请求到达,首先根据请求的path去从上到下进行匹配,路径匹配正确则进入该Layer,否则跳出该Layer。
  4. 若匹配到该Layer,则进行请求方式的匹配,若匹配方式匹配正确,则执行该对应Route中的函数。

上述解释的比较简单,后续会在细节部分进一步阐述。

三、体会细节

通过上述对Express设计原理的分析,下面将从两个方面做进一步的源码解读,下面流程图是一个常见的Express项目的过程,首先会进行app实例初始化、然后调用一系列中间件,最后建立监听。对于整个工程的运行来说,主要分为两个阶段:初始化阶段、请求处理阶段,下面将以app.get()为例来阐述一下该核心细节。

3.1 初始化阶段

下面利用app.get()这个路由来了解一下工程的初始化阶段。

  1. 首先来看一下app.get()的内容(源代码中app.get()是通过遍历methods的方式产生)

    app.get = function(path){
        // ……
        this.lazyrouter();
    
        var route = this._router.route(path);
        route.get.apply(route, slice.call(arguments, 1));
        return this;
    };
  2. 在app.lazyrouter()会完成router的实例化过程

    app.lazyrouter = function lazyrouter() {
      if (!this._router) {
        this._router = new Router({
          caseSensitive: this.enabled('case sensitive routing'),
          strict: this.enabled('strict routing')
        });
    
        // 此处会使用一些中间件
        this._router.use(query(this.get('query parser fn')));
        this._router.use(middleware.init(this));
      }
    };

    注意:该过程中其实是利用了单例模式,保证整个过程中获取router实例的唯一性。

  3. 调用router.route()方法完成layer的实例化、处理及保存,并返回实例化后的route。(注意源码中是proto.route)

    router.prototype.route = function route(path) {
      var route = new Route(path);
      var layer = new Layer(path, {
        sensitive: this.caseSensitive,
        strict: this.strict,
        end: true
      }, route.dispatch.bind(route));
    
      layer.route = route;// 把route放到layer上
    
      this.stack.push(layer); // 把layer放到数组中
      return route;
    };
  4. 将该app.get()中的函数存储到route的stack中。(注意源码中也是通过遍历method的方式将get挂载到route的prototype上)

    Route.prototype.get = function(){
        var handles = flatten(slice.call(arguments));
    
        for (var i = 0; i < handles.length; i++) {
          var handle = handles[i];
          // ……
          // 给route添加layer,这个层中需要存放方法名和handler
          var layer = Layer('/', {}, handle);
          layer.method = method;
    
          this.methods[method] = true;
          this.stack.push(layer);
        }
    
        return this;
      };

注意:上述代码均删除了源码中一些异常判断逻辑,方便读者看清整体框架。

通过上述的分析,可以看出初始化阶段主要做了两件事情:

  1. 将路由处理方式(app.get()、app.post()……)、app.use()等划分为路由系统中的一个Layer。
  2. 对于每一个层中的处理函数全部存储至Route对象中,一个Route对象与一个Layer相互映射。

3.2 请求处理阶段

当服务启动后即进入监听状态,等待请求到达后进行处理。

  1. app.listen()使服务进入监听状态(实质上是调用了http模块)

    app.listen = function listen() {
      var server = http.createServer(this);
      return server.listen.apply(server, arguments);
    };
  2. 当连接建立会调用app实例,app实例中会立即执行app.handle()函数,app.handle()函数会立即调用路由系统的处理函数router.handle()

    app.handle = function handle(req, res, callback) {
      var router = this._router;
      // 如果路由系统中处理不了这个请求,就调用done方法
      var done = callback || finalhandler(req, res, {
        env: this.get('env'),
        onerror: logerror.bind(this)
      });
      //……
      router.handle(req, res, done);
    };
  3. router.handle()主要是根据路径获取是否有匹配的layer,当匹配到之后则调用layer.prototype.handle_request()去执行route中内容的处理

    router.prototype.handle = function handle(req, res, out) {
      // 这个地方参数out就是done,当所有都匹配不到,就从路由系统中出来,名字很形象
      var self = this;
      // ……
      var stack = self.stack;
      
      // ……
    
      next();
    
      function next(err) {
        // ……
        // get pathname of request
        var path = getPathname(req);
    
        // find next matching layer
        var layer;
        var match;
        var route;
    
        while (match !== true && idx < stack.length) {
          layer = stack[idx++];
          match = matchLayer(layer, path);
          route = layer.route;
          // ……
        }
    
        // no match
        if (match !== true) {
          return done(layerError);
        }
        // ……
    
        // Capture one-time layer values
        req.params = self.mergeParams
          ? mergeParams(layer.params, parentParams)
          : layer.params;
        var layerPath = layer.path;
    
        // this should be done for the layer
        self.process_params(layer, paramcalled, req, res, function (err) {
          if (err) {
            return next(layerError || err);
          }
    
          if (route) {
            return layer.handle_request(req, res, next);
          }
    
          trim_prefix(layer, layerError, layerPath, path);
        });
      }
    
      function trim_prefix(layer, layerError, layerPath, path) {
        // ……
    
        if (layerError) {
          layer.handle_error(layerError, req, res, next);
        } else {
          layer.handle_request(req, res, next);
        }
      }
    };
    
  4. layer.handle_request()会调用route.dispatch()触发route中内容的执行

    Layer.prototype.handle_request = function handle(req, res, next) {
      var fn = this.handle;
    
      if (fn.length > 3) {
        // not a standard request handler
        return next();
      }
    
      try {
        fn(req, res, next);
      } catch (err) {
        next(err);
      }
    };
  5. route中的通过判断请求的方法和route中layer的方法是否匹配,匹配的话则执行相应函数,若所有route中的layer都不匹配,则调到外层的layer中继续执行。

    Route.prototype.dispatch = function dispatch(req, res, done) {
      var idx = 0;
      var stack = this.stack;
      if (stack.length === 0) {
        return done();
      }
    
      var method = req.method.toLowerCase();
      // ……
        
      next();
      // 此next方法是用户调用的next,如果调用next会执行内层的next方法,如果没有匹配到会调用外层的next方法
      function next(err) {
        // ……
    
        var layer = stack[idx++];
        if (!layer) {
          return done(err);
        }
    
        if (layer.method && layer.method !== method) {
          return next(err);
        }
    
        // 如果当前route中的layer的方法匹配到了,执行此layer上的handler
        if (err) {
          layer.handle_error(err, req, res, next);
        } else {
          layer.handle_request(req, res, next);
        }
      }
    };

通过上述的分析,可以看出初始化阶段主要做了两件事情:

  1. 首先判断layer中的path和请求的path是否一致,一致则会进入route进行处理,否则调到下一层layer
  2. 在route中会判断route中的layer与请求方法是否一致,一致的话则函数执行,否则不执行,所有route中的layer执行完后跳到下层的layer进行执行。

1.如果觉得这篇文章还不错,来个分享、点赞、吧,让更多的人也看到

2.关注公众号执鸢者,领取学习资料,定期为你推送原创深度好文

欢迎大家关注公众号(回复“书籍”获取大量前端学习资料,回复“前端视频”获取大量前端教学视频)
查看原文

赞 5 收藏 5 评论 0

执鸢者 发布了文章 · 9月27日

一篇搞定前端高频手撕算法题(36道)

关注公众号“执鸢者”,获取大量教学视频及私人总结面筋并进入专业交流群.
image.png

目前互联网行业目前正在处于内卷状态,各个大厂不断提高招人门槛,前端工程师找工作也越发艰难,为了助力各位老铁能够在面试过程中脱颖而出,我结合自己的面试经验,准备了这三十六道面试过程中的手撕算法题,与各位共享。

一、冒泡排序

冒泡排序的思路:遍历数组,然后将最大数沉到最底部;<br/>时间复杂度:O(N^2);<br/>空间复杂度:O(1)
function BubbleSort(arr) {
    if(arr == null || arr.length <= 0){
        return [];
    }
    var len = arr.length;
    for(var end = len - 1; end > 0; end--){
        for(var i = 0; i < end; i++) {
            if(arr[i] > arr[i + 1]){
                swap(arr, i, i + 1);
            }
        }
    }
    return arr;
}
function swap(arr, i, j){
    // var temp = arr[i];
    // arr[i] = arr[j];
    // arr[j] = temp;
    //交换也可以用异或运算符
    arr[i] = arr[i] ^ arr[j];
    arr[j] = arr[i] ^ arr[j];
    arr[i] = arr[i] ^ arr[j];
}

二、选择排序

选择排序的实现思路:遍历数组,把最小数放在头部;<br/>时间复杂度:O(N^2);<br/>空间复杂度:O(1)
function SelectionSort(arr) {
    if(arr == null || arr.length < 0) {
        return [];
    }
    for(var i = 0; i < arr.length - 1; i++) {
        var minIndex = i;
        for(var j = i + 1; j < arr.length; j++) {
            minIndex = arr[j] < arr[minIndex] ? j : minIndex;
        }
        swap(arr, i, minIndex);
    }
    return arr;
}

function swap(arr, i, j) {
    arr[i] = arr[i] ^ arr[j];
    arr[j] = arr[i] ^ arr[j];
    arr[i] = arr[i] ^ arr[j];
}

三、插入排序

插入排序实现思路:将一个新的数,和前面的比较,只要当前数小于前一个则和前一个交换位置,否则终止;<br/>时间复杂度:O(N^2);<br/>空间复杂度:O(1)
function insertSort(arr) {
    if(arr == null  || arr.length <= 0){
        return [];
    }
    var len = arr.length;
    for(var i = 1; i < len; i++) {
        for(var j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
            swap(arr, j, j + 1);
        }
    }
    return arr;
}

function swap(arr, i, j){
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

四、归并排序

归并排序的思路:<br/>1.先左侧部分排好序<br/>2.再右侧部分排好序<br/>3.再准备一个辅助数组,用外排的方式,小的开始填,直到有个动到末尾,将另一个数组剩余部分拷贝到末尾<br/>4.再将辅助数组拷贝回原数组<br/>时间复杂度:O(N * logN)<br/>空间复杂度:O(N)
// 递归实现

function mergeSort(arr){
    if(arr == null  || arr.length <= 0){
        return [];
    }
    sortProcess(arr, 0, arr.length - 1);
    return arr;
}

function sortProcess(arr, L, R){
    //递归的终止条件,就是左右边界索引一样
    if(L == R){
        return;
    }
    var middle = L + ((R - L) >> 1);//找出中间值
    sortProcess(arr, L, middle);//对左侧部分进行递归
    sortProcess(arr, middle + 1, R);//对右侧部分进行递归
    merge(arr, L, middle, R);//然后利用外排方式进行结合
}

function merge(arr, L, middle, R){
    var help = [];
    var l = L;
    var r = middle + 1;
    var index = 0;
    //利用外排方式进行
    while(l <= middle && r <= R){
        help[index++] = arr[l] < arr[r] ? arr[l++] : arr[r++];
    }
    while(l <= middle){
        help.push(arr[l++]);
    }
    while(r <= R){
        help.push(arr[r++]);
    }

    for(var i = 0; i < help.length; i++) {
        arr[L + i] = help[i];
    }
    //arr.splice(L, help.length, ...help);//这个利用了ES6的语法
}
// 循环实现

function mergeSort(arr){
    if(arr ==null || arr.length <= 0){
        return [];
    }
    var len = arr.length;
    //i每次乘2,是因为每次合并以后小组元素就变成两倍个了
    for(var i = 1; i < len; i *= 2){
        var index = 0;//第一组的起始索引
        while( 2 * i  + index <= len){
            index += 2 * i;
            merge(arr, index - 2 * i, index - i, index);
        }
        //说明剩余两个小组,但其中一个小组数据的数量已经不足2的幂次方个
        if(index + i < len){
            merge(arr, index, index + i, len);
        }
    }
    return arr;
}

//利用外排的方式进行结合
function merge(arr, start, mid, end){
    //新建一个辅助数组
    var help = [];
    var l = start, r = mid;
    var i = 0;
    while(l < mid && r < end){
        help[i++] = arr[l] < arr[r] ? arr[l++] : arr[r++];
    }
    while(l < mid){
        help[i++] = arr[l++];
    }
    while(r < end){
        help[i++] = arr[r++];
    }
    for(var j = 0; j < help.length; j++){
        arr[start + j] = help[j];
    }
}

五、快速排序

快速排序实现思路:随机取出一个值进行划分,大于该值放右边,小于该值放左边(该算法在经典快排的基础上经过荷兰国旗思想和随机思想进行了改造)<br/>时间复杂度:O(N*logN) <br/>空间复杂度:O(logN)
function quickSort(arr) {
    if(arr == null || arr.length <= 0){
        return [];
    }
    quick(arr, 0, arr.length - 1);
}

function quick(arr, L, R){
    //递归结束条件是L >= R
    if(L < R){
        //随机找一个值,然后和最后一个值进行交换,将经典排序变为快速排序
        swap(arr, L + Math.floor(Math.random() * (R - L + 1)), R);
        //利用荷兰国旗问题获得划分的边界,返回的值是小于区域的最大索引和大于区域的最小索引,在这利用荷兰国旗问题将等于区域部分就不用动了
        var tempArr = partition(arr, L, R, arr[R]);
        quick(arr, L, tempArr[0]);
        quick(arr, tempArr[1], R);
    }
}
//返回值是小于区域最后的索引和大于区域的第一个索引
function partition(arr, L, R, num){
    var less = L - 1;
    var more = R + 1;
    var cur = L;
    while(cur < more){
        if(arr[cur] < num){
            swap(arr, ++less, cur++);
        }else if(arr[cur] > num) {
            swap(arr, --more, cur);
        }else{
            cur++;
        }
    }
    return [less, more];
}
function swap(arr, i, j){
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

六、堆排序

堆排序思路:<br/>1.让数组变成大根堆<br/>2.把最后一个位置和堆顶做交换<br/>3.则最大值在最后,则剩下部分做heapify,则重新调整为大根堆,则堆顶位置和该部分最后位置做交换<br/>4.重复进行,直到减完,则这样最后就调整完毕,整个数组排完序(为一个升序)<br/>时间复杂度:O(N * logN)<br/>空间复杂度:O(1)
function heapSort(arr) {
    if(arr == null || arr.length <= 0) {
        return [];
    }

    //首先是建立大顶堆的过程
    for(var i = 0; i < arr.length; i++) {
        heapInsert(arr, i);
    }
    var size = arr.length;//这个值用来指定多少个数组成堆,当得到一个排序的值后这个值减一
    //将堆顶和最后一个位置交换
    /**
     * 当大顶堆建立完成后,然后不断将最后一个位置和堆顶交换;
     * 这样最大值就到了最后,则剩下部分做heapify,重新调整为大根堆,则堆顶位置和倒数第二个位置交换,重复进行,直到全部排序完毕*/
    //由于前面已经是大顶堆,所以直接交换
    swap(arr, 0, --size);
    while(size > 0) {
        //重新变成大顶堆
        heapify(arr, 0, size);
        //进行交换
        swap(arr, 0, --size);
    }
}

//加堆过程中
function heapInsert(arr, index) {
    //比较当前位置和其父位置,若大于其父位置,则进行交换,并将索引移动到其父位置进行循环,否则跳过
    //结束条件是比父位置小或者到达根节点处
    while(arr[index] > arr[parseInt((index - 1) / 2)]){
        //进行交换
        swap(arr, index, parseInt((index - 1) / 2));
        index = parseInt((index - 1) / 2);
    }
}
//减堆过程
/**
 * size指的是这个数组前多少个数构成一个堆
 * 如果你想把堆顶弹出,则把堆顶和最后一个数交换,把size减1,然后从0位置经历一次heapify,调整一下,剩余部分变成大顶堆*/
function heapify(arr, index, size) {
    var left = 2 * index + 1;
    while(left < size) {
        var largest = (left + 1 < size && arr[left] < arr[left + 1]) ? left + 1 : left;
        largest = arr[index] > arr[largest] ? index : largest;

        //如果最大值索引和传进来索引一样,则该值到达指定位置,直接结束循环
        if(index == largest) {
            break;
        }

        //进行交换,并改变索引和其左子节点
        swap(arr, index, largest);
        index = largest;
        left = 2 * index + 1;
    }
}

function swap(arr, i, j) {
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

七、桶排序

桶排序会经历三次遍历:准备一个数组、遍历一遍数组、重构一遍数组,是非基于比较的排序,下面以一个问题来阐述其思路。<br/>问题:<br/> 给定一个数组,求如果排序之后,相邻两个数的最大差值,要求时间复杂度O(N),且要求不能用基于比较的排序<br/>
思路:<br/>1.准备桶:数组中有N个数就准备N+1个桶<br/>2.遍历一遍数组,找到最大值max和最小值min
。若min = max,则差值=0;若min≠max,则最小值放在0号桶,最大值放在N号桶,剩下的数属于哪个范围就进哪个桶<br/>3.根据鸽笼原理,则肯定有一个桶为空桶,设计该桶的目的是为了否定最大值在一个桶中,则最大差值的两个数一定来自于两个桶,但空桶两侧并不一定是最大值<br/>4.所以只记录所有进入该桶的最小值min和最大值max和一个布尔值表示该桶有没有值<br/>5.然后遍历这个数组,如果桶是空的,则跳到下一个数,如果桶非空,则找前一个非空桶,则最大差值=当前桶min - 上一个非空桶max,用全局变量更新最大值<br/>时间复杂度:O(N)<br/>空间复杂度:O(N)
function maxGap(arr) {
    if(arr == null || arr.length <= 0) {
        return 0;
    }
    var len = arr.length;
    var max = -Infinity, min = Infinity;
    //遍历一遍数组,找到最大值max和最小值min
    for(var i = 0; i < len; i++) {
        max = max > arr[i] ? max : arr[i];
        min = min > arr[i] ? arr[i] : min;
    }

    //若min = max,则差值为0;
    if(min == max) {
        return 0;
    }

    var hasNum = new Array(len + 1);
    var mins = new Array(len + 1);
    var maxs = new Array(len + 1);

    var bid = 0;//指定桶的编号

    for(var i = 0; i < len; i++) {
        bid = bucket(arr[i], min, max, len);//获得该值是在哪个桶//由于有N+1个桶,所以间隔就是N个,所以此处除以的是len,然后通过这个函数得到应该放到哪个桶里
        maxs[bid] = hasNum[bid] ? Math.max(arr[i], maxs[bid]) : arr[i];
        mins[bid] = hasNum[bid] ? Math.min(arr[i], mins[bid]) : arr[i];
        hasNum[bid] = true;
    }

    var res = 0;
    var lastMax = maxs[0];

    for(var i = 0; i < len + 1; i++) {
        if(hasNum[i]) {
            res = Math.max(mins[i] - lastMax, res);
            lastMax = maxs[i];
        }
    }
    return res;
}

//获得桶号
//这个函数用于判断在哪个桶中,参数分别为值、最小值、最大值、桶间隔
function bucket(value, min, max, len) {
    return parseInt((value - min) / ((max - min) / len));
}

八、new

function New (Fn, ...arg) {
    // 一个新的对象被创建
    const result = {};
    // 该对象的__proto__属性指向该构造函数的原型
    if (Fn.prototype !== null) {
        Object.setPrototypeOf(result, Fn.prototype);
    }
    // 将执行上下文(this)绑定到新创建的对象中
    const returnResult = Fn.apply(result, arg);
    // 如果构造函数有返回值,那么这个返回值将取代第一步中新创建的对象。否则返回该对象
    if ((typeof returnResult === "object" || typeof returnResult === "function") && returnResult !== null) {
        return returnResult;
    }
    return result;
}

九、instanceof

function Instanceof(left, right) {
    let leftVal = Object.getPrototypeOf(left);
    const rightVal = right.prototype;

    while (leftVal !== null) {
        if (leftVal === rightVal)
            return true;
        leftVal = Object.getPrototypeOf(leftVal);
    }
    return false;
}

十、 Object.create()

Object.ObjectCreate = (proto, propertiesObject)=> {
    // 对输入进行检测
    if (typeof proto !== 'object' && typeof proto !== 'function' && proto !== null) {
        throw new Error(`Object prototype may only be an Object or null:${proto}`);
    }
    // 新建一个对象
    const result = {};
    // 将该对象的原型设置为proto
    Object.setPrototypeOf(result, proto);
    // 将属性赋值给该对象
    Object.defineProperties(result, propertiesObject);
    // 返回该对象
    return result;
}

十一、 Object assign()

function ObjectAssign(target, ...sources) {
    // 对第一个参数的判断,不能为undefined和null
    if (target === undefined || target === null) {
        throw new TypeError('cannot convert first argument to object');
    }

    // 将第一个参数转换为对象(不是对象转换为对象)
    const targetObj = Object(target);
    // 将源对象(source)自身的所有可枚举属性复制到目标对象(target)
    for (let i = 0; i < sources.length; i++) {
        let source = sources[i];
        // 对于undefined和null在源角色中不会报错,会直接跳过
        if (source !== undefined && source !== null) {
            // 将源角色转换成对象
            // 需要将源角色自身的可枚举属性(包含Symbol值的属性)进行复制
            // Reflect.ownKeys(obj)  返回一个数组,包含对象自身的所有属性,不管属性名是Symbol还是字符串,也不管是否可枚举
            const keysArray = Reflect.ownKeys(Object(source));
            for (let nextIndex = 0; nextIndex < keysArray.length; nextIndex ++) {
                const nextKey = keysArray[nextIndex];
                // 去除不可枚举属性
                const desc = Object.getOwnPropertyDescriptor(source, nextKey);
                if (desc !== undefined && desc.enumerable) {
                    // 后面的属性会覆盖前面的属性
                    targetObj[nextKey] = source[nextKey];
                }
            }
        }
    }

    return targetObj;
}
// 由于挂载到Object的assign是不可枚举的,直接挂载上去是可枚举的,所以采用这种方式
if (typeof Object.myAssign !== 'function') {
    Object.defineProperty(Object, "myAssign", {
        value : ObjectAssign,
        writable: true,
        enumerable: false,
        configurable: true
    });
}

十二、 map

Array.prototype.myMap = function(fn) {
    // 判断输入的第一个参数是不是函数
    if (typeof fn !== 'function') {
        throw new TypeError(fn + 'is not a function');
    }

    // 获取需要处理的数组内容
    const arr = this;
    const len = arr.length;
    // 新建一个空数组用于装载新的内容
    const temp = new Array(len);

    // 对数组中每个值进行处理
    for (let i = 0; i < len; i++) {
        // 获取第二个参数,改变this指向
        let result = fn.call(arguments[1], arr[i], i, arr);
        temp[i] = result;
    }
    // 返回新的结果
    return temp;
}

十三、 filter

Array.prototype.myFilter = function (fn) {
    if (typeof fn !== 'function') {
        throw new TypeError(`${fn} is not a function`);
    }

    // 获取该数组
    const arr = this;
    // 获取该数组长度
    const len = this.length >>> 0;
    // 新建一个新的数组用于放置该内容
    const temp = [];

    // 对数组中每个值进行处理
    for (let i = 0; i < len; i++) {
        // 处理时注意this指向
        const result = fn.call(arguments[1], arr[i], i, arr);
        result && temp.push(arr[i]);
    }

    return temp;
}

十四、 reduce

Array.prototype.myReduce = function(fn) {
    if (typeof fn !== 'function') {
        throw new TypeError(`${fn} is not a function`);
    }

    const arr = this;
    const len = arr.length >>> 0;
    let value;// 最终返回的值
    let k = 0;// 当前索引

    if (arguments.length >= 2) {
        value = arguments[1];
    } else {
        // 当数组为稀疏数组时,判断数组当前是否有元素,如果没有索引加一
        while (k < len && !( k in arr)) {
            k++;
        }
        // 如果数组为空且初始值不存在则报错
        if (k >= len) {
            throw new TypeError('Reduce of empty array with no initial value');
        }
        value = arr[k++];
    }
    while (k < len) {
        if (k in arr) {
            value = fn(value, arr[k], k, arr);
        }
        k++;
    }

    return value;
}

十五、 flat

// 使用reduce和concat
Array.prototype.flat1 = function () {
    return this.reduce((acc, val) => acc.concat(val), []);
}
// 使用reduce + concat + isArray +recursivity
Array.prototype.flat2 = function (deep = 1) {
    const flatDeep = (arr, deep = 1) => {
        // return arr.reduce((acc, val) => Array.isArray(val) && deep > 0 ? [...acc, ...flatDeep(val, deep - 1)] : [...acc, val], []);
        return deep > 0 ? arr.reduce((acc, val) => acc.concat(Array.isArray(val) ? flatDeep(val, deep - 1) : val), []) : arr.slice();
    }

    return flatDeep(this, deep);
}
// 使用forEach + concat + isArray +recursivity
// forEach 遍历数组会自动跳过空元素
Array.prototype.flat3 = function (deep = 1) {
    const result = [];
    (function flat(arr, deep) {
        arr.forEach((item) => {
            if (Array.isArray(item) && deep > 0) {
                flat(item, deep - 1);
            } else {
                result.push(item);
            }
        })
    })(this, deep);

    return result;
}
// 使用for of + concat + isArray +recursivity
// for of 遍历数组会自动跳过空元素
Array.prototype.flat4 = function (deep = 1) {
    const result = [];
    (function flat(arr, deep) {
        for(let item of arr) {
            if (Array.isArray(item) && deep > 0) {
                flat(item, deep - 1);
            } else {
                // 去除空元素,因为void 表达式返回的都是undefined,不适用undefined是因为undefined在局部变量会被重写
                item !== void 0 && result.push(item);
            }
        }
    })(this, deep);

    return result;
}
// 使用堆栈stack
Array.prototype.flat5 = function(deep = 1) {
    const stack = [...this];
    const result = [];
    while (stack.length > 0) {
        const next = stack.pop();
        if (Array.isArray(next)) {
            stack.push(...next);
        } else {
            result.push(next);
        }
    }

    // 反转恢复原来顺序
    return result.reverse();
}

十六、 call

Function.prototype.call1 = function(context, ...args) {
    // 获取第一个参数(注意第一个参数为null或undefined是,this指向window),构建对象
    context = context ? Object(context) : window;
    // 将对应函数传入该对象中
    context.fn = this;
    // 获取参数并执行相应函数
    let result = context.fn(...args);
    delete context.fn;
    

十七、 apply

Function.prototype.apply1 = function(context, arr) {
    context = context ? Object(context) : window;
    context.fn = this;

    let result = arr ? context.fn(...arr) : context.fn();

    delete context.fn;

    return result;
}

十八、 bind

Function.prototype.bind1 = function (context, ...args) {
    if (typeof this !== 'function') {
        throw new TypeError('The bound object needs to be a function');
    }

    const self = this;
    const fNOP = function() {};
    const fBound = function(...fBoundArgs) {
        // 指定this
        // 当作为构造函数时,this 指向实例,此时 this instanceof fBound 结果为 true
        return self.apply(this instanceof fNOP ? this : context, [...args, ...fBoundArgs]);
    }

    //  修改返回函数的 prototype 为绑定函数的 prototype,为了避免直接修改this的原型,所以新建了一个fNOP函数作为中介
    if (this.prototype) {
        fNOP.prototype = this.prototype;
    }
    fBound.prototype = new fNOP();

    return fBound;
}

十九、 防抖

function debounce(fn, wait, immediate) {
    let timer = null;
    return function(...args) {
        // 立即执行的功能(timer为空表示首次触发)
        if (immediate && !timer) {
            fn.apply(this, args);
        }
        // 有新的触发,则把定时器清空
        timer && clearTimeout(timer);
        // 重新计时
        timer = setTimeout(() => {
            fn.apply(this, args);
        }, wait)
    }
}

二十、 节流

// 时间戳版本
function throttle(fn, wait) {
    // 上一次执行时间
    let previous = 0;
    return function(...args) {
        // 当前时间
        let now = +new Date();
        if (now - previous > wait) {
            previous = now;
            fn.apply(this, args);
        }
    }
}
// 定时器版本
function throttle(fn, wait) {
    let timer = null;
    return function(...args) {
        if (!timer) {
            timer = setTimeout(() => {
                fn.apply(this, args);
                timer = null;
            }, wait)
        }
    }
}

二十一、深拷贝

// 乞巧版
function cloneDeep1(source) {
    return JSON.parse(JSON.stringify(source));
}
// 递归版
function cloneDeep2(source) {
    // 如果输入的为基本类型,直接返回
    if (!(typeof source === 'object' && source !== null)) {
        return source;
    }

    // 判断输入的为数组函数对象,进行相应的构建
    const target = Array.isArray(source) ? [] : {};

    for (let key in source) {
        // 判断是否是自身属性
        if (Object.prototype.hasOwnProperty.call(source, key)) {
            if (typeof source === 'object' && source !== null) {
                target[key] = cloneDeep2(source[key]);
            } else {
                target[key] = source[key];
            }
        }
    }

    return target;
}
// 循环方式
function cloneDeep3(source) {
    if (!(typeof source === 'object' && source !== null)) {
        return source;
    }

    const root = Array.isArray(source) ? [] : {};
    // 定义一个栈
    const loopList = [{
        parent: root,
        key: undefined,
        data: source,
    }];

    while (loopList.length > 0) {
        // 深度优先
        const node = loopList.pop();
        const parent = node.parent;
        const key = node.key;
        const data = node.data;

        // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
        let res = parent;
        if (typeof key !== 'undefined') {
            res = parent[key] = Array.isArray(data) ? [] : {};
        }

        for (let key in data) {
            if (data.hasOwnProperty(key)) {
                if (typeof data[key] === 'object' && data !== null) {
                    loopList.push({
                        parent: res,
                        key: key,
                        data: data[key],
                    });
                } else {
                    res[key] = data[key];
                }
            }
        }
    }

    return root;
}

二十二、 根据Promise/A+规范实现Promise

人家有相关标准,我们就要遵守,毕竟遵纪守法才是好公民,现在只能硬着头皮把这个标准过一遍。

下面就是基于Promise/A+规范实现的代码,已经经过promises-aplus-tests库进行了验证。
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
/**
 * Promise构造函数
 * excutor: 内部同步执行的函数
 */
class Promise {
    constructor(excutor) {
        const self = this;
        self.status = PENDING;
        self.onFulfilled = [];// 成功的回调
        self.onRejected = [];// 失败的回调

        // 异步处理成功调用的函数
        // PromiseA+ 2.1 状态只能由Pending转为fulfilled或rejected;fulfilled状态必须有一个value值;rejected状态必须有一个reason值。
        function resolve(value) {
            if (self.status === PENDING) {
                self.status = FULFILLED;
                self.value = value;
                 // PromiseA+ 2.2.6.1 相同promise的then可以被调用多次,当promise变为fulfilled状态,全部的onFulfilled回调按照原始调用then的顺序执行
                self.onFulfilled.forEach(fn => fn());
            }
        }

        function reject(reason) {
            if (self.status === PENDING) {
                self.status = REJECTED;
                self.reason = reason;
                // PromiseA+ 2.2.6.2 相同promise的then可以被调用多次,当promise变为rejected状态,全部的onRejected回调按照原始调用then的顺序执行
                self.onRejected.forEach(fn => fn());
            }
        }

        try {
            excutor(resolve, reject);
        } catch (e) {
            reject(e);
        }
    }

    then(onFulfilled, onRejected) {
        // PromiseA+ 2.2.1 onFulfilled和onRejected是可选参数
        // PromiseA+ 2.2.5 onFulfilled和onRejected必须被作为函数调用
        // PromiseA+ 2.2.7.3 如果onFulfilled不是函数且promise1状态是fulfilled,则promise2有相同的值且也是fulfilled状态
        // PromiseA+ 2.2.7.4 如果onRejected不是函数且promise1状态是rejected,则promise2有相同的值且也是rejected状态
        onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
        onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };

        const self = this;
        const promise = new Promise((resolve, reject) => {
            const handle = (callback, data) => {
                // PromiseA+ 2.2.4 onFulfilled或者onRejected需要在自己的执行上下文栈里被调用,所以此处用setTimeout
                setTimeout(() => {
                    try {
                         // PromiseA+ 2.2.2 如果onFulfilled是函数,则在fulfilled状态之后调用,第一个参数为value
                        // PromiseA+ 2.2.3 如果onRejected是函数,则在rejected状态之后调用,第一个参数为reason
                        const x = callback(data);
                        // PromiseA+ 2.2.7.1 如果onFulfilled或onRejected返回一个x值,运行这[[Resolve]](promise2, x)
                        resolvePromise(promise, x, resolve, reject);
                    } catch (e) {
                        // PromiseA+ 2.2.7.2 onFulfilled或onRejected抛出一个异常e,promise2必须以e的理由失败
                        reject(e);
                    }
                })
            }
            if (self.status === PENDING) {
                self.onFulfilled.push(() => {
                    handle(onFulfilled, self.value);
                });

                self.onRejected.push(() => {
                    handle(onRejected, self.reason);
                })
            } else if (self.status === FULFILLED) {
                setTimeout(() => {
                    handle(onFulfilled, self.value);
                })
            } else if (self.status === REJECTED) {
                setTimeout(() => {
                    handle(onRejected, self.reason);
                })
            }
        })

        return promise;
    }
}

function resolvePromise(promise, x, resolve, reject) {
    // PromiseA+ 2.3.1 如果promise和x引用同一对象,会以TypeError错误reject promise
    if (promise === x) {
        reject(new TypeError('Chaining Cycle'));
    }

    if (x && typeof x === 'object' || typeof x === 'function') {
        // PromiseA+ 2.3.3.3.3 如果resolvePromise和rejectPromise都被调用,或者对同一个参数进行多次调用,那么第一次调用优先,以后的调用都会被忽略。 
        let used;
        try {
            // PromiseA+ 2.3.3.1 let then be x.then
            // PromiseA+ 2.3.2 调用then方法已经包含了该条(该条是x是promise的处理)。
            let then = x.then;

            if (typeof then === 'function') {
                // PromiseA+ 2.3.3.3如果then是一个函数,用x作为this调用它。第一个参数是resolvePromise,第二个参数是rejectPromise
                // PromiseA+ 2.3.3.3.1 如果resolvePromise用一个值y调用,运行[[Resolve]](promise, y)
                // PromiseA+ 2.3.3.3.2 如果rejectPromise用一个原因r调用,用r拒绝promise。
                then.call(x, (y) => {
                    if (used) return;
                    used = true;
                    resolvePromise(promise, y, resolve, reject)
                }, (r) => {
                    if (used) return;
                    used = true;
                    reject(r);
                })
            } else {
                // PromiseA+ 如果then不是一个函数,变为fulfilled状态并传值为x
                if (used) return;
                used = true;
                resolve(x);
            }
        } catch (e) {
            // PromiseA+ 2.3.3.2 如果检索属性x.then抛出异常e,则以e为原因拒绝promise
            // PromiseA+ 2.3.3.4 如果调用then抛出异常,但是resolvePromise或rejectPromise已经执行,则忽略它
            if (used) return;
            used = true;
            reject(e);
        }

    } else {
        // PromiseA+ 2.3.4 如果x不是一个对象或函数,状态变为fulfilled并传值x
        resolve(x);
    }
}

二十三、 Promise.resolve()

class Promise {
    // ...
    // 将现有对象转为 Promise 对象
    static resolve(value) {
        // 如果参数是 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。
        if (value instanceof Promise) return value;

        // 参数是一个thenable对象(具有then方法的对象),Promise.resolve方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then方法。
        if (typeof value === 'object' || typeof value === 'function') {
            try {
                let then = value.then;
                if (typeof then === 'function') {
                    return new Promise(then.bind(value));
                }
            } catch (e) {
                return new Promise((resolve, reject) => {
                    reject(e);
                })
            }
        }

        // 参数不是具有then方法的对象,或根本就不是对象,Promise.resolve方法返回一个新的 Promise 对象,状态为resolved。
        return new Promise((resolve, reject) => {
            resolve(value);
        })
    }
}

二十四、 Promise.reject()

class Promise {
    // ...
    // 返回一个新的 Promise 实例,该实例的状态为rejected。
    static reject(reason) {
        return new Promise((resolve, reject) => {
            reject(reason);
        })
    }
}

二十五、 Promise.all()

class Promise {
    // ...
    // 用于将多个 Promise 实例,包装成一个新的 Promise 实例。只有所有状态都变为fulfilled,p的状态才会是fulfilled
    static all(promises) {
        const values = [];
        let resolvedCount = 0;
        return new Promise((resolve, reject) => {
            promises.forEach((p, index) => {
                Promise.resolve(p).then(value => {
                    resolvedCount++;
                    values[index] = value;
                    if (resolvedCount === promises.length) {
                        resolve(values);
                    }
                }, reason => {
                    reject(reason);
                })
            })
        })
    }
}

二十六、 Promise.race()

class Promise {
    // ...
     // 只要有一个实例率先改变状态,状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给回调函数。
    static race(promises) {
        return new Promise((resolve, reject) => {
            promises.forEach((p, index) => {
                Promise.resolve(p).then(value => {
                    resolve(value);
                }, reason => {
                    reject(reason);
                })
            })
        })
    }
}

二十七、 Promise.catch()

class Promise {
    // ...
    // 是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数。
    catch(onRejected) {
        return this.then(undefined, onRejected);
    }
}

二十八、 Promise.finally()

class Promise {
    // ...
    // 用于指定不管 Promise 对象最后状态如何,都会执行的操作。
    finally(callback) {
        return this.then(
            value => Promise.resolve(callback()).then(() => value),
            reason => Promise.resolve(callback()).then(() => { throw reason })
        )
    }
}

二十九、Async实现原理

这是Async的实现原理,即将Generator函数作为参数放入run函数中,最终实现自动执行并返回Promise对象。
function run(genF) {
    // 返回值是Promise
    return new Promise((resolve, reject) => {
        const gen = genF();
        function step(nextF) {
            let next;
            try {
                // 执行该函数,获取一个有着value和done两个属性的对象
                next = nextF();
            } catch (e) {
                // 出现异常则将该Promise变为rejected状态
                reject(e);
            }

            // 判断是否到达末尾,Generator函数到达末尾则将该Promise变为fulfilled状态
            if (next.done) {
                return resolve(next.value);
            }

            // 没到达末尾,则利用Promise封装该value,直到执行完毕,反复调用step函数,实现自动执行
            Promise.resolve(next.value).then((v) => {
                step(() => gen.next(v))
            }, (e) => {
                step(() => gen.throw(e))
            })
        }

        step(() => gen.next(undefined));
    })
}

三十、发布订阅模式

// 发布订阅(TypeScript版)
interface Publish {
    registerObserver(eventType : string, subscribe : Subscribe) : void;
    remove(eventType : string, subscribe ?: Subscribe) : void;
    notifyObservers(eventType : string) : void;
}
interface SubscribesObject{
    [key : string] : Array<Subscribe>
}
class ConcretePublish implements Publish {
    private subscribes : SubscribesObject;

    constructor() {
        this.subscribes = {};
    }

    registerObserver(eventType : string, subscribe : Subscribe) : void {
        if (!this.subscribes[eventType]) {
            this.subscribes[eventType] = [];
        }

        this.subscribes[eventType].push(subscribe);
    }

    remove(eventType : string, subscribe ?: Subscribe) : void {
        const subscribeArray = this.subscribes[eventType];
        if (subscribeArray) {
            if (!subscribe) {
                delete this.subscribes[eventType];
            } else {
                for (let i = 0; i < subscribeArray.length; i++) {
                    if (subscribe === subscribeArray[i]) {
                        subscribeArray.splice(i, 1);
                    }
                }
            }
        }
    }

    notifyObservers(eventType : string, ...args : any[]) : void {
        const subscribes = this.subscribes[eventType];
        if (subscribes) {
            subscribes.forEach(subscribe => subscribe.update(...args))
        }
    }
}

interface Subscribe {
    update(...value : any[]) : void;
}

class ConcreteSubscribe1 implements Subscribe {
    public update(...value : any[]) : void {
        console.log('已经执行更新操作1,值为', ...value);
    }
}
class ConcreteSubscribe2 implements Subscribe {
    public update(...value : any[]) : void {
        console.log('已经执行更新操作2,值为', ...value);
    }
}

function main() {
    const publish = new ConcretePublish();
    const subscribe1 = new ConcreteSubscribe1();
    const subscribe2 = new ConcreteSubscribe2();

    publish.registerObserver('1', subscribe1);
    publish.registerObserver('2', subscribe2);

    publish.notifyObservers('2', '22222');
}

main();

三十一、懒加载

1)首先,不要将图片地址放到src属性中,而是放到其它属性(data-original)中。<br/>
2)页面加载完成后,根据scrollTop判断图片是否在用户的视野内,如果在,则将data-original属性中的值取出存放到src属性中。<br/>
3)在滚动事件中重复判断图片是否进入视野,如果进入,则将data-original属性中的值取出存放到src属性中。<br/>
elementNode.getAttribute(name):方法通过名称获取属性的值。<br/>
elementNode.setAttribute(name, value):方法创建或改变某个新属性。<br/>
elementNode.removeAttribute(name):方法通过名称删除属性的值。
//懒加载代码实现
var viewHeight = document.documentElement.clientHeight;//可视化区域的高度

function lazyload () {
    //获取所有要进行懒加载的图片
    let eles = document.querySelectorAll('img[data-original][lazyload]');//获取属性名中有data-original的
    Array.prototype.forEach.call(eles, function(item, index) {
        let rect;
        if(item.dataset.original === '') {
            return;
        }

        rect = item.getBoundingClientRect();

        //图片一进入可视区,动态加载
        if(rect.bottom >= 0 && rect.top < viewHeight) {
            !function () {
                let img = new Image();
                img.src = item.dataset.original;
                img.onload = function () {
                    item.src = img.src;
                }
                item.removeAttribute('data-original');
                item.removeAttribute('lazyload');
            }();
        }
    })
}

lazyload();

document.addEventListener('scroll', lazyload);

三十二、FileReader使用

function uploadMulFile(uploadFile) {
    return new Promise((resolve, reject) => {
        let fileLength = 0;
        let reader = new FileReader();
        reader.readAsText(uploadFile[fileLength]);
        reader.onabort = function(e) {
            console.log("文件读取异常");
        }
        reader.onerror = function(e) {
            console.log("文件读取错误");
        }

        reader.onload = function(e){
            if(e.target.result) {

                fileLength++;
                if(fileLength < uploadFile.length) {
                    reader.readAsText(uploadFile[fileLength]);
                }else{
                    resolve({
                        carArr,
                        crossArr,
                        roadArr
                    })
                }
            }
        }
    })
}

三十三、Ajax使用(非Promise版)

function ajax(url) {
    var XHR;
    //进行性能检测
    if(window.ActiveXObject) {
        XHR = new ActiveXObject("Microsoft.XMLHTTP");//兼容IE//IE浏览器中使用的请求的方法,需实例化
    }else if(window.XMLHttpRequest) {
        XHR = new XMLHttpRequest();//标准浏览器中的使用的请求方法,需实例化
    }else{
        XHR = null;
    }

    if(XHR) {
        XHR.onreadystatechange = function() {
            //readyState:Ajax请求服务的状态
            if(XHR.readyState == 4) {
                //status:页面的响应码
                if(XHR.status == 200) {
                    //返回的数据以string的形式返回
                    console.log(XHR.responseText);
                }
            }
        };

        XHR.open("get", url);//open(‘method’,‘url’,boolean);参数1:请求方式;参数2:请求文件的地址;参数3:设置是否异步,true表示异步, 默认值为 true 所以可以不写。
        XHR.send();
    }
}

三十四、Ajax使用(Promise版)

//Promise形式
function ajaxPromise(method, url, data) {
    var xhr = null;
    if(window.ActiveXObject) {
        xhr = new ActiveXObject("Microsoft.XMLHTTP");
    }else{
        xhr = new XMLHttpRequest();
    }

    return new Promise(function(resolve, reject) {
        xhr.onreadystatechange = function(){
            if(xhr.readyState === 4 ) {
                if(xhr.status === 200) {
                    resolve(JSON.parse(xhr.responseText));
                }else{
                    reject(xhr.status);
                }
            }
        };

        if(method.toUpperCase() == "GET") {
            var arr = [];
            for(var key in data) {
                arr.push(key + '=' + data[key]);
            }

            var getData = arr.join('&');
            xhr.open("GET", url + "?" + getData, true);//true表示异步
            xhr.send(null);
        }else if(method.toUpperCase() == "POST") {
            xhr.open("POST", url, true);
            xhr.responseType = "json";
            xhr.setRequestHeader('Content', 'application/x-www-form-urlencoded;charset=utf-8');
            xhr.send(data);
        }


    })
}

三十五、JsonP

function jsonp(url, onsuccess, onerror, charset) {
    var hash = Math.random().toString().slice(2);
    window['jsonp' + hash] = function(data) {
        if(onsuccess && typeof onsuccess == 'function') {
            onsuccess(data);
        }
    }

    var script = createScript(url + "?callback=jsonp" + hash, charset); // 动态产检一个script标签

    //监听加载成功的事件,获取数据,这个位置用了两个事件onload和onreadystatechange是为了兼容IE,因为IE9之前不支持onload事件,只支持onreadystatechange事件
    script.onload = script.onreadystatechange = function() {
        //若不存在readyState事件则证明不是IE浏览器,可以直接执行,若是的话,必须等到状态变为loaded或complete才可以执行
        if(!this.readyState || this.readyState == 'loaded' || this.readyState == 'complete') {
            script.onload = script.onreadystatechange = null;
            // 移除该script的DOM对象
            if(script.parentNode) {
                script.parentNode.removeChild(script);
            }

            //删除函数或变量
            window['jsonp' + hash] = null;
        }
    }

    script.onerror = function () {
        if(onerror && typeof onerror == 'function') {
            onerror();
        }
    }

    document.getElementsByTagName('head')[0].appendChild(script);//往html中增加这个标签,目的是把请求发送出去
}

function createScript(url, charset) {
    var script = document.createElement('script');
    script.setAttribute('type', 'text/javascript');
    charset && script.setAttribute('charset', charset);
    script.setAttribute('src', url);
    script.async = true;
}

三十六、将一个字符串转换为驼峰形式

//方式一:操作字符串数组
function transformStr2Hump1(str) {
    if(str == null) {
        return "";
    }
    var strArr = str.split('-');
    for(var i = 1; i < strArr.length; i++) {
        strArr[i] = strArr[i].charAt(0).toUpperCase() + strArr[i].substring(1);
    }
    return strArr.join('');
}

//方式二:操作字符数组
function transformStr2Hump2(str) {
    if(str == null) {
        return "";
    }
    var strArr  =str.split('');
    for(var i = 0; i < strArr.length; i++) {
        if(strArr[i] == "-"){
            //删除-
            strArr.splice(i, 1);
            //将该处改为大写
            if(i < strArr.length) {
                strArr[i] = strArr[i].toUpperCase();
            }
        }
    }
    return strArr.join("");
}

//方式三:利用正则
function transformStr2Hump3(str) {
    if(str == null) {
        return "";
    }
    var reg = /-(\w)/g;//匹配字母或数字或下划线或汉字
    return str.replace(reg, function($0, $1) {
        return $1.toUpperCase();
    })
}
欢迎大家关注公众号(回复“书籍”获取大量前端学习资料,回复“前端视频”获取大量前端教学视频)
查看原文

赞 40 收藏 31 评论 2

执鸢者 发布了文章 · 9月26日

十七张图玩转Node进程——榨干它

关注公众号“执鸢者”,获取大量教学视频及私人总结面筋并进入专业交流群,回复“Node”获取本节思维导图

众所周知,Node是单线程的,该应用只会占用一个CPU,但是当前服务器都含有多核CPU,Node应用运行在该机器上根本就不能榨干所有性能,为了榨干它,我们的多进程架构就出来了。今天就让我们一起来唠一唠Node如何应用多进程。

一、现状

Node是单线程的,但是服务器具有多个CPU,作为节约“粮食”的好孩子,肯定要想办法解决这个问题。

二、解决措施

为了解决该问题,发展出了多进程架构

三、child_process模块

3.1 创建子进程

child_process模块帮助我们构建多进程架构

3.2 事件

3.3 常用属性

3.4 常用方法

3.5 进程间通信(IPC)

3.6 保证系统健壮性

四、cluster模块

cluster模块帮助解决多核cpu利用率问题及帮助处理进程的健壮性问题。

4.1 优点

4.2 创建工作进程

4.3 cluster暴露内容

4.4 Worker类

4.5 cluster缺点

4.6 注意点

欢迎老铁们加群或者私聊
查看原文

赞 17 收藏 12 评论 2

执鸢者 发布了文章 · 9月20日

理论与API相结合理解Node中的网络通信

关注公众号“执鸢者”,获取大量教学视频及私人总结面筋(公众号原创文章)并进入专业交流群

随着Node发展,其应用越来越常见,特别是在我们前端圈,给我们注入了新的活力。在使用中,Node的网络通信部分主要涉及到传输层和应用层,本次就来一起唠一唠常见的TCP、UDP、HTTP协议的使用,通过理论与实践相结合,更加清楚的了解与使用这些协议。

一、传输层

1.1 TCP

TCP是面向连接的,并且通过流量控制、、序号、确认和定时器,确保正确的、按序的将数据从发送进程交付给接收进程。按照服是否进行监听进行划分,TCP分为服务器和客户端。下面来分开聊一下。

认真读Node的API,发现其与TCP的原理暗暗贴合,妈妈再也不用担心我记不住这些API了。先看看TCP的三次握手与四次挥手过程。
  • 三次握手

  1. TCP服务器进程先创建传输控制块TCB(线程控制块),时刻准备接受客户进程的连接请求,此时服务器就进入了LISTEN(监听)状态;
  2. TCP客户进程也是先创建传输控制块TCB,然后向服务器发出连接请求报文,这时报文首部中的同部位SYN=1,同时选择一个初始序列号 seq=x ,此时,TCP客户端进程进入了 SYN-SENT(同步已发送状态)状态。TCP规定,SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号。
  3. TCP服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己初始化一个序列号 seq=y,此时,TCP服务器进程进入了SYN-RCVD(同步收到)状态。这个报文也不能携带数据,但是同样要消耗一个序号。
  4. TCP客户进程收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=y+1,自己的序列号seq=x+1,此时,TCP连接建立,客户端进入ESTABLISHED(已建立连接)状态。TCP规定,ACK报文段可以携带数据,但是如果不携带数据则不消耗序号。
  5. 当服务器收到客户端的确认后也进入ESTABLISHED状态,此后双方就可以开始通信了。
  • 四次挥手

  1. 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
  2. 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
  3. 客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
  4. 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
  5. 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
  6. 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。

1.1.1 TCP服务器

在上述图中,仔细品,可以总结为如何创建服务、服务相关、连接相关。然后并将其中事件与三次握手、四次挥手进行联系,很容易发现其实关键过程都被进行了监听。
  1. 如何创建服务:TCP建立连接前需要进入监听状态,所以这就涉及到图中的创建方式,创建服务后才可以监听特定地址
  2. 服务相关:服务创建之后就要观察其状态,并根据其状态进行一些列的处理,这就是服务器事件做的事情。
  3. 连接相关:服务器监听的目的是有连接建立并进行通信,当连接建立之后就需要对整个数据传输过程进行监听,这就是连接事件。

1.1.2 TCP客户端

TCP客户端做的事情比较简单,只需要与对应服务器建立连接,即可进行数据的传输了。

1.1.3 注意事项

1.2 UDP

一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务.

二、应用层

2.1 HTTP

HTTP应该是日常开发中用的最多的协议,其是建立在TCP传输层之上的应用层协议。搞懂这个协议对前端整体水平的提高至关重要。

2.1.1 服务器

2.1.2 客户端

2.1.3 两个关键对象

IncomingMessage和ServerResponse两个对象在整个协议的使用中直观重要,特别是IncomingMessage。当我们在使用的时候,是不是不懂的如何去使用,这个时候是不是应该去考虑一下请求报文与响应报文的结构了?个人认为其关键属性刚好与请求报文和响应报文相呼应
  1. HTTP请求报文主要包括:请求行(方法字段、URI字段和协议版本)、请求头部以及请求的数据(实体)。
  2. HTTP响应报文分为三个部分:状态行(版本、状态码和原因语句)、首部行和实体。

欢迎老铁们加群或者私聊
查看原文

赞 1 收藏 1 评论 0

执鸢者 回答了问题 · 9月16日

解决CANVAS旋转图片,可旋转顺序为什么不对呢?

旋转中心的问题吧,应该将旋转中心移动到图片中心:ctx.translate(widrh/2,height/2)

关注 4 回答 4

执鸢者 回答了问题 · 9月15日

解决WebSocket实际项目使用的问题

  1. 你的6个接口是来自一个服务器吗?如果来自不同的服务器肯定要6个websocket
  2. 如果你是同一个服务器,设置一个类型来区分这6个接口数据即可。

关注 5 回答 5

执鸢者 发布了文章 · 9月13日

金九银十跳槽季——七种排序算法

关注公众号“执鸢者”,获取大量教学视频及私人总结面筋(公众号原创文章)并进入专业交流群

被前端面试中算法虐惨的小林准备大干一场,好好准备一下面试中的高频算法题,由于前端算法相比于后端手撕的算法较容易,所以小林准备从最基础的七种排序算法开始。前方高能,请抓住方向盘……

一、冒泡排序

冒泡排序的思路:遍历数组,然后将最大数沉到最底部;<br/>时间复杂度:O(N^2);<br/>空间复杂度:O(1)
function BubbleSort(arr) {
    if(arr == null || arr.length <= 0){
        return [];
    }
    var len = arr.length;
    for(var end = len - 1; end > 0; end--){
        for(var i = 0; i < end; i++) {
            if(arr[i] > arr[i + 1]){
                swap(arr, i, i + 1);
            }
        }
    }
    return arr;
}
function swap(arr, i, j){
    // var temp = arr[i];
    // arr[i] = arr[j];
    // arr[j] = temp;
    //交换也可以用异或运算符
    arr[i] = arr[i] ^ arr[j];
    arr[j] = arr[i] ^ arr[j];
    arr[i] = arr[i] ^ arr[j];
}

二、选择排序

选择排序的实现思路:遍历数组,把最小数放在头部;<br/>时间复杂度:O(N^2);<br/>空间复杂度:O(1)
function SelectionSort(arr) {
    if(arr == null || arr.length < 0) {
        return [];
    }
    for(var i = 0; i < arr.length - 1; i++) {
        var minIndex = i;
        for(var j = i + 1; j < arr.length; j++) {
            minIndex = arr[j] < arr[minIndex] ? j : minIndex;
        }
        swap(arr, i, minIndex);
    }
    return arr;
}

function swap(arr, i, j) {
    arr[i] = arr[i] ^ arr[j];
    arr[j] = arr[i] ^ arr[j];
    arr[i] = arr[i] ^ arr[j];
}

三、插入排序

插入排序实现思路:将一个新的数,和前面的比较,只要当前数小于前一个则和前一个交换位置,否则终止;<br/>时间复杂度:O(N^2);<br/>空间复杂度:O(1)
function insertSort(arr) {
    if(arr == null  || arr.length <= 0){
        return [];
    }
    var len = arr.length;
    for(var i = 1; i < len; i++) {
        for(var j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
            swap(arr, j, j + 1);
        }
    }
    return arr;
}

function swap(arr, i, j){
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

四、归并排序

归并排序的思路:<br/>1.先左侧部分排好序<br/>2.再右侧部分排好序<br/>3.再准备一个辅助数组,用外排的方式,小的开始填,直到有个动到末尾,将另一个数组剩余部分拷贝到末尾<br/>4.再将辅助数组拷贝回原数组<br/>时间复杂度:O(N * logN)<br/>空间复杂度:O(N)
// 递归实现

function mergeSort(arr){
    if(arr == null  || arr.length <= 0){
        return [];
    }
    sortProcess(arr, 0, arr.length - 1);
    return arr;
}

function sortProcess(arr, L, R){
    //递归的终止条件,就是左右边界索引一样
    if(L == R){
        return;
    }
    var middle = L + ((R - L) >> 1);//找出中间值
    sortProcess(arr, L, middle);//对左侧部分进行递归
    sortProcess(arr, middle + 1, R);//对右侧部分进行递归
    merge(arr, L, middle, R);//然后利用外排方式进行结合
}

function merge(arr, L, middle, R){
    var help = [];
    var l = L;
    var r = middle + 1;
    var index = 0;
    //利用外排方式进行
    while(l <= middle && r <= R){
        help[index++] = arr[l] < arr[r] ? arr[l++] : arr[r++];
    }
    while(l <= middle){
        help.push(arr[l++]);
    }
    while(r <= R){
        help.push(arr[r++]);
    }

    for(var i = 0; i < help.length; i++) {
        arr[L + i] = help[i];
    }
    //arr.splice(L, help.length, ...help);//这个利用了ES6的语法
}
// 循环实现

function mergeSort(arr){
    if(arr ==null || arr.length <= 0){
        return [];
    }
    var len = arr.length;
    //i每次乘2,是因为每次合并以后小组元素就变成两倍个了
    for(var i = 1; i < len; i *= 2){
        var index = 0;//第一组的起始索引
        while( 2 * i  + index <= len){
            index += 2 * i;
            merge(arr, index - 2 * i, index - i, index);
        }
        //说明剩余两个小组,但其中一个小组数据的数量已经不足2的幂次方个
        if(index + i < len){
            merge(arr, index, index + i, len);
        }
    }
    return arr;
}

//利用外排的方式进行结合
function merge(arr, start, mid, end){
    //新建一个辅助数组
    var help = [];
    var l = start, r = mid;
    var i = 0;
    while(l < mid && r < end){
        help[i++] = arr[l] < arr[r] ? arr[l++] : arr[r++];
    }
    while(l < mid){
        help[i++] = arr[l++];
    }
    while(r < end){
        help[i++] = arr[r++];
    }
    for(var j = 0; j < help.length; j++){
        arr[start + j] = help[j];
    }
}

五、快速排序

快速排序实现思路:随机取出一个值进行划分,大于该值放右边,小于该值放左边(该算法在经典快排的基础上经过荷兰国旗思想和随机思想进行了改造)<br/>时间复杂度:O(N*logN) <br/>空间复杂度:O(logN)
function quickSort(arr) {
    if(arr == null || arr.length <= 0){
        return [];
    }
    quick(arr, 0, arr.length - 1);
}

function quick(arr, L, R){
    //递归结束条件是L >= R
    if(L < R){
        //随机找一个值,然后和最后一个值进行交换,将经典排序变为快速排序
        swap(arr, L + Math.floor(Math.random() * (R - L + 1)), R);
        //利用荷兰国旗问题获得划分的边界,返回的值是小于区域的最大索引和大于区域的最小索引,在这利用荷兰国旗问题将等于区域部分就不用动了
        var tempArr = partition(arr, L, R, arr[R]);
        quick(arr, L, tempArr[0]);
        quick(arr, tempArr[1], R);
    }
}
//返回值是小于区域最后的索引和大于区域的第一个索引
function partition(arr, L, R, num){
    var less = L - 1;
    var more = R + 1;
    var cur = L;
    while(cur < more){
        if(arr[cur] < num){
            swap(arr, ++less, cur++);
        }else if(arr[cur] > num) {
            swap(arr, --more, cur);
        }else{
            cur++;
        }
    }
    return [less, more];
}
function swap(arr, i, j){
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

六、堆排序

堆排序思路:<br/>1.让数组变成大根堆<br/>2.把最后一个位置和堆顶做交换<br/>3.则最大值在最后,则剩下部分做heapify,则重新调整为大根堆,则堆顶位置和该部分最后位置做交换<br/>4.重复进行,直到减完,则这样最后就调整完毕,整个数组排完序(为一个升序)<br/>时间复杂度:O(N * logN)<br/>空间复杂度:O(1)
function heapSort(arr) {
    if(arr == null || arr.length <= 0) {
        return [];
    }

    //首先是建立大顶堆的过程
    for(var i = 0; i < arr.length; i++) {
        heapInsert(arr, i);
    }
    var size = arr.length;//这个值用来指定多少个数组成堆,当得到一个排序的值后这个值减一
    //将堆顶和最后一个位置交换
    /**
     * 当大顶堆建立完成后,然后不断将最后一个位置和堆顶交换;
     * 这样最大值就到了最后,则剩下部分做heapify,重新调整为大根堆,则堆顶位置和倒数第二个位置交换,重复进行,直到全部排序完毕*/
    //由于前面已经是大顶堆,所以直接交换
    swap(arr, 0, --size);
    while(size > 0) {
        //重新变成大顶堆
        heapify(arr, 0, size);
        //进行交换
        swap(arr, 0, --size);
    }
}

//加堆过程中
function heapInsert(arr, index) {
    //比较当前位置和其父位置,若大于其父位置,则进行交换,并将索引移动到其父位置进行循环,否则跳过
    //结束条件是比父位置小或者到达根节点处
    while(arr[index] > arr[parseInt((index - 1) / 2)]){
        //进行交换
        swap(arr, index, parseInt((index - 1) / 2));
        index = parseInt((index - 1) / 2);
    }
}
//减堆过程
/**
 * size指的是这个数组前多少个数构成一个堆
 * 如果你想把堆顶弹出,则把堆顶和最后一个数交换,把size减1,然后从0位置经历一次heapify,调整一下,剩余部分变成大顶堆*/
function heapify(arr, index, size) {
    var left = 2 * index + 1;
    while(left < size) {
        var largest = (left + 1 < size && arr[left] < arr[left + 1]) ? left + 1 : left;
        largest = arr[index] > arr[largest] ? index : largest;

        //如果最大值索引和传进来索引一样,则该值到达指定位置,直接结束循环
        if(index == largest) {
            break;
        }

        //进行交换,并改变索引和其左子节点
        swap(arr, index, largest);
        index = largest;
        left = 2 * index + 1;
    }
}

function swap(arr, i, j) {
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

七、桶排序

桶排序会经历三次遍历:准备一个数组、遍历一遍数组、重构一遍数组,是非基于比较的排序,下面以一个问题来阐述其思路。<br/>问题:<br/> 给定一个数组,求如果排序之后,相邻两个数的最大差值,要求时间复杂度O(N),且要求不能用基于比较的排序<br/>
思路:<br/>1.准备桶:数组中有N个数就准备N+1个桶<br/>2.遍历一遍数组,找到最大值max和最小值min
。若min = max,则差值=0;若min≠max,则最小值放在0号桶,最大值放在N号桶,剩下的数属于哪个范围就进哪个桶<br/>3.根据鸽笼原理,则肯定有一个桶为空桶,设计该桶的目的是为了否定最大值在一个桶中,则最大差值的两个数一定来自于两个桶,但空桶两侧并不一定是最大值<br/>4.所以只记录所有进入该桶的最小值min和最大值max和一个布尔值表示该桶有没有值<br/>5.然后遍历这个数组,如果桶是空的,则跳到下一个数,如果桶非空,则找前一个非空桶,则最大差值=当前桶min - 上一个非空桶max,用全局变量更新最大值<br/>时间复杂度:O(N)<br/>空间复杂度:O(N)
function maxGap(arr) {
    if(arr == null || arr.length <= 0) {
        return 0;
    }
    var len = arr.length;
    var max = -Infinity, min = Infinity;
    //遍历一遍数组,找到最大值max和最小值min
    for(var i = 0; i < len; i++) {
        max = max > arr[i] ? max : arr[i];
        min = min > arr[i] ? arr[i] : min;
    }

    //若min = max,则差值为0;
    if(min == max) {
        return 0;
    }

    var hasNum = new Array(len + 1);
    var mins = new Array(len + 1);
    var maxs = new Array(len + 1);

    var bid = 0;//指定桶的编号

    for(var i = 0; i < len; i++) {
        bid = bucket(arr[i], min, max, len);//获得该值是在哪个桶//由于有N+1个桶,所以间隔就是N个,所以此处除以的是len,然后通过这个函数得到应该放到哪个桶里
        maxs[bid] = hasNum[bid] ? Math.max(arr[i], maxs[bid]) : arr[i];
        mins[bid] = hasNum[bid] ? Math.min(arr[i], mins[bid]) : arr[i];
        hasNum[bid] = true;
    }

    var res = 0;
    var lastMax = maxs[0];

    for(var i = 0; i < len + 1; i++) {
        if(hasNum[i]) {
            res = Math.max(mins[i] - lastMax, res);
            lastMax = maxs[i];
        }
    }
    return res;
}

//获得桶号
//这个函数用于判断在哪个桶中,参数分别为值、最小值、最大值、桶间隔
function bucket(value, min, max, len) {
    return parseInt((value - min) / ((max - min) / len));
}
欢迎老铁们加群或者私聊
image
查看原文

赞 11 收藏 10 评论 0

执鸢者 发布了文章 · 8月30日

图解Node(上)——直击灵魂的十条拷问

关注公众号“<font color=red>执鸢者</font>”,获取大量教学视频并进入专业交流群,回复“Node”获取本节思维导图”。

近期小林迷恋上了Node这位小美女,倾慕之情溢于言表,为了打动她的芳心,决定接受直击灵魂深处的十大拷问,拷问内容分为两期,这是第一期,下周更新第二期。
  1. Node到底是什么?
  2. Node的技术架构和工作流程架构分别是什么?
  3. V8、libuv分别在Node架构中承担什么角色
  4. Node引入模块的步骤是什么?
  5. Node中模块有几类?它们的关系是什么?
  6. Node的特点有哪些?
  7. Node真的是单线程吗?异步I/O的整个流程是什么?事件循环是什么?
  8. Node有哪些非异步I/O的API?其原理是什么?
  9. Node适合用在什么场景?
  10. node xxx.js时,Node到底做了什么?

一、定义

二、整体架构

三、模块

3.1 引入模块步骤

3.2 模块分类

3.3 模块依赖层级关系

四、Node特点

4.1 单线程

4.2 异步I/O

4.3 事件循环

五、非I/O的异步API

六、Node应用场景

七、node指令做了什么?

欢迎大家关注公众号(回复“Node”获取本节的思维导图,回复“书籍”获取大量前端学习资料,回复“前端视频”获取大量前端教学视频)
查看原文

赞 9 收藏 8 评论 1

执鸢者 发布了文章 · 8月23日

(2.6w字)网络知识点灵魂拷问——前端面试必问

关注公众号“<font color=red>执鸢者</font>”,获取大量教学视频并进入专业交流群

一、当浏览器输入一个url请求会经历什么?


1.浏览器的地址栏输入URL并按下回车
2.DNS域名解析
(1)在浏览器DNS缓存中搜索
(2)如果浏览器缓存中没有,操作系统会先检查自己本地的hosts文件是否有这个网址映射关系,如果有,就先调用这个IP地址映射,完成域名解析。
(3)如果hosts里没有这个域名的映射,则查找本地DNS解析器缓存,是否有这个网址映射关系,如果有,直接返回,完成域名解析。
(4)如果hosts与本地DNS解析器缓存都没有相应的网址映射关系,则会找本地DNS服务器,如果要查询的域名包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析,此解析具有权威性。
(5)如果要查询的域名,不由本地DNS服务器区域解析,但该服务器已缓存了此网址映射关系,则调用这个IP地址映射,完成域名解析,此解析不具有权威性。
(6)如果上述方法都失效,由本地DNS服务器进行迭代查询,先向根域名DNS服务器发出请求,再查二级域、三级域,直到查询到要解析的地址或名字为止,本地DNS服务器收到应答后,先在缓存中存储,然后将解析结果返回客户机。
3.建立TCP连接(三次握手)
TCP协议采用了三次握手策略。发送端首先发送一个带SYN(synchronize)标志的数据包给接收方,接收方收到后,回传一个带有SYN/ACK(acknowledegment)标志的数据包以示传达确认信息。最后发送方再回传一个带ACK标志的数据包,代表握手结束。

4.HTTP发起请求
完整的HTTP请求包含请求起始行、请求头部、请求主体三部分。
请求方法:

GET:获取资源
POST:传输实体主体
HEAD:获取报文首部
PUT:传输文件
DELETE:删除文件
OPTIONS:询问支持的方法
TRACE:追踪路径

5.服务器处理请求,浏览器接收HTTP响应
服务器在收到浏览器发送的HTTP请求之后,会将收到的HTTP报文封装成HTTP的Request对象,并通过不同的Web服务器进行处理,处理完的结果以HTTP的Response对象返回,主要包括状态码,响应头,响应报文三个部分。
状态码:

1**:代表请求已经被接收,需要继续处理。
2**:成功状态码

200---OK/请求已经正常处理完毕
204---请求处理成功,但没有资源返回
206---表示客户端进行了范围请求,而服务器成功执行了这部分的GET请求

3**:重定向状态码

301---/请求永久重定向 被请求的资源已永久移动到新位置,并且将来任何对此资源的引用都应该使用本响应返回的若干个 URI 之一。如果可能,拥有链接编辑功能的客户端应当自动把请求的地址修改为从服务器反馈回来的地址。
302---/请求临时重定向 由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。只有在Cache-Control或Expires中进行了指定的情况下,这个响应才是可缓存的。
304---/表示客户端发送附带条件的请求(指采用GET方法的请求报文中包含If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since中任一首部)时,服务端允许请求访问资源,但未满足条件的情况
307---临时重定向,与302含义相同,但是307会遵照浏览器标准,不会从POST变成GET

4**:客户端错误状态码

400---/客户端请求存在语法错误
401---/当前请求需要用户验证。
403---/服务器已经理解请求,但是拒绝执行它。与401响应不同的是,身份验证并不能提供任何帮助
404---/请求失败,请求所希望得到的资源未被在服务器上发现。
405---/请求行中指定的请求方法不能被用于请求相应的资源。

5**:服务器错误状态码

500---/服务器遇到了一个未曾预料的状况,导致了它无法完成对请求的处理。
501---/服务器不支持当前请求所需要的某个功能。当服务器无法识别请求的方法,并且无法支持其对任何资源的请求。
503---/由于临时的服务器维护或者过载,服务器当前无法处理请求。
505---/服务器不支持,或者拒绝支持在请求中使用的 HTTP 版本。
6.浏览器解析渲染页面
(1)构建文档对象模型(DOM)
(2)构建CSS对象模型(CSSOM)
(3)构建渲染树(Render Tree)
(4)布局
(5)绘制
7.连接结束(四次挥手)
通过四次挥手关闭连接
    第一次挥手是浏览器发完数据后,发送FIN请求断开连接。
  第二次挥手是服务器发送ACK表示同意。
    第三次挥手是服务器发送FIN请求断开连接。(考虑到服务器可能还有数据要发送)
  第四次挥手是浏览器需要返回ACK表示同意。

二、HTTP缓存?

HTTP缓存有多种规则,根据是否需要重新向服务器发起请求来分类,将其分为强制缓存和对比缓存。
(1)强制缓存:判断HTTP首部字段:cache-Control、Expires

       Expires是一个绝对时间,即服务器时间。浏览器检查当前时间,如果还没到失效时间就直接使用缓存文件。但是该方法存在一个问题:服务器时间与客户端时间可能不一致。因此该字段已经很少使用。
  cache-control中的max-age保存一个相对时间。例如Cache-Control: max-age = 484200,表示浏览器收到文件后,缓存在484200s内均有效。 如果同时存在cache-control和Expires,浏览器总是优先使用cache-control。
Cache-Control 是最重要的规则。
常见的取值有private、public、no-cache、max-age,no-store,默认为private。
private: 客户端可以缓存
public: 客户端和代理服务器都可缓存(前端的同学,可以认为public和private是一样的)
max-age=xxx: 缓存的内容将在 xxx 秒后失效
no-cache: 需要使用对比缓存来验证缓存数据(后面介绍)
no-store: 所有内容都不会缓存,强制缓存,对比缓存都不会触发(对于前端开发)
(2)对比缓存:通过HTTP的last-modified、Etag字段进行判断

浏览器第一次请求数据时,服务器会将缓存标识与数据一起返回给客户端,客户端将二者备份至缓存数据库中。
last-modified是第一次请求资源时,服务器返回的字段,表示最后一次更新的时间。下一次浏览器请求资源时就发送if-modified-since字段。服务器用本地Last-modified时间与if-modified-since时间比较,如果不一致则认为缓存已过期并返回新资源给浏览器;如果时间一致则发送304状态码,让浏览器继续使用缓存。
Etag:资源的实体标识(哈希字符串),当资源内容更新时,Etag会改变。服务器会判断Etag是否发生变化,如果变化则返回新资源,否则返回304。

三、DNS服务器

1.为什么需要DNS解析域名为IP地址
网络通讯大部分是基于TCP/IP的,而TCP/IP是基于IP地址的,所以计算机在网络上进行通讯时只能识别IP地址而不能认识域名。
2.DNS作用
DNS是域名系统,它所提供的服务是用来将主机名和域名转换为IP地址的工作。
3.假设运行在用户主机上的某些应用程序(如Web浏览器或者邮件阅读器)需要将主机名转换为IP地址。这些应用程序将调用DNS的客户机端,并指明需要被转换的主机名。用户主机的DNS客户端接收到后,向网络中发送一个DNS查询报文。所有DNS请求和回答报文使用的UDP数据报经过端口53发送经过若干ms到若干s的延时后,用户主机上的DNS客户端接收到一个提供所希望映射的DNS回答报文。
4.DNS不采用单点的集中式的设计方式,而是使用分布式集群的工作方式,是因为集中式设计或有单点故障、通信容量、远距离时间延迟、维护开销大。
5.DNS查询的过程如下图所示

大致来说,有三种类型的DNS服务器:根DNS服务器、顶级域DNS服务器、权威DNS服务器

还有另一类重要的DNS,称为本地DNS服务器,一台本地DNS服务器严格来说并不属于该服务器的层次结构,但他对DNS层次结构很重要
当主机发出DNS请求时,该请求被发往本地DNS服务器,他起着代理的作用,并将该请求转发到DNS服务器层次结构中。

在上述中,从请求主机到本地DNS服务器的查询是递归的,其余查询是迭代的。
DNS提供了两种查询过程:
递归查询:在该模式下DNS服务器接收客户请求,必须使用一个准确的查询结果回复客户机,如果DNS服务器没有存储DNS值,那么该服务器会询问其它服务器,并将返回一个查询结果给客户机。
迭代查询:DNS服务器会向客户机提供其他能够解释查询请求的DNS服务器,当客户机发送查询时DNS并不直接回复查询结果,而是告诉客户机,另一台DNS服务器的地址,客户机再向这台DNS服务器提交请求,依次循环直接返回结果。

四、TCP与UDP的区别和优缺点

4.1 TCP与UDP总结

(1)TCP面向连接;UDP是无连接的,即发送数据之前不需要建立连接;
(2)TCP提供可靠数据传输,通过使用流量控制、序号、确认和定时器,TCP确保正确的、按序的将数据从发送进程交付给接收进程;UDP尽最大努力交付,即不保证可靠交付;
(3)UDP具有较好的实时性,工作效率比TCP高,适用于对高速传输和实时性比较高的通信或广播通信;
(4)每一条TCP连接只能是点对点的,UDP支持一对一,一对多,多对一和多对多的交互通信
(5)TCP对系统资源要求较多,UDP对系统资源要求较少。

4.2 UDP应用场景

(1)面向数据报方式
(2)网络数据大多为短消息
(3)拥有大量Client
(4)对数据安全性无特殊要求
(5)网络负担非常重,但对响应速度要求高

4.3 TCP如何提供可靠数据传输

通过使用流量控制、序号、确认和定时器,TCP确保正确的、按序的将数据从发送进程交付给接收进程。

五、TCP发送方有三个与发送和重传有关的事件

从上层应用程序接收数据

TCP从应用程序接收数据,将数据封装在一个报文段中(含有第一个数据字节的流编号),然后交给IP。

定时器超时

超时后,TCP重传超时报文,然后,重启定时器。

收到ACK

收到ACK后,将确认报文中确认号与发送方的SendBase(最早未被确认的字节序号)比较。TCP采取累积确认,所以确认号之前的字节都被接收方收到。
当 确认号 > SendBase 时,则该ACK是在确认一个或多个先前未被确认的报文段,此时发送方更新SendBase的值
如果当前有未被确认的报文段,TCP重启定时器

六、TCP协议在工作过程中的几种简单情况

6.1 由于确认丢失而重传      


  如上图所示,B发送给A的ACK丢失,引起了主机A的重传,B在接收到重传数据报后根据序号得知这是重传报文,于是丢弃该报文,向A发送ACK。

6.2 连续发送的报文段的ACK延迟    


  A连续向B发送了两个报文段,但是他们的ACK都延迟了,导致定时器超时,于是最早的未被确认的报文段92被重传,接着他们的ACK到达,它们就不会被再次重传,A收到确认后,就会将SendBase后移,并重启定时器。

6.3 累积确认避免先前报文段重传     


  A还是向B连续发送了两个报文段,但是第一个报文段的ACK丢失啦。但是好的是在定时器超时之前,第二个报文段的ACK到达,因为TCP采取了累计确认,第二个报文段ACK到达,说明了第一个报文段是被正确接收了哒。所以第一个报文段不会被重传。

七、快速重传

超时重传存在的问题之一就是超时周期可能较长。当一个报文段丢失时,通过超时重传来恢复报文,就会增加端到端的时延。Luckily,可以通过检测收到的冗余ACK来进行对丢失报文段的重传。
至于为啥可以通过这样的方式来确信此报文段丢失是因为:
  ①接送方接到丢失报文段后的报文(也就是失序报文段)会将失序报文段缓存,并向发送方发送最近接收的未失序报文段的最大编号。
  ②如果接收方连续接收多个失序报文,那么发送方将会收到对一个报文段的多个ACK,由此发送方可知该ACK代表的报文段的后一个报文丢失了,于是,发送方重传丢失报文。
  当发送方收到3个冗余ACK,就说明被确认过三次的报文段之后的那个报文段已经丢失,TCP就执行快重传(fast retransmit),在丢失报文段定时器超时之前重传丢失报文段。

八、TCP中是回退N步还是选择重传

根据前面对TCP描述,可以得知TCP确认是采用累积确认方式,并且对失序报文不会给出确认。这让TCP看起来像是一个GBN协议,但是与GBN不同的是,TCP会缓存失序的分组。所以,TCP提出的一种修改意见是选择确认(slective acknowledgment)[RFC 2018],它允许TCP接收方有选择地确认失序报文段,而不是累积确认最后一个正确接收的有序报文段。当将该机制和选择重传机制结合起来使用时(即跳过重传那些已被接收方选择确认过的报文段),TCP就像我们通常的SR协议。
  因此,TCP的差错恢复机制为GBN协议和SR协议的混合体。

九、TCP中流量控制与拥塞控制

他们都是对发送方的遏制

9.1 流量控制(为了避免接收方缓存溢出问题)

1.为什么要提供流量控制服务
简单地说,提供流控就是为了避免接收方缓存溢出问题。
  接收方接收到数据后,会将其放入接收缓存中,待上层应用程序读取数据。但是上层应用可能忙于其他事务或者读取数据的速度比较慢,而发送方发送数据的太多,速率太快,此时就会导致接收方的缓存溢出。
  流量控制也是一个速率匹配服务。
2.TCP如何提供流量控制服务?
这里为了从整体上看问题,我们假设,TCP接收方会丢弃失序的报文。

TCP让发送方A维护一个称为接收窗口(receive window)的变量来提供流量控制。这个窗口代表接收方B有多少可用的缓存空间
主机A和主机B之间建立TCP连接后,主机B为连接分配了一个接收缓存,用RcvBuffer表示

定义如下变量

LastByteRead:主机B的应用进程从缓存中取出的数据流最后一个字节的编号
LastByteRevd:主机B缓存的数据流的最后一个字节编号

缓存不能溢出需满足
LastByteRevd - LastByteRead <= RevBuffer
接收窗口rwnd根据缓存可用空间设置:
rwnd = RevBuffer - [LastByteRevd-LastByteRead]

主机B通过把当前的rwnd放到它发送给主机A的报文段的接收窗口字段,已通知主机A当前它还有多少空间可用。
主机A始终跟踪两个LastByteSend和LastByteAcked,[LastByteSend-LastByteAcked]就是主机A中发送但未被确认的数据量。使这个值小于主机B的rwnd,就可以使主机B的缓存不会溢出。

9.2 拥塞控制

TCP的发送方也可能会因为IP网络拥塞而被遏制,这种形式的控制被称为拥塞控制。
一般原理:发生拥塞控制的原因:资源(带宽、交换节点的缓存、处理机)的需求>可用资源。
1.拥塞的标志
重传计时器超时或接收到三个重复确认。
2.慢启动与拥赛避免
(1)慢启动不是指cwnd(拥塞窗口)的增长速度慢(指数增长),而是指TCP开始发送设置cwnd=1。
(2)思路:不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大逐渐增加拥塞窗口的大小。
(3)为了防止cwnd增长过大引起网络拥塞,设置一个慢启动阈值
当cnwd<ssthresh,使用慢开始算法
当cnwd=ssthresh,既可使用慢开始算法,也可以使用拥塞避免算法
当cnwd>ssthresh,使用拥塞避免算法
3.拥塞避免
(1)拥塞避免并非完全能够避免拥塞,是说在拥塞避免阶段将拥塞窗口控制为按线性规律增长,使网络比较不容易出现拥塞。
(2)思路:让拥塞窗口cwnd缓慢地增大,即每经过一个往返时间RTT就把发送方的拥塞控制窗口加一。
无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞,就把慢开始门限设置为出现拥塞时的发送窗口大小的一半。然后把拥塞窗口设置为1,执行慢启动算法。

4.快重传与快恢复

快重传
(1)快重传要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时捎带确认。快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。
(2)由于不需要等待设置的重传计时器到期,能尽早重传未被确认的报文段,能提高整个网络的吞吐量。
快恢复(与快重传配合使用)
(1)采用快恢复算法时,慢启动只在TCP连接建立时和网络出现超时时才使用。
(2)当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把ssthresh门限减半。但是接下去并不执行慢启动算法。
(3)考虑到如果网络出现拥塞的话就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞。所以此时不执行慢启动算法,而是将cwnd设置为ssthresh的大小,然后执行拥塞避免算法。
5.拥塞窗口
发送方为一个动态变化的窗口叫做拥塞窗口,拥塞窗口的大小取决于网络的拥塞程度。发送方让自己的发送窗口=拥塞窗口,但是发送窗口不是一直等于拥塞窗口的,在网络情况好的时候,拥塞窗口不断的增加,发送方的窗口自然也随着增加,但是接受方的接受能力有限,在发送方的窗口达到某个大小时就不在发生变化了。

9.3 拥塞控制和流量控制的区别

拥塞控制是防止过多的数据注入到网络中,可以使网络中的路由器或链路不致过载,是一个全局性的过程。
流量控制是点对点通信量的控制,是一个端到端的问题,主要就是抑制发送端发送数据的速率,以便接收端来得及接收。

十、TCP三次握手和四次挥手全过程

确认ACK,仅当ACK=1时,确认号字段才有效。TCP规定,在连接建立后所有报文的传输都必须把ACK置1;
同步SYN,在连接建立时用来同步序号。当SYN=1,ACK=0,表明是连接请求报文,若同意连接,则响应报文中应该使SYN=1,ACK=1;
终止FIN,用来释放连接。当FIN=1,表明此报文的发送方的数据已经发送完毕,并且要求释放;

10.1 三次握手

最开始的时候客户端和服务器都是处于CLOSED状态。主动打开连接的为客户端,被动打开连接的是服务器。

1.TCP服务器进程先创建传输控制块TCB(线程控制块),时刻准备接受客户进程的连接请求,此时服务器就进入了LISTEN(监听)状态;
2.TCP客户进程也是先创建传输控制块TCB,然后向服务器发出连接请求报文,这时报文首部中的同部位SYN=1,同时选择一个初始序列号 seq=x ,此时,TCP客户端进程进入了 SYN-SENT(同步已发送状态)状态。TCP规定,SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号。
3.TCP服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己初始化一个序列号 seq=y,此时,TCP服务器进程进入了SYN-RCVD(同步收到)状态。这个报文也不能携带数据,但是同样要消耗一个序号。
4.TCP客户进程收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=y+1,自己的序列号seq=x+1,此时,TCP连接建立,客户端进入ESTABLISHED(已建立连接)状态。TCP规定,ACK报文段可以携带数据,但是如果不携带数据则不消耗序号。
5.当服务器收到客户端的确认后也进入ESTABLISHED状态,此后双方就可以开始通信了。
扩展:为什么TCP客户端最后还要发送一次确认呢?
一句话,主要防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。
如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。
如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。

10.2 四次挥手


1.客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
2.服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
3.客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
4.服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
5.客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
6.服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
扩展:为什么客户端最后要等待2MSL?
MSL(Maximum Segment Lifetime),TCP允许不同的实现可以设置不同的MSL值。
第一,保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。
第二,防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。

10.3 为什么建立连接是三次握手,关闭连接是四次挥手呢?

建立连接的时候, 服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。
而关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。

10.4 如果已经建立了连接,但是客户端突然出现故障了怎么办?

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

十一、HTTP协议(请求报文和响应报文)

11.1 请求报文

HTTP请求报文主要包括:请求行、请求头部以及请求的数据(实体)。
1.请求行:方法字段、URI字段和协议版本
方法字段:GET(请求获取内容)、POST(提交表单)、HEAD(请求资源响应消息报头)、PUT(传输文件)、DELETE(请求删除URI指向的资源)、OPTIONS(查询针对请求URI指定的资源支持的方法)、TRACE(追踪请求经过路径)、CONNECT(要求用隧道协议连接代理)
2.请求头部
常见标头有:Connection标头(连接管理)、Host标头(指定请求资源的主机)、Range标头(请求实体的字节范围)、User-Agent标头(包含发出请求的用户信息)、Accept标头(首选的媒体类型)、Accept-Language(首选的自然语言)

3.HTTP请求的body主要用于提交表单场景。实际上,http请求的bodt是比较自由的,只要浏览器端发送的body服务端认可就可以了。一些常见的body格式是:

application/json
application/x-www-form-urlencoded
multipart/form-data
text/xml

使用html的form标签提交产生的html请求,默认会产生 application/x-www-form-urlencoded 的数据格式,当有文件上传时,则会使用multipart/form-data。

11.2 响应报文

HTTP响应报文分为三个部分:状态行、首部行和实体;<br/>
1.状态行:版本、状态码和原因语句<br/>
状态码:<br/>
(1)1xx:这一类型的状态码,代表请求已被接受,需要继续处理。这类响应是临时响应,只包含状态行和某些可选的响应头信息,并以空行结束;<br/>
(2)2xx:这一类型的状态码,代表请求已成功被服务器接收、理解并接受;<br/>
(3)3xx:这类状态码代表需要客户端采取进一步的操作才能完成请求。通常,这些状态码用来重定向,后续的请求地址(重定向目标)在本次响应的Location域中指明。<br/>
(4)4xx:这类状态码代表客户端类的错误;<br/>
(5)5xx:服务器类的错误。<br/>
200---OK/请求已经正常处理完毕<br/>
204---请求处理成功,但没有资源返回<br/>
206---表示客户端进行了范围请求,而服务器成功执行了这部分的GET请求<br/>
301---/请求永久重定向 被请求的资源已永久移动到新位置,并且将来任何对此资源的引用都应该使用本响应返回的若干个 URI 之一。如果可能,拥有链接编辑功能的客户端应当自动把请求的地址修改为从服务器反馈回来的地址。<br/>
302---/请求临时重定向 由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。只有在Cache-Control或Expires中进行了指定的情况下,这个响应才是可缓存的。<br/>
303---表示由于请求对应的资源存在着另一个URI,应使用GET方法定向获取请求的资源<br/>
304---/表示客户端发送附带条件的请求(指采用GET方法的请求报文中包含If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since中任一首部)时,服务端允许请求访问资源,但未满足条件的情况<br/>
307---临时重定向,与302含义相同,但是307会遵照浏览器标准,不会从POST变成GET<br/>
400---/客户端请求存在语法错误<br/>
401---/当前请求需要用户验证。<br/>
403---/服务器已经理解请求,但是拒绝执行它。与401响应不同的是,身份验证并不能提供任何帮助<br/>
404---/请求失败,请求所希望得到的资源未被在服务器上发现。<br/>
405---/请求行中指定的请求方法不能被用于请求相应的资源。<br/>
500---/服务器遇到了一个未曾预料的状况,导致了它无法完成对请求的处理。<br/>
501---/服务器不支持当前请求所需要的某个功能。当服务器无法识别请求的方法,并且无法支持其对任何资源的请求。<br/>
503---/由于临时的服务器维护或者过载,服务器当前无法处理请求。<br/>
505---/服务器不支持,或者拒绝支持在请求中使用的 HTTP 版本。<br/>
2.响应首部
Date(响应的时间)、Via(报文经过的中间节点)、Last-Modified(上一次修改时间)、Etag(与此实体相关的实体标记)、Connection(连接状态)、Accept-Ranges(服务器可接收的范围类型)、Content-Type(资源类型)

十二、Cookie和Session

12.1 Cookie 大小4KB

Cookie的工作机制是用户识别及状态管理。

1.Set-Cookie
该首部字段用来告知客户端有关Cookie的各种信息

2.Cookie
该首部字段用来告知服务器,当客户端想获得HTTP状态管理支持时,就会在请求中包含从服务器接收到的Cookie。
3.HTTP是无状态的协议,即HTTP协议自身不对请求和响应之间的通信状态进行保存。但为了实现期望的保持状态功能,于是引入了Cookie技术。
Cookie会根据从服务端发送的响应报文内的一个叫做Set-Cookie的首部字段信息,通知客户端保存Cookie。当下次客户端再往该服务器发送请求时,客户端会自动在请求报文中加入Cookie值发送出去。

4.expires/Max-Age 字段为此cookie超时时间。若设置其值为一个时间,那么当到达此时间后,此cookie失效。不设置的话默认值是Session,意思是cookie会和session一起失效。当浏览器关闭(不是浏览器标签页,而是整个浏览器) 后,此cookie失效。

12.2 Session

Session是另一种记录客户状态的机制,不同的是Cookie保存在客户端浏览器中,而Session保存在服务器上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。这就是Session。客户端浏览器再次访问时只需要从该Session中查找该客户的状态就可以了。
1.后端怎么存储session?
在服务器内存中开辟一块地址空间,专门存放每个客户端的私有数据,每个客户端根据cookie中保持的sessionId,可以获取到session数据。

12.3 Cookie与Session区别

1)、Cookie和Session都是会话技术,Cookie是运行在客户端,Session是运行在服务器端。
2)、Cookie有大小限制4K以及浏览器在存cookie的个数也有限制,Session是没有大小限制和服务器的内存大小有关。
3)、Cookie有安全隐患,通过拦截或本地文件找得到你的cookie后可以进行攻击。
4)、Session是保存在服务器端上会存在一段时间才会消失,如果session过多会增加服务器的压力。
session用于保存重要的信息,cookie用于保存不重要的用户信息

十三、登陆验证

由于HTTP是无状态的,一次请求结束,连接断开,下次服务器再收到请求,它就不知道这个请求是哪个用户发过来的。所以需要状态管理,以便服务端能够准确的知道http请求是哪个用户发起的,从而判断用户是否有权限继续这个请求。这个过程就是常说的会话管理。

13.1 同域登陆

如果前端,后台API部署在同域下,不存在跨域的情况,登录方式相对简单
1.基于Session登录
服务器端使用Session技术,浏览器端使用Cookie技术。

session在一开始并不具备会话管理的作用。它只有在用户登录认证成功之后,并且往sesssion对象里面放入了用户登录成功的凭证,才能用来管理会话。管理会话的逻辑也很简单,只要拿到用户的session对象,看它里面有没有登录成功的凭证,就能判断这个用户是否已经登录。当用户主动退出的时候,会把它的session对象里的登录凭证清掉。所以在用户登录前或退出后或者session对象失效时,肯定都是拿不到需要的登录凭证的。
其实上述可以解释为:
登录时带着自身的用户名和密码,将用户登录成功的凭证信息存储在Session中,并生成Cookie,并通过Set-Cookie在响应中返回;第二次登陆时,在请求中添加Cookie后发送,服务端检查Cookie,然后进行响应。
2.基于Token登录

(1).用户在浏览器中输入用户和密码,后台服务器通过加密或者其他逻辑,生成一个Token。
(2).前端获取到Token,存储到cookie或者localStorage中,在接下来的请求中,将token通过url参数或者HTTP Header头部传入到服务器
(3).服务器获取token值,通过查找数据库判断当前token是否有效

13.2 跨域登陆

由于浏览器同源策略,凡是发送请求的url的协议、域名和端口号三者之间任意一个与当前页面不同则视为跨域
1.解决同源策略
基于Session和Token登录都要解决
通过在服务端设置Header,设置Access-Control-Allow-Origin
如果要发送Cookie,Access-Control-Allow-Origin就不能设置星号,必须指定明确的、与请求网页一致的域名。
2.解决请求带上Cookie信息
CORS默认不发送Cookie和HTTP认证信息,一方面服务器制定Access-Control-Allow-Credentials:true
另一方面开发者必须在Ajax请求中添加withCredentials属性。

十四、HTTP1.0、HTTP1.1、HTTP2.0

14.1 HTTP的基本优化

影响HTTP网络请求的因素主要有两个:带宽和延迟
1.带宽
已经得到极大提升
2.延迟
(1)浏览器阻塞:浏览器对于同一个域名,同时只能有6个连接(不同浏览器不同),超过浏览器最大连接数限制,后续请求就会被阻塞。
(2)DNS查询:浏览器需要知道目标服务器的IP才能建立连接,通常可以利用DNS缓存结果来达到减少这个时间的目的。
(3)建立连接:HTTP 是基于 TCP 协议的,浏览器最快也要在第三次握手时才能捎带 HTTP 请求报文,达到真正的建立连接,但是这些连接无法复用会导致每次请求都经历三次握手和慢启动。三次握手在高延迟的场景下影响较明显,慢启动则对文件类大请求影响较大。

14.2 HTTP1.0和HTTP1.1的一些区别

1.缓存处理
在HTTP1.0中主要使用header里的If-Modified-Since,Expires来做为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略例如Etag,If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略。
2.带宽优化及网络连接的使用
HTTP1.0中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1则在请求头引入了range头域,它允许只请求资源的某个部分,即返回码是206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。
3.错误通知的管理
在HTTP1.1中新增了24个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。
4.Host头处理
在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)。
5.长连接
HTTP 1.1支持长连接(PersistentConnection)和管线化处理,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟,在HTTP1.1中默认开启Connection: keep-alive,一定程度上弥补了HTTP1.0每次请求都要创建连接的缺点。

14.3 扩展:HTTP1.1的几个特点

1.持久连接
每个TCP连接开始都有三次握手,要经历一次客户端与服务器间完整的往返,而开启了持久化连接就能不必每次都要握手。
持久化连接Connection:keep-alive

2.HTTP管道
持久HTTP多次请求必须严格满足先进先出(FIFO)的队列顺序:发送请求,等待响应完成,再发送客户端队列中的下一个请求。
HTTP管道可以让我们把FIFO队列从客户端(请求队列)迁移到服务器(响应队列)。
但HTTP 1.x不允许一个连接上的多个响应数据交错到达(多路复用),因而一个响应必须完全返回后,下一个响应才会开始传输。

14.4 HTTP的瓶颈

一条连接上只可发送一个请求;
请求只能从客户端开始。客户端不可以接收除响应以外的指令;
请求/响应首部未经压缩就发送。首部信息越多延迟越大;
发送冗长的首部。每次互相发送相同的首部造成的浪费较多;
可任意选择数据压缩格式。非强制压缩发送;

14.5 HTTP2.0

HTTP2.0是在SPDY基础上形成的下一代互联网通信协议。HTTP/2的目的是通过支持请求和响应的多路复用来减少延迟,通过压缩HTTP首部字段将协议开销降低,同时增加请求优先级和服务端推送的支持。
一、HTTP2.0相对于HTTP1.X的新特性
1.二进制分帧层
二进制分帧层是HTTP2.0性能增强的核心
HTTP 1.x在应用层以纯文本的形式进行通信,而HTTP 2.0将所有的传输信息分割为更小的消息和帧,并对它们采用二进制格式编码。这样,客户端和服务端都需要引入新的二进制编码和解码的机制
(1)帧
HTTP2.0通信的最小单位,包括帧首部、流标识符、优先值和帧净荷等。

其中,帧类型又可以分为:
DATA:用于传输HTTP消息体;
HEADERS:用于传输首部字段;
SETTINGS:用于约定客户端和服务端的配置数据。比如设置初识的双向流量控制窗口大小;
WINDOW_UPDATE:用于调整个别流或个别连接的流量
PRIORITY: 用于指定或重新指定引用资源的优先级。
RST_STREAM: 用于通知流的非正常终止。
PUSH_ PROMISE: 服务端推送许可。
PING: 用于计算往返时间,执行“ 活性” 检活。
GOAWAY: 用于通知对端停止在当前连接中创建流。
(2)消息
消息是指逻辑上的HTTP消息(请求/响应)。一系列数据帧组成了一个完整的消息。
(3)流
流是连接中的一个虚拟信道,可以承载双向消息传输。每个流有唯一整数标识符。为了防止两端流ID冲突,客户端发起的流具有奇数ID,服务端发起的流具有偶数ID。
所有HTTP2.0通信都在一个TCP连接上完成,这个连接可以承载任意数量的双向数据流Stream。相应地,每个数据流以消息的形式发送,而消息由一个或多个帧组成,这些帧可以乱序发送,然后根据每个帧首部的流标识符重新组装。
2.多路复用共享连接
HTTP消息被分解为独立的帧,而不破坏消息本身的语义,交错发送出去,最后在另一端根据流ID和首部将它们重新组合起来。
HTTP2.0成功解决了HTTP1.x的队首阻塞问题(TCP层的阻塞仍无法解决),同时减少了TCP连接数对服务器性能有很大提升。

  • 可以并行交错地发送请求,请求之间互不影响; 可以并行交错地发送响应,响应之间互不干扰; 只使用一个连接即可并行发送多个请求和响应; 消除不必要的延迟,从而减少页面加载的时间; 不必再为绕过 HTTP 1.x 限制而多做很多工作;

3.请求优先级
流可以带有一个31bit的优先级:

0:表示最高优先级
2的31次方-1 表示最低优先级。

服务端按优先级返回结果有利于高效利用底层连接,提高用户体验。
4.服务端推送
HTTP2.0增加了服务端推送功能,服务端可以根据客户端的请求,提前返回多个响应,推送额外的资源给客户端。

HTTP 2.0 连接后,客户端与服务器交换SETTINGS 帧,借此可以限定双向并发的流的最大数量。因此,客户端可以限定推送流的数量,或者通过把这个值设置为0 而完全禁用服务器推送。
所有推送的资源都遵守同源策略。换句话说,服务器不能随便将第三方资源推送给客户端,而必须是经过双方确认才行。
PUSH_PROMISE帧是服务端向客户端有意推送资源的信号。
如果客户端不需要服务端Push,可在SETTINGS帧中设定服务端流的值为0,禁用此功能
PUSH_PROMISE帧中只包含预推送资源的首部。如果客户端对PUSH_PROMISE帧没有意见,服务端在PUSH_PROMISE帧后发送响应的DATA帧开始推送资源。如果客户端已经缓存该资源,不需要再推送,可以选择拒绝PUSH_PROMISE帧。
PUSH_PROMISE必须遵循请求-响应原则,只能借着对请求的响应推送资源。
几点限制:

服务器必须遵循请求- 响应的循环,只能借着对请求的响应推送资源
PUSH_PROMISE 帧必须在返回响应之前发送,以免客户端出现竞态条件。

5.首部压缩
HTTP1.x每一次通信(请求/响应)都会携带首部信息用于描述资源属性。HTTP2.0在客户端和服务端之间使用“首部表”来跟踪和存储之前发送的键值对.首部表在连接过程中始终存在,新增的键值对会更新到表尾,因此,不需要每次通信都需要再携带首部。
HTTP2.0使用了首部压缩技术,可让报头更紧凑、更快速传输。HTTP 2.0关注的是首部压缩,而我们常用的gzip等是报文内容(body)的压缩。
二、HTTP2.0的完整通信过程
在两端使用HTTP2.0通信之前,必然存在协议协商的过程。
1.基于ALPN的协商过程
HTTPS 协商过程中有一个环节会使用ALPN(应用层协议协商)。减少网络延迟是HTTP 2.0 的关键条件,因此在建立HTTPS 连接时一定会用到ALPN协商。
支持HTTP 2.0的浏览器可以在TLS会话层自发完成和服务端的协议协商以确定是否使用HTTP 2.0通信。其原理是TLS 1.2中引入了扩展字段,以允许协议的扩展,其中ALPN协议(Application Layer Protocol Negotiation, 应用层协议协商, 前身是NPN)用于客户端和服务端的协议协商过程。
2.基于HTTP的协商过程
客户端使用HTTP也可以开启HTTP 2.0通信。只不过因为HTTP 1. 0和HTTP 2. 0都使用同一个 端口(80), 又没有服务器是否支持HTTP 2. 0的其他任何 信息,此时 客户端只能使用HTTP Upgrade机制(OkHttp, nghttp2等组件均可实现,也可以自己编码完成)通过协调确定适当的协议
3.完整通信过程
(1)TCP连接建立
(2)TLS握手和HTTP2.0通信过程
1)TLS握手
2)客户端向服务端发送SETTINGS帧,约定配置
3)流量控制
4)客户端向服务端发送HEADERS帧
5)服务端返回配置
6)DATA帧传输数据
三、发起新流

在发送应用数据之前,必须创建一个新流并随之发送相应的元数据,比如流优先级、HTTP 首部等;
客户端通过发送HEADERS帧来发起新流;
服务器通过发送 PUSH_PROMISE 帧来发起推送流。

四、发送应用数据

创建新流并发送HTTP 首部之后,接下来就是利用DATA 帧。应用数据可以分为多个DATA 帧,最后一帧要翻转帧首部的END_STREAM 字段
HTTP 2.0 标准要求DATA 帧不能超过2的14次方-1(16383)字节。长度超过这个阀值的数据,就得分帧发送。

五、去掉对HTTP1.X的优化

每个来源使用一个连接,HTTP 2.0 通过将一个TCP 连接的吞吐量最大化来提升性能。
去掉不必要的文件合并和图片拼接:HTTP 2.0,很多小资源都可以并行发送
利用服务器推送:之前针对HTTP 1.x 而嵌入的大多数资源,都可以而且应该通过服务器推送来交付。

十五、HTTPS

15.1 HTTP的缺点

1.通信使用明文(不加密),内容可能会被窃听;
TCP/IP是可能被窃听的网络:按TCP/IP协议族的工作机制,通信内容在所有线路上都有可能遭到窥视。
加密处理防止被窃听,加密的对象如下:
(1)通信的加密
通过和SSL(Secure Socket Layer,安全套接层)或TLS(Transport Layer Security,安全层传输协议)的组合使用,加密HTTP的通信内容。用SSL建立安全通信线路之后,就可以在这条线路上进行HTTP通信了。与SSL组合使用的HTTP称为HTTPS。
这种方式将整个通信线路加密
(2)内容的加密
由于HTTP协议中没有加密机制,那么就对HTTP协议传输的内容本身加密。
为了做到有效的内容加密,前提是要求客户端和服务器同时具备加密和解密机制。
该方式不同于将整个通信线路加密处理,内容仍有被篡改的风险。
2.不验证通信方的身份,因此有可能遭遇伪装;
(1)任何人都可发起请求
HTTP协议不论是谁发送过来的请求都会返回响应,因此不确认通信方,会存在以下隐患:
无法确定请求发送至目标的Web服务器是否是按真是意图返回响应的那台服务器,有可能是已伪装的Web服务器。
无法确定响应返回到的客户端是否是按照真实意图接收响应的那个客户端,有可能是伪装的客户端。
无法确定正在通信的对方是否具备访问权限,因为某些Web服务器上保存着重要的信息,只想发给特定用户通信的权限。
无法判断请求是来自何方、出自谁手
即使是无意义的请求也会照单全收,无法阻止海量请求下的Dos攻击(Denial of Service,拒绝服务攻击)。
(2)查明对手的证书
SSL不仅提供加密处理,还使用了一种被称为证书的手段,可用于确定通信方。
证书由值得信任的第三方机构颁发,用以证明服务器和客户端是实际存在的。
使用证书,以证明通信方就是意料中的服务器,对使用者个人来讲,也减少了个人信息泄露的危险性。另外,客户端持有证书即可完成个人身份的确认,也可用于对Web网站的认证环节。
3.无法证明报文的完整性,所以有可能已遭遇篡改。
(1)接收到的内容可能有误
在请求或响应送出之后直到对方接收之前的这段时间内,即使请求或响应的内容遭到篡改,也没有办法获悉。
在请求或响应的传输途中,遭攻击者拦截并篡改内容的攻击称为中间人攻击(Man-in-the-Middle attack)。
(2)如何防止篡改
仅靠HTTP确保完整性是非常困难的,便有赖于HTTPS来实现。SSL提供认证和加密处理及摘要功能。

15.2 HTTPS是如何加密数据的

把添加了加密、认证机制、完整性保护的HTTP称为HTTPS

HTTPS 并非是应用层的一种新协议。 只是 HTTP 通信接口部分用SSL(Secure Socket Layer)和TLS(Transport Layer Security) 协议代替而已。
TLS的前身是SSL

通常,HTTP直接和TCP通信,当使用SSL时,则演变成先和SSL通信,再由SSL和TCP通信了,简言之,所谓HTTPS其实就是身披SSL协议这层外壳的HTTP
SSL协议使用通信双方的客户证书以及CA根证书,允许客户/服务器应用以一种不能被偷听的方式通信,在通信双方间建立起了一条安全的、可信任的通信通道。
1.加密分为对称加密和非对称加密(也叫公开密钥加密)
(1)对称加密的意思就是,加密数据用的密钥,跟解密数据用的密钥是一样的。
1)对称加密的优点在于加密、解密效率通常比较高。速度快,对称性加密通常在消息发送方需要加密大量数据时使用,算法公开、计算量小、加密速度快、加密效率高。
2)缺点在于,数据发送方、数据接收方需要协商、共享同一把密钥,并确保密钥不泄露给其他人。此外,对于多个有数据交换需求的个体,两两之间需要分配并维护一把密钥,这个带来的成本基本是不可接受的。
(2)非对称加密的意思就是,加密数据用的密钥(公钥),跟解密数据用的密钥(私钥)是不一样的。
公钥就是公开的密钥,谁都可以查到;私钥就是非公开的密钥,一般是由网站的管理员持有。
公钥与私钥的联系:简单的说就是,通过公钥加密的数据,只能通过私钥解开。通过私钥加密的数据,只能通过公钥解开。
2.公开密钥存在问题
问题一:公钥如何获取;
问题二:数据传输仅单向安全(因为私钥加密数据,公钥也能解开,所以只是单向安全)
(1)对于公钥如何获取这个问题
这里涉及两个重要概念:证书、CA(证书颁发机构)
证书:可以暂时把它理解为网站的身份证。这个身份证里包含了很多信息,其中就包含了上面提到的公钥。当访问相应网站时,他就会把证书发给浏览器;
证书来自于CA(证书颁发机构)
(2)证书可能存在的问题:

证书是伪造的:压根不是CA颁发的
证书被篡改过:比如将XX网站的公钥给替换了

数字签名、摘要是证书防伪非常关键的武器。
“摘要”就是对传输的内容,通过hash算法计算出一段固定长度的串。然后,在通过CA的私钥对这段摘要进行加密,加密后得到的结果就是“数字签名”。
明文 --> hash运算 --> 摘要 --> 私钥加密 --> 数字签名
数字签名只有CA的公钥才能够解密。
证书格式,需要关注的有几个点:

证书包含了颁发证书的机构的名字 -- CA
证书内容本身的数字签名(用CA私钥加密)
证书持有者的公钥
证书签名用到的hash算法

其中CA本身有自己的证书,称为根证书。
1)对于完全伪造的证书
这种情况对证书进行检查

证书颁发的机构是伪造的:浏览器不认识,直接认为是危险证书
证书颁发的机构是确实存在的,于是根据CA名,找到对应内置的CA根证书、CA的公钥。用CA的公钥,对伪造的证书的摘要进行解密,发现解不了。认为是危险证书

2)篡改过的证书

检查证书,根据CA名,找到对应的CA根证书,以及CA的公钥。
用CA的公钥,对证书的数字签名进行解密,得到对应的证书摘要AA
根据证书签名使用的hash算法,计算出当前证书的摘要BB
对比AA跟BB,发现不一致--> 判定是危险证书

15.3 HTTPS流程

1.客户端发起HTTPS请求
2.服务端响应,下发证书(公开密钥证书)
3.客户端检查证书,如果证书没问题,那么就生成一个随机值,然后用证书(公钥)对该随机值进行加密。
4.将经过公钥加密的随机值发送到服务端(非对称加密),以后客户端和服务器的通信就可以通过这个随机值进行加密解密了。
5.服务端用私钥解密后,得到了客户端传过来的随机值,然后把内容通过该值进行对称加密。
6.后期的数据传输都是基于该随机值进行加密解密。

15.4 对于公钥获取这个问题

涉及到证书和CA(证书颁发机构)
证书可能存在的问题:1.证书是伪造的,压根不是CA颁发的;
2.证书被篡改过
数字签名和摘要是证书防伪非常关键的武器
明文--->hash算法--->摘要---->私钥加密----->数字签名
数字签名只有CA的公钥才能够解密
CA证书本身有自己的证书,称为根证书。
(1)对于完全伪造的证书
这种情况对证书进行检查

证书颁发的机构是伪造的:浏览器不认识,直接认为是危险证书
证书颁发的机构是确实存在的,于是根据CA名,找到对应内置的CA根证书、CA的公钥。用CA的公钥,对伪造的证书的数字签名进行解密,发现解不了。认为是危险证书

(2)篡改过的证书

检查证书,根据CA名,找到对应的CA根证书,以及CA的公钥。
用CA的公钥,对证书的数字签名进行解密,得到对应的证书摘要AA
根据证书签名使用的hash算法,计算出当前证书的摘要BB
对比AA跟BB,发现不一致--> 判定是危险证书

15.5 客户端证书

HTTPS中还可以使用客户端证书,以客户端证书进行客户端认证,证明服务器正在通信的对方始终是预料之内的客户端。想获取证书时,用户得自行安装客户端证书。
现状是,安全性极高的认证机构可颁发客户端证书但仅用于特殊用途的业务,比如网上银行等。

15.6 HTTPS问题

1.与纯文本相比,加密通信会消耗更多的CPU及内存资源
由于HTTPS还需要做服务器、客户端双方加密及解密处理,因此会消耗CPU和内存等硬件资源。
和HTTP通信相比,SSL通信部分消耗网络资源,而SSL通信部分,由因为要对通信进行处理,所以时间上又延长了。
SSL慢分两种,一种是指通信慢;另一种是指由于大量消耗CPU及内存等资源,导致处理速度变慢。
2.购买证书需要开销。

十六、WebSocket

16.1 WebSocket API

1.WS和WSS
WebSocket资源URL采用了自定义模式,ws表示纯文本通信,wss表示使用加密信道通信(TCP+TLS)
WebSocket的主要目的,是在浏览器中的应用与服务器之间提供优化的、双向通信机制。可是,WebSocket的连接协议也可以用于浏览器之外的场景,可以通过非HTTP协商机制交换数据。
2.接收文本和二进制数据
WebSocket通信只涉及消息,应用代码无需担心缓存、解析、重建接收到的数据。比如,服务器发来了一个1MB的净荷,应用的onmessage回掉只会在客户端接收到全部数据时才会被调用。
净荷可以是文本也可以是二进制数据。
浏览器接收到新消息后,如果是文本数据,会自动将其转换成DOMString对象,如果是二进制数据或Blob对象(Blob对象一般代表一个不可变的文件对象或原始数据。如果你不需要修改它或者不需要把它切分成更小的块,这种格式是理想的,若需要处理则选择ArrayBuffer更合适),会直接将其转交给应用。唯一可以多余设置的,就是告诉浏览器把接收到的二进制数据转换成ArrayBuffer而非Blob

➊ 如果接收到二进制数据,将其强制转换成 ArrayBuffer
3.发送文本和二进制数据
客户端就可以随时发送或接收 UTF-8 或二进制消息
这里的send()方法是异步的:提供的数据会在客户端排队,而函数则立即返回。特别是传输大文件的时候,千万别因为返回快,就认为数据已经发送出去了。可以通过bufferedAmount属性来监控浏览器中排队的数据量。ws.bufferedAmount
4.子协议协商
WebSocket协议对每条消息的格式事先不作任何假设:仅用一位标记消息是文本还是为二进制,以便客户端和服务器有效地解码数据,而除此之外的消息内容就是未知的。
此外,与 HTTP 或 XHR 请求不同——它们是通过每次请求和响应的 HTTP 首部来沟通元数据, WebSocket 并没有等价的机制。因此,如果需要沟通关于消息的元数据,客户端和服务器必须达成沟通这一数据的子协议。
WebSocket为此提供了一个简单便捷的子协议协商API。客户端可以在初次连接握手时,告诉服务器自己支持哪种协议:

WebSocket 构造函数可以接受一个可选的子协议名字的数组,通过这个数组,客户端可以向服务器通告自己能够理解或希望服务器接受的协议。这个协议数组会发送给服务器,服务器可以从中挑选一个。
如果子协议协商成功,就会触发客户端的 onopen 回调,应用可以查询 WebSocket 对
象上的 protocol 属性,从而得知服务器选定的协议。另一方面,服务器如果不支持客户端声明的任何一个协议,则 WebSocket 握手是不完整的,此时会触发 onerror 回调,连接断开。

16.2 WebSocket协议

WebSocket通信协议包含两个高层组件:开放性HTTP握手用于协商连接参数,二进制消息分帧机制用于支持低开销的基于消息的文本和二进制数据传输。
1.二进制分帧层
WebSocket使用了自定义的二进制分帧格式,把每个消息切分成一或多个帧,发送到目的地之后再组装起来,等到接收到完整的消息后再通知接收端。

帧:最小的通信单位,包含可变长度的帧首部和净荷部分
消息:一系列帧
FIN:表示当前帧是不是消息的最后一帧。
操作码(4位):表示被传输帧的类型:文本(1),二进制(2)
掩码位表示净荷是否有掩码(只适用于客户端发送给服务器的消息)。
算下来,服务器发送的每个WebSocket帧会产生2~10字节的分帧开销。而客户端必须发送掩码键,这又会增加4字节,结果就是6~14字节的开销。除此之外,没有其他元素据。
2.WebSocket的多路复用及队首阻塞
WebSocket很容易发生队首阻塞的情况:消息可能会被分成一个或多个帧,但不同消息的帧不能交错发送,因为没有与HTTP2.0分帧机制中“流ID”对等的字段。
WebSocket不支持多路复用,还意味着每个WebSocket连接都需要一个专门的TCP连接。对于HTTP1.x而言,由于浏览器针对每个来源有连接数量限制,因此可能会导致问题。
虽然通过HTTP2.0传输WebSocket帧的官方规范尚未发布,但相对来说容易很多。因为HTTP2.0内置了流的多路复用,只要通过HTTP2.0的分帧机制来封装WebSocket帧,多个WebSokcet连接就可以在一个会话中传输。
3.协议扩展
(1)多路复用扩展
这个扩展可以将WebSocket的逻辑连接独立出来,实现共享底层的TCP连接。
(2)压缩扩展
给WebSocket协议增加了压缩功能。
如前所述,每个 WebSocket 连接都需要一个专门的 TCP 连接,这样效率很低。多路复用扩展解决了这个问题。它使用“信道 ID”扩展每个 WebSocket 帧,从而实现多个虚拟的 WebSocket 信道共享一个 TCP 连接。
类似地,基本的 WebSocket 规范没有压缩数据的机制或建议,每个帧中的净荷就是应用提供的净荷。虽然这对优化的二进制数据结构不是问题,但除非应用实现自己的压缩和解压缩逻辑,否则很多情况下都会造成传输载荷过大的问题。实际上,压缩扩展就相当于 HTTP 的传输编码协商。
要使用扩展,客户端必须在第一次的Upgrade握手中通知服务器,服务器必须选择并确认要在商定连接中使用的扩展。
4.HTTP升级协商
在交换数据之前,客户端必须与服务器协商适当的参数以建立连接。
利用HTTP完成握手有几个好处。首先,让WebSocket与现有HTTP基础设施兼容:WebSocket服务器可以运行在80和443端口上,这通常是对客户端唯一开放的端口。其次,让我们可以重用并扩展HTTP的Upgrade流,为其添加自定义的WebSocket
首部,以完成协商。

与浏览器中客户端发起的任何连接一样,WebSocket请求也必须遵守同源策略:浏览器会自动在升级握手请求中追加Origin首部,远程服务器可能使用CORS判断接受或拒绝跨源请求。要完成握手,服务器必须返回一个成功的“Switching Protocols”(切换协议)响应,并确认选择了客户端发送的哪个选项:


如果握手成功,该连接就可以用作双向通信信道交换WebSocket消息。从此以后,客户端与服务器之间不会再发生HTTP通信,一切由WebSocket协议接管。

16.3 WebSocket使用场景及性能

1.请求和响应流
WebSocket是唯一一个能通过同一个TCP连接实现双向通信的机制,客户端和服务器随时可以交换数据。
2.消息开销
应用消息会被拆分为一或多个帧,每个帧会添加2~14字节的开销。而且,由于分帧是按照自定义的二进制格式完成的, UTF-8 和二进制应用数据可以有效地通过相同的机制编码。
(1)SSE会给每个消息添加5个字节,但仅限于UTF-8内容
(2)HTTP 1.x 请求(XHR 及其他常规请求)会携带 500~800 字节的 HTTP 元数据,加上 cookie
(3)HTTP 2.0 压缩 HTTP 元数据,这样可以显著减少开销
3.数据效率及压缩
除非应用通过细致优化自己的二进制净荷实现自己的压缩逻辑,同时也针对文本消息实现自己的压缩逻辑,否则传输数据过程中一定会产生很大的字节开销!
4.自定义应用协议

16.4 图解HTTP上解释

WebSocket技术主要是为了解决Ajax和Comet里XMLHttpRequest附带的缺陷所引起的问题。
WebSocket是建立在HTTP基础上的协议,因此连接的发起方仍然是客户端。
Websocket只需要一次HTTP握手,所以说整个通讯过程是建立在一次连接/状态中,也就避免了HTTP的非状态性,服务端会一直知道你的信息,直到你关闭请求
(1)WebSocket的主要特点
1)推送功能
支持由服务器向客户端推送数据的推送功能。
2)减少通信量
只要建立起WebSocket连接,就希望一直保持连接状态。和HTTP相比,不但每次连接时的总开销减少,而且由于WebSokcet的首部信息很小,通信量也相应减少了;
(2)握手请求
为了实现WebSocket通信,在HTTP连接建立之后,需要完成一次“握手”的步骤。
为了实现WebSocket通信,需要用到HTTP的Upgrade首部字段,告知服务器通信协议发生改变,以达到握手的目的。

Sec-WebSocket-Key:记录握手需要的键值;
Sec-WebSocket-Protocol:记录使用的子协议;

(3)握手响应
对于之前的请求,返回状态码101 Switching Protocols的响应。成功握手确立WebSocket连接之后,通信时不再使用HTTP的数据帧,而采用WebSockte独立的数据帧;

Sec-WebSocket-Accept:该字段值是由握手请求中的Sec-WebSocket-Key的字段值生成的;

16.5 WebSocket优点(对比参照HTTP协议)

https://yq.aliyun.com/articles/633679?spm=a2c4e.11153940.0.0.5e293dd6CNm3ZH
(1)支持双向通信,实时性更强;
(2)更好的二进制支持;
(3)较少的控制开销:
连接创建后,ws客户端、服务端进行数据交换时,协议控制的数据包头部较小。在不包含头部的情况下,服务端到客户端的包头只有2~10字节(取决于数据包长度),客户端到服务端的的话,需要加上额外的4字节的掩码。而HTTP协议每次通信都需要携带完整的头部;
(4)支持扩展:
ws协议定义了扩展,用户可以扩展协议,或者实现自定义的子协议(比如支持自定义压缩算法等)。

16.6 如何建立连接

WebSocket复用了HTTP的握手通道。具体指的是,客户端通过HTTP请求与WebSocket服务端协商升级协议。协议升级完成后,后续的数据交换则遵照WebSocket的协议。
1.客户端:申请协议升级
首先,客户端发起协议升级请求。可以看到,采用的是标准的HTTP报文格式,且只支持GET方法
Connection: Upgrade:表示要升级协议
Upgrade: websocket:表示要升级到websocket协议。
Sec-WebSocket-Version: 13:表示websocket的版本。如果服务端不支持该版本,需要返回一个Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。
Sec-WebSocket-Key:与后面服务端响应首部的Sec-WebSocket-Accept是配套的,提供基本的防护,比如恶意的连接,或者无意的连接。
2.服务端:响应协议升级
返回状态码101表示协议切换

16.7 数据帧格式

客户端、服务器数据的交换,离不开数据帧格式的定义。
WebSocket客户端、服务端通信的最小单位是帧,由一个或多个帧组成一条完整的消息。
发送端:将消息切割成多个帧,并发送给服务端;
接收端:接收消息帧,并将关联的帧重新组装成完整的消息。
帧内容见上面二小结

16.8 数据传递

一旦WebSocket客户端、服务端建立连接后,后续的操作都是基于数据帧的传递。WebSocket根据opcode来区分操作的类型。比如0x8表示断开连接,0x0-0x2表示数据交互。
1.数据分片
WebSocket的每条消息可能被切分成多个数据帧。当WebSocket的接收方收到一个数据帧时,会根据FIN的值来判断,是否已经收到消息的最后一个数据帧。
FIN=1表示当前数据帧为消息的最后一个数据帧,此时接收方已经收到完整的消息,可以对消息进行处理。FIN=0,则接收方还需要继续监听接收其余的数据帧。
opcode在数据交换的场景下,表示的是数据的类型。0x01表示文本,0x02表示二进制。而0x00比较特殊,表示延续帧(continuation frame),顾名思义,就是完整消息对应的数据帧还没接收完。

16.9 连接保持+心跳

WebSocket为了保持客户端、服务端的实时双向通信,需要确保客户端、服务端之间的TCP通道保持连接没有断开。
有些场景,客户端、服务端虽然长时间没有数据往来,但仍需要保持连接,这个时候可以采用心跳来实现:
发送方->接收方:ping
接收方->发送方:pong
ping、pong的操作,对应的是WebSocket的两个控制帧,opcode分别是0x9、0xA
例如:ws.ping('', false, true);

16.10 Sec-WebSocket-Key/Accept的作用

Sec-WebSocket-Key/Sec-WebSocket-Accept在主要作用在于提供基础的防护,减少恶意连接、意外连接。

16.11 数据掩码的作用

WebSocket协议中,数据掩码的作用是增强协议的安全性。但数据掩码并不是为了保护数据本身(因为算法本身是公开的,运算也不复杂),而是为了防止早期版本的协议中存在的代理缓存污染攻击等问题。

十七、状态码301和302的区别

(1)什么是301重定向?
301重定向/跳转一般,表示本网页永久性转移到另一个地址。
301是永久性转移(Permanently Moved),SEO(搜索引擎优化)常用的招式,会把旧页面的PR(永久居留)等信息转移到新页面;
(2)什么是302重定向?
302重定向表示临时性转移(Temporarily Moved),当一个网页URL需要短期变化时使用。
(3)301重定向与302重定向的区别
301重定向是永久的重定向,搜索引擎在抓取新内容的同时也将旧的网址替换为重定向之后的网址。
302重定向是临时的重定向,搜索引擎会抓取新的内容而保留旧的网址。因为服务器返回302代码,搜索引擎认为新的网址只是暂时的。

十八、强缓存引起问题的解决方案(如何更新网站资源)

存在问题:发布时资源更新问题,更新了资源,但是用户每次请求时依然从缓存中获取原来的资源,除非用户清除或者强刷新,否则看不到最新的效果。
1、利用304定向重定向,让浏览器本地缓存即协商缓存。
缺点:协商缓存还是需要和浏览器通信一次。
2、强制浏览器使用本地缓存,不和服务器通信。
缺点:无法更新缓存。
3、通过更新页面中引用的资源路径,让浏览器主动放弃缓存,加载新资源。例如在文件名称后面增加一个版本号,下次上线时更改版本号。
缺点:当有多个静态缓存文件,只有一个文件更改时,却需要更新所有文件。

采用数据摘要算法

想要解决这个问题:考虑到让url的修改与文件内容相关联即只有文件内容变化时,才会导致相应的url的变更,从而实现文件级别的精确缓存控制。
为了进一步提升网站性能,会把静态资源和动态网页分集群部署,静态资源会被部署到CDN节点上,网页中引用的资源也会变成对应的部署路径。当我要更新静态资源的时候,同时也会更新html中的引用。若这次发布,同时改了页面结构和样式,也更新了静态资源对应的url地址,现在要发布代码上线,咱们是先上线页面,还是先上线静态资源?

先部署页面,再部署资源:在二者部署的时间间隔内,如果有用户访问页面,就会在新的页面结构中加载旧的资源,并且把这个旧版本的资源当做新版本缓存起来,其结果就是:用户访问到了一个样式错乱的页面,除非手动刷新,否则在资源缓存过期之前,页面会一直执行错误。
先部署资源,再部署页面:在部署时间间隔之内,有旧版本资源本地缓存的用户访问网站,由于请求的页面是旧版本的,资源引用没有改变,浏览器将直接使用本地缓存,这种情况下页面展现正常;但没有本地缓存或者缓存过期的用户访问网站,就会出现旧版本页面加载新版本资源的情况,导致页面执行错误,但当页面完成部署,这部分用户再次访问页面又会恢复正常了。

上述先部署谁都有问题,这是因为采用的是覆盖式发布,用 待发布资源 覆盖 已发布资源,就有这种问题。解决它也好办,就是实现 非覆盖式发布。
大公司的静态资源优化方案:
1.配置超长时间的本地缓存 —— 节省带宽,提高性能
2.采用内容摘要作为缓存更新依据 —— 精确的缓存控制
3.静态资源CDN部署 —— 优化网络请求
4.更新资源发布路径实现非覆盖式发布 —— 平滑升级

十九、滑动窗口原理

19.1 TCP滑动窗口(发送窗口和接收窗口)

TCP滑动窗口主要有两个作用,一是提供TCP的可靠性,二是提供TCP的流控特性(TCP的滑动窗口是动态的)。同时滑动窗口机制还体现了TCP面向字节流的设计思路。
TCP的Window是一个16bit位字段,它代表的是窗口的字节容量,也就是TCP的标准窗口最大为2^16 - 1 = 65535个字节。
1.对于TCP会话的发送方,任何时候在其发送缓存内的数据都可以分为4类,“已经发送并得到对端ACK的”,“已经发送但还未收到对端ACK的”,“未发送但对端允许发送的”,“未发送且对端不允许发送”。“已经发送但还未收到对端ACK的”和“未发送但对端允许发送的”这两部分数据称之为发送窗口(中间两部分)。

当收到接收方新的ACK对于发送窗口中后续字节的确认是,窗口滑动,滑动原理如下图。

当收到ACK=36时窗口滑动
2.对于TCP的接收方,在某一时刻在它的接收缓存内存有3种。“已接收”,“未接收准备接收”,“未接收并未准备接收。其中“未接收准备接收”称之为接收窗口。
3.发送窗口和接收窗口关系
TCP是双工的协议,会话的双方都可以同时接收、发送数据。TCP会话的双方都各自维护一个”发送窗口“和一个”接收窗口“。其中各自的”接收窗口“大小取决于应用、系统、硬件的限制(TCP传输速率不能大于应用的数据处理速率)。各种的”发送窗口“则要求取决于对端通告的”接收窗口“,要求相同。
4.滑动窗口实现面向流的可靠性
最基本的传输可靠性来源于”确认重传“机制。
TCP的滑动窗口的可靠性也是建立在”确认重传“基础上的。
发送窗口只有收到对端对于本段发送窗口内字节的ACK确认,才会移动发送窗口的左边界。
接收窗口只有在前面所有的段都确认的情况下才会移动左边界。当在前面还有字节未接收但收到后面字节的情况下,窗口不会移动,并不对后续字节确认。以此确保对端会对这些数据重传。

19.2 TCP协议上的网络协议分类

目前建立在TCP协议上的网络协议特别多,有telnet,ssh,有ftp,有http等等。这些协议又可以根据数据吞吐量来大致分成两大类:
(1)交互数据类型,例如telnet,ssh,这种类型的协议在大多数情况下只是做小流量的数据交换,比如说按一下键盘,回显一些文字等等。
(2)数据成块类型,例如ftp,这种类型的协议要求TCP能尽量的运载数据,把数据的吞吐量做到最大,并尽可能的提高效率。

19.3 滑动窗口协议相关术语

滑动窗口本质上是描述接受方的TCP数据报缓冲区大小的数据,发送方根据这个数据来计算自己最多能发送多长的数据。如果发送发收到接收方的窗口大小为0的TCP数据报,那么发送方将停止发送数据,等到接收方发送窗口大小不为0的数据报的到来。
关于滑动窗口协议介绍了三个术语,分别是:

窗口合拢:当窗口从左边向右边靠近的时候,这种现象发生在数据被发送和确认的时候。
窗口张开:当窗口的右边沿向右边移动的时候,这种现象发生在接受端处理了数据以后。
窗口收缩:当窗口的右边沿向左边移动的时候,这种现象不常发生

19.4 举一个例子来说明一下滑动窗口的原理

TCP并不是每一个报文段都会回复ACK的,可能会对两个报文段发送一个ACK,也可能会对多个报文段发送1个ACK【累计ACK】,比如说发送方有1/2/3 3个报文段,先发送了2,3 两个报文段,但是接收方期望收到1报文段,这个时候2,3报文段就只能放在缓存中等待报文1的空洞被填上,如果报文1,一直不来,报文2/3也将被丢弃,如果报文1来了,那么会发送一个ACK对这3个报文进行一次确认。<br/>
举一个例子来说明一下滑动窗口的原理:<br/>
1. 假设32~45 这些数据,是上层Application发送给TCP的,TCP将其分成四个Segment来发往internet<br/>
2. seg1 32~34 seg3 35~36 seg3 37~41 seg4 42~45 这四个片段,依次发送出去,此时假设接收端之接收到了seg1 seg2 seg4<br/>
3. 此时接收端的行为是回复一个ACK包说明已经接收到了32~36的数据,并将seg4进行缓存(保证顺序,产生一个保存seg3 的hole)<br/>
4. 发送端收到ACK之后,就会将32~36的数据包从发送并没有确认切到发送已经确认,提出窗口,这个时候窗口向右移动<br/>
5. 假设接收端通告的Window Size仍然不变,此时窗口右移,产生一些新的空位,这些是接收端允许发送的范畴<br/>
6. 对于丢失的seg3,如果超过一定时间,TCP就会重新传送(重传机制),重传成功会seg3 seg4一块被确认,不成功,seg4也将被丢弃<br/>
就是不断重复着上述的过程,随着窗口不断滑动,将真个数据流发送到接收端,实际上接收端的Window Size通告也是会变化的,接收端根据这个值来确定何时及发送多少数据,从对数据流进行流控。原理图如下图所示:

19.5 滑动窗口动态调整

主要是根据接收端的接收情况,动态去调整Window Size,然后来控制发送端的数据流量
客户端不断快速发送数据,服务器接收相对较慢,看下实验的结果
a. 包175,发送ACK携带WIN = 384,告知客户端,现在只能接收384个字节
b. 包176,客户端果真只发送了384个字节,Wireshark也比较智能,也宣告TCP Window Full
c. 包177,服务器回复一个ACK,并通告窗口为0,说明接收方已经收到所有数据,并保存到缓冲区,但是这个时候应用程序并没有接收这些数据,导致缓冲区没有更多的空间,故通告窗口为0, 这也就是所谓的零窗口,零窗口期间,发送方停止发送数据
d. 客户端察觉到窗口为0,则不再发送数据给接收方
e. 包178,接收方发送一个窗口通告,告知发送方已经有接收数据的能力了,可以发送数据包了
f. 包179,收到窗口通告之后,就发送缓冲区内的数据了.

总结一点,就是接收端可以根据自己的状况通告窗口大小,从而控制发送端的接收,进行流量控制

二十、Chrome中的from memory cache与from disk cache

1. 在浏览器开发者工具的Network的Size列会出现的三种情况

  • from memory cache
  • from disk cache
  • 资源本身大小(比如:13.6K)

2. 三级缓存原理

先查找内存,如果内存中存在,从内存中加载;
如果内存中未查找到,选择硬盘获取,如果硬盘中有,从硬盘中加载;
如果硬盘中未查找到,那就进行网络请求;
加载到的资源缓存到硬盘和内存;

3. HTTP状态码及区别

  • 200 form memory cache

不访问服务器,一般已经加载过该资源且缓存在了内存当中,直接从内存中读取缓存。浏览器关闭后,数据将不存在(资源被释放掉了),再次打开相同的页面时,不会出现from memory cache。

  • 200 from disk cache

不访问服务器,已经在之前的某个时间加载过该资源,直接从硬盘中读取缓存,关闭浏览器后,数据依然存在,此资源不会随着该页面的关闭而释放掉下次打开仍然会是from disk cache。

  • 200 大小(如3.4k)

访问服务器,size显示为实际资源的大小

  • 304 Not Modified

访问服务器,发现数据没有更新,服务器返回此状态码。然后从缓存中读取数据。这种在请求头中有两个请求参数:If-Modified-Since 和 If-None-Match。<br/>

状态码类型说明
200form memory cache不请求网络资源,资源在内存当中
200form disk ceche不请求网络资源,在磁盘当中
200资源大小数值从服务器下载最新资源
304报文大小请求服务端发现资源没有更新,使用本地资源

以上是chrome在请求资源是最常见的两种http状态码,由此可见样式表一般在磁盘中,不会缓存到内存中去,因为css样式加载一次即可渲染出网页。但是脚本却可能随时会执行,如果脚本在磁盘当中,在执行该脚本需要从磁盘中取到内存当中来,这样的IO开销是比较大的,有可能会导致浏览器失去响应。

欢迎大家关注公众号(回复“nginx”获取本节的思维导图,回复“书籍”获取大量前端学习资料,回复“前端视频”获取大量前端教学视频)
查看原文

赞 38 收藏 32 评论 1

认证与成就

  • 获得 319 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2019-02-21
个人主页被 1.1k 人浏览