0%

跨域

跨域是指一个域下的文档或脚本试图去请求另一个域下的资源,这里跨域是广义的。

广义的跨域:

1
2
3
1.) 资源跳转: A链接、重定向、表单提交
2.) 资源嵌入: <link><script><img><frame>等dom标签,还有样式中background:url()、@font-face()等文件外链
3.) 脚本请求: js发起的ajax请求、dom和js对象的跨域操作等

同源政策

同源策略/SOP(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。所谓同源是指”协议+域名+端口”三者相同,即便两个不同的域名指向同一个ip地址,也非同源。

同源策略限制以下几种行为:

1
2
3
1.) Cookie、LocalStorage 和 IndexDB 无法读取
2.) DOM 和 Js对象无法获得
3.) 不能发送网络(http)请求

常见跨域场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
URL                                      说明                    是否允许通信
http://www.domain.com/a.js
http://www.domain.com/b.js 同一域名,不同文件或路径 允许
http://www.domain.com/lab/c.js

http://www.domain.com:8000/a.js
http://www.domain.com/b.js 同一域名,不同端口 不允许

http://www.domain.com/a.js
https://www.domain.com/b.js 同一域名,不同协议 不允许

http://www.domain.com/a.js
http://192.168.4.12/b.js 域名和域名对应相同ip 不允许

http://www.domain.com/a.js
http://x.domain.com/b.js 主域相同,子域不同 不允许
http://domain.com/c.js

http://www.domain1.com/a.js
http://www.domain2.com/b.js 不同域名 不允许

