最后

编程基础的初级开发者,计算机科学专业的学生,以及平时没怎么利用过数据结构与算法的开发人员希望复习这些概念为下次技术面试做准备。或者想学习一些计算机科学的基本概念,以优化代码,提高编程技能。这份笔记都是可以作为参考的。

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

名不虚传!字节技术官甩出的"保姆级"数据结构与算法笔记太香了

第2个响应

  • Content-Length:1200

  • Content-Range:bytes 1200-2399/5000

第3个响应

  • Content-Length:1200

  • Content-Range:bytes 2400-3599/5000

第4个响应

  • Content-Length:1400

  • Content-Range:bytes 3600-5000/5000

如果每个请求都成功了,服务端返回的response头中有一个 Content-Range 的字段域,Content-Range 用于响应头,告诉了客户端发送了多少数据,它描述了响应覆盖的范围和整个实体长度。一般格式:

Content-Range: bytes (unit first byte pos) - [last byte pos]/[entity length]Content-Range:字节 开始字节位置-结束字节位置/文件大小

浏览器支持情况

主流浏览器目前都支持这个特性。

image-20200916002624861

服务器支持

Nginx

在版本nginx版本 1.9.8 后,(加上 ngx_http_slice_module)默认自动支持,可以将 max_ranges 设置为 0的来取消这个设置。

Node

Node 默认不提供 对 Range方法的处理,需要自己写代码进行处理。

router.get(‘/api/rangeFile’, async(ctx) => {

const { filename } = ctx.query;

const { size } = fs.statSync(path.join(__dirname, ‘./static/’, filename));

const range = ctx.headers[‘range’];

if (!range) {

ctx.set(‘Accept-Ranges’, ‘bytes’);

ctx.body = fs.readFileSync(path.join(__dirname, ‘./static/’, filename));

return;

}

const { start, end } = getRange(range);

if (start >= size || end >= size) {

ctx.response.status = 416;

ctx.body = ‘’;

return;

}

ctx.response.status = 206;

ctx.set(‘Accept-Ranges’, ‘bytes’);

ctx.set(‘Content-Range’, bytes ${start}-${end ? end : size - 1}/${size});

ctx.body = fs.createReadStream(path.join(__dirname, ‘./static/’, filename), { start, end });

})

或者你可以使用 koa-send 这个库。

https://github.com/pillarjs/send/blob/0.17.1/index.js#L680

Range实践


架构总览

我们先来看下流程架构图总览。单线程很简单,正常下载就可以了,不懂的可以参看我上一篇文章。多线程的话,会比较麻烦一些,需要按片去下载,下载好后,需要进行合并再进行下载。(关于blob等下载方式依旧可以参看上一篇)

1600705973008

服务端代码

很简单,就是对Range做了兼容。

router.get(‘/api/rangeFile’, async(ctx) => {

const { filename } = ctx.query;

const { size } = fs.statSync(path.join(__dirname, ‘./static/’, filename));

const range = ctx.headers[‘range’];

if (!range) {

ctx.set(‘Accept-Ranges’, ‘bytes’);

ctx.body = fs.readFileSync(path.join(__dirname, ‘./static/’, filename));

return;

}

const { start, end } = getRange(range);

if (start >= size || end >= size) {

ctx.response.status = 416;

ctx.body = ‘’;

return;

}

ctx.response.status = 206;

ctx.set(‘Accept-Ranges’, ‘bytes’);

ctx.set(‘Content-Range’, bytes ${start}-${end ? end : size - 1}/${size});

ctx.body = fs.createReadStream(path.join(__dirname, ‘./static/’, filename), { start, end });

})

html

然后来编写 html ,这没有什么好说的,写两个按钮来展示。

串行下载

多线程下载

js公共参数

const m = 1024 * 520;  // 分片的大小

const url = ‘http://localhost:8888/api/rangeFile?filename=360_0388.jpg’; // 要下载的地址

