function getParamsByName(name){
name = name.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]");
var regex = new RegExp("[\\?&]"+name+"=([^&#]*)"),
result = regex.exec(location.search);
return results ==null?"":decodeURIComponent(results[1])
}
复制代码
/** * 功能:1.对象数组根据某一个key进行去重 key为id, * 1:[{id:1,name:'2'},{id:1,name:'n1'},{id:2,name:'n2'}] =>[{id:1,name:'2'},{id:2,name:'n2'}] * * 2.对象数组按key进行去重,并合并为一个针对key的数据格式 key为id,isExtract =true * [{id:1,name:'2'},{id:1,name:'n1'},{id:2,name:'n2'}] =>[1,2] * 参数:soucreArray:数组 key :根据哪一个字段进行去重 isExtract * */
function sweepDuplicatte(sourceArray,key,isExtract){
let storeDesignationkey ={};
const targetArray = sourceArray.reduce((initalTargetArray,currentItem)=>{
if(!storeDesignationKey[currentItem[key]]){
storeDesignationKey[currentItem[key]]=true;
if(isExtract){
initalTargetArray.push(currentItem[key])
}else{
initalTargetArray.push(currentItem);
}
}
return initalTargetArray;
},[]);
return targetArray;
}
复制代码
? 问号表示某个模式出现0次或1次,等同于{0, 1}。
* 星号表示某个模式出现0次或屡次,等同于{0,}。
+ 加号表示某个模式出现1次或屡次,等同于{1,}。javascript
replace方法的第二个参数能够使用美圆符号$,用来指代所替换的内容。css
$&:匹配的子字符串。
$`:匹配结果前面的文本。
$’:匹配结果后面的文本。
$n:匹配成功的第n组内容,n是从1开始的天然数。
$$:指代美圆符号$。html
//先后的字符替换位置
'hello world'.replace(/(\w+)\s(\w+)/, '$2 $1')
'abc'.replace('b', '[$`-$&-$\']') // "a[a-b-c]c" 复制代码
function loadScript(url,callback){
var script = doucument.createElement('script');
script.type ='text/javascript';
if(script.readyState){
//IE
script.onreadystatechange =function(){
if(script.readyState =='loaded'||script.readyState=='complete'){
script.onreadysatatechange=null;
callback()
}
}
}else{
scripte.onload = function(){
callback();
}
}
script.src =url;
document.getElementByTagName('head')[0].appendChild(script);
}
复制代码
每个js函数都是一个对象(是Function
对象的一个实例)。前端
内部属性[[Scope]]包含一个函数被建立的做用域中对象的集合(做用域链)。函数做用域中的每一个对象被称为一个可变对象,而且以"键值对"的形式存在。java
执行函数时会建立一个执行环境(execution context)的内部对象。一个执行环境定义了一个函数执行时的环境。每次执行都不同。函数执行完毕,执行环节会被销毁。node
每一个执行环境都有本身的做用域链,用于解析标识符。当执行环境被建立时,它的做用域链初始化为当前运行函数的[[Scope]]属性中的对象。这个复制过程完成后,一个呗称为"活动对象(activation object)"的新对象就为执行环境建立好了。活动对象做为函数运行时的变量对象,包含了全部局部变量,命名参数,参数集合以及this.而后此对象被推入做用域链的最前端。nginx
容许函数访问局部做用域以外的数据。c++
文档对象模型(DOM)是一个独立于语言的,用于操做XML和HTML文档的程序接口(API)。web
浏览器中一般会把DOM和JavaScript独立实现。正则表达式
浏览器 | js引擎 | 渲染引擎 |
---|---|---|
IE | JScript(旧) Chakra(新) |
Trident |
Safari | JavaScriptCore(SquirreFish) | WebCore |
Chorme | V8 | WebCore |
FireFox | TraceMonkey | Gecko |
HTTP over TCP/IP
HTTP 是一个用在计算机世界里的协议,它确立了一种计算机之间交流通讯的规范,以及相关的各类控制和错误处理方式。
HTTP 专门用来在两点之间传输数据,不能用于广播、寻址或路由。
HTTP 传输的是文字、图片、音频、视频等超文本数据。
IP 协议是“Internet Protocol”的缩写,主要目的是解决寻址和路由问题,以及如何在两点间传送数据包。IP 协议使用“IP 地址”的概念来定位互联网上的每一台计算机。
TCP 协议是“Transmission Control Protocol”的缩写,意思是“传输控制协议”,它位于 IP 协议之上,基于 IP 协议提供可靠的、字节流形式的通讯,是 HTTP 协议得以实现的基础。
“可靠”是指保证数据不丢失,“字节流”是指保证数据完整,因此在 TCP 协议的两端能够如同操做文件同样访问传输的数据,就像是读写在一个密闭的管道里“流动”的字节。
URI(Uniform Resource Identifier)
中文名称是 统一资源标识符,使用它就可以惟一地标记互联网上资源。
URL(Uniform Resource Locator)
URI 另外一个更经常使用的表现形式是 , 统一资源定位符,也就是咱们俗称的“网址”,它其实是 URI 的一个子集
http://nginx.org/en/download.html
复制代码
HTTP over SSL/TLS也就是运行在 SSL/TLS 协议上的 HTTP.
为HTTP 套了一个安全的外壳
HTTP+SSL/TLS+TCP/IP
SSL 使用了许多密码学最早进的研究成果,综合了对称加密、非对称加密、摘要算法、数字签名、数字证书等技术,可以在不安全的环境中为通讯的双方建立出一个秘密的、安全的传输通道,为 HTTP 套上一副坚固的盔甲。
负责在以太网、WiFi 这样的底层网络上发送原始数据包,工做在网卡这个层次,使用 MAC 地址来标记网络上的设备,因此有时候也叫 MAC 层。
IP 协议就处在这一层。由于 IP 协议定义了IP 地址的概念,因此就能够在“连接层”的基础上,用 IP 地址取代 MAC 地址,把许许多多的局域网、广域网链接成一个虚拟的巨大网络,在这个网络里找设备时只要把 IP 地址再“翻译”成 MAC 地址就能够了
保证数据在 IP 地址标记的两点之间“可靠”地传输,是 TCP 协议工做的层次,另外还有它的一个“小伙伴”UDP。
两个协议的另外一个重要区别在于数据的形式。
Telnet、SSH、FTP、SMTP,HTTP。
开放式系统互联通讯参考模型(Open System Interconnection Reference Model)
使用字符串来代替 IP 地址,方便用户记忆,本质上一个名字空间系统
DNS 是一个树状的分布式查询系统,但为了提升查询效率,外围有多级的缓存
偏向于不容忍隐式类型转换
偏向于容忍隐式类型转换。譬如说C语言的int能够变成double
编译的时候就知道每个变量的类型,由于类型错误而不能作的事情是语法错误。
编译的时候不知道每个变量的类型,由于类型错误而不能作的事情是运行时错误
渐进复杂度,包括时间复杂度和空间复杂度,用来分析算法执行效率与数据规模之间的增加关系,能够粗略地表示,越高阶复杂度的算法,执行效率越低
从低阶到高阶有:O(1)、O(logn)、O(n)、O(nlogn)、O(n2 )
一种线性表数据结构。它用一组连续的内存空间,来存储一组具备相同类型的数据。
线性表
As JavaScript arrays are implemented as hash-maps(哈希表) or dictionaries(字典) and not contiguous.(非连续)
HTTP 协议是运行在 TCP/IP 基础上的,依靠 TCP/IP 协议来实现数据的可靠传输。因此浏览器要用 HTTP 协议收发数据,首先要作的就是创建 TCP 链接
具体交互过程以下
在使用域名访问网页的时候,多一步操做就是DNS的解析。在拿到对应的IP地址以后,操做流程同IP访问是同样的。
拿TCP 报文来举例,它在实际要传输的数据以前附加了一个 20 字节的头部数据,存储 TCP 协议必须的额外信息,例如发送方的端口号、接收方的端口号、包序号、标志位等等
HTTP 协议也是与 TCP/UDP 相似,一样也须要在实际传输的数据前附加一些头数据,不过与 TCP/UDP 不一样的是,它是一个纯文本的协议,因此头数据都是 ASCII 码的文本。
HTTP 协议的请求报文和响应报文的结构基本相同,由三大部分组成:
其中前两部分起始行和头部字段常常又合称为“请求头”或“响应头”,消息正文又称为“实体”,但与“header”对应,不少时候就直接称为“body”。
HTTP 协议规定报文必须有 header,但能够没有 body,并且在 header 以后必需要有一个空行,也就是“CRLF”,十六进制的“0D0A”。
HTTP 报文的格式以下:
如上的报文中:第一行“GET / HTTP/1.1”就是请求行,然后面的“Host”“Connection”等等都属于 header,报文的最后是一个空白行结束,没有 body
描述了客户端想要如何操做服务器端的资源
请求行由三部分构成:
描述了服务器响应的状态
状态行也是由三部分组成
请求行或状态行再加上头部字段集合就构成了 HTTP 报文里完整的请求头或响应头
它并不须要一块连续的内存空间,它经过“指针”将一组零散的内存块串联起来使用
内存分配
链表经过指针将一组零散的内存块串联在一块儿。其中,咱们把内存块称为链表的“结点”
其中有两个结点是比较特殊的,它们分别是第一个结点和最后一个结点。咱们习惯性地把第一个结点叫做头结点,把最后一个结点叫做尾结点。其中,头结点用来记录链表的基地址。有了它,咱们就能够遍历获得整条链表。而尾结点特殊的地方是:指针不是指向下一个结点,而是指向一个空地址 NULL,表示这是链表上最后一个结点。
循环链表的尾结点指针是指向链表的头结点
双向链表须要额外的两个空间来存储后继结点和前驱结点的地址。因此,若是存储一样多的数据,双向链表要比单链表占用更多的内存空间。
数组中随机访问,指的是按下标的随机访问
大体的结构以下:
size和head为LinkedList构造函数私有属性,size记录链表中有多少个节点,head指向链表的头结点
/**
* 自定义链表:对外公开的方法有
* append(element) 在链表最后追加节点
* insert(index, element) 根据索引index, 在索引位置插入节点
* remove(element) 删除节点
* removeAt(index) 删除指定索引节点
* removeAll(element) 删除全部匹配的节点
* set(index, element) 根据索引,修改对应索引的节点值
* get(index) 根据索引获取节点信息
* indexOf(element) 获取某个节点的索引位置
* clear() 清空全部节点
* length() 返回节点长度
* print() 打印全部节点信息
* toString() 打印全部节点信息,同print
* */
const LinkedList = function(){
let head = null;
let size = 0; //记录链表元素个数
//Node模型
function LinkNode(element, next){
this.element = element;
this.next = next;
}
//元素越界检查, 越界抛出异常
function outOfBounds(index){
if (index < 0 || index >= size){
throw("抱歉,目标位置不存在!");
}
}
//根据索引,获取目标对象
function node(index){
outOfBounds(index);
let obj = head;
for (let i = 0; i < index; i++){
obj = obj.next;
}
return obj;
}
//新增一个元素
function append(element){
if (size == 0){
head = new LinkNode(element, null);
}
else{
let obj = node(size-1);
obj.next = new LinkNode(element, null);
}
size++;
}
//插入一个元素
function insert(index, element){
if (index == 0){
head = new LinkNode(element, head);
}
else{
let obj = node(index-1);
obj.next = new LinkNode(element, obj.next);
}
size++;
}
//修改元素
function set(index, element){
let obj = node(index);
obj.element = element;
}
//根据值移除节点元素
function remove(element){
if (size < 1) return null;
if (head.element == element){
head = head.next;
size--;
return element;
}
else{
let temp = head;
while(temp.next){
if (temp.next.element == element){
temp.next = temp.next.next;
size--;
return element;
}
else{
temp = temp.next;
}
}
}
return null;
}
//根据索引移除节点
function removeAt(index){
outOfBounds(index);
let element = null;
if (index == 0){
element = head.element;
head = head.next;
}
else{
let prev = node(index-1);
element = prev.next.element;
prev.next = prev.next.next;
}
size--;
return element;
}
//移除链表里面的全部匹配值element的元素
function removeAll(element){
let virHead = new LinkNode(null, head); //建立一个虚拟头结点,head为次节点
let tempNode = virHead, ele = null;
while(tempNode.next){
if (tempNode.next.element == element){
tempNode.next = tempNode.next.next;
size--;
ele = element;
}
else{
tempNode = tempNode.next;
}
}
//从新赋值
head = virHead.next;
return ele;
}
//获取某个元素
function get(index){
return node(index).element;
}
//获取元素索引
function indexOf(element){
let obj = head, index = -1;
for (let i = 0; i < size; i++){
if (obj.element == element){
index = i;
break;
}
obj = obj.next;
}
return index;
}
//清除全部元素
function clear(){
head = null;
size = 0;
}
//属性转字符串
function getObjString(obj){
let str = "";
if (obj instanceof Array){
str += "[";
for (let i = 0; i < obj.length; i++){
str += getObjString(obj[i]);
}
str = str.substring(0, str.length - 2);
str += "], "
}
else if (obj instanceof Object){
str += "{";
for (var key in obj){
let item = obj[key];
str += "\"" + key + "\": " + getObjString(item);
}
str = str.substring(0, str.length-2);
str += "}, "
}
else if (typeof obj == "string"){
str += "\"" + obj + "\"" + ", ";
}
else{
str += obj + ", ";
}
return str;
}
function toString(){
let str = "", obj = head;
for (let i = 0; i < size; i++){
str += getObjString(obj.element);
obj = obj.next;
}
if (str.length > 0) str = str.substring(0, str.length -2);
return str;
}
//打印全部元素
function print(){
console.log(this.toString())
}
//对外公开方法
this.append = append;
this.insert = insert;
this.remove = remove;
this.removeAt = removeAt;
this.removeAll = removeAll;
this.set = set;
this.get = get;
this.indexOf = indexOf;
this.length = function(){
return size;
}
this.clear = clear;
this.print = print;
this.toString = toString;
}
////测试
// let obj = new LinkedList();
// let obj1 = { title: "全明星比赛", stores: [{name: "张飞vs岳飞", store: "2:3"}, { name: "关羽vs秦琼", store: "5:5"}]};
//
// obj.append(99);
// obj.append("hello")
// obj.append(true)
// obj.insert(3, obj1);
// obj.insert(0, [12, false, "Good", 81]);
// obj.print();
// console.log("obj1.index: ", obj.indexOf(obj1));
// obj.remove(0);
// obj.removeAll(obj1);
// obj.print();
////测试2
console.log("\n\n......test2.....")
var obj2 = new LinkedList();
obj2.append(8); obj2.insert(1,99); obj2.append('abc'); obj2.append(8); obj2.append(false);
obj2.append(12); obj2.append(8); obj2.append('123'); obj2.append(8);
obj2.print();
obj2.removeAll(8); //删除全部8
obj2.print();
复制代码
/**
*
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var reverseList = function(head) {
let prev = null;
let curr = head;//定义"哨兵"
while (curr != null) {
let nextTemp = curr.next;//暂存剩余链表
curr.next = prev;//与剩余链表断开链接,并将指向变动
prev = curr;//两个节点交互位置
curr = nextTemp;//指针下移
}
return prev;
};
复制代码
统一资源标识符(Uniform Resource Identifier)。由于它常常出如今浏览器的地址栏里,因此俗称为“网络地址”,简称“网址”。
URI 本质上是一个字符串,这个字符串的做用是惟一地标记资源的位置或者名字。
具体格式以下:
note: user:passwd@ 已经不推荐使用了
URI 里只能使用 ASCII 码
URI 引入了编码机制,对于 ASCII 码之外的字符集和特殊字符作一个特殊的操做,把它们转换成与 URI 语义不冲突的形式。这在 RFC 规范里称为“escape”和“unescape”,俗称“转义”。
响应状态行格式以下:
RFC 标准把状态码分红了五类
在 TCP/IP 协议栈里,传输数据基本上都是“header+body”的格式。
MIME:多用途互联网邮件扩展”(Multipurpose Internet Mail Extensions)用于发送 ASCII 码之外的任意数据
MIME 把数据分红了八大类,每一个大类下再细分出多个子类,形式是type/subtype的字符串。
经常使用的以下:
HTTP 在传输时为了节约带宽,有时候还会压缩数据,有一个“Encoding type”,告诉数据是用的什么编码格式,这样对方才能正确解压缩,还原出原始的数据。
有了 MIME type 和 Encoding type,不管是浏览器仍是服务器就均可以轻松识别出 body 的类型,也就可以正确处理数据了。
HTTP 协议为此定义了两个 Accept 请求头字段和两个** Content** 实体头字段,用于客户端和服务器进行“内容协商”。也就是说,客户端用 Accept 头告诉服务器但愿接收什么样的数据,而服务器用 Content 头告诉客户端实际发送了什么样的数据。
Accept字段标记的是客户端可理解的 MIME type,能够用“,”作分隔符列出多个类型,让服务器有更多的选择余地。
相应的,服务器会在响应报文里用头字段Content-Type告诉实体数据的真实类型。
MIME type 和 Encoding type 解决了计算机理解 body 数据的问题,但互联网遍及全球,不一样国家不一样地区的人使用了不少不一样的语言,虽然都是 text/html,但如何让浏览器显示出每一个人均可理解可阅读的语言文字呢?
这实际上就是“国际化”的问题。HTTP 采用了与数据类型类似的解决方案,又引入了两个概念:语言类型与字符集。
语言类型:人类使用的天然语言,例如英语、汉语、日语等,而这些天然语言可能还有下属的地区性方言,因此在须要明确区分的时候也要使用“type-subtype”的形式,不过这里的格式与数据类型不一样,分隔符不是/,而是-。
HTTP 协议也使用 Accept 请求头字段和 Content 实体头字段,用于客户端和服务器就语言与编码进行“内容协商”。
Accept-Language字段标记了客户端可理解的天然语言,也容许用“,”作分隔符列出多个类型,例如:
Accept-Language: zh-CN, zh, en
复制代码
相应的,服务器应该在响应报文里用头字段Content-Language告诉客户端实体数据使用的实际语言类型:
Content-Language: zh-CN
复制代码
字符集在 HTTP 里使用的请求头字段是Accept-Charset,但响应头里却没有对应的 Content-Charset,而是在Content-Type字段的数据类型后面用charset=xxx来表示,这点须要特别注意。
一般浏览器在发送请求时都会带着Accept-Encoding头字段,里面是浏览器支持的压缩格式列表,例如 gzip、deflate、br 等,这样服务器就能够从中选择一种压缩算法,放进Content-Encoding响应头里,再把原数据压缩后发给浏览器。
压缩是把大文件总体变小,若是大文件总体不能变小,那就把它拆开,分解成多个小块,把这些小块分批发给浏览器,浏览器收到后再组装复原。
这种化整为零的思路在 HTTP 协议里就是chunked分块传输编码,在响应报文里用头字段Transfer-Encoding: chunked来表示,意思是报文里的 body 部分不是一次性发过来的,而是分红了许多的块(chunk)逐个发送。
分块传输也能够用于流式数据,例如由数据库动态生成的表单页面,这种状况下 body 数据的长度是未知的,没法在头字段Content-Length里给出确切的长度,因此也只能用 chunked 方式分块发送。
Transfer-Encoding: chunked和Content-Length这两个字段是互斥的,也就是说响应报文里这两个字段不能同时出现,一个响应报文的传输要么是长度已知,要么是长度未知(chunked)。
有了分块传输编码,服务器就能够轻松地收发大文件了,可是若是想获取一个大文件其中的片断数据,而分块传输并无这个能力。
HTTP 协议为了知足这样的需求,提出了范围请求(range requests)的概念,容许客户端在请求头里使用专用字段来表示只获取文件的一部分,至关因而客户端的“化整为零”。
范围请求不是 Web 服务器必备的功能,能够实现也能够不实现,因此服务器必须在响应头里使用字段Accept-Ranges: bytes明确告知客户端,支持范围请求。
请求头Range是 HTTP 范围请求的专用字段,格式是bytes=x-y,其中的 x 和 y 是以字节为单位的数据范围。
x、y 表示的是“偏移量”,范围必须从 0 计数。
服务器收到 Range 字段后,须要作四件事。
第一,它必须检查范围是否合法,好比文件只有 100 个字节,但请求“200-300”,这就是范围越界了。服务器就会返回状态码416,意思是“你的范围请求有误,我没法处理,请再检查一下”。
第二,若是范围正确,服务器就能够根据 Range 头计算偏移量,读取文件的片断了,返回状态码206 Partial Content,和 200 的意思差很少,但表示 body 只是原数据的一部分。
第三,服务器要添加一个响应头字段Content-Range,告诉片断的实际偏移量和资源的总大小,格式是bytes x-y/length,与 Range 头区别在没有“=”,范围后多了总长度。例如,对于“0-10”的范围请求,值就是“bytes 0-10/100”。
最后剩下的就是发送数据了,直接把片断用** TCP** 发给客户端,一个范围请求就算是处理完了。
刚才说的范围请求一次只获取一个片断,其实它还支持在 Range 头里使用多个x-y,一次性获取多个片断数据。
这种状况须要使用一种特殊的 MIME 类型:multipart/byteranges,表示报文的 body 是由多段字节序列组成的,而且还要用一个参数boundary=xxx给出段之间的分隔标记。
关于栈,有一个很是贴切的例子,就是一摞叠在一块儿的盘子。咱们平时放盘子的时候,都是从下往上一个一个放;取的时候,咱们也是从上往下一个一个地依次取,不能从中间任意抽出。后进者先出,先进者后出,这就是典型的“栈”结构。
从栈的操做特性上来看
栈是一种“操做受限”的线性表,只容许在一端插入和删除数据。
实际上,栈既能够用数组来实现,也能够用链表来实现。用数组实现的栈,咱们叫做顺序栈,用链表实现的栈,咱们叫做链式栈。(这也仅仅针对的是C,C++,JAVA等静态类型语言)
操做系统给每一个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构, 用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量做为一个栈帧入栈,当被调用函数执行完成,返回以后,将这个函数对应的栈帧出栈。
function main() {
let a = 1;
let ret = 0;
let res = 0;
ret = add(3, 5);
res = a + ret;
console.log(`处理结果为 ${res}`);
reuturn 0;
}
function add( x, y) {
let sum = 0;
sum = x + y;
return sum;
}
复制代码
图中显示的是,在执行到 add() 函数时,函数调用栈的状况。
实际上,编译器就是经过两个栈来实现的。其中一个保存操做数的栈,另外一个是保存运算符的栈。咱们从左向右遍历表达式,当遇到数字,咱们就直接压入操做数栈;当遇到运算符,就与运算符栈的栈顶元素进行比较。
将 3+5*8-6 这个表达式的计算过程画成了一张图,以下:
###实现浏览器的前进、后退功能
使用两个栈,X 和 Y,咱们把首次浏览的页面依次压入栈 X,当点击后退按钮时,再依次从栈 X 中出栈,并将出栈的数据依次放入栈 Y。当咱们点击前进按钮时,咱们依次从栈 Y 中取出数据,放入栈 X 中。当栈 X 中没有数据时,那就说明没有页面能够继续后退浏览了。当栈 Y 中没有数据,那就说明没有页面能够点击前进按钮浏览了。
如此反复如上操做,就能够简单解释浏览器回退和前进的操做步骤。
内存中的堆栈和数据结构堆栈不是一个概念,能够说内存中的堆栈是真实存在的物理区,数据结构中的堆栈是抽象的数据存储结构。
内存空间在逻辑上分为三部分:代码区、静态数据区和动态数据区,动态数据区又分为栈区和堆区。
HTTP 协议最初(0.9/1.0)是个很是简单的协议,通讯过程也采用了简单的请求 - 应答方式。
它底层的数据传输基于 TCP/IP,每次发送请求前须要先与服务器创建链接,收到响应报文后会当即关闭链接。
由于客户端与服务器的整个链接过程很短暂,不会与服务器保持长时间的链接状态,因此就被称为短链接(short-lived connections)。早期的 HTTP 协议也被称为是无链接的协议。
由于在 TCP 协议里,创建链接和关闭链接都是很是“昂贵”的操做。TCP 创建链接要有“三次握手”,发送 3 个数据包,须要 1 个 RTT;关闭链接是“四次挥手”,4 个数据包须要 2 个 RTT。
而 HTTP 的一次简单“请求 - 响应”一般只须要 4 个包,若是不算服务器内部的处理时间,最可能是 2 个 RTT。这么算下来,浪费的时间就是“3÷5=60%”,有三分之二的时间被浪费掉了,传输效率低得惊人。
RTT(Round-Trip Time):往返时延。是指数据从网络一端传到另外一端所需的时间。一般,时延由发送时延、传播时延、排队时延、处理时延四个部分组成。
针对短链接暴露出的缺点,HTTP 协议就提出了长链接的通讯方式,也叫持久链接(persistent connections)、链接保活(keep alive)、链接复用(connection reuse)。
其实解决办法也很简单,用的就是成本均摊的思路,既然 TCP 的链接和关闭很是耗时间,那么就把这个时间成本由原来的一个“请求 - 应答”均摊到多个“请求 - 应答”上。
一个短链接与长链接的对比示意图。
因为长链接对性能的改善效果很是显著,因此在 HTTP/1.1 中的链接都会默认启用长链接。不须要用什么特殊的头字段指定,只要向服务器发送了第一次请求,后续的请求都会重复利用第一次打开的 TCP 链接,也就是长链接,在这个链接上收发数据。
固然,咱们也能够在请求头里明确地要求使用长链接机制,使用的字段是Connection,值是keep-alive。
服务器支持长链接,它总会在响应报文里放一个Connection: keep-alive字段。
由于 TCP 链接长时间不关闭,服务器必须在内存里保存它的状态,这就占用了服务器的资源。若是有大量的空闲长链接只连不发,就会很快耗尽服务器的资源,致使服务器没法为真正有须要的用户提供服务。
长链接也须要在恰当的时间关闭,不能永远保持与服务器的链接,这在客户端或者服务器均可以作到。
在客户端,能够在请求头里加上Connection: close字段,告诉服务器:“此次通讯后就关闭链接”。
服务器端一般不会主动关闭链接,但也能够使用一些策略。拿 Nginx 来举例,它有两种方式:
队头阻塞与短链接和长链接无关,而是由 HTTP 基本的请求 - 应答模型所致使的。
由于 HTTP 规定报文必须是一发一收,这就造成了一个先进先出的“串行”队列。队列里的请求没有轻重缓急的优先级,只有入队的前后顺序,排在最前面的请求被最优先处理。
主动跳转:跳转动做是由浏览器的使用者主动发起(经过
<a/>
标签进行页面跳转)
被动跳转:服务器来发起的,浏览器使用者没法控制,这在 HTTP 协议里有个专门的名词,叫作重定向(Redirection)。
一次重定向实际上发送了两次 HTTP 请求,第一个请求返回了 302,而后第二个请求就被重定向到了目标地址(URI)。
Location字段属于响应字段,必须出如今响应报文里。但只有配合 301/302 状态码才有意义,它标记了服务器要求重定向的** URI**。
在Location里的 URI 既能够使用绝对 URI,也能够使用相对 URI。
绝对 URI:就是完整形式的 URI,包括 scheme、host:port、path 等。
相对 URI:就是省略了 scheme 和 host:port,只有 path 和 query 部分,是不完整的,但能够从请求上下文里计算获得。
最多见的重定向状态码就是 301 和 302
HTTP 是无状态的,这既是优势也是缺点。优势是服务器没有状态差别,能够很容易地组成集群,而缺点就是没法支持须要记录状态的事务操做。
Cookie 是服务器委托浏览器存储的一些数据,让服务器有了“记忆能力”;
响应头字段Set-Cookie和请求头字段Cookie。
当用户经过浏览器第一次访问服务器的时候,服务器确定是不知道他的身份的。因此,就要建立一个独特的身份标识数据,格式是key=value,而后放进 Set-Cookie 字段里,随着响应报文一同发给浏览器。
浏览器收到响应报文,看到里面有 Set-Cookie,知道这是服务器给的身份标识,因而就保存起来,下次再请求的时候就自动把这个值放进 Cookie 字段里发给服务器。
由于第二次请求里面有了 Cookie 字段,服务器就知道这个用户不是新人,以前来过,就能够拿出 Cookie 里的值,识别出用户的身份,而后提供个性化的服务。
不过由于服务器的“记忆能力”实在是太差,一张小纸条常常不够用。因此,服务器有时会在响应头里添加多个 Set-Cookie,存储多个“key=value”。但浏览器这边发送时不须要用多个 Cookie 字段,只要在一行里用“;”隔开就行。
Cookie 就是服务器委托浏览器存储在客户端里的一些数据,而这些数据一般都会记录用户的关键识别信息。因此,就须要在“key=value”外再用一些手段来保护,防止外泄或窃取,这些手段就是** Cookie 的属性**。
让它只能在一段时间内可用,就像是食品的“保鲜期”,一旦超过这个期限浏览器就认为是 Cookie 失效,在存储里删除,也不会发送给服务器。
Cookie 的有效期能够使用 Expires 和** Max-Age **两个属性来设置。
Expires俗称过时时间,用的是绝对时间点,能够理解为“截止日期”(deadline)。
Max-Age用的是相对时间,单位是秒,浏览器用收到报文的时间点再加上 Max-Age,就能够获得失效的绝对时间。
Expires 和 Max-Age 能够同时出现,二者的失效时间能够一致,也能够不一致,但浏览器会优先采用 Max-Age 计算失效期。
让浏览器仅发送给特定的服务器和 URI,避免被其余网站盗用。
做用域的设置比较简单,Domain和Path指定了 Cookie 所属的域名和路径,浏览器在发送 Cookie 前会从 URI 中提取出 host 和 path 部分,对比 Cookie 的属性。若是不知足条件,就不会在请求头里发送 Cookie。
在 JS 脚本里能够用 document.cookie来读写 Cookie 数据,这就带来了安全隐患,有可能会致使跨站脚本(XSS)攻击窃取数据。
属性HttpOnly会告诉浏览器,此 Cookie 只能经过浏览器 HTTP 协议传输,禁止其余方式访问,浏览器的 JS 引擎就会禁用 document.cookie 等一切相关的 API,脚本攻击也就无从谈起了。
另外一个属性SameSite能够防范跨站请求伪造(XSRF)攻击,设置成SameSite=Strict能够严格限定 Cookie 不能随着跳转连接跨站发送.
缓存(Cache)是计算机领域里的一个重要概念,是优化系统性能的利器。
基于请求 - 应答模式的特色,能够大体分为客户端缓存和服务器端缓存。
HTTP缓存流程 就是:
服务器标记资源有效期使用的头字段是Cache-Control,里面的值max-age=x就是资源的有效时间,至关于告诉浏览器,“这个页面只能缓存 x 秒,以后就算是过时,不能用。”
Cache-Control字段里的max-age和 Cookie 有点像,都是标记资源的有效期。
这里的max-age是“生存时间”(又叫“新鲜度”“缓存寿命”,相似 TTL,Time-To-Live),时间的计算起点是响应报文的建立时刻(即 Date 字段,也就是离开服务器的时刻),而不是客户端收到报文的时刻,也就是说包含了在链路传输过程当中全部节点所停留的时间。
max-age是 HTTP 缓存控制最经常使用的属性,此外在响应报文里还能够用其余的属性来更精确地指示浏览器应该如何使用缓存:
把服务器的缓存控制策略画了一个流程图,
不止服务器能够发Cache-Control头,浏览器也能够发Cache-Control,也就是说请求 - 应答的双方均可以用这个字段进行缓存控制,互相协商缓存的使用策略。
当你点刷新按钮的时候,浏览器会在请求头里加一个Cache-Control: max-age=0。由于 max-age 是“生存时间”,max-age=0 的意思就是我要最新的数据,而本地缓存里的数据至少保存了几秒钟,因此浏览器就不会使用缓存,而是向服务器发请求。服务器看到 max-age=0,也就会用一个最新生成的报文回应浏览器。
Ctrl+F5 的强制刷新,它实际上是发了一个Cache-Control: no-cache,含义和**max-age=**0基本同样,就看后台的服务器怎么理解,一般二者的效果是相同的。
浏览器用Cache-Control作缓存控制只能是刷新数据,不能很好地利用缓存数据,又由于缓存会失效,使用前还必需要去服务器验证是不是最新版。
HTTP 协议就定义了一系列If开头的条件请求字段,专门用来检查验证资源是否过时。
条件请求一共有 5 个头字段,咱们最经常使用的是if-Modified-Since和If-None-Match这两个。须要第一次的响应报文预先提供Last-modified和ETag,而后第二次请求时就能够带上缓存里的原值,验证资源是不是最新的。
若是资源没有变,服务器就回应一个304 Not Modified,表示缓存依然有效,浏览器就能够更新一下有效期,而后放心大胆地使用缓存了。
ETag 是实体标签(Entity Tag)的缩写,是资源的一个惟一标识,主要是用来解决修改时间没法准确区分文件变化的问题。
ETag 还有“强”“弱”之分。 强 ETag 要求资源在字节级别必须彻底相符,弱 ETag 在值前有个“W/”标记,只要求资源在语义上没有变化,但内部可能会有部分发生了改变(例如 HTML 里的标签顺序调整,或者多了几个空格)。
都会保存到浏览器中,并能够设置过时时间。
引入 HTTP 代理后,原来简单的双方通讯就变复杂了一些,加入了一个或者多个中间人,但总体上来看,仍是一个有顺序关系的链条,并且链条里相邻的两个角色仍然是简单的一对一通讯,不会出现越级的状况。
链条的起点仍是客户端(也就是浏览器),中间的角色被称为代理服务器(proxy server),链条的终点被称为源服务器(origin server),意思是数据的“源头”“起源”。
代理服务就是指服务自己不生产内容,而是处于中间位置转发上下游的请求和响应,具备双重身份:面向下游的用户时,表现为服务器,表明源服务器响应客户端的请求;而面向上游的源服务器时,又表现为客户端,表明客户端发送请求。
代理最基本的一个功能是负载均衡。由于在面向客户端时屏蔽了源服务器,客户端看到的只是代理服务器,源服务器究竟有多少台、是哪些 IP 地址都不知道。因而代理服务器就能够掌握请求分发的“大权”,决定由后面的哪台服务器来响应请求。
在负载均衡的同时,代理服务还能够执行更多的功能,好比:
代理服务器须要用字段Via标明代理的身份。
Via 是一个通用字段,请求头或响应头里均可以出现。每当报文通过一个代理节点,代理服务器就会把自身的信息追加到字段的末尾,就像是经手人盖了一个章。
例以下图中有两个代理:proxy1 和 proxy2,客户端发送请求会通过这两个代理,依次添加就是“Via: proxy1, proxy2”,等到服务器返回响应报文的时候就要反过来走,头字段就是“Via: proxy2, proxy1”。
服务器的 IP 地址应该是保密的,关系到企业的内网安全,因此通常不会让客户端知道。不过反过来,一般服务器须要知道客户端的真实 IP 地址,方便作访问控制、用户画像、统计分析。
HTTP 标准里并无为此定义头字段,但已经出现了不少事实上的标准,最经常使用的两个头字段是X-Forwarded-For和X-Real-IP。
X-Forwarded-For的字面意思是为谁而转发,形式上和Via差很少,也是每通过一个代理节点就会在字段里追加一个信息。但Via追加的是代理主机名(或者域名),而X-Forwarded-For追加的是请求方的 IP 地址。因此,在字段里最左边的 IP 地址就客户端的地址。
X-Real-IP是另外一种获取客户端真实 IP 的手段,它的做用很简单,就是记录客户端 IP 地址,没有中间的代理信息,至关因而“X-Forwarded-For”的简化版。若是客户端和源服务器之间只有一个代理,那么这两个字段的值就是相同的。
在没有缓存的时候,代理服务器每次都是直接转发客户端和服务器的报文,中间不会存储任何数据,只有最简单的中转功能。
###加入了缓存后以后
代理服务收到源服务器发来的响应数据后须要作两件事。第一个固然是把报文转发给客户端,而第二个就是把报文存入本身的 Cache 里。
下一次再有相同的请求,代理服务器就能够直接发送 304 或者缓存数据,没必要再从源服务器那里获取。这样就下降了客户端的等待时间,同时节约了源服务器的网络带宽。
在 HTTP 的缓存体系中,缓存代理的身份十分特殊,它既是客户端,又是服务器,同时也既不是客户端,又不是服务器。
缓存代理便是客户端又是服务器
是由于它面向源服务器时是客户端,在面向客户端时又是服务器,因此它便可以用客户端的缓存控制策略也能够用服务器端的缓存控制策略,也就是说它能够同时使用各类Cache-Control属性。
缓存代理即不是客户端又不是服务器
由于它只是一个数据的中转站,并非真正的数据消费者和生产者,因此还须要有一些新的Cache-Control属性来对它作特别的约束
服务器端的Cache-Control属性**:max-age、no_store、no_cache 和 must-revalidate这 4 种缓存属性能够约束客户端,也能够约束代理**。
但客户端和代理是不同的,客户端的缓存只是用户本身使用,而代理的缓存可能会为很是多的客户端提供服务。因此,须要对它的缓存再多一些限制条件。
首先,要区分客户端上的缓存和代理上的缓存,能够使用两个新属性private和public。
private表示缓存只能在客户端保存,是用户私有的,不能放在代理上与别人共享。而public的意思就是缓存彻底开放,谁均可以存,谁均可以用。
而后,缓存失效后的从新验证也要区分开(即便用条件请求Last-modified和ETag),must-revalidate是只要过时就必须回源服务器验证,而新的proxy-revalidate只要求代理的缓存过时后必须验证,客户端没必要回源,只验证到代理这个环节就好了。
再次,缓存的生存时间能够使用新的s-maxage(s 是 share 的意思,注意 maxage 中间没有“-”),只限定在代理上可以存多久,而客户端仍然使用“max_age”。
还有一个代理专用的属性no-transform。代理有时候会对缓存下来的数据作一些优化,好比把图片生成 png、webp 等几种格式,方便从此的请求处理,而no-transform就会禁止这样作,不准“偷偷摸摸搞小动做”。
下面的流程图是完整的服务器端缓存控制策略,能够同时控制客户端和代理。
客户端在 HTTP 缓存体系里要面对的是代理和源服务器
max-stale的意思是若是代理上的缓存过时了也能够接受,但不能过时太多,超过 x 秒也会不要。min-fresh的意思是缓存必须有效,并且必须在 x 秒后依然有效。
队列跟栈同样,也是一种操做受限的线性表数据结构。
队列跟栈同样,也是一种抽象的数据结构。它具有先进先出的特性,支持在队尾插入元素,在队头删除元素。
队列能够用数组来实现,也能够用链表来实现。用数组实现的栈叫做顺序栈,用链表实现的栈叫做链式栈。一样,用数组实现的队列叫做顺序队列,用链表实现的队列叫做链式队列。
// 用数组实现的队列
const ArrayQueue=function(){
// 数组:items,数组大小:n
let items =[];
let n = 0;
// head 表示队头下标,tail 表示队尾下标
let head = 0;
let tail = 0;
// 申请一个大小为 capacity 的数组
function createArrayQueue(capacity) {
items = new Array(capacity);
n = capacity;
}
// 入队操做,将 item 放入队尾
function enqueue(item) {
// tail == n 表示队列末尾没有空间了
if (tail == n) {
// tail ==n && head==0,表示整个队列都占满了
if (head == 0) return false;
// 数据搬移
for (int i = head; i < tail; ++i) {
items[i-head] = items[i];
}
// 搬移完以后从新更新 head 和 tail
tail -= head;
head = 0;
}
items[tail] = item;
++tail;
return true;
}
// 出队
function dequeue() {
// 若是 head == tail 表示队列为空
if (head == tail) return null;
// 为了让其余语言的同窗看的更加明确,把 -- 操做放到单独一行来写了
let ret = items[head];
++head;
return ret;
}
this.createArrayQueue = createArrayQueue;
this.enqueue = enqueue;
this.dequeue = dequeue;
}
复制代码
用数组实现的队列,在 tail==n 时,会有数据搬移操做,这样入队操做性能就会受到影响。
想要避免数据搬移,能够用循环队列来处理。
咱们能够看到,图中这个队列的大小为 8,当前 head=4,tail=7。当有一个新的元素 a 入队时,咱们放入下标为 7 的位置。但这个时候,咱们并不把 tail 更新为 8,而是将其在环中后移一位,到下标为 0 的位置。当再有一个元素 b 入队时,咱们将 b 放入下标为 0 的位置,而后 tail 加 1 更新为 1。因此,在 a,b 依次入队以后,循环队列中的元素就变成了下面的样子:
经过这样的方法,咱们成功避免了数据搬移操做。
const CircularQueue = function {
// 数组:items,数组大小:n
let items;
let n = 0;
// head 表示队头下标,tail 表示队尾下标
let head = 0;
let tail = 0;
// 申请一个大小为 capacity 的数组
function createCircularQueuee(capacity) {
items = new Array(capacity);
n = capacity;
}
// 入队
function enqueue(item) {
// 队列满了
if ((tail + 1) % n == head) return false;
items[tail] = item;
tail = (tail + 1) % n;
return true;
}
// 出队
function dequeue() {
// 若是 head == tail 表示队列为空
if (head == tail) return null;
String ret = items[head];
head = (head + 1) % n;
return ret;
}
this.createCircularQueuee = createCircularQueuee;
this.enqueue = enqueue;
this.dequeue = dequeue;
}
复制代码
HTTP 的一些缺点,其中的无状态在加入 Cookie 后获得了解决,而另两个缺点——明文和不安全仅凭 HTTP 自身是无力解决的,须要引入新的 HTTPS 协议。
为何要有 HTTPS?简单的回答是由于 HTTP 不安全。因为 HTTP 天生明文的特色,整个传输过程彻底透明,任何人都可以在链路中截获、修改或者伪造请求 / 响应报文,数据不具备可信性。
若是通讯过程具有了四个特性,就能够认为是安全的,这四个特性是:
HTTPS 实际上是一个很是简单的协议,RFC 文档很小,只有短短的 7 页,里面规定了新的协议名https,默认端口号 443,至于其余的什么请求 - 应答模式、报文结构、请求方法、URI、头字段、链接管理等等都彻底沿用 HTTP,没有任何新的东西。
它把 HTTP 下层的传输协议由 TCP/IP 换成了** SSL/TLS**,由HTTP over TCP/IP变成了HTTP over SSL/TLS,让 HTTP 运行在了安全的 SSL/TLS 协议上,收发报文再也不使用 Socket API,而是调用专门的安全接口。
SSL 即安全套接层(Secure Sockets Layer),在 OSI 模型中处于第 5 层(会话层)。
互联网工程组 IETF 在 1999 年把它更名为 TLS(传输层安全,Transport Layer Security),正式标准化。
TLS 由记录协议、握手协议、警告协议、变动密码规范协议、扩展协议等几个子协议组成,综合使用了对称加密、非对称加密、身份认证等许多密码学前沿技术。
浏览器和服务器在使用 TLS 创建链接时须要选择一组恰当的加密算法来实现安全通讯,这些算法的组合被称为密码套件(cipher suite,也叫加密套件)。
TLS 的密码套件命名很是规范,格式很固定。基本的形式是密钥交换算法 + 签名算法 + 对称加密算法 + 摘要算法。
HTTPS 的安全性是由 TLS来保证的。它为 HTTP 增长了机密性、完整性,身份认证和不能否认等特性。
机密性是信息安全的基础,缺少机密性 TLS 就会成为无水之源、无根之木。
实现机密性最经常使用的手段是加密(encrypt),就是把消息用某种方式转换成谁也看不懂的乱码,只有掌握特殊钥匙的人才能再转换出原始文本。
这里的钥匙就叫作密钥(key),加密前的消息叫明文(plain text/clear text),加密后的乱码叫密文(cipher text),使用密钥还原明文的过程叫解密(decrypt),是加密的反操做,加密解密的操做过程就是加密算法。
因为 HTTPS、TLS 都运行在计算机上,因此密钥就是一长串的数字,但约定俗成的度量单位是位(bit),而不是字节(byte)。
1字节 = 8位
按照密钥的使用方式,加密能够分为两大类:对称加密和非对称加密。
对称加密很好理解,就是指加密和解密时使用的密钥都是同一个,是对称的。只要保证了密钥的安全,那整个通讯过程就能够说具备了机密性。
TLS 里有很是多的对称加密算法可供选择,好比 RC四、DES、3DES、AES、ChaCha20 等。目前经常使用的只有 AES 和 ChaCha20。
对称算法还有一个分组模式的概念,它可让算法用固定长度的密钥加密任意长度的明文,把小秘密(即密钥)转化为大秘密(即密文)。
对称加密看上去好像完美地实现了机密性,但其中有一个很大的问题:如何把密钥安全地传递给对方,术语叫密钥交换。
出现了非对称加密(也叫公钥加密算法)
它有两个密钥,一个叫公钥(public key),一个叫私钥(private key)。两个密钥是不一样的,不对称,公钥能够公开给任何人使用,而私钥必须严格保密。
公钥和私钥有个特别的单向性,虽然均可以用来加密解密,但公钥加密后只能用私钥解密,反过来,私钥加密后也只能用公钥解密。
非对称加密能够解决“密钥交换”的问题。网站秘密保管私钥,在网上任意分发公钥,你想要登陆网站只要用公钥加密就好了,密文只能由私钥持有者才能解密.
非对称加密没有密钥交换的问题,但由于它们都是基于复杂的数学难题,运算速度很慢,即便是 ECC 也要比 AES 差上好几个数量级。若是仅用非对称加密,虽然保证了安全,但通讯速度很慢,实用性就变成了零。
混合加密
在通讯刚开始的时候使用非对称算法,好比 RSA、ECDHE,首先解决密钥交换的问题。
而后用随机数产生对称算法使用的会话密钥(session key),再用公钥加密。由于会话密钥很短,一般只有 16 字节或 32 字节,因此慢一点也无所谓。
对方拿到密文后用私钥解密,取出会话密钥。这样,双方就实现了对称密钥的安全交换,后续就再也不使用非对称加密,全都使用对称加密。
对称加密和非对称加密,以及二者结合起来的混合加密,实现了机密性。仅有机密性,离安全还差的很远。
因此,在机密性的基础上还必须加上完整性、身份认证等特性,才能实现真正的安全。
实现完整性的手段主要是摘要算法(Digest Algorithm),也就是常说的散列函数、哈希函数(Hash Function)。
把摘要算法近似地理解成一种特殊的压缩算法,它可以把任意长度的数据压缩成固定长度、并且独一无二的摘要字符串,就好像是给这段数据生成了一个数字指纹。
换一个角度,也能够把摘要算法理解成特殊的单向加密算法,它只有算法,没有密钥,加密后的数据没法解密,不能从摘要逆推出原文。
摘要算法其实是把数据从一个大空间映射到了小空间,因此就存在冲突(collision,也叫碰撞)的可能性,可能会有两份不一样的原文对应相同的摘要。好的摘要算法必须可以“抵抗冲突”,让这种可能性尽可能地小。
由于摘要算法对输入具备单向性和雪崩效应,输入的微小不一样会致使输出的剧烈变化,因此也被 TLS 用来生成伪随机数(PRF,pseudo random function)。
目前 TLS 推荐使用的是 SHA-1 的后继者:SHA-2。
摘要算法保证了数字摘要和原文是彻底等价的。因此,咱们只要在原文后附上它的摘要,就可以保证数据的完整性。
不过摘要算法不具备机密性,若是明文传输,那么黑客能够修改消息后把摘要也一块儿改了,网站仍是鉴别不出完整性。
因此,真正的完整性必需要创建在机密性之上,在混合加密系统里用会话密钥加密消息和摘要,这样黑客没法得知明文,也就没有办法动手脚了。
这有个术语,叫哈希消息认证码(HMAC)。
加密算法结合摘要算法,咱们的通讯过程能够说是比较安全了。但这里还有漏洞,就是通讯的两个端点(endpoint)。
使用私钥再加上摘要算法,就可以实现数字签名,同时实现“身份认证”和“不能否认”。
数字签名的原理其实很简单,就是把公钥私钥的用法反过来,以前是公钥加密、私钥解密,如今是私钥加密、公钥解密。