跨域解决方案

  1. jsonp

    jsonp的原理就是利用<script>标签没有跨域限制,通过<script>标签src属性,发送带有callback参数的GET请求,服务端将接口返回数据拼凑到callback函数中,返回给浏览器,浏览器解析执行,从而前端拿到callback函数返回的数据。

    1
    <script src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback'/>

    jsonp(只支持get请求,支持老的IE浏览器)适合加载不同域名的js、css,img等静态资源;

  2. document.domain+iframe适合主域名相同,子域名不同两个页面的跨域请求,可以在两个页面设置document.domain = “主域名” 。

    例如:http://A.h5course.com/a.html页面和http://B.h5course.com/b.html页面能够相互访问,可以在两个文件中设置document.domain = “h5course.com”,两个页面文档里通过iframe引入对方的文件。

    iframe元素:iframe 元素会创建包含另外一个文档的内联框架(即行内框架)。

  3. react项目在package.json中使用proxy(代理)配置可以解决跨域问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    "proxy": {
    "/api/admin": {
    "target": "http://localhost:12610",
    "pathRewrite": {
    "^/api/admin": "/"
    }
    },
    "/api": {
    "target": "http://47.74.215.209:9000"
    }
    }
  4. 设置webpack 的proxy 代理实现跨域

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    dev: {
    env: require('./dev.env'),
    port: 8080,
    autoOpenBrowser: true,
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',
    proxyTable: {
    '/api': {
    target: 'http://www.abc.com', //目标接口域名
    changeOrigin: true, //是否跨域
    pathRewrite: {
    '^/api': '/api' //重写接口
    }
    },
    cssSourceMap: false
    }
  5. 使用 http-proxy-middleware 代理跨域

    a. 安装http-proxy-middleware

    1
    2
    $ npm install http-proxy-middleware --save
    $ # or$ yarn add http-proxy-middleware

    b. 创建 src/setupProxy.js

    c. 第三步 设置代理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // setupProxy.js
    const { createProxyMiddleware } = require('http-proxy-middleware');
    module.exports = function(app) {
    app.use(
    '/api', //使用"/api"来代替"http://localhost:3000"
    createProxyMiddleware({
    target: 'http://localhost:3000', //源地址
    changeOrigin: true, //改变源
    pathRewrite: {
    '^/api': 'http://localhost:3000' //路径重写
    }
    })
    );
    app.use(
    '/apc',
    createProxyMiddleware({
    target: 'https://test.tosharing.com',
    changeOrigin: true,
    pathRewrite: {
    '^/api': 'https://test.tosharing.com'
    }
    })
    );
    };
  6. CORS跨域资源共享

      参考阮一峰老师的文章 http://www.ruanyifeng.com/blog/2016/04/cors.html

一、使用场景

  • 宣传/品牌/banner等固定文案
  • 字体图标中使用

二、例子

1
2
3
4
5
6
7
8
9
10
11
12
13
@font-face {
font-family: '自定义字体名称';
src: url('../font/字体名称.eot');
src:url('../font/字体名称.woff') format('woff'),
url('../font/字体名称.ttf') format('truetype'),
url('../font/字体名称.svg') format('svg');
}
/* 使用方法:html中的代码中加一个h1或者其他的,里面写你自己想要的特殊文字 */
h1 {
font-size:36px;
color:#ccc;
font-family: "自定义字体名称";
}

Javascript是一门单线程的非堵塞的脚本语言,也是用来与浏览器交互的语言。

单线程是:代码执行的任何时候,都只有一个主线程来处理所有的任务。

非堵塞:当代码需要进行一项异步任务的时候,主线程会挂起这个异步任务,继续向下执行,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调。

JS中的堆、栈、队列

堆(heap)

堆(heap)是指程序运行时申请的动态内存,在JS运行时用来存放对象。

栈(stack)

栈(stack)遵循的原则是“先进后出”,Javascript中的基本数据类型与指向对象的地址的指针存放在栈内存中,此外还有一块栈内存用来执行JS主线程–执行栈(execution context stack)。

队列(queue)

队列(queue)遵循的原则是“先进先出”,JS中除了主线程之外还存在一个“任务队列”,任务队列又分为微任务和宏任务。

什么是Event Loop线程?

在程序中设置两个线程:一个负责程序本身的运行,称为”主线程”;另一个负责主线程与其它进程(主要是各种I/O操作)的通信,被称为”Event Loop线程”

Event Loop线程处理的任务被分为两类即 微任务(micro task)和宏任务(macro task) ,这两个任务分别维护一个队列,都是采用先进先出的策略进行执行。

宏任务:setTimeout、setInterval、I/O。

微任务:Promise.then、 MutationObserver。

当遇到异步请求的时候,主线程就让Event Loop线程去通知相应的异步请求程序,然后接着往后运行,等到异步程序完成操作,Event Loop线程再把结果返回主线程,主线程就调用事先设定的回调函数,完成整个任务。

当前栈执行完后,Event Loop线程首先会执行微任务队列中事件,然后执行宏任务队列的事件,如果这时发现了微任务,则执行微任务,然后执行宏任务,反复循环至所有事件执行完毕。微任务永远在宏任务之前执行。

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
console.log(1);
setTimeout(function() {
console.log(2);
}, 0);
setTimeout(function() {
new Promise(function(resolve) {
console.log(6);
resolve(Date.now());
}).then(function() {
console.log(7);
});
setTimeout(function() {
console.log(10);
}, 0);
}, 0);
new Promise(function(resolve) {
console.log(8);
resolve(Date.now());
}).then(function() {
console.log(9);
});
new Promise(function(resolve) {
console.log(3);
resolve(Date.now());
}).then(function() {
console.log(4);
});
console.log(5);

解析执行步骤:

  1. 主线程先执行log(1)
  2. log(2)加入宏任务,log(6,7,10)加入宏任务,执行log(8),把log(9)加入微任务
  3. 执行log(3),把log(4)加入微任务
  4. 执行log(5)
  5. 发现微任务,开始处理微任务,先进先出的规则,执行log(9),log(4),微任务执行完毕。
  6. 接着处理宏任务,发现宏任务,执行log(2),执行log(6),把log(7)加入微任务,把log(10)加入宏任务
  7. 发现微任务,执行微任务,执行log(7),微任务执行完毕。
  8. 接着执行宏任务,执行log(10),宏任务和微任务均执行完毕。

执行结果: 1,8,3,5,9,4,2,6,7,10

一、常见的CSS预处理器?

css预处理器:用一种专门的编程语言,为CSS增加了一些编程的特性,将CSS作为目标生成文件,然后开发者就只要使用这种语言进行编码工作,可以让你的CSS更加简洁、适应性更强、可读性更佳,更易于代码的维护等诸多好处

  • Sass(Scss):2007,ruby编写,
  • SCSS 是 Sass 3 引入新的语法,其语法完全兼容 CSS3,并且继承了 Sass 的强大功能。Sass 和 SCSS 其实是同一种东西,我们平时都称之为 Sass,两者之间不同之处有以下两点:Sass 是以“.sass”后缀为扩展名,而 SCSS 是以“.scss”后缀为扩展名;Sass 是以严格的缩进式语法规则来书写,不带大括号({})和分号(;),而 SCSS 的语法书写和我们的 CSS 语法书写方式非常类似。
  • Less: 2009,js编译

使用流程?

  1. 基于CSS的另外一种语言
  2. 通过工具编译成CSS
  3. 添加了很多CSS不具备的特性
  4. 同时CSS文件的组织方式

预处理器的作用有哪些?

  1. 帮助开发者更好组织CSS代码(变量,mixIn)
  2. 提高代码的复用性
  3. 代码的可维护性增强

二、Less/Sass预处理器的功能

  1. 嵌套: 反映层级和约束
  2. 变量和计算: 减少重复代码
  3. Extend和mixIn: 代码片段的抽离
  4. 循环: 适用于复杂有规律的样式
  5. import:可以实现CSS文件的模块化

三、CSS预处理器的优缺点?

  • 优点: 提高代码的复用率和可维护性
  • 缺点: 引入了编译的过程,有一定的学习成本

前端工程化:预处理不再是唯一的手段了,可以使用工程化构建工具进行处理

四、Sass和less的区别?

  1. 编译环境不一样
  2. 变量符不一样,Less是@,而Sass是$,而且变量的作用域也不一样。
  3. 输出设置,Less没有输出设置,Sass提供4种输出选项:
  4. Sass支持条件语句,可以使用if{}else{},for{}循环等等。而Less不支持。
  5. 引用外部CSS文件
  6. 工具库不同

数组中有 n 个数,比较每相邻两个数,如果前者大于后者,就把两个数交换位置;这样一来,第一轮就可以选出一个最大的数放在最后面;那么经过 n-1(数组的 length - 1) 轮,就完成了所有数的排序。

例子:现有数组arr:[14,3,13,33,61,20,11]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const bubbleSort = arr => {
for (let i = 0; i < arr.length - 1; i++) {
let done = true;
for (let j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
const temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
done = false;
}
}
if (done) {
break;
}
}
return arr;
};