单线程部分

单线程下载代码,直接去请求以blob方式获取,然后用blobURL 的方式下载。

download1.onclick = () => {

console.time(“直接下载”);

function download(url) {

const req = new XMLHttpRequest();

req.open(“GET”, url, true);

req.responseType = “blob”;

req.onload = function (oEvent) {

const content = req.response;

const aTag = document.createElement(‘a’);

aTag.download = ‘360_0388.jpg’;

const blob = new Blob([content])

const blobUrl = URL.createObjectURL(blob);

aTag.href = blobUrl;

aTag.click();

URL.revokeObjectURL(blob);

console.timeEnd(“直接下载”);

};

req.send();

}

download(url);

}

多线程部分

首先发送一个 head 请求,来获取文件的大小,然后根据 length 以及设置的分片大小,来计算每个分片是滑动距离。通过Promise.all的回调中,用concatenate函数对分片 buffer 进行一个合并成一个 blob,然后用blobURL 的方式下载。

// script

function downloadRange(url, start, end, i) {

return new Promise((resolve, reject) => {

const req = new XMLHttpRequest();

req.open(“GET”, url, true);

req.setRequestHeader(‘range’, bytes=${start}-${end})

req.responseType = “blob”;

req.onload = function (oEvent) {

req.response.arrayBuffer().then(res => {

resolve({

i,

buffer: res

});

})

};

req.send();

})

}

// 合并buffer

function concatenate(resultConstructor, arrays) {

let totalLength = 0;

for (let arr of arrays) {

totalLength += arr.length;

}

let result = new resultConstructor(totalLength);

let offset = 0;

for (let arr of arrays) {

result.set(arr, offset);

offset += arr.length;

}

return result;

}

download2.onclick = () => {

axios({

url,

method: ‘head’,

}).then((res) => {

// 获取长度来进行分割块

console.time(“并发下载”);

const size = Number(res.headers[‘content-length’]);

const length = parseInt(size / m);

const arr = []

for (let i = 0; i < length; i++) {

let start = i * m;

let end = (i == length - 1) ?  size - 1  : (i + 1) * m - 1;

arr.push(downloadRange(url, start, end, i))

}

Promise.all(arr).then(res => {

const arrBufferList = res.sort(item => item.i - item.i).map(item => new Uint8Array(item.buffer));

const allBuffer = concatenate(Uint8Array, arrBufferList);

const blob = new Blob([allBuffer], {type: ‘image/jpeg’});

const blobUrl = URL.createObjectURL(blob);

const aTag = document.createElement(‘a’);

aTag.download = ‘360_0388.jpg’;

aTag.href = blobUrl;

aTag.click();

URL.revokeObjectURL(blob);

console.timeEnd(“并发下载”);

})

})

}

完整示例

https://github.com/hua1995116/node-demo

// 进入目录

cd file-download

// 启动

node server.js

// 打开

http://localhost:8888/example/download-multiple/index.html

由于谷歌浏览器在 HTTP/1.1 对于单个域名有所限制,单个域名最大的并发量是 6.

这一点可以在源码以及官方人员的讨论中体现。

讨论地址

https://bugs.chromium.org/p/chromium/issues/detail?id=12066

Chromium 源码

// https://source.chromium.org/chromium/chromium/src/+/refs/tags/87.0.4268.1:net/socket/client_socket_pool_manager.cc;l=47

// Default to allow up to 6 connections per host. Experiment and tuning may

// try other values (greater than 0).  Too large may cause many problems, such

// as home routers blocking the connections!?!?  See http://crbug.com/12066.

//

// WebSocket connections are long-lived, and should be treated differently

// than normal other connections. Use a limit of 255, so the limit for wss will

// be the same as the limit for ws. Also note that Firefox uses a limit of 200.

// See http://crbug.com/486800

int g_max_sockets_per_group[] = {

6,   // NORMAL_SOCKET_POOL

255  // WEBSOCKET_SOCKET_POOL

};

