CSS相关
CSS选择器
- id选择器 #myId
- 类选择器 .myClassName
- 标签选择器 div, h1, p
- 相邻选择器 p + span ( p元素后面第一个必须是 span 才起作用 )
- 相邻选择器 p ~ span (在 p 元素后面与 p 元素同级的 span )
- 后代选择器 div ul ( div 元素下面所有的ul,包括孙子节点 ul )
- 子选择器 div > ul ( div 元素下面直接子级 ul,不包括孙子节点 ul )
- 通配符 *
- 属性选择器 a[rel="external"]
- 伪类选择器
- nth-child .container span:nth-child(2){} 指的是类container下面的第二个元素是span的样式
- nth-of-type .container span:nth-of-type(2){} 指的是类container下面的span类型的第二个元素的样式
- 伪元素选择器
- ::after 用来创建一个伪元素,作为已选中元素的最后一个子元素。通常配合content属性来为该元素添加装饰内容。这个虚拟元素默认是行内元素。
- ::before 创建一个伪元素,其将成为匹配选中的元素的第一个子元素。常通过 content 属性来为一个元素添加修饰性的内容。此元素默认为行内元素。
- ::first-line 在某 block-level element (块级元素)的第一行应用样式。第一行的长度取决于很多因素,包括元素宽度,文档宽度和文本的文字大小
- ::first-letter 选中某 block-level element(块级元素)第一行的第一个字母,并且文字所处的行之前没有其他内容(如图片和内联的表格
伪元素 允许我们对选择元素的特定部分修改样式
可继承的属性:font-size font-family color opacity 不可继承的属性:border padding margin width height
优先级(就近原则):!important > id > class > tag
!important比内联优先级高
CSS3新特性
- RGBA
- background-size background-image background-origin(content-box/padding-box/border-box)
- word-wrap(对于长的不可分割单词换行) word-wrap: break-word
- 文字阴影:text-shadow: 5px 5px 5px #FF0000; (水平阴影,垂直阴影,模糊距离,阴影颜色)
- font-face属性:定义自己的字体
- 圆角 border-radius
- 盒阴影:box-shadow: 10px 10px 5px #888888
- 媒体查询:定义两套CSS,当浏览器的尺寸变化时采用不用的属性
盒模型
// height和width是内容的高度和宽度,不包括padding和border
box-sizing: content-box;
// height和width包括padding和border,可以利用这一特点生成固定宽和高的区域。
box-sizing: border-box;
CSS中定位的特点
BFC
怎么触发形成BFC
- 根元素
- 绝对定位元素(position:absolute; position: fixed)
- 浮动元素(float:left; float: right;)
- overflow不为默认值visible
- 行内块元素(display:inline-block)
- 表格单元格(display: table-cell)
- 表格标题(display: table-caption)
- 匿名表格单元格元素(元素的 display 为 table、table-row、 table-row-group、table-header-group、table-footer-group(分别是HTML table、row、tbody、thead、tfoot 的默认属性)或 inline-table)
- display值为flow-root
- contain 值为 layout、content 或 paint 的元素
- 弹性元素(display: flex 或 inline-flex)
- 网格元素(display: grid 或 inline-grid)
BFC的特点
- 解决父子元素margin collapsing(外边距重叠)的问题;
- 包含浮动元素;
- 同层元素不和浮动元素重叠。 juejin.cn/post/684490…
原点
transform: translate(30px, 30px); 默认原点是元素的中心
transition 默认原点是左上角
-
transform 是改变所在元素的外观,它有很多转换函数来改变外观;例如位移(translate)、缩放(scale)、旋转(rotate)。transform没有动画的效果,改变了它的值,元素的样子刷的改变了。
-
transition 是过渡,指的是CSS属性值如何平滑的进行改变;监听基本属性,例如height或width,也可以是all;
- transition: [属性名][持续时间][速度曲线][延迟时间], [属性名][持续时间][速度曲线][延迟时间]
opacity和transform动画的高性能是由于其数学原理决定了可以使用缓存信息,而并不是因为它被硬件加速了。blog.csdn.net/devcloud/ar…
总结解决CSS3动画卡顿方案:
1、尽量使用transform当成动画,避免使用height,width,margin,paddiing等
2、要求较高时,可以开启浏览器GPU硬件加速。
www.jianshu.com/p/9596c8208…
www.cnblogs.com/zyyz/p/4975…
3、使用3D硬件加速提升动画性能时,最好给元素增加一个z-index属性,认为干扰复合层的排序,可以有效减少chrome创建不必要的复合层,提升渲染性能,移动端优化效果尤为明显。
flex
对于父容器定义 display: flex; 父容器可以设置的属性:
- flex-wrap: nowrap; 默认不换行
- flex-direction: row; 默认在一行排列;column 在一列排列
- justify-content: flex-start; 设置弹性盒子在主轴方向上的对齐方式,默认子元素位于容器开头
- center 子元素位于容器的中心
- space-between 子元素 之间 留出空白
- space-around 子元素被空白包围,首尾元素的左边和右边也是空白
- align-items: stretch; 默认值 元素被拉伸以适应容器。
- center 元素位于容器的中心
- flex-start 元素位于容器的开头
- flex-end 元素位于容器的结尾
对于子元素:
- order 定义子元素出现的顺序
- flex-grow 子元素放大的比例。默认是0,不放大
- flex-shrink 子元素缩小比例。默认是1,父元素空间不够的时候会缩小
- flex-basis 子元素的大小。默认是 auto, 即元素本身的大小。这里要注意的是子元素设置了权重 max-width > flex-basis > width
- flex; flex是 flex-grow、flex-shrink、 flex-basis的简写
- flex: 1; => flex: 1 1 0%;
- flex: none; => flex: 0 0 auto;
- flex: auto; => flex: 1 1 auto;
z-index
z-index只有在设置了position为relative,absolute,fixed时才会有效。
长宽比恒定的矩形
以长宽比 2:1 为例:
方法一:width + padding-bottom
.wrap {
width: 30%;
padding-bottom: 15%;
height: 0;
}
width 使用百分数,表示父容器宽度的百分比,padding 使用百分数,也是表示占父容器宽度的百分比;设置 height:0; 这样内容就不会撑开容器,打破长宽比了。
方法二:vw 或者 vh 单位表示宽度和高度
.wrap {
width: 30vw;
height: 15vw;
}
background-size
background-size 属性指定背景图片大小,可以取值:
- length。 如
background-size: 100px 100px;设置背景图片宽度和高度,第一个值设置宽度,第二个值设置高度;如果只给出一个值,第二个值自动设置为auto. - percentage。将计算相对于背景定位区域的百分比。第一个值设置宽度,第二个值设置高度。如果只给出一个值,第二个值自动设置为
auto. - cover 保持图片的纵横比并将图像放大至完全覆盖容器的最小大小。
- contain 保持图片的纵横比将图片缩小至容器能包容图片的最大大小。
<style>
.attr{
background: url("https://images2015.cnblogs.com/blog/561794/201603/561794-20160310002800647-50077395.jpg");
background-repeat: no-repeat;
background-size: 100px 100px;
width: 200px;
height: 100px;
border: 1px solid black;
}
</style>
<body>
<div class="attr">
</div>
</body>
回流和重绘
回流 比如我们增删DOM节点,修改一个元素的宽高,页面布局发生变化,DOM树结构发生变化,那么肯定要重新构建DOM树,而DOM树与渲染树是紧密相连的。DOM树构建完,渲染树也随之对页面再次渲染,这个过程就叫回流。(结构会变)
导致回流的操作:
- 页面首次渲染
- 浏览器窗口大小发生变化
- 元素尺寸发生改变(包括外边距、内边距、边框大小、高度和宽度)
- 元素的位置发生变化
- 元素内容变化(文字数量或图片大小等等)
- 元素字体大小变化 重绘 当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。(结构不变,样式改变)
回流一定会触发重绘,而重绘不一定会回流
Javascript相关
js数据类型和存储方式
基本数据类型:null undefined string boolean number symbol(Symbol生成的一个全局唯一的值) BigInt(ES2020) 引用数据类型:Object Date Array Function
- 基本数据类型存储在栈内存中;基本数据类型 a=b; 复制会开辟出一个新的内存空间,将值复制到新的内存空间;复制完成后改变 a 值,不会影响 b
- 引用数据类型存储在堆内存中;变量保存的只是指向该内存的地址;在复制引用类型的时候 a=b,其实只复制了该内存的地址;复制完成后改变 a 值,会影响 b
Symbol用处: - 作为对象的属性;for in遍历不到Symbol类型的属性,可以通过Reflect.ownKeys遍历到
- 替代代码中多次使用的字符串
- 使用同一个Symbol,可以用Symbol.for()
如何取消冒泡
- 在IE的事件模型中,必须设置事件对象的 cancelBubble 属性为 true。例如: window.event.cancelBubble = true
- 在W3c事件模型中必须调用事件的 stopPropagation()方法。例如:e.stopPropagation()。此方法可以阻止所有事件冒泡向外传播。
//兼容写法
function stopPropagation(event){
if(event.stopPropataion){
event.stopPropagation();
}else{
event.cancelBubble = true;
}
}
事件委托
事件委托能避免对每个元素添加事件监听器。原理是:利用事件冒泡,将子元素的事件统一在父元素进行处理。父元素根据判断事件来源确定是哪个子元素触发,分开进行不同的处理。
详细说明:juejin.cn/post/684790…
new操作符做了什么
- 首先内部创建了一个空对象,obj
- 将新对象的__proto__指向构造函数的prototype对象
- 将构造函数的作用域赋值给新的对象(也就是this指向新对象)
- 执行构造函数中的代码(为这个新对象添加属性)
- 返回apply执行的结果或者返回新对象
function create(fn, ...rest) {
//相当于 let obj.__proto__ = fn.prototype;
let obj = Object.create(fn.prototype);
let result = fn.apply(obj, rest);
if(typeof result==='object'){
return result;
} else {
return obj;
}
}
let p = create(Person, 'zs', 12);
console.log(p instanceof Person, p);
对象的浅拷贝和深拷贝
//深拷贝
function deepCopy(obj) {
let result;
if(Array.isArray(result)){
result = [];
} else {
result = {};
}
for(let key in obj){
let tmp = obj[key];
if(typeof tmp==='object'){
result[key] = deepCopy(tmp);
}else{
result[key] = tmp;
}
}
return result;
}
let arr1 = [{}, 2, 11];
let arr2 = arr1.concat([]);
let arr3 = arr1.slice();
let arrDeep = deepCopy(arr1);
arr1.push(3);
arr1[0].name = 'zs';
// console.log(arr1, arr2, arr3, arrDeep);
//浅拷贝
function shallowCopy(obj){
let result = {};
for(let key in obj){
result[key] = obj[key];
}
return result;
}
let obj = {name: 'zs', types: []};
let obj1 = Object.assign({}, obj);
let obj2 = {...obj};
let obj3 = shallowCopy(obj);
let obj4 = deepCopy(obj);
obj.age = 12;
obj.types.push(100);
console.log(obj, obj1, obj2, obj3, obj4);
深拷贝的方法:
- 自己实现函数,递归实现;遍历对象的key,值是对象重新new对象赋值
- lodash里面的 _.cloneDeep()
- JSON.parse(JSON.stringify()) 浅拷贝的方法:
- ES6解构赋值 ... 和 slice(0)
- Object.assign
- lodash 里面 _.clone() JSON.parse(JSON.stringify())复制拷贝使用有哪些问题?
- 值为undefined会被省略掉
- Date类型会变成字符串类型
- RegExp、Error对象识别不了,只能得到空对象
- NaN、Infinity和-Infinity,则序列化的结果会变成null www.jianshu.com/p/b084dfaad…
apply() call() bind()
- 三个都是用来改变this指向的
- fn.apply(obj,rest); rest是数组
- fn.call(obj,params1, params2, params3);
- fn.bind(obj, params1, params2, params3)返回的是一个函数,要加()执行
手写bind
var name="window";
var person = {
name: "lisi",
say: function (age) {
console.log(this.name+", age: "+age);
}
}
person.say.apply({name: 'amanda'}, [200]);
person.say.apply({}, [200]);
person.say.apply(null, [200]);
console.log("----------------------------------")
person.say.bind({name: 'amanda'}, 200)();
person.say.bind({name: 'amanda'})(200);
person.say.bind({})(200);
person.say.bind(null)(200);
person.say.bind()(200);
console.log("----------------------------------");
function Point(x,y) {
this.x = x;
this.y = y;
}
// new 方法
let p = new Point(100, 100);
let pb1 = new (Point.bind())(200,200);
console.log(p, pb1);
let pb2 = new (Point.bind({}))(200,200);
let pb3 = new (Point.bind(null))(200,200);
console.log(pb2, pb3);
console.log("----------------------------------");
Function.prototype.myBind = function (context) {
if(typeof this !== 'function'){
return new Error("not function");
}
const _this = this;
let args = [...arguments].slice(1);
return function F(){
args = args.concat(...arguments);
if(this instanceof F){
return new _this(...args);
}else{
_this.apply(context, args);
}
}
}
person.say.myBind({name: 'bind'}, 200)();
let bindP = new (Point.myBind())(900, 900);
console.log("bind", bindP)
typeof instanceof Object.prototype.toString.call
typeof
- 用来判断数据类型,返回值为6个字符串,分别是string, boolean, number, function, object, undefined
- typeof在判断null、Array、Object以及(new + 函数)时,得到的都是object,这个时候就要用instanceof了
instanceof
- 用来判断数据类型, obj1 instanceof obj2 返回true或者false
- str instanceof String
- n instanceof Number/Boolean/Object/Date/Array
null undefined
一般null用来给引用对象初始值;undefined一般用来判断某个对象有没有某个属性
万能 Object.prototype.toString.call
class Person{
constructor(name){
this.name = name;
}
}
let a1 = null;
let a2;
let a3 = function(){};
let a4 = new Object();
let a5 = new Person("person");
console.log(Object.prototype.toString.call(a1)); // [object Null]
console.log(Object.prototype.toString.call(a2)); // [object Undefined]
console.log(Object.prototype.toString.call(a3)); // [object Function]
console.log(Object.prototype.toString.call(a4)); // [object Object]
console.log(Object.prototype.toString.call(a5)); // [object Object]
一些判断
null === null
undefined === undefined
NaN === NaN
isNaN()、isFinite()、 parseInt() 、parseFloat() 实际上都是 Global 对象的方法。
上面的 null === null 是 true,undefined === undefined 是 true,NaN === NaN 是 false
var let const的区别
var定义在函数外面是全局变量,那么会挂载在window对象上,成为它的属性;var定义的变量有变量提升的效果,let cosnt定义的没有。let cosnt有暂时性死区的概念。
let、const是ES6的语法,有块级作用域的概念(if for语句中);let const定义的变量不会挂载在window对象上;const定义的变量不能被修改。
总结:
1. let不能重复声明
2. let 具有块级作用域:在for循环中,不仅引入了一个新的变量环境,而且针对每次迭代都会创建一个新的作用域,效果同立即执行的函数表达式
3. let 声明不会被提前,具有暂时性死区。
4. 不会挂载在window对象上。
闭包
作用域
ES6出现之前,一般来说只有函数作用域和全局作用域
变量提升
- var定义的变量声明会在js代码编译阶段提升到最前面,初始值为 undefined。即 var a = undefined;
- 非表达式的函数的声明和定义会在js代码编译阶段提升到最前面。 根据这两点,看下面的代码:
var bar = function(){
console.log("bar2");
}
function bar(){
console.log("bar1");
}
bar()
执行结果是输出 bar2。为什么呢?虽然函数bar定义在后面,但是在JS编译阶段,函数bar的声明和定义会被提升到前面,同时 var bar 在执行的时候会覆盖掉函数bar的定义。
闭包
函数嵌套函数的时候,内层函数引用了外层函数作用域下的变量。并且内层函数在全局环境下可访问,进而形成了闭包。
闭包使用不当极有可能引发内存泄漏。
内存泄漏是指内存空间明明已经不再使用,但由于某种原因并没有被释放的现象。 由于IE9之前的版本对JScript对象和COM对象使用不同的垃圾收集例程(方法),因此闭包在IE9之前的版本中会导致一些特殊的问题。具体来说,如果闭包的作用域链中保存着一个HTML元素,那么就意味着该元素将无法被销毁。如下面的例子:
function assignHandler(){
var element = document.getElementById("someElement");
element.onclick = function(){
alert(element.id);
};
}
assignHandler();
assignHandler函数执行后, element变量并不会被销毁;一方面是由于闭包的存在,闭包可以访问到定义内部函数的外层函数的变量;另一方面,内部函数调用了 element这个变量 elment.id 。解决方法:
function assignHandler(){
var element = document.getElementById("someElement");
var id = element.id;
element.onclick = function(){
alert(id);
};
element = null;
}
assignHandler();
这样闭包的作用域链中就不会保存HTML元素了,解除对DOM对象的引用,顺利地减少其引用次数,确保正常回收其占用的内存。
闭包的用途:
柯里化, 可以理解为提取接收部分参数,延迟执行,不立即输出结果,而是返回一个接收剩余参数的函数。柯里化,是一个逐步接收参数的过程。
www.jianshu.com/p/2975c25e4…
juejin.cn/post/684490…
function add() {
let args = Array.prototype.slice.call(arguments);
let _add = function () {
args.push(...arguments);
return _add;
};
_add.toString = function () {
return args.reduce((cur, next)=>cur+next);
}
return _add();
}
模块化
(function () {
var a = 10;
var b = 20;
function add(num1, num2) {
var num1 = !!num1 ? num1 : a;
var num2 = !!num2 ? num2 : b;
return num1 + num2;
}
window.add = add;
})();
console.log(add(0, 2));
数组
- arr.forEach((item,index)=>{})仅仅是循环数组
- let newArr = arr.map((item,index)=>{return item}); map返回一个数组
const arr = ["jin", 9, "88ui", 0, 56];
const newArr = arr.map(item=>{
if(typeof(item)=='string'){
return item;
}
})
console.log(newArr);
即使不返回也是存在这些项的
- let newArr = arr.filter((item,index)=>{return item}); filter返回一个数组,只包含要返回的项
const arr = ["jin", 9, "88ui", 0, 56];
const newArr = arr.filter(item=>{
if(typeof(item)=='string'){
return item;
}
})
console.log(newArr);
const newArr2 = arr.filter(item=>{
})
console.log(newArr2);
- find()函数用来查找目标元素,找到就返回该元素(第一个满足条件的元素),找不到返回undefined; ES6新增
- findIndex()函数也是查找目标元素,找到就返回元素的位置(第一个满足条件的元素的位置),找不到就返回-1;ES6新增
let isFind = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].find((value,index, arr)=>{
return value > 4;
});
console.log(isFind);
let indexFind = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].findIndex((value,index, arr)=>{
return value > 4;
});
console.log(indexFind);
- arrayObject.concat(arrayX,arrayX,......,arrayX)方法用于连接两个或多个数组;该方法不会改变现有的数组,而仅仅会返回被连接数组的一个副本;
- arrayObject.slice(start,end)方法可从已有的数组中返回选定的元素;返回一个新数组,包含从start到end(不包括该元素);该方法不会改变现有的数组;
- arrayObject.splice(index,howmany,item1,.....,itemX) 该方法会改变现有的数组
const arr = [1, 2, 2, 4, 3];
console.log(arr.concat(100), arr);
console.log(arr.slice(1,3), arr);
console.log(arr.splice(1,2, 100), arr);
- arrayObject.reduce((previousValue, currentValue, index, arr)=>{}, initVal) 第一个参数是回调函数接收四个参数;第二个参数表示回调函数计算传入的初始值,可选
- previousValue: 初始值或上一次回调函数叠加的值
- currentValue:本次回调将要执行的值
- index:currentValue 的索引值
- arr:数组本身 对reduce的理解,如果没有initVal这个参数的话,初始值默认数组第一个元素。细微区别请看下面的例子:
let arr = [11, 15, 12];
arr.reduce((pre, cur, index, self)=>{
console.log(pre, cur, index, self);
})
对于有三个数据的数组,如果没有初始值initVal,则会循环 2(arr.length-1)次
let arr = [11, 15, 12];
arr.reduce((pre, cur, index, self)=>{
console.log(pre, cur, index, self);
}, 0)
对于有三个数据的数组,如果有初始值initVal,则会循环 3(arr.length) 次
扁平化
flatter应用
const arr = [1, [2, 3, [4, [6, 7]]]];
function flatter(arr) {
let newArr = [];
arr.forEach(item=>{
if(Array.isArray(item)){
// newArr = newArr.concat(flatter(item));
newArr.splice(newArr.length-1, 0 , ...flatter(item));
}else{
newArr.push(item);
}
})
return newArr;
}
//或者
function flatArr(arr){
return arr.reduce((cal, cur)=> Array.isArray(cur) ? cal.concat(flatArr(cur)) : cal.concat(cur), [])
}
去重
//方法一
function removal(arr) {
return arr.filter((item,key)=>arr.indexOf(item)===key);
}
//方法二
new Set(arr)
类数组变成数组
类数组是具有length属性,但不具有数组原型上的方法。比如 arguments,DOM操作返回的结果就是类数组。将类数组变为数组的方法:
- Array.from(document.querySelectorAll("div"))
- Array.prototype.slicec.call(document.querySelectorAll("div"))
- [...document.querySelectorAll("div")]
如何删除数组成员
- delete arr[index] 删除成员同时不改变后面成员的索引值
- arr.splice(index,1) 删除成员的同时,删除所占位置的使用,后面成员会自动补上了
for..of for...in
- for..in 用于遍历对象;如果for..in遍历数组的话,会把数组原型上的方法以及数组的属性遍历出来;
- for..of 用于遍历数组;遍历数组不会遍历数组原型上的方法以及数组的属性遍历处理;for..of可以遍历具有迭代器对象的集合,如Map、Set以及字符串。www.jianshu.com/p/c43f418d6…
Object.prototype.sayName = function () {
console.log(this.name);
}
const person = {age: 12, name: 'zs'};
for(let key in person){
console.log(key, person[key]);
}
for(let key in person){
if(person.hasOwnProperty(key)){
console.log(key, person[key]);
}
}
let arr = [1,2,3,4];
arr.name = 'test';
for(let key in arr){
console.log(key, arr[key]);
}
for(let key of arr){
console.log(key);
}
正则表达式
去掉字符串前后所有空格
function trim(str){
return str.replace(/(^\s*)|(\s*$)/g, "");
}
^ 表示匹配以什么开头;
\s 匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。注意 Unicode 正则表达式会匹配全角空格符。
* 匹配前面的子表达式零次或多次。
$ 表示匹配以什么结尾。
节流和防抖 写一个节流函数,但是还要执行节流时间内重复触发的最后一次行为
看到这两个就感觉看到了 Tom and Jerry 一样,要成对出现。必定是要一起问的问题。
节流:高频事件触发,在n秒内只会执行一次,所以节流会稀释函数的执行频率。
防抖:高频事件触发后n秒内只会执行一次,如果n秒内高频事件再次触发,则重新计算时间。
两者的区别是:函数节流是固定时间做某一件事,比如每隔1秒发一次请求;而函数防抖是在频繁触发后,只执行一次。
节流
高频事件触发,无论多频繁触发在n秒内只会执行一次,所以节流会稀释函数的执行频率。
scroll加载更多可以用到节流
<input type="text" id="search" value="12">
function jieliu(fn, duration, isFirst) {
let timer;
return function (...args) {
if(isFirst){
isFirst = false;
fn.apply(this, args);
return;
}
if(!timer){
timer = setTimeout(()=>{
fn.apply(this, args);
timer = null;
}, duration)
}
}
}
document.getElementById('search').addEventListener("input", jieliu((e)=>{
console.log(e.target.value);
}, 1000, true))
防抖
你尽管触发事件,但是我一定在事件触发 n 秒后才执行,如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行,总之,就是要等你触发完事件 n 秒内不再触发事件,我才执行,真是任性呐! 常见的例子:有一个搜索输入框,为了提升用户体验,希望在用户输入后可以立即展现搜索结果,而不是每次输入完后还要点击搜索按钮
<input type="text" id="search">
function fangdou(fn, duration) {
let timer;
return function (...args) {
if(timer){
clearTimeout(timer);
}
timer = setTimeout(()=>{
fn.apply(this, args);
}, duration)
}
}
document.getElementById('search').addEventListener('input', fangdou((e)=>{
console.log(e.target.value)
}, 1000));
github.com/mqyqingfeng…
github.com/mqyqingfeng…
浏览器相关
浏览器输入URL到页面展示的整个过程
(1)DNS解析:进行域名解析,找到对应的服务IP;
(2)TCP连接:进行三次握手,确保可以进行数据传输;
(3)发送HTTP请求,发送具体的请求信息
(4)服务器处理请求并返回HTTP报文。服务器返回详细的内容,具体包括:状态码、响应报文头、响应报文
(5)浏览器解析渲染页面
(6)断开连接:断开TCP连接(4次挥手)
其中第(5)步的内容有包括:
- 根据 HTML 代码生成 DOM Tree
- 根据 CSS 代码生成 CSSOM Tree
- 将 DOM Tree 和 CSSOM Tree 整合形成 Render Tree
- 根据 Render Tree 渲染页面
- 遇到
<script>则暂停渲染,优先加载并执行 JS 代码,完成再继续渲染 - 直至 Render Tree 渲染完成 segmentfault.com/a/119000002…
window.onload 和 DOMContentLoaded 区别
window.addEventListener('load', ()=>{
//页面全部资源加载完才会执行,包括图片、视频等
console.log("load");
})
window.addEventListener('DOMContentLoaded', ()=>{
//DOM 渲染完即可执行,此时图片、视频还可能没有加载完
console.log("DOMContentLoaded")
})
性能优化
- 让加载更快
- 减少资源体积:压缩代码
- 减少访问次数:合并代码,SSR 服务端渲染,缓存
- 使用更快的网络:CDN(Content Delivery Network)内容分发网络。
- 让渲染更快
- CSS 放在 head, JS 放在 body 最下面
- 尽早开始执行 JS, 用
DOMContentLoaded触发 - 懒加载(图片懒加载,上滑加载更多)
- 对 DOM 查询进行缓存
- 频繁 DOM 操作,利用
document.createDocumentFragment() - 节流和防抖
SSR: 服务器端渲染(将网页和数据一起加载,一起渲染);
非SSR(前后端分离):先加载网页,再加载数据,再渲染数据
垃圾回收
Javascript具有自动垃圾回收机制,也就是说,执行环境会负责管理代码执行过程中使用的内存。垃圾收集器会按照固定的时间间隔,周期性地执行这一操作。
标记清除
垃圾回收器会在程序运行的时候给存储在内存中的所有变量加上标记,然后去掉环境中的变量,以及被环境中变量所引用的变量(闭包)的标记,在完成这些之后仍然存在标记的就是要清除的变量。
引用计数法
引用计数的含义是跟踪记录每个值被引用的次数。当有其他对象引用这个值时计数器+1,反之引用解除时减一。垃圾回收器就会释放哪些引用次数为0的值所占用的内存。
缺点:无法回收循环引用的存储对象。
function f(){
var o1 = {};
var o2 = {};
o1.p = o2; // o1引用o2
o2.p = o1; // o2引用o1
}
f();
IE9之前的问题
IE9之前,BOM和DOM中的对象是使用C++以COM(Component Object Model,组件对象模型)对象的形式实现的。而COM对象的垃圾收集机制采用的就是引用计数策略。IE的Javascript引擎是使用标记清除策略来实现的。
So 只要在IE中涉及COM对象,就会存在引用计数循环引用的问题。
var element = document.getElementById("some_element");
var myObject = {};
myObject.element = element;
element.someObject = myObject;
这个例子在一个DOM元素(element)与一个原生Javascript对象(myObject)之间创建了循环引用。由于存在这个循环引用,即使将例子中的DOM从页面中移除,它也永远不会被收回。
为了解决上述问题,IE9把BOM和DOM对象都转换成了真正的Javascript对象。这样,就避免了两种垃圾收集算法并存导致的问题,也就消除了常见的内存泄漏现象。
浏览器内核
IE:trident
firefox: gecko
chrome: webkit
cookie和session
- cookie数据存放在用户的浏览器上,session数据存放在服务器上
- cookie不是很安全,可以通过F12浏览器,在console控制台通过 document.cookie 获取某个域名下的非Http-Only的cookie;可以在Application下看到该域名的所有cookie
- 一个cookie保存的数据不能超过 4KB,很多浏览器都限制一个站点最多能保存 20个cookie。
- session是存储在服务器上的,当访问量增多时,会影响服务器的性能。
- session的原理其实是cookie,当服务器端设置了session的时候,在response的header中新增Set-Cookie: JSEEIONID=089JK98hJKaAEAEENHI;之后浏览器再发送请求的时候会自动带上这些cookie
- JavaScript操作cookie
//新增cookie;document.cookie="cookie_name=cookie_value"只能设置一个cookie
document.cookie = "Cookie_YY=Shiwo;Cookie_YY2=Shiwo;"; //只有第一个Cookie_YY=Shiwo设置的生效
document.cookie = "Cookie_Test=test";
//读取该域名下的所有cookie(不包括 HttpOnly 的cookie
document.write(document.cookie);
document.getElementById("btn").onclick=function(){
//删除一个cookie的方法就是设置其过期时间
document.cookie="Cookie_Test=test;expires=Thu, 01 Jan 1970 00:00:00 GMT";
}
GET请求 POST请求 PUT请求
-
GET请求用于获取数据。
-
GET请求参数通过URL传递,POST请求参数放在Request body中。
-
GET请求比POS请求更不安全,GET请求的参数直接暴露在URL上,所以不能用来传递敏感信息。
-
GET参数有长度限制(受限于url长度,具体的数值取决于浏览器和服务器的限制,最长2048字节),而POST无限制。
-
GET请求是幂等的,请求一次跟请求两次的效果是一样的。
-
PUT请求也是幂等的,用于修改数据(A修改为B,修改操作一次和多次效果是一样的)
-
POST请求一般用于新增数据.
-
本质上,GET和POST请求都是tcp连接,只是由于http协议规定和浏览器或者服务器的限制,导致他们在应用过程中体现形式不同。
-
GET产生一个TCP数据包;POST产生两个TCP数据包。对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。 juejin.cn/post/684490… www.cnblogs.com/logsharing/… blog.csdn.net/qq_33223761…
跨域
浏览器同源策略
源是由协议(http/https/websoket)、主机名(域名domian或者IP)以及端口共同组成的部分。这三种任意一个不同称为跨源(跨源)。
同时满足下面3个条件会产生跨域:
- 浏览器限制
- 跨域(跨源)
- XHR请求
如果是前后端代码一起的项目,是不用考虑跨域的问题;或者如果前后端代码编写不在一个项目,比如打包部署在同一个tomcat下面,那其实也不用考虑跨域。
跨域并不是请求发不出去,请求能发出去,服务器能收到请求并能正常返回结果,只是被浏览器拦截了。
解决跨域方案
跨域资源共享 CORS
跨域资源共享(CORS,Cross-Origin Resource Sharing)是浏览器为AJAX请求设置的一种跨域机制。是W3C的一个工作草案,定义了在必须访问跨域资源时,浏览器和服务器应该如何沟通。CORS背后的基本思想,就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或相应是应该成功,还是应该失败。
比如一个简单的使用GET或POST发送的请求,它没有自定义头部,而主体内容是text/plain。在发送请求时,需要给它附加一个额外的Origin头部,其中包含请求页面的源信息(协议、域名和端口),以便服务器根据这个头部信息来决定是否给与响应。下面是Origin头部的一个示例:
Origin: www.nczonline.net
如果服务器认为这个请求可以接受,就在 Access-Control-Allow-Origin 头部中回发相同的源
信息(如果是公共资源,可以回发"*")。例如:
Access-Control-Allow-Origin: www.nczonline.net
如果没有这个头部,或者有这个头部但源信息不匹配,浏览器就会驳回请求。正常情况下,浏览器 会处理请求。注意,请求和响应都不包含 cookie 信息
当跨域请求需要携带cookie是,请求头中需要设置 xhr.withCredentials = true; 同时,后端也要设置 response.setHeader("Access-Control-Allow-Credentials", true);
当后端 Access-Control-Allow-Credentials值为true时,后端 Access-Control-Allow-Origin 必须有明确的值,不能是通配符(*)
Access to XMLHttpRequest at 'http://localhost:8081/workMgt/stage/selectAllStage' from origin 'http://localhost:63342' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
JSONP
JSONP包括两部分 json数据和回调函数。利用的是script标签跨域引用js文件不会受到浏览器同源策略的限制。浏览器发送GET请求的时候,将回调函数以参数的形式发送给服务器。服务器会以回调函数的形式返回给浏览器,其中数据被包含在回调函数中。浏览器动态创建script标签,接收服务器的返回,回调函数被执行。浏览器可以得到返回的数据。
JSONP存在的问题:
- 只能发送GET请求,限制了参数和类型
- 请求过程无法终止,导致弱网络下处理超时请求比较麻烦
- 无法捕获服务端返回的异常信息
例子: suggest.taobao.com/sug?code=ut…
解决跨域原因:JSONP发送不是XHR请求,不是ajax异步请求
代理转发
跨域是为了突破浏览器的同源策略限制,既然同源策略只存在于浏览器,那可以换个思路,在服务端进行跨域,比如设置代理转发。这种在服务端设置的代理称为“反向代理”,对于用户而言是无感知的。
Nginx代理转发设置:
//nginx.conf文件
server {
# Nginx启动的端口
listen 3000;
server_name localhost;
location ~ ^.*\.(html|js|css|png|gif|jpg|tpl|woff|woff2|tff|map|eot|coffee|exe|pkg)$ {
# 前端打包后文件放的位置
root /weblogic/webapp;
# 设置采用协议缓存
add_header Cache-Control no-cache;
}
# 将 /server-web/ 开头的请求进行转发,转发到proxy_pass设置的IP上
location /server-web/ {
# 转发地址
proxy_pass http://XXXXx;
# 设置采用协议缓存
add_header Cache-Control max-age=no-cache;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
XSS攻击 CSRF攻击 SQL注入
XSS攻击
XSS(Cross Site Scripting)攻击跨站脚本攻击:是指恶意攻击者往Web页面(如评论、回复输入框)里插入恶意script代码,当用户浏览该页面时,嵌入其中web里面的Script代码会被执行,从而达到恶意攻击用户的目的。
<script>alert('xss')</script>
破解方法: 编码(将特殊字符串<进行转义)、过滤(移除用户上传DOM属性,如onerror;移除用户上传的style节点、script节点、iframe节点等)
CSRF攻击
CSRF(cross site request forgery)跨站请求伪造,XSS利用的是站点内信任用户;而CSRF攻击:最大的特点就是窃取身份,伪装成网站受信任的用户来发起请求实施攻击。在用户没有退出网站A之前,在同一个浏览器中,打开一个钓鱼网站B;网站B启动后,直接执行或返回一些攻击性代码,并发起一个请求要求访问网站A。通俗的说,攻击者盗用了你的身份,以你的名义向第三方网站发送恶意请求。CSRF能做的事情包括利用你的身份发邮件、发短信、进行交易转账等。
攻击举例
假设某银行网站A以GET请求来发起转账操作,转账的地址为www.xxx.com/transfer.do?accountNum=l000l&money=10000,参数accountNum表示转账的账户,参数money表示转账金额。 而某大型论坛B上,一个恶意用户上传了一张图片,而图片的地址栏中填的并不是图片的地址,而是前而所说的砖账地址:<img src="http://www.xxx.com/transfer.do?accountNum=l000l&money=10000">
当你登录网站A后,没有及时登出,这时你访问了论坛B,不幸的事情发生了,你会发现你的账号里面少了10000块...
为什么会这样呢,在你登录银行A时,你的浏览器端会生成银行A的cookie,而当你访问论坛B的时候,页面上的标签需要浏览器发起一个新的HTTP请求,以获得图片资源,当浏览器发起请求时,请求的却是银行A的转账地址
www.xxx.com/transfer.do?accountNum=l000l&money=10000, 并且会带上银行A的cookie信息,结果银行的服务器收到这个请求后,会以为是你发起的一次转账操作,因此你的账号里边便少了10000块。
链接:www.jianshu.com/p/67408d73c…
CSRF防御
- 将cookie设置为HttpOnly
- 通过Referer识别
- 增加token
- 增加验证,例如密码、短信验证码、指纹等。
SQL注入
'or 1 = 1 -- 是恶意攻击输入的内容;前一半引号被闭合,后一半引号被 -- 注释掉了;中间多了一个永远成立的条件 1=1 ,这就造成任何字符串都能成功登陆的结果。
为什么PreparedStatement可以防止SQL注入?
- PreparedStatement是预编译的;
- PreparedStatement参数不是简单拼接生成SQL,而是先用?占位,然后根据参数产生SQL。
- PreparedStatement不是将参数简单拼凑成sql,而是做了一些预处理,将参数转换为string,两端加单引号,将参数内的一些特殊字符(换行,单双引号,斜杠等)做转义处理,这样就很大限度的避免了sql注入。 blog.csdn.net/u011649691/…
浏览器缓存
强制缓存
- expires: http1.0的产物;根据一个绝对时间来确定是否要利用缓存;
- cache-control: http1.1的产物,根据一个相对时间来确定是否利用缓存。
- 两者同时存在,cache-control生效;expires是兼容性写法。
Cache-Control 是 http1.1 时出现的 header 信息,主要是利用该字段的 max-age 值来进行判断,它是一个相对时间,例如 Cache-Control:max-age=3600,代表着资源的有效期是 3600 秒。cache-control 除了该字段外,还有下面几个比较常用的设置值:
- no-cache:需要进行协商缓存,发送请求到服务器确认是否使用缓存。
- no-store:禁止使用缓存,每一次都要重新请求数据。
- public:可以被所有的用户缓存,包括终端用户和 CDN 等中间代理服务器。
- private:只能被终端用户的浏览器缓存,不允许 CDN 等中继缓存服务器对其缓存
- max-age>0 时 直接从游览器缓存中 提取
- max-age<=0 时 向server 发送http 请求确认 ,该资源是否有修改; 有的话 返回200 ,无的话 返回304.
协议缓存, 协议缓存生效后端返回 304
- Last-Modified/If-modified-since:
- Etag/If-None-Match
Etag 主要为了解决 Last-Modified 无法解决的一些问题:
1、一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;
2、某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒)
3、某些服务器不能精确的得到文件的最后修改时间;
若强制缓存生效则直接使用缓存;若不生效则进行协商缓存。
刷新与缓存
- 打开网页,地址栏输入地址: 查找 disk cache 中是否有匹配。如有则使用;如没有则发送网络请求。
- 普通刷新 (F5):因为 TAB 并没有关闭,因此 memory cache 是可用的,会被优先使用(如果匹配的话)。其次才是 disk cache。
- 强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有 Cache-control: no-cache(为了兼容,还带了 Pragma: no-cache),服务器直接返回 200 和最新内容。
网络
HTTP简史
HTTP 1.0
HTTP 1.0 最核心的部分是增加了头部设定,头部内容以键值对的形式设置。请求头部通过Accept字段来告诉服务端可以接收的文件类型,响应头部再通过 Content-Type 字段来告诉浏览器返回文件的类型。
HTTP 1.0 存在两个大问题:
- TCP连接无法复用,每次请求都需要重新建立TCP通道,这就需要重复进行三次握手和四次挥手,也就是说每个TCP连接只能发送一个请求。
- 队头阻塞,每个请求都要过“独木桥”,桥宽为一个请求的宽度,也就是说,即使多个请求并行发出,也只能一个接一个进行排队
HTTP 1.1
- 长连接:HTTP1.1支持长连接(Persistent Connection),且会默认开启Connection: keep-alive,这样在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。
- 管线化: 管线化在长连接的基础上使用多个请求可以用同一个TCP连接,这样复用TCP连接就使得并行发送请求成为可能。管线化只是可以使浏览器并行发出请求,并没有从根本上解决队头阻塞的问题。因为请求的响应(服务端)仍然遵循先进先出的原则,第一个请求的处理结果返回后,第二个请求才会得到响应。同时,浏览器供应商很难实现管线化,而且大多数浏览器默认禁用管线化特性,有的甚至完全删除了它。
- 增加了与缓存相关的请求头(cache-control)
- 进行了带宽优化,并能够使用range头等来支持断点续传功能。
- 新增错误类型,并增强了错误和响应码的语义特性
- 新增Host头处理,如果请求消息中没有Host头,则会报错。
HTTP 2.0
- 二进制传输,HTTP2.0的协议解析采用二进制格式,而非HTTP 1.x的文本格式;可以设置请求的优先级。
- 多路复用,解决了对头阻塞的问题,实现了完全双向的请求和响应消息复用。
- 报头压缩
- 服务器端推送
一个问题
一个TCP连接可以对应几个HTTP请求,这些HTTP请求是否可以一起发送?
不管HTTP1.0还是HTTP1.1,单个TCP连接在同意时刻都只能处理一个请求,意思是说:两个请求的生命周期不能重叠。
虽然HTTP1.1规范中规定了可以用Pipelining来解决这个问题,但是这个功能在浏览器中默认是关闭的。
因此,在HTTP1.1中,一个支持持久连接的客户端可以在一个连接中发送多个请求(不需要等待任意请求的响应),收到请求的服务器必须按照请求收到的顺序发送响应。在HTTP2.0中,由于多路复用特点存在,多个HTTP请求是可以在同一个TCP连接中并行传输的。
响应码
- 1XX 表示服务器收到请求
- 2XX 表示请求成功
- 3XX 表示重定向
- 4XX 表示客户端错误
- 5XX 表示服务端错误
- 200 OK 请求成功
- 204 No Content 该响应没有响应内容,只有响应头。如非简单请求会先发送一个预检请求(Preflight),这个预检请求为 OPTIONS 方法。响应头部添加 Access-Control-Allow-Origin 字段之外,至少还会添加 Access-Control-Allow-Methods 字段来告诉浏览器服务端允许的请求方法,并返回 204 状态码。
- 301 永久重定向(配合location,浏览器自动处理)
- 304 Not Modified 协商缓存返回码。表示缓存仍然有效,告诉浏览器直接使用缓存。
- 404 Not Found 服务器找不到所请求的资源,通常是客户端的url不正确
- 500 Internal Server Error 服务器遇到未知的无法解决的问题. 通常是服务器代码有问题,报错导致。
Restful API
传统的methods
- get 获取服务器的数据
- post 向服务器提交数据
- 简单的网页功能,就这两个操作
现在的 methods
- get 获取数据
- post 新建数据
- patch/put 更新数据
- patch方法用来更新局部资源, 部分字段
- put虽然也是更新资源,但要求前端提供的一定是一个完整的资源对象,理论上说,如果你用了put,但却没有提供完整的UserInfo,那么缺了的那些字段应该被清空
- delete 删除数据
Restful API
- 一种新的 API 设计方法(早已推广使用)
- 传统API:把每个URL当做一个功能
- Restful API设计:把每个 URL 当做一个唯一的资源
- Restful API 特点: 不使用url参数;用 method 表示操作类型
http headers
Request Headers
- Accept: 浏览器可接受的数据格式
- Accept-Encoding: 浏览器可接受的压缩算法,如 gzip
- Accept-Languange: 浏览器可接受的语言,如 zh-CN
- Connection: keep-alive 一次TCP连接重复使用
- Cookie
- Host 主机
- User-Agent (简称UA)浏览器信息
- Content-Type 发送数据的格式 如 application/json
Response Headers
- Content-Type: 返回数据的格式,如 application/json
- Content-Length 返回数据的大小,多少字节
- Content-Encoding 返回数据的压缩算法,如 gzip
- Set-Cookie
- 强缓存:Cache-Control Expires
- 协议缓存 Last-Modified If-Modified-Since Etag If-None-Match
Service Worker
作用: 加速重复访问;支持离线。
注意: 延长了首屏时间,但页面总加载时间减少;兼容性问题;只能在localhost或https下使用。
加密和解密
对称加密
加密和解密使用同一个密钥的方式称为对称密钥加密。
-
如果服务端和所有客户端(浏览器)使用同一个密钥,关键问题:怎么将密钥发送给客户端,而不被黑客拦截。
-
如果服务端分发给客户端不同的密钥,这么多密钥怎么存储?
上面2个问题的存在导致,对称加密在服务端和客户端行不通。
非对称加密
使用加密和解密的密钥不同,我们称为非对称加密. 对外公开的密钥称为 公钥,用来解密的自己知道的密钥称为 私钥。
如果服务器将公钥发送给客户端,客户端使用公钥加密数据,服务器使用私钥进行解密。客户端到服务器发送数据是没有问题的。
- 存在的问题是服务器如果也用公钥进行加密数据发送给客户端,客户端不知道私钥,无法解密数据。
- 如果服务器使用私钥进行数据加密,那么黑客是知道公钥的,会对服务器发送给浏览器的数据进行拦截,这是不安全的。
对称加密+非对称加密结合
这个过程看似完美,却会出现"中间人"拦截的问题。 模拟服务端,从而截获用户的数据信息。
https
TCP的三次握手和四次挥手
三次握手
www.jianshu.com/p/ef892323e…
所谓三次握手(Three-Way Handshake)即建立TCP连接,就是指建立一个TCP连接时,需要客户端和服务端总共发送3个包以确认连接的建立。在socket编程中,这一过程由客户端执行connect来触发,整个流程如下图所示:
(1)第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。
(2)第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。
(3)第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。
简单来说,就是
1、建立连接时,客户端发送SYN包(SYN=i)到服务器,并进入到SYN-SEND状态,等待服务器确认
2、服务器收到SYN包,必须确认客户的SYN(ack=i+1),同时自己也发送一个SYN包(SYN=k),即SYN+ACK包,此时服务器进入SYN-RECV状态
3、客户端收到服务器的SYN+ACK包,向服务器发送确认报ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手,客户端与服务器开始传送数据。
SYN攻击:
在三次握手过程中,Server发送SYN-ACK之后,收到Client的ACK之前的TCP连接称为半连接(half-open connect),此时Server处于SYN_RCVD状态,当收到ACK后,Server转入ESTABLISHED状态。SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,Server回复确认包,并等待Client的确认,由于源地址是不存在的,因此,Server需要不断重发直至超时,这些伪造的SYN包将产时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络堵塞甚至系统瘫痪。SYN攻击时一种典型的DDOS攻击,检测SYN攻击的方式非常简单,即当Server上有大量半连接状态且源IP地址是随机的,则可以断定遭到SYN攻击了,使用如下命令可以让之现行:
#netstat -nap | grep SYN_RECV
四次挥手
所谓四次挥手(Four-Way Wavehand)即终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在socket编程中,这一过程由客户端或服务端任一方执行close来触发,整个流程如下图所示:
由于TCP连接时全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭,上图描述的即是如此。
(1)第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。
(2)第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。
(3)第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。
(4)第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。
为什么建立连接是三次握手,而关闭连接却是四次挥手呢? 这是因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方也未必全部数据都发送给对方了,所以己方可以立即close,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送。
为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态? 原因有二: 一、保证TCP协议的全双工连接能够可靠关闭 二、保证这次连接的重复数据段从网络中消失
先说第一点,如果Client直接CLOSED了,那么由于IP协议的不可靠性或者是其它网络原因,导致Server没有收到Client最后回复的ACK。那么Server就会在超时之后继续发送FIN,此时由于Client已经CLOSED了,就找不到与重发的FIN对应的连接,最后Server就会收到RST而不是ACK,Server就会以为是连接错误把问题报告给高层。这样的情况虽然不会造成数据丢失,但是却导致TCP协议不符合可靠连接的要求。所以,Client不是直接进入CLOSED,而是要保持TIME_WAIT,当再次收到FIN的时候,能够保证对方收到ACK,最后正确的关闭连接。
再说第二点,如果Client直接CLOSED,然后又再向Server发起一个新连接,我们不能保证这个新连接与刚关闭的连接的端口号是不同的。也就是说有可能新连接和老连接的端口号是相同的。一般来说不会发生什么问题,但是还是有特殊情况出现:假设新连接和已经关闭的老连接端口号是一样的,如果前一次连接的某些数据仍然滞留在网络中,这些延迟数据在建立新连接之后才到达Server,由于新连接和老连接的端口号是一样的,又因为TCP协议判断不同连接的依据是socket pair,于是,TCP协议就认为那个延迟的数据是属于新连接的,这样就和真正的新连接的数据包发生混淆了。所以TCP连接还要在TIME_WAIT状态等待2倍MSL,这样可以保证本次连接的所有数据都从网络中消失。
链接:www.jianshu.com/p/ef892323e…
www.jianshu.com/p/d3725391a…
blog.csdn.net/qq_38950316…
感谢
如果本文有帮助到你的地方,记得点赞哦,这将是我坚持不断创作的动力~