时间复杂度: 平均时间复杂度O(n*n) 、最好情况O(n)、最差情况O(n*n)
空间复杂度: O(1)
稳定性:稳定

快速排序的过程:

  1. 在数据中选择一个元素作为基准( 一般选择中间的数字或者最左边的数字 ),所有小于基准的元素都移到左边的子集中,所有大于基准的元素都移到右边的子集中
  2. 对基准左右两边的两个子集不断重复第一步,直到所有子集只剩下一个元素为止。

例子:现有数组arr:[14,3,13,33,61,20,11]

  1. 第一次分类选择中间元素的位置Math.floor(arr.length/2) = 3,则中间元素是13,小于13的放左边, 大于13的放右边

    3,11//左边 **13 ** 14,33,61,20//右边

  2. 第二次则分别对左右两边的子集重复第一次排序。

    3 11 13 14 ,20 33 61

  3. 第四次

    3,11,13 ,14,20,33,61

使用JavaScript实现快排算法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const quickSort = arr => {
if (arr.length <= 1) {
return arr;
}

const pivotIndex = Math.floor(arr.length / 2) - 1;
const pivot = arr.splice(pivotIndex, 1)[0];
const leftArr = [];
const rightArr = [];

for (let i = 0; i < arr.length; i++) {
if (arr[i] < pivot) {
leftArr.push(arr[i]);
} else {
rightArr.push(arr[i]);
}
}

return quickSort(leftArr).concat([pivot], quickSort(rightArr));
};

时间复杂度: 平均时间复杂度O(nlogn) 、最好情况O(n)、最差情况O(n*n)
空间复杂度: O(logn)
稳定性: 不稳定

这里如果是二分查找,则logn的底数是2

对数函数和指数函数复习:

如果a(a大于0,且a不等于1)的b的次幂等于N,那么数b叫做以a为底N的对数,记作 logaN = b,读作以a为底N的对数,a叫做对数的底数,N叫做真数。