因此为了配合这个特性我将文件分成6个片段,每个片段为520kb (没错,写个代码都要搞个爱你的数字),即开启6个线程进行下载。

我用单个线程和多个线程进行分别下载了6次,看上去速度是差不多的。那么为什么和我们预期的不一样呢?

image-20200919165242745

探索失败的原因


我开始仔细对比两个请求,观察这两个请求的速度。

6个线程并发

image-20200919170313455

单个线程

image-20200919170512650

我们按照3.7M 82ms 的速度来算的话,大约为 1ms 下载 46kb,而实际情况可以看到,533kb ,平均就要下载 20ms 左右(已经刨去了连接时间,纯 content 下载时间)。

我就去查找了一些资料,明白了有个叫做下行速度和上行速度的东西。

网络的实际传输速度要分上行速度和下行速度,上行速率就是发送出去数据的速度,下行就是收到数据的速度。ADSL是根据我们平时上网,发出数据的要求相对下载数据的较小这种习惯来实现的一种传输方式。我们说对于4M的宽带,那么我们的l理论最高下载速度就是512K/S,这就是所说的下行速度。 --百度百科

那我们现在的情况是怎么样的呢?

把服务器比作一根大水管,我来用图模拟一下我们单个线程和多个线程下载的情况。左侧为服务器端,右侧为客户端。(以下所有情况都是考虑理想情况下,只是为了模拟过程,不考虑其他一些程序的竞态影响。)

单线程

IMG_01

多线程

IMG_02

没错,由于我们的服务器是一根大水管,流速是一定的,并且我们客户端没有限制。如果是单线程跑的话,那么会跑满用户的最大的速度。如果是多线程呢,以3个线程为例子的话,相当于每个线程都跑了原先线程三分之一的速度。合起来的速度和单个线程是没有差别的。

下面我就分几种情况来讲解一下,什么样的情况才我们的多线程才会生效呢?

服务器带宽大于用户带宽,不做任何限制

这种情况其实我们遇到的情况差不多的。

服务器带宽远大于用户带宽,限制单连接网速

IMG_03

如果服务器限制了单个宽带的下载速度,大部分也是这种情况,例如百度云就是这样,例如明明你是 10M 的宽带,但是实际下载速度只有 100kb/s ,这种情况下,我们就可以开启多线程去下载,因为它往往限制的是单个TCP的下载,当然在线上环境不是说可以让用户开启无限多个线程,还是会有限制的,会限制你当前IP的最大TCP。这种情况下下载的上限往往是你的用户最大速度。按照上面的例子,如果你开10个线程已经达到了最大速度,因为再大,你的入口已经被限制死了,那么各个线程之间就会抢占速度,再多开线程也没有用了。

改进方案


由于 Node 我暂时没有找到比较简单地控制下载速度的方法,因此我就引入了 Nginx。

我们将每个TCP连接的速度控制在 1M/s。

加入配置 limit_rate 1M;

准备工作

1.nginx_conf

server {

listen 80;

server_name limit.qiufeng.com;

access_log /opt/logs/wwwlogs/limitqiufeng.access.log;

error_log /opt/logs/wwwlogs/limitqiufeng.error.log;

add_header Cache-Control max-age=60;

add_header Access-Control-Allow-Origin *;

add_header Access-Control-Allow-Methods ‘GET, OPTIONS’;

add_header Access-Control-Allow-Headers ‘DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,range,If-Range’;

if ($request_method = ‘OPTIONS’) {

return 204;

}

limit_rate 1M;

location / {

root 你的静态目录;

index index.html;

}

}

2.配置本地 host

127.0.0.1 limit.qiufeng.com

查看效果,这下基本上速度已经是正常了,多线程下载比单线程快了速度。基本是 5-6 : 1 的速度,但是发现如果下载过程中快速点击数次后,使用Range下载会越来越快(此处怀疑是 Nginx 做了什么缓存,暂时没有深入研究)。

