html语义化的理解
-
代码结构: 使页面在没有css的情况下,也能够呈现出好的内容结构
-
有利于SEO: 爬虫根据标签来分配关键字的权重,因此可以和搜索引擎建立良好的沟通,帮助爬虫抓取更多的有效信息
-
方便其他设备解析: 如屏幕阅读器、盲人阅读器、移动设备等,以有意义的方式来渲染页面
-
便于团体的开发和维护: 语义化使代码更具有可读性,让其他开发人员更加理解你的html结构,减少差异化。遵循 W3C 标准的团队都遵循这个标准。
常用的语义元素有:header、nav、main、footer、article、section、aside
更多的语义化标签可以参阅:developer.mozilla.org/en-US/docs/…
iframe
iframe称之为嵌入式框架、嵌入式框架可以把完整的网页内容嵌入到现有的网页中。
优点
- 重载页面时不需要重载整个页面只需要重载页面中的一个框架页
- 可以使脚本、可以并行下载
- 可以实现跨子域通信
缺点
- 会产生很多页面,不容易管理
- 调用外部页面,需要额外调用 CSS,给页面带来额外的请求次数
- iframe 会阻塞主页面的 onload 事件
- 浏览器的后退按钮无效
- 无法被一些搜索引擎索引识别
- 多数小型的移动设备无法完全显示框架
BOM和DOM分别是什么
BOM是浏览器对象模型: 用来获取或设置浏览器的属性、行为。例如:新建窗口、获取屏幕分辨率、浏览器版本号等
DOM是文档对象模型: 用来获取或设置文档中标签的属性、例如获取或者设置input表单的value值
CSS盒模型
盒模型都是由四个部分组成的,分别是margin、border、padding和content
标准盒模型和IE盒模型的区别在于设置width和height时,所对应的范围不同:
- 标准盒模型的width和height属性的范围只包含了content,
- IE盒模型的width和height属性的范围包含了border、padding和content。
可以通过修改元素的box-sizing属性来改变元素的盒模型:
box-sizing: content-box
表示标准盒模型(默认值)box-sizing: border-box
表示IE盒模型(怪异盒模型)
css级别顺序
!important > 行内样式 > ID选择器 > 类选择器 > 标签 > 通配符 > 继承 > 浏览器默认属性
css有哪些属性不会被继承
display
:指定元素如何显示,如 block、inline、none 等,不会被继承。float
:指定元素的浮动方式,如 left、right,不会被继承。clear
:指定元素旁边不允许浮动的元素,如 left、right、both、none,不会被继承。position
:指定元素的定位方式,如 relative、absolute、fixed 等,不会被继承。z-index
:指定元素的堆叠顺序,不会被继承。overflow
:指定元素内容溢出时的处理方式,如 visible、hidden、scroll、auto 等,不会被继承。columns
:指定多列布局的列数、宽度等属性,不会被继承。- 等等…
怎么让一个 div 水平垂直居中
- 通过绝对定位实现垂直居中
- 通过 transform 实现垂直居中
- 使用弹性盒子居中
BFC
所谓 BFC,指的是一个独立的布局环境,BFC 内部的元素布局与外部互不影响。
触发 BFC 的方式有很多,常见的有:
- 设置浮动
- overflow 设置为 auto、scroll、hidden
- positon 设置为 absolute、fixed
常见BFC的应用
- 解决浮动元素令父元素高度坍塌的问题
- 解决非浮动元素被浮动元素覆盖问题
- 解决外边距垂直方向重合问题
JS 的基本数据类型有哪些?基本数据类型和引用数据类型的区别
基本数据类型
JavaScript共有八种数据类型,分别是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt
这些数据可以分为原始数据类型和引用数据类型:
- 栈:原始数据类型(Undefined、Null、Boolean、Number、String)
- 堆:引用数据类型(对象、数组和函数)
基本数据类型和引用数据类型的区别
-
访问方式:
- 原始值:访问到的是值
- 引用值:访问到的是引用地址 (
js不允许直接访问保存在堆中的对象、首先得到在堆中的地址,然后按照这个地址去获得对象的值
)
-
比较方式:
- 原始值:比较的是值
- 引用值: 比较的是引用的地址
-
变量赋值:
- 原始值赋值:赋值的是新值,与原来互不影响
- 引用值赋值:赋值的是地址,指向原值所在堆内存中的地址
-
动态属性:
- 原始值:赋值的是值
- 引用值:赋值的是地址
Js数据类型检测的方式有哪些
-
typeof
- 其中数组、对象、null都会被判断为object,其他判断都正确
- typeof返回的类型都是字符串形式
-
instanceof
instanceof
可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型
-
constructor
-
Object.prototype.toString.call()
Object.prototype.toString.call()
使用 Object 对象的原型方法 toString 来判断数据类型:
常用的的js方法
-
fill方法:用某个数据填充数组
-
pop方法:删除数组的最后一个元素,并返回这个元素
-
push方法:在数组的末尾添加一个或多个元素,并放回数组的新长度
-
reverse方法:颠倒数组中的元素排列顺序
-
shift方法:删除数组的第一个元素,并返回这个元素
-
sort方法:对数组元素进行随机排序,并返回当前数组
-
splice方法:在任意的位置给数组添加或删除任意各元素
-
unshift方法:在数组的开头添加一个或者多个元素,并返回数组的新长度
纯函数、无副作用函数:不会导致当前对象发生改变
- concat方法:连接俩个数组
- includes方法:数组中是否包含满足条件的元素
- join方法:连接所有数组元素组成一个字符串
- slice方法:抽取当前数组中的一段元素组合成一个新数组
- indexOf方法:返回数组中第一个与指定值相等的元素的索引,如果找不到这样的元素,则返回-1
- lastIndexOf方法:返回数组中最后一个(从右边数第一个)与指定值相等的元素的索引,如果找不到这样的元素,则返回-1
- forEach方法:为数组中的每个元素执行一次回调函数
- every方法:如果数组中每个元素都满足测试函数,则返回true,否则返回false
- some方法:数组中至少有一个元素满足条件
- filter方法:过滤,得到满足条件的元素组成的新数组
- find方法:找到第一个满足测试函数的元素,并返回那个元素的值,如果找不到,则返回undefined
- findIndex方法:查找第一个满足条件的元素,返回元素的下标
- map方法:映射,将数组的每一项映射称为另外一项
- reduce方法:统计,累计
map和forEach的区别
forEach 会修改原来的数组,而map方法会得到一个新的数组并返回。
ES6 新增哪些东西
- 箭头函数
- 字符串模板
- 支持模块化(import、export)
- 类(class、constructor、extends)
- let、const 关键字
- 新增一些数组、字符串等内置构造函数方法,例如 Array.from、Array.of 、Math.sign、Math.trunc 等
- 新增一些语法,例如扩展操作符、解构、函数默认参数等
- 新增一种基本数据类型 Symbol
- 新增元编程相关,例如 proxy、Reflect
- Set 和 Map 数据结构
- Promise
- Generator 生成器
let const var 的区别?什么是块级作用域?如何用?
-
var
定义的变量,是函数作用域,没有块的概念,可以跨块访问, 不能跨函数访问,有变量提升。 -
let
定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问,无变量提升,不可以重复声明。 -
const
用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改,无变量提升,不可以重复声明。 -
var
不存在暂时性死区,let
和const
存在暂时性死区 -
let
和const
创建的全局变量没有给window设置相应的属性
暂时性死区:使用 let / const 命令声明变量之前,该变量都是不可用的
箭头函数与普通函数的区别
-
箭头函数没有自己的this、会捕获其所在的上下文的this值,作为自己的this值
-
箭头函数继承来的this指向永远不会改变
-
call()、apply()、bind()等方法不能改变箭头函数中this的指向
-
箭头函数是匿名函数,不能作为构造函数,不能使用new
-
箭头函数没有自己的arguments
-
箭头函数没有prototype(原型)
-
箭头函数不能用作Generator函数,不能使用yeild关键字
bind,apply,call三者的区别
-
三者都可以改变函数的
this
对象指向 -
三者第一个参数都是
this
要指向的对象,如果如果没有这个参数或参数为undefined
或null
,则默认指向全局window
-
三者都可以传参,但是
apply
是数组,而call
是参数列表,且apply
和call
是一次性传入参数,而bind
可以分为多次传入 -
bind
是返回绑定this之后的函数,apply
、call
则是立即执行
是否了解 JavaScript 中的包装类型?
包装对象 : 就是当基本类型以对象的方式去使用时,JavaScript 会转换成对应的包装类型,相当于 new 一个对象,内容和基本类型的内容一样,然后当操作完成再去访问的时候,这个临时对象会被销毁,然后再访问时候就是 undefined
代码理解:
var str = 'hello';
str.number = 10;
//假设我们想给字符串添加一个属性 number ,后台会有如下步骤 (
var _str = new String('hello');
// 1 找到对应的包装对象类型,然后通过包装对象创建出一个和基本类型值相同的对象
_str.number = 10;
// 2 通过这个对象调用包装对象下的方法 但结果并没有被任何东西保存
_str =null;
// 3 这个对象又被销毁 )
console.log(str.number); // undefined 当执行到这一句的时候,因为基本类型本来没有属性,后台又会重新重复上面的步骤 (
var str = new String('hello');
// 1 找到基本包装对象,然后又新开辟一个内存,创建一个值为 hello 对象
str.number = undefined;
// 2 因为包装对象下面没有 number 这个属性,所以又会重新添加,因为没有值,所以值是未定义;然后弹出结果
str =null; // 3 这个对象又被销毁 )
JS 中如何进行数据类型的转换?
类型转换可以分为两种,隐性转换和显性转换
显性转换
主要分为三大类:数值类型、字符串类型、布尔类型
数值类型(引用类型转换)
Number({a: 1})// NaN Number([1, 2, 3]) // NaN Number([5])// 5
//第一步,调用对象自身的`valueOf`方法。如果返回原始类型的值,则直接对该值使用`Number`函数,不再进行后续步骤。
//第二步,如果`valueOf`方法返回的还是对象,则改为调用对象自身的`toString`方法。如果`toString`方法返回原始类型的值, 则对该值使用`Number`函数,不再进行后续步骤。
//第三步,如果`toString`方法返回的是对象,就报错。
//补充一点:`valueOf`和`toString`方法,都是可以自定义的
字符串类型(引用类型转换)
``String({a: 1}) // "[object Object]"
String([1, 2, 3]) // "1,2,3" `String`方法背后的转换规则,与`Number`方法基本相同,只是互换了`valueOf`方法和`toString`方法的执行顺序。``
隐性转换
类型 | 转换前 | 转换后 |
---|---|---|
number | 4 | 4 |
string | “1” | 1 |
string | “” | 0 |
boolean | true | 1 |
boolean | false | 0 |
undefined | undefined | NaN |
null | null | 0 |
闭包
闭包是指有权访问另一个函数作用域中的变量的函数 ———— 《JavaScript高级程序设计》
-
闭包用途:
- 能够访问函数定义时所在的词法作用域(阻止其被回收)
- 私有变量化
- 模拟块级作用域
- 创建模块
-
闭包缺点:闭包调用函数的变量,并且这个变量在函数执行完之后,不能释放,会导致函数的变量一直保存在内存中,过多的闭包可能会导致内存泄漏。
-
解决:变量设置成null
原型和原型链
原型
- 每个实例对象都有一个 proto 属性,该属性指向自己的原型对象
- 每个构造函数都有一个 prototype 属性,该属性指向实例对象的原型对象
- 原型对象里的 constructor 指向构造函数本身
原型链
当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾null
作用域和作用域链的理解
作用域
(1)全局作用域
- 最外层函数和最外层函数外面定义的变量拥有全局作用域
- 所有未定义直接赋值的变量自动声明为全局作用域
- 所有window对象的属性拥有全局作用域
- 全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,容易引起命名冲突。
(2)函数作用域
- 函数作用域声明在函数内部的变量,一般只有固定的代码片段可以访问到
- 作用域是分层的,内层作用域可以访问外层作用域,反之不行
(3)块级作用域
- 使用ES6中新增的let和const指令可以声明块级作用域,块级作用域可以在函数中创建也可以在一个代码块中的创建(由
{ }
包裹的代码片段) - let和const声明的变量不会有变量提升,也不可以重复声明
- 在循环中比较适合绑定块级作用域,这样就可以把声明的计数器变量限制在循环内部。
作用域链
- 在当前作用域中查找所需变量,但是该作用域没有这个变量,那这个变量就是自由变量。
- 如果在自己作用域找不到该变量就去父级作用域查找,依次向上级作用域查找,直到访问到window对象就被终止,这一层层的关系就是作用域链。
作用域链有一个非常重要的特性,那就是作用域中的值是在函数创建的时候,就已经被存储了,是静态的。
宏任务和微任务
- 常见的宏任务有:_setTimeout、setInterval、requestAnimationFrame、script_等。
- 常见的微任务有:new Promise( ).then(回调)、MutationObserver 等。
宏任务和微任务的执行流程,总结起来就是:
js在调用时,优先取出微任务,并且在执行过程中如果创建了新的作业,则放在本次执行完后紧接着调用,微任务执行完成后,再取出宏任务执行
哪些情况会导致内存泄漏
以下四种情况会造成内存的泄漏:
- 意外的全局变量: 由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
- 被遗忘的计时器或回调函数: 设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
- 脱离 DOM 的引用: 获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。
- 闭包: 不合理的使用闭包,从而导致某些变量一直被留在内存当中。
请介绍一下 JavaScript 中的垃圾回收站机制
JavaScript 具有自动垃圾回收机制。垃圾收集器会按照固定的时间间隔周期性的执行。
JavaScript 常见的垃圾回收方式:标记清除、引用计数方式。
标记清除方式:
- 工作原理:当变量进入环境时,将这个变量标记为“进入环境”。当变量离开环境时,则将其标记为“离开环境”。标记“离开环境”的就回收内存。
- 工作流程:
- 垃圾回收器,在运行的时候会给存储在内存中的所有变量都加上标记;
- 去掉环境中的变量以及被环境中的变量引用的变量的标记;
- 被加上标记的会被视为准备删除的变量;
- 垃圾回收器完成内存清理工作,销毁那些带标记的值并回收他们所占用的内存空间。
引用计数方式:
- 工作原理:跟踪记录每个值被引用的次数。
- 工作流程:
- 声明了一个变量并将一个引用类型的值赋值给这个变量,这个引用类型值的引用次数就是 1;
- 同一个值又被赋值给另一个变量,这个引用类型值的引用次数加 1;
- 当包含这个引用类型值的变量又被赋值成另一个值了,那么这个引用类型值的引用次数减 1;
- 当引用次数变成 0 时,说明没办法访问这个值了;
- 当垃圾收集器下一次运行时,它就会释放引用次数是 0 的值所占的内存。
防抖,节流是什么
- 函数防抖(debounce),指的是短时间内多次触发同一事件,只执行最后一次,或者只执行最开始的一次,中间的不执行
- 函数节流(throttle),指连续触发事件但是在 n 秒中只执行一次函数。即 2n 秒内执行 2 次…
深拷贝和浅拷贝
-
浅拷贝:只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用,我们把这种拷贝叫做浅拷贝(浅复制) 浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。
-
深拷贝:在堆中重新分配内存,并且把源对象所有属性都进行新建拷贝,以保证深拷贝的对象的引用图不包含任何原有对象或对象图上的任何对象,拷贝后的对象与原来的对象是完全隔离,互不影响。
事件冒泡和事件捕获
事件冒泡
微软提出了名为事件冒泡(event bubbling)的事件流。事件冒泡可以形象地比喻为把一颗石头投入水中,泡泡会一直从水底冒出水面。也就是说,事件会从最内层的元素开始发生,一直向上传播,直到document对象。
p -> div -> body -> html -> document
事件捕获
网景提出另一种事件流名为事件捕获(event capturing)。与事件冒泡相反,事件会从最外层开始发生,直到最具体的元素。
document -> html -> body -> div -> p
HTTP状态码
状态码的类别:
类别 | 原因 | 描述 |
---|---|---|
1xx | Informational(信息性状态码) | 接受的请求正在处理 |
2xx | Success(成功状态码) | 请求正常处理完毕 |
3xx | Redirection(重定向状态码) | 需要进行附加操作一完成请求 |
4xx | Client Error (客户端错误状态码) | 服务器无法处理请求 |
5xx | Server Error(服务器错误状态码) | 服务器处理请求出错 |
Http和Https区别
HTTP
的URL 以http:// 开头,而HTTPS 的URL 以https:// 开头HTTP
是不安全的,而 HTTPS 是安全的HTTP
标准端口是80 ,而 HTTPS 的标准端口是443在OSI
网络模型中,HTTP工作于应用层,而HTTPS 的安全传输机制工作在传输层HTTP
无法加密,而HTTPS 对传输的数据进行加密HTTP
无需证书,而HTTPS 需要CA机构wosign的颁发的SSL证书
GET 和 POST 的区别
从 http 协议的角度来说,GET 和 POST 它们都只是请求行中的第一个单词,除了语义不同,其实没有本质的区别。
之所以在实际开发中会产生各种区别,主要是因为浏览器的默认行为造成的。
受浏览器的影响,在实际开发中,GET 和 POST 有以下区别:
-
浏览器在发送 GET 请求时,不会附带请求体
-
GET 请求的传递信息量有限,适合传递少量数据;POST 请求的传递信息量是没有限制的,适合传输大量数据。
-
GET 请求只能传递 ASCII 数据,遇到非 ASCII 数据需要进行编码;POST 请求没有限制
-
大部分 GET 请求传递的数据都附带在 path 参数中,能够通过分享地址完整的重现页面,但同时也暴露了数据,若有敏感数据传递,不应该使用 GET 请求,至少不应该放到 path 中
-
刷新页面时,若当前的页面是通过 POST 请求得到的,则浏览器会提示用户是否重新提交。若是 GET 请求得到的页面则没有提示。
-
GET 请求的地址可以被保存为浏览器书签,POST 不可以
浏览器渲染机制、重绘、重排
网页生成过程:
HTML
被HTML解析器解析成DOM
树css
则被css解析器解析成CSSOM
树- 结合
DOM
树和CSSOM
树,生成一棵渲染树(Render Tree
) - 生成布局(
flow
),即将所有渲染树的所有节点进行平面合成 - 将布局绘制(
paint
)在屏幕上
重排(也称回流): 当DOM的变化影响了元素的几何属性(DOM对象的位置和尺寸大小),浏览器需要重新计算元素的几何属性,将其安放在界面中正确的位置,这个过程就叫回流。触发:
- 添加或者删除可见的DOM元素
- 元素尺寸改变——边距、填充、边框、宽度和高度
重绘: 当一个元素的外观发生了改变,但没有改变布局,重新把元素外观绘制出来的过程,叫做重绘,触发:
- 改变元素的color、background、box-shadow属性
url按下回车之后发生了什么
1 解析域名
2 缓存判断 --有资源直接返回、否则向服务器发起新的请求
3 DNS解析
4 获取MAC地址
5 会把请求的内容存储到dns
6 TCP三次握手
7 HTTPS握手
8 返回数据
9 TCP四次挥手
cookie、sessionStorage、localStorage的区别
- cookie 可以设置失效时间
- localStorage 除非手动清除,否则永久保存
- sessionStorage 尽在当前网页有效,关闭页面就被清除
- cookie储存大小是4k,localStorage和sessionStorage是5M
- 请求时:cookie自动携带HTTP头部中,localStorage和sessionStorage仅在浏览器保存,不参与服务器通信
- cookie是由服务器写入的,而sessionStorage、localStorage是由前端写入的
- cookie用于储存登录信息,localStorage常用于不易变动的数据,sessionStorage常用于检查用户是否是刷新进入页面、如回复播放器进度条的功能
Promise.all和Promise.race的区别的使用场景
(1)Promise.all Promise.all
可以将多个Promise
实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。
Promise.all中传入的是数组,返回的也是是数组,并且会将进行映射,传入的promise对象返回的值是按照顺序在数组中排列的,但是注意的是他们执行的顺序并不是按照顺序的,除非可迭代对象为空。
需要注意,Promise.all获得的成功结果的数组里面的数据顺序和Promise.all接收到的数组顺序是一致的,这样当遇到发送多个请求并根据请求顺序获取和使用数据的场景,就可以使用Promise.all来解决。
(2)Promise.race
顾名思义,Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。当要做一件事,超过多长时间就不做了,可以用这个方法来解决:
Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})
同源策略
浏览器有一个重要的安全策略,称之为同源策略
其中,协议、端口号、域名必须一致,,称之为同源,两个源不同,称之为跨源或跨域
同源策略是指,若页面的源和页面运行过程中加载的源不一致时,出于安全考虑,浏览器会对跨域的资源访问进行一些限制
如何解决跨域问题
jsonp
(利用script
标签没有跨域限制的漏洞实现。缺点:只支持GET
请求)CORS
(设置Access-Control-Allow-Origin
:指定可访问资源的域名Node
中间件代理Nginx
反向代理
谈谈你对 TCP 三次握手和四次挥手的理解
TCP 协议通过三次握手建立可靠的点对点连接,具体过程是:
首先服务器进入监听状态,然后即可处理连接
-
第一次握手:建立连接时,客户端发送 syn 包到服务器,并进入 SYN_SENT 状态,等待服务器确认。在发送的包中还会包含一个初始序列号 seq。此次握手的含义是客户端希望与服务器建立连接。
-
第二次握手:服务器收到 syn 包,然后回应给客户端一个 SYN+ACK 包,此时服务器进入 SYN_RCVD 状态。此次握手的含义是服务端回应客户端,表示已收到并同意客户端的连接请求。
-
第三次握手:客户端收到服务器的 SYN 包后,向服务器再次发送 ACK 包,并进入 ESTAB_LISHED 状态。
最后,服务端收到客户端的 ACK 包,于是也进入 ESTAB_LISHED 状态,至此,连接建立完成
当需要关闭连接时,需要进行四次挥手才能关闭
-
Client 向 Server 发送 FIN 包,表示 Client 主动要关闭连接,然后进入 FIN_WAIT_1 状态,等待 Server 返回 ACK 包。此后 Client 不能再向 Server 发送数据,但能读取数据。
-
Server 收到 FIN 包后向 Client 发送 ACK 包,然后进入 CLOSE_WAIT 状态,此后 Server 不能再读取数据,但可以继续向 Client 发送数据。
-
Client 收到 Server 返回的 ACK 包后进入 FIN_WAIT_2 状态,等待 Server 发送 FIN 包。
-
Server 完成数据的发送后,将 FIN 包发送给 Client,然后进入 LAST_ACK 状态,等待 Client 返回 ACK 包,此后 Server 既不能读取数据,也不能发送数据。
-
Client 收到 FIN 包后向 Server 发送 ACK 包,然后进入 TIME_WAIT 状态,接着等待足够长的时间(2MSL)以确保 Server 接收到 ACK 包,最后回到 CLOSED 状态,释放网络资源。
-
Server 收到 Client 返回的 ACK 包后便回到 CLOSED 状态,释放网络资源。
简单的理解
三次握手
- 客户端向服务端发信息:你能接收到我的信息吗
- 服务端回复客户端信息:我能收到你的信息,你能收到我的回复信息吗
- 客户信息向服务端回复:收到你的信息了,那我开始向你传输数据了
四次挥手
- 客户端告诉服务端:数据发送完了
- 服务端回复客户端:知道你发完了,我还没接受完等一下
- 服务端回复客户端:数据接收完了
- 客户端回复服务端:知道你接收完成