一般函数 y = log(a)X (其中a 是常数,a大于0且不等于1)叫做对数函数,

对数函数是指数函数的反函数,则指数函数为 x = a^y

时间复杂度指的是一个算法执行所耗费的时间
空间复杂度指运行完一个程序所需内存的大小
稳定指,如果a=b,a在b的前面,排序后a仍然在b的前面
不稳定指,如果a=b,a在b的前面,排序后可能会交换位置

利用虚拟DOM能够提高页面的渲染效率,目前流行的前端框架React和Vue都引用了虚拟DOM。下面整理了一下关于虚拟DOM的相关内容

关于DOM

DOM(Document Object Modal)文档对象模型就是页面上的元素节点。

原生JS是命令式的开发,如果需要改变DOM只能一步步的对DOM进行增删改查,可能很多代码都在操作DOM。

React是声明式开发。它有响应式的设计,定义好数据和模板,React会自动渲染DOM,只需要改变数据它就会帮我们改变DOM。

虚拟DOM

Virtual DOM 本质上是Javascript对象,是对真实DOM的一种描述方式。是JS对象模拟的DOM结构,将DOM变化的对比放在JS层来做。

React可以自动渲染出DOM结构,但是当数据改变时,React就会重新渲染DOM,JS对DOM的操作很消耗性能的,如果只改了一小部分的数据就要重新渲染整个DOM树,在性能上是极大的浪费。所以React引入了虚拟DOM的概念。

虚拟DOM是JS中的一个对象,是根据数据和模板生成的对象,包含了整个DOM结构。

1
['div', {id: "id"}, ['p', {class: 'description', 'hello world'}]]

上面代码就相当于一个虚拟的DOM,div里面有个p标签,p标签里面写了hello world,当然也包含元素上的各个属性 。

有了虚拟DOM后,当我们改变数据,React会根据当前的数据和模板生成一个虚拟DOM,然后和上一次生成的虚拟DOM进行比对,找出差异,然后根据差异对DOM进行渲染,当然它不会重新渲染,只会渲染被改变的部分。而虚拟DOM只是JS里面的一个对象,JS对对象的操作是非常快的。所以这种方式极大的提高了性能。

虚拟dom中的diff算法

React中Diff算法运用在比较原始虚拟DOM和新的虚拟DOM的比对中。两个虚拟DOM的对比就叫Difference算法,大大提高了效果。当调用setState()的时候,就会发生虚拟DOM的对比。setState()是异步函数。

为什么给dom的key值赋值时候,不建议使用index?

循环遍历DOM的时候要给DOM设置一个key值,就相当于给每个dom起了一个名字。在做虚拟dom对比的时候,我们只需要按名字对比,不需要通过dom内容现找到对应的dom再进行对比,这样就节省了新能。index不具备稳定性,当你删除某一个dom后,可能你整个dom树的key值都会发生变化。使用dom的内容去做key,内容改变时对应的dom就需要重新渲染。

为什么使用虚拟DOM

虚拟DOM最大的特点是将页面的状态抽象为 JS 对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。如 React 就借助 虚拟DOM 实现了服务端渲染、浏览器渲染和移动端渲染等功能。

在进行页面更新的时候,借助虚拟DOM,DOM 元素的改变可以在内存中进行比较,再结合框架的事务机制将多次比较的结果合并后一次性更新到页面,从而有效地减少页面渲染的次数,提高渲染效率。

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大 。 Promise出现之前解决异步编程用回调,当出现按顺序请求多个异步代码时就只能用回调嵌套来解决,当请求的异步代码很多时就会出现 ”回调地狱“。Promise的出现使得回调看起来变得可控。使用async,await使得异步代码看起来是在同步运行。

ES6 规定,**Promise对象**是一个构造函数,用来生成Promise实例。