修改代码中的下载地址

const url = ‘http://localhost:8888/api/rangeFile?filename=360_0388.jpg’;

变成

const url = ‘http://limit.qiufeng.com/360_0388.jpg’;

测试下载速度

image-20200919201613507

还记得上面说的吗,关于 HTTP/1.1 同一站点只能并发 6 个请求,多余的请求会放到下一个批次。但是 HTTP/2.0 不受这个限制,多路复用代替了 HTTP/1.x序列和阻塞机制。让我们来升级 HTTP/2.0 来测试一下。

需要本地生成一个证书。(生成证书方法: https://juejin.im/post/6844903556722475021)

server {

listen 443 ssl http2;

ssl on;

ssl_certificate /usr/local/openresty/nginx/conf/ssl/server.crt;

ssl_certificate_key /usr/local/openresty/nginx/conf/ssl/server.key;

ssl_session_cache shared:le_nginx_SSL:1m;

ssl_session_timeout 1440m;

ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;

ssl_ciphers RC4:HIGH:!aNULL:!MD5;

ssl_prefer_server_ciphers on;

server_name limit.qiufeng.com;

学习笔记

主要内容包括html,css,html5,css3,JavaScript,正则表达式,函数,BOM,DOM,jQuery,AJAX,vue等等

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

HTML/CSS

**HTML:**HTML基本结构,标签属性,事件属性,文本标签,多媒体标签,列表 / 表格 / 表单标签,其他语义化标签,网页结构,模块划分

**CSS:**CSS代码语法,CSS 放置位置,CSS的继承,选择器的种类/优先级,背景样式,字体样式,文本属性,基本样式,样式重置,盒模型样式,浮动float,定位position,浏览器默认样式

HTML5 /CSS3

**HTML5:**HTML5 的优势,HTML5 废弃元素,HTML5 新增元素,HTML5 表单相关元素和属性

**CSS3:**CSS3 新增选择器,CSS3 新增属性,新增变形动画属性,3D变形属性,CSS3 的过渡属性,CSS3 的动画属性,CSS3 新增多列属性,CSS3新增单位,弹性盒模型

JavaScript

**JavaScript:**JavaScript基础,JavaScript数据类型,算术运算,强制转换,赋值运算,关系运算,逻辑运算,三元运算,分支循环,switch,while,do-while,for,break,continue,数组,数组方法,二维数组,字符串

学习笔记

主要内容包括html,css,html5,css3,JavaScript,正则表达式,函数,BOM,DOM,jQuery,AJAX,vue等等

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

HTML/CSS

**HTML:**HTML基本结构,标签属性,事件属性,文本标签,多媒体标签,列表 / 表格 / 表单标签,其他语义化标签,网页结构,模块划分

**CSS:**CSS代码语法,CSS 放置位置,CSS的继承,选择器的种类/优先级,背景样式,字体样式,文本属性,基本样式,样式重置,盒模型样式,浮动float,定位position,浏览器默认样式

[外链图片转存中…(img-ZEtdI6ah-1715883065513)]

HTML5 /CSS3

**HTML5:**HTML5 的优势,HTML5 废弃元素,HTML5 新增元素,HTML5 表单相关元素和属性

**CSS3:**CSS3 新增选择器,CSS3 新增属性,新增变形动画属性,3D变形属性,CSS3 的过渡属性,CSS3 的动画属性,CSS3 新增多列属性,CSS3新增单位,弹性盒模型

[外链图片转存中…(img-tlpm3V3R-1715883065514)]

JavaScript

**JavaScript:**JavaScript基础,JavaScript数据类型,算术运算,强制转换,赋值运算,关系运算,逻辑运算,三元运算,分支循环,switch,while,do-while,for,break,continue,数组,数组方法,二维数组,字符串

[外链图片转存中…(img-ICL8xX30-1715883065515)]

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