1
2
3
4
5
6
7
8
9
const promise = new Promise(function(resolve, reject) {
// ... some code

if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。 它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。

1
2
3
4
5
promise.then(function(value) {
// success
}, function(error) {
// failure
});

then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数。

有了 Promise 对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise 对象提供统一的接口,使得控制异步操作更加容易。

Promise规范如下:

  • 一个promise可能有三种状态:等待(pending)、已完成(fulfilled)、已拒绝(rejected)

  • 一个promise的状态只可能从“等待”转到“完成”态或者“拒绝”态,不能逆向转换,同时“完成”态和“拒绝”态不能相互转换

  • promise必须实现then方法(可以说,then就是promise的核心),而且then必须返回一个promise,同一个promise的then可以调用多次,并且回调的执行顺序跟它们被定义时的顺序一致

  • then方法接受两个参数,第一个参数是成功时的回调,在promise由“等待”态转换到“完成”态时调用,另一个是失败时的回调,在promise由“等待”态转换到“拒绝”态时调用。同时,then可以接受另一个promise传入,也接受一个“类then”的对象或方法,即thenable对象。

promise 的缺点:

  • 无法取消 Promise,一旦新建它就会立即执行,无法中途取消。
  • 如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。第三,当处于 Pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

用代码实现Promise:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function myPromise(constructor){ let self=this;
self.status="pending" //定义状态改变前的初始状态
self.value=undefined;//定义状态为resolved的时候的状态
self.reason=undefined;//定义状态为rejected的时候的状态
function resolve(value){
//两个==="pending",保证了了状态的改变是不不可逆的
if(self.status==="pending"){
self.value=value;
self.status="resolved";
}
}
function reject(reason){
//两个==="pending",保证了了状态的改变是不不可逆的
if(self.status==="pending"){
self.reason=reason;
self.status="rejected";
}
}
//捕获构造异常
try{
constructor(resolve,reject);
}catch(e){
reject(e);
}
}
myPromise.prototype.then=function(onFullfilled,onRejected){
let self=this;
switch(self.status){
case "resolved": onFullfilled(self.value); break;
case "rejected": onRejected(self.reason); break;
default:
}
}

// 测试
var p=new myPromise(function(resolve,reject){resolve(1)});
p.then(function(x){console.log(x)})
//输出1

CDN的全称是(Content Delivery Network),即内容分发网络。其目的是通过在现有的Internet中增加一层新的CACHE(缓存)层,将网站的内容发布到最接近用户的网络”边缘“的节点,使用户可以就近取得所需的内容,提高用户访问网站的响应速度。从技术上全面解决由于网络带宽小、用户访问量大、网点分布不均等原因,提高用户访问网站的响应速度。

简单的说,CDN的工作原理就是将您源站的资源缓存到位于全球各地的CDN节点上,用户请求资源时,就近返回节点上缓存的资源,而不需要每个用户的请求都回您的源站获取,避免网络拥塞、缓解源站压力,保证用户访问资源的速度和体验。

传统的网站访问过程

  • 用户在浏览器中输入要访问的域名;
  • 浏览器向域名解析服务器发出解析请求,获得此域名对应的IP 地址;
  • 浏览器利用所得到的IP 地址,向该IP对应的服务器发出访问请求;
  • 服务器对此响应,将数据回传至用户浏览器端显示出来。

使用CDN 缓存后的网站访问过程

  1. 用户在浏览器中输入要访问的域名;

  2. 浏览器向域名解析服务器发出解析请求,由于CDN 对域名解析过程进行了调整,所以用户端一般得到的是该域名对应的 CNAME 记录,此时浏览器需要再次对获得的 CNAME 域名进行解析才能得到缓存服务器实际的IP 地址。 注:在此过程中,全局负载均衡DNS 解析服务器会根据用户端的源IP 地址,如地理位置(北京还是上海)、接入网类型(电信还是网通)将用户的访问请求定位到离用户路由最短、位置最近、负载最轻的Cache 节点(缓存服务器)上,实现就近定位。定位优先原则可按位置、可按路由、也可按负载等。

  3. 再次解析后浏览器得到该域名CDN 缓存服务器的实际IP 地址,向缓存服务器发出访问请求;

  4. 缓存服务器根据浏览器提供的域名,通过Cache 内部专用DNS 解析得到此域名源服务器的真实IP 地址,再由缓存服务器向此真实IP 地址提交访问请求;

  5. 缓存服务器从真实IP 地址得到内容后,一方面在本地进行保存,以备以后使用,同时把得到的数据发送到客户端浏览器,完成访问的响应过程;

  6. 用户端得到由缓存服务器传回的数据后显示出来,至此完成整个域名访问过程。

CNAME记录(CNAME record)

CNAME即别名( Canonical Name );可以用来把一个域名解析到另一个域名,当 DNS 系统在查询 CNAME 左面的名称的时候,都会转向 CNAME 右面的名称再进行查询,一直追踪到最后的 PTR 或 A 名称,成功查询后才会做出回应,否则失败。
例如,你有一台服务器上存放了很多资料,你使用docs.example.com去访问这些资源,但又希望通过documents.example.com也能访问到这些资源,那么你就可以在您的DNS解析服务商添加一条CNAME记录,将documents.example.com指向docs.example.com,添加该条CNAME记录后,所有访问documents.example.com的请求都会被转到docs.example.com,获得相同的内容。

CNAME域名

接入CDN时,在CDN提供商控制台添加完加速域名后,您会得到一个CDN给您分配的CNAME域名, 您需要在您的DNS解析服务商添加CNAME记录,将自己的加速域名指向这个CNAME域名,这样该域名所有的请求才会都将转向CDN的节点,达到加速效果。

DNS

DNS即Domain Name System,是域名解析服务的意思。它在互联网的作用是:把域名转换成为网络可以识别的ip地址。人们习惯记忆域名,但机器间互相只认IP地址,域名与IP地址之间是一一对应的,它们之间的转换工作称为域名解析,域名解析需要由专门的域名解析服务器来完成,整个过程是自动进行的。比如:上网时输入的www.baidu.com会自动转换成为220.181.112.143
常见的DNS解析服务商有:阿里云解析,万网解析,DNSPod,新网解析,Route53(AWS),Dyn,Cloudflare等。

释放连接之前,在进行数据传送,都处于ESTABLISHED状态,即已建立连接。
书上说数据传输结束之后,通信的双方都可以释放连接。
这里演示的是客户端A主动释放连接。

TCP传输连接管理 - 连接释放 - 四次握手

第一次握手

客户端A的应用程序先向其TCP发出释放报文段,并停止传输数据,主动关闭TCP连接。
A把释放报文段首部的 终止控制位FIN = 1,其序号 seq = u,u 等于已传送过来的数据的最后一个字节的序号加1。
此时,客户端A进入 FIN-WAIT-1状态。

第二次握手

服务器端B收到释放报文段后,发出确认,ACK=1,确认号是 ack = u+1,而这个确认报文段自己的序号是seq = v ,v等于B前面已经传送过的数据的最后一个字节的序号加 1 。
然后服务器B就进入到 CLOSE-WAIT状态,即等待关闭。
此时!!! 处于半关闭状态。
即:A已经没有数据发送到B,但是B如果有数据要发送给A的话,A仍然要接收。
也就是说,A到B这个方向的连接已经关闭,B到A这个方向的连接并没有关闭,可能还要持续一段时间。

第三次握手

客户端A收到服务器端B的确认后,进入FIN-WAIT-2状态,等待B发出的连接释放报文段。
如果B已经没有数据要发送给A了,那么B就会发出释放报文段: FIN = 1,ACK = 1,假定B的序号 seq = w。B还必须重复上次已发送的确认号 ack = u+1
这时,服务器B就进入 LAST-ACK状态了,即最后确认状态,等待A的确认。

第四次握手

客户端A收到服务器B发来的报文后,对此进行确认。在确认报文段,把 ACK = 1,序列号 seq = u+1,确认号 ack = w+1
然后进入到TIME-WAIT(时间等待)状态。
注意,四次握手之后,连接并没有马上被释放掉。必须经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。
MSL叫做最长报文段寿命,建议设置为两分钟
设置两倍最长报文段寿命的目的是:
1.保证A发送的最后一个ACK报文段能够到达B。这个报文段可能会丢失,B收不到最后的确认,就会超时重传,然后A因为还没关闭,所以就能再次传送最后的ACK报文。
2.为了防止“已失效的请求连接报文段”出现在本次连接中,2MSL时间后,就可以是的本连接持续时间内所发生的所有报文段都从网络中消失。这样就可以使下一个新的连接中不会出现这种旧的连接请求报文段。

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

这是因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方是否现在关闭发送数据通道,需要上层应用来决定,因此,己方ACK和FIN一般都会分开发送。