• 前端入职学习笔记-第一周第三天(HTTP、Promise、fetch、Axios基础)


    学习路径

    HTTP、Promise、fetch、Axois基础  

    1、HTTP

      超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络传输协议,所有的WWW文件都必须遵守这个标准。设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法。HTTP 协议是互联网的基础协议,也是网页开发的必备知识,最新版本 HTTP/2 更是让它成为技术热点。HTTP 是基于 TCP/IP 协议的应用层协议。它不涉及数据包(packet)传输,主要规定了客户端和服务器之间的通信格式,默认使用80端口。

    1.1 一次完整的HTTP请求

    三次握手:

    首先解析服务器DNS,找到IP,然后开始建立连接:

    1.第一次握手: 建立连接,客户端A发送SYN=1、随机产生Seq=client_isn的数据包到服务器B,等待服务器确认。

    2.第二次握手: 服务器B收到请求后确认联机(可以接受数据),发起第二次握手请求,ACK=(A的Seq+1)、SYN=1,随机产生Seq=client_isn的数据包到A。

    3.第三次握手: A收到后检查ACK是否正确,若正确,A会在发送确认包ACK=服务器B的Seq+1、ACK=1,服务器B收到后确认Seq值与ACK值,若正确,则建立连接。

     

    四次挥手:

    1.第一次挥手:TCP发送一个FIN(结束)标识,用来关闭客户到服务端的连接。

    2.第二次挥手:服务端收到这个FIN标识,他发回一个ACK(确认)标识,确认收到序号为收到序号+1,和SYN一样,一个FIN将占用一个序号。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。(服务器端继续发送未发送完的数据)
    3.第三次挥手:服务端发送一个FIN(结束)标识到客户端,服务端关闭客户端的连接。
    4.第四次挥手:客户端发送ACK(确认)标识报文确认,并将确认的序号+1,这样关闭完成。

    1.2 无状态协议

    什么是Http协议无状态协议?怎么解决Http协议无状态协议?

    • 无状态协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息
      • 也就是说,当客户端一次HTTP请求完成以后,客户端再发送一次HTTP请求,HTTP并不知道当前客户端是一个”老用户“。
    • 可以使用Cookie来解决无状态的问题,Cookie就相当于一个通行证,第一次访问的时候给客户端发送一个Cookie,当客户端再次来的时候,拿着Cookie(通行证),那么服务器就知道这个是”老用户“。

    1.3 URI和URL的区别

    URI,是uniform resource identifier,统一资源标识符,用来唯一的标识一个资源。

    • Web上可用的每种资源如HTML文档、图像、视频片段、程序等都是一个来URI来定位的
    • URI一般由三部组成:
    • ①访问资源的命名机制
    • ②存放资源的主机名
    • ③资源自身的名称,由路径表示,着重强调于资源。

    URL是uniform resource locator,统一资源定位器,它是一种具体的URI,即URL可以用来标识一个资源,而且还指明了如何locate这个资源。

    • URL是Internet上用来描述信息资源的字符串,主要用在各种WWW客户程序和服务器程序上,特别是著名的Mosaic。
    • 采用URL可以用一种统一的格式来描述各种信息资源,包括文件、服务器的地址和目录等。URL一般由三部组成:
    • ①协议(或称为服务方式)
    • ②存有该资源的主机IP地址(有时也包括端口号)
    • ③主机资源的具体地址。如目录和文件名等

    URN,uniform resource name,统一资源命名,是通过名字来标识资源,比如mailto:java-net@java.sun.com。

    • URI是以一种抽象的,高层次概念定义统一资源标识,而URL和URN则是具体的资源标识的方式。URL和URN都是一种URI。笼统地说,每个 URL 都是 URI,但不一定每个 URI 都是 URL。这是因为 URI 还包括一个子类,即统一资源名称 (URN),它命名资源但不指定如何定位资源。上面的 mailto、news 和 isbn URI 都是 URN 的示例。

    在Java的URI中,一个URI实例可以代表绝对的,也可以是相对的,只要它符合URI的语法规则。而URL类则不仅符合语义,还包含了定位该资源的信息,因此它不能是相对的。在Java类库中,URI类不包含任何访问资源的方法,它唯一的作用就是解析。相反的是,URL类可以打开一个到达资源的流。

    1.4 常用的HTTP方法

    • GET: 用于请求访问已经被URI(统一资源标识符)识别的资源,可以通过URL传参给服务器
    • POST:用于传输信息给服务器,主要功能与GET方法类似,但一般推荐使用POST方式。
    • PUT: 传输文件,报文主体中包含文件内容,保存到对应URI位置。
    • HEAD: 获得报文首部,与GET方法类似,只是不返回报文主体,一般用于验证URI是否有效。
    • DELETE:删除文件,与PUT方法相反,删除对应URI位置的文件。
    • OPTIONS:查询相应URI支持的HTTP方法。

    1.5 HTTP请求报文与响应报文格式

    请求报文包含四部分:

    • a、请求行:包含请求方法、URI、HTTP版本信息
    • b、请求首部字段
    • c、请求内容实体
    • d、空行

    响应报文包含四部分:

    • a、状态行:包含HTTP版本、状态码、状态码的原因短语
    • b、响应首部字段
    • c、响应内容实体
    • d、空行

    常见的首部:

    • 通用首部字段(请求报文与响应报文都会使用的首部字段)
      • Date:创建报文时间
      • Connection:连接的管理
      • Cache-Control:缓存的控制
      • Transfer-Encoding:报文主体的传输编码方式
    • 请求首部字段(请求报文会使用的首部字段)
      • Host:请求资源所在服务器
      • Accept:可处理的媒体类型
      • Accept-Charset:可接收的字符集
      • Accept-Encoding:可接受的内容编码
      • Accept-Language:可接受的自然语言
    • 响应首部字段(响应报文会使用的首部字段)
      • Accept-Ranges:可接受的字节范围
      • Location:令客户端重新定向到的URI
      • Server:HTTP服务器的安装信息
    • 实体首部字段(请求报文与响应报文的的实体部分使用的首部字段)
      • Allow:资源可支持的HTTP方法
      • Content-Type:实体主类的类型
      • Content-Encoding:实体主体适用的编码方式
      • Content-Language:实体主体的自然语言
      • Content-Length:实体主体的的字节数
      • Content-Range:实体主体的位置范围,一般用于发出部分请求时使用

    1.6 HTTPS工作原理

    • 一、首先HTTP请求服务端生成证书,客户端对证书的有效期、合法性、域名是否与请求的域名一致、证书的公钥(RSA加密)等进行校验;
    • 二、客户端如果校验通过后,就根据证书的公钥的有效, 生成随机数,随机数使用公钥进行加密(RSA加密);
    • 三、消息体产生的后,对它的摘要进行MD5(或者SHA1)算法加密,此时就得到了RSA签名;
    • 四、发送给服务端,此时只有服务端(RSA私钥)能解密。
    • 五、解密得到的随机数,再用AES加密,作为密钥(此时的密钥只有客户端和服务端知道)。

    具体的参考链接:blog.csdn.net/sean_cd/art…

    1.7 Http与Https的区别

    1. HTTP 的URL 以http:// 开头,而HTTPS 的URL 以https:// 开头
    2. HTTP 是不安全的,而 HTTPS 是安全的
    3. HTTP 标准端口是80 ,而 HTTPS 的标准端口是443
    4. 在OSI 网络模型中,HTTP工作于应用层,而HTTPS 的安全传输机制工作在传输层
    5. HTTP 无法加密,而HTTPS 对传输的数据进行加密
    6. HTTP无需证书,而HTTPS 需要CA机构wosign的颁发的SSL证书

    1.8 常见的HTTP相应状态码

    • 200:请求被正常处理
    • 204:请求被受理但没有资源可以返回
    • 206:客户端只是请求资源的一部分,服务器只对请求的部分资源执行GET方法,相应报文中通过Content-Range指定范围的资源。
    • 301:永久性重定向
    • 302:临时重定向
    • 303:与302状态码有相似功能,只是它希望客户端在请求一个URI的时候,能通过GET方法重定向到另一个URI上
    • 304:发送附带条件的请求时,条件不满足时返回,与重定向无关
    • 307:临时重定向,与302类似,只是强制要求使用POST方法
    • 400:请求报文语法有误,服务器无法识别
    • 401:请求需要认证
    • 403:请求的对应资源禁止被访问
    • 404:服务器无法找到对应资源
    • 500:服务器内部错误
    • 503:服务器正忙

     

     1.9 Http1.0、Http1.1、Http2.0区别

    HTTP1.0和HTTP1.1最主要的区别就是:

    • HTTP1.1默认是持久化连接

      相对于持久化连接还有另外比较重要的改动:

      • HTTP 1.1增加host字段
      • HTTP 1.1中引入了Chunked transfer-coding,范围请求,实现断点续传(实际上就是利用HTTP消息头使用分块传输编码,将实体主体分块传输)
      • HTTP 1.1管线化(pipelining)理论,客户端可以同时发出多个HTTP请求,而不用一个个等待响应之后再请求
        • 注意:这个pipelining仅仅是限于理论场景下,大部分桌面浏览器仍然会选择默认关闭HTTP pipelining!
        • 所以现在使用HTTP1.1协议的应用,都是有可能会开多个TCP连接的!
    • 在HTTP1.0中,发送一次请求时,需要等待服务端响应了才可以继续发送请求。
    • 在HTTP1.1中,发送一次请求时,不需要等待服务端响应了就可以发送请求了,但是回送数据给客户端的时候,客户端还是需要按照响应的顺序来一一接收
    • 所以说,无论是HTTP1.0还是HTTP1.1提出了Pipelining理论,还是会出现阻塞的情况。从专业的名词上说这种情况,叫做线头阻塞(Head of line blocking)简称:HOLB

    HTTP2与HTTP1.1最重要的区别就是解决了线头阻塞的问题!其中最重要的改动是:多路复用 (Multiplexing)

    • 多路复用意味着线头阻塞将不在是一个问题,允许同时通过单一的 HTTP/2 连接发起多重的请求-响应消息,合并多个请求为一个的优化将不再适用。

    HTTP2还有一些比较重要的改动:

    • 流量控制
      • 针对传输中的进行控制(TCP默认的粒度是针对连接)
    • 流优先级(Stream Priority)它被用来告诉对端哪个流更重要
    总结;

    HTTP1.1新改动:

    • 持久连接
    • 请求管道化
    • 增加缓存处理(新的字段如cache-control)
    • 增加Host字段、支持断点传输等

    HTTP2新改动:

    • 二进制分帧
    • 多路复用
    • 头部压缩
    • 服务器推送

     1.10 HTTP优化方案

    • TCP复用:TCP连接复用是将多个客户端的HTTP请求复用到一个服务器端TCP连接上,而HTTP复用则是一个客户端的多个HTTP请求通过一个TCP连接进行处理。前者是负载均衡设备的独特功能;而后者是HTTP 1.1协议所支持的新功能,目前被大多数浏览器所支持。
    • 内容缓存:将经常用到的内容进行缓存起来,那么客户端就可以直接在内存中获取相应的数据了。
    • 压缩:将文本数据进行压缩,减少带宽
    • SSL加速(SSL Acceleration):使用SSL协议对HTTP协议进行加密,在通道内加密并加速
    • TCP缓冲:通过采用TCP缓冲技术,可以提高服务器端响应时间和处理效率,减少由于通信链路问题给服务器造成的连接负担。

     

    1.11 WebSocket

      WebSocket 是什么原理?为什么可以实现持久连接?

    2、Promise

    2.1 Promise示例

    复制代码
    //构造函数 Promise
    
    //unresolved 等待任务完成
    //resolved 任务完成并且没有任何问题 => 回调函数then
    //reject 任务完成,但是出现问题 => 回调函数catch
    
    let promise = new Promise((resolve,reject) => {
        // resolve();
        reject();
    });
    // console.log(promise);
    
    promise
        .then(() => console.log("成功,没有任何问题!"))
        .then(() => console.log("成功,可以无限调用then方法!"))
        .catch(() => console.log("出现了重大问题!"));
    复制代码

    测试网址:http://jsonplaceholder.typicode.com/

    2.2 Promise标准解读

    1. 只有一个then方法,没有catch,race,all等方法,甚至没有构造函数

      Promise标准中仅指定了Promise对象的then方法的行为,其它一切我们常见的方法/函数都并没有指定,包括catch,race,all等常用方法,甚至也没有指定该如何构造出一个Promise对象,另外then也没有一般实现中(Q, $q等)所支持的第三个参数,一般称onProgress

    2. then方法返回一个新的Promise

      Promise的then方法返回一个新的Promise,而不是返回this,此处在下文会有更多解释

      promise2 = promise1.then(alert)
      promise2 != promise1 // true
      
    3. 不同Promise的实现需要可以相互调用(interoperable)

    4. Promise的初始状态为pending,它可以由此状态转换为fulfilled(本文为了一致把此状态叫做resolved)或者rejected,一旦状态确定,就不可以再次转换为其它状态,状态确定的过程称为settle

    5. 更具体的标准见这里

    2.3 Promise/A+实现

    史上最易读懂的 Promise/A+ 完全实现

    3、fetch

    fetch 实现

    复制代码
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Document</title>
        <link href="https://cdn.bootcss.com/skeleton/2.0.4/skeleton.css" rel="stylesheet">
    </head>
    <body>
    
        <div class="container">
            <h1>Fetch Api sandbox</h1>
            <button id="button1">请求纯文本</button>
            <button id="button2">请求本地json数据</button>
            <button id="button3">请求网络接口</button>
            <br><br>
            <div id="output"></div>
        </div>
    
        <script src="fetch2.js"></script>
    </body>
    </html>
    复制代码

     JS文件

    document.getElementById('button1').addEventListener('click',getText);
    document.getElementById('button2').addEventListener('click',getJson);
    document.getElementById('button3').addEventListener('click',getExternal);
    
    //获取本地纯文本数据
    function getText(){
        fetch("test.txt")
            .then((res) => res.text())
            .then(data => {
                console.log(data);
                document.getElementById('output').innerHTML = data;
            })
            .catch(err => console.log(err));        
    }
    
    //获取本地json数据
    function getJson(){
        fetch("post.json")
            .then((res) => res.json())
            .then(data => {
                console.log(data);
    
                let output='';
                data.forEach((post) => {
                    output += `<li>${post.title}</li>`;
                });
                document.getElementById('output').innerHTML = output;
            })
            .catch(err => console.log(err)); 
    }
    
    //请求网络api
    function getExternal(){
        //http://api.github.com/users
        fetch("http://api.github.com/users")
            .then((res) => res.json())
            .then(data => {
                console.log(data);
    
                let output='';
                data.forEach((user) => {
                    output += `<li>${user.login}</li>`;
                });
                document.getElementById('output').innerHTML = output;
            })
            .catch(err => console.log(err));
    }

    4、Axios

      Axios 是一个基于 promise 的 HTTP 库,简单的讲就是可以发送get、post请求。

    4.1 实现Axios特性及应用场景

    1、可以在浏览器中发送 XMLHttpRequests
    2、可以在 node.js 发送 http 请求
    3、支持 Promise API
    4、拦截请求和响应
    5、转换请求数据和响应数据
    6、能够取消请求
    7、自动转换 JSON 数据
    8、客户端支持保护安全免受 XSRF 攻击

    在特性里面已经有提到,浏览器发送请求,或者Node.js发送请求都可以用到Axios。像Vue、React、Node等项目就可以使用Axios,如果你的项目里面用了Jquery,此时就不需要多此一举了,jquery里面本身就可以发送请求。

    4.2 Axios使用方法及示例

    安装模块

    npm install axios

    或者直接引入

    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>

    引入模块后可以直接使用

    示例(一)

    // GET
    axios.get('/user', {
      params: {
        ID: 12345
      }
    })
    .then(function (response) {
      console.log(response);
    })
    .catch(function (error) {
      console.log(error);
    });
    
    // POST
    axios.post('/user', {
      name: 'Javan',
      website: 'www.javanx.cn'
    })
    .then(function (response) {
      console.log(response);
    })
    .catch(function (error) {
      console.log(error);
    });

    上面的参数是可选的

    如果你想并发多个请求,可以看下方代码

    function getUserAccount() {
      return axios.get('/user/12345');
    }
    
    function getUserPermissions() {
      return axios.get('/user/12345/permissions');
    }
    
    axios.all([getUserAccount(), getUserPermissions()])
      .then(axios.spread(function (acct, perms) {
        // 两个请求都执行完成才会执行
      }));

    示例(二)

    除了上面的方式外,你可以通过向 axios 传递相关配置来创建请求,如:

    // POST
    axios({
      method: 'post',
      url: '/user/12345',
      data: {
        firstName: 'Fred',
        lastName: 'Flintstone'
      }
    });

     

    语法

    axios(url[, config])

    config

    {
      // `url` 是用于请求的服务器 URL
      url: '/user',
    
      // `method` 是创建请求时使用的方法
      method: 'get', // 默认是 get
    
      // `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
      // 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL
      baseURL: 'https://some-domain.com/api/',
    
      // `transformRequest` 允许在向服务器发送前,修改请求数据
      // 只能用在 'PUT', 'POST' 和 'PATCH' 这几个请求方法
      // 后面数组中的函数必须返回一个字符串,或 ArrayBuffer,或 Stream
      transformRequest: [function (data) {
        // 对 data 进行任意转换处理
    
        return data;
      }],
    
      // `transformResponse` 在传递给 then/catch 前,允许修改响应数据
      transformResponse: [function (data) {
        // 对 data 进行任意转换处理
    
        return data;
      }],
    
      // `headers` 是即将被发送的自定义请求头
      headers: {'X-Requested-With': 'XMLHttpRequest'},
    
      // `params` 是即将与请求一起发送的 URL 参数
      // 必须是一个无格式对象(plain object)或 URLSearchParams 对象
      params: {
        ID: 12345
      },
    
      // `paramsSerializer` 是一个负责 `params` 序列化的函数
      // (e.g. https://www.npmjs.com/package/qs, http://api.jquery.com/jquery.param/)
      paramsSerializer: function(params) {
        return Qs.stringify(params, {arrayFormat: 'brackets'})
      },
    
      // `data` 是作为请求主体被发送的数据
      // 只适用于这些请求方法 'PUT', 'POST', 和 'PATCH'
      // 在没有设置 `transformRequest` 时,必须是以下类型之一:
      // - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
      // - 浏览器专属:FormData, File, Blob
      // - Node 专属: Stream
      data: {
        firstName: 'Fred'
      },
    
      // `timeout` 指定请求超时的毫秒数(0 表示无超时时间)
      // 如果请求话费了超过 `timeout` 的时间,请求将被中断
      timeout: 1000,
    
      // `withCredentials` 表示跨域请求时是否需要使用凭证
      withCredentials: false, // 默认的
    
      // `adapter` 允许自定义处理请求,以使测试更轻松
      // 返回一个 promise 并应用一个有效的响应 (查阅 [response docs](#response-api)).
      adapter: function (config) {
        /* ... */
      },
    
      // `auth` 表示应该使用 HTTP 基础验证,并提供凭据
      // 这将设置一个 `Authorization` 头,覆写掉现有的任意使用 `headers` 设置的自定义 `Authorization`头
      auth: {
        username: 'janedoe',
        password: 's00pers3cret'
      },
    
      // `responseType` 表示服务器响应的数据类型,可以是 'arraybuffer', 'blob', 'document', 'json', 'text', 'stream'
      responseType: 'json', // 默认的
    
      // `xsrfCookieName` 是用作 xsrf token 的值的cookie的名称
      xsrfCookieName: 'XSRF-TOKEN', // default
    
      // `xsrfHeaderName` 是承载 xsrf token 的值的 HTTP 头的名称
      xsrfHeaderName: 'X-XSRF-TOKEN', // 默认的
    
      // `onUploadProgress` 允许为上传处理进度事件
      onUploadProgress: function (progressEvent) {
        // 对原生进度事件的处理
      },
    
      // `onDownloadProgress` 允许为下载处理进度事件
      onDownloadProgress: function (progressEvent) {
        // 对原生进度事件的处理
      },
    
      // `maxContentLength` 定义允许的响应内容的最大尺寸
      maxContentLength: 2000,
    
      // `validateStatus` 定义对于给定的HTTP 响应状态码是 resolve 或 reject  promise 。如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),promise 将被 resolve; 否则,promise 将被 rejecte
      validateStatus: function (status) {
        return status >= 200 && status < 300; // 默认的
      },
    
      // `maxRedirects` 定义在 node.js 中 follow 的最大重定向数目
      // 如果设置为0,将不会 follow 任何重定向
      maxRedirects: 5, // 默认的
    
      // `httpAgent` 和 `httpsAgent` 分别在 node.js 中用于定义在执行 http 和 https 时使用的自定义代理。允许像这样配置选项:
      // `keepAlive` 默认没有启用
      httpAgent: new http.Agent({ keepAlive: true }),
      httpsAgent: new https.Agent({ keepAlive: true }),
    
      // 'proxy' 定义代理服务器的主机名称和端口
      // `auth` 表示 HTTP 基础验证应当用于连接代理,并提供凭据
      // 这将会设置一个 `Proxy-Authorization` 头,覆写掉已有的通过使用 `header` 设置的自定义 `Proxy-Authorization` 头。
      proxy: {
        host: '127.0.0.1',
        port: 9000,
        auth: : {
          username: 'mikeymike',
          password: 'rapunz3l'
        }
      },
      // `cancelToken` 指定用于取消请求的 cancel token
      // (查看后面的 Cancellation 这节了解更多)
      cancelToken: new CancelToken(function (cancel) {
      })
    }

    示例(三)

    我们还可以使用自定义配置新建一个 axios 实例,并且可以在请求或响应被 then 或 catch 处理前拦截它们。

    // 如文件名叫http.js
    import axios from 'axios'
    
    // 创建实例时设置配置的默认值
    var instance = axios.create({
      baseURL: 'https://some-domain.com/api/',
      timeout: 1000,
      headers: {'X-Custom-Header': 'foobar'}
    });
    
    // 添加请求拦截器
    instance.interceptors.request.use(function (config) {
      // 在发送请求之前做些什么
      /**
        1、比如添加token之类的请求头信息
        2、添加每次请求loading等
      */
      return config;
    }, function (error) {
      // 对请求错误做些什么
      return Promise.reject(error);
    });
    
    // 添加响应拦截器
    instance.interceptors.response.use(function (response) {
      // 对响应数据做点什么
      /**
        1、集中处理响应数据(如错误码处理)
      */
      return response;
    }, function (error) {
      // 对响应错误做点什么
      return Promise.reject(error);
    });
    
    export default instance

    如何使用上面的http.js???

    import http from 'xxx/http'
    
    http({
      method: 'POST',
      url: '/user',
      data: {
        name: 'Javan',
        website: 'www.javanx.cn'
      }
    }).then((response) => {
      // 200响应
    }, (err) => {
      // 500响应
    })
    

    示例(四)

    如何取消接口???
    场景:一个搜索框,每次输入字符都会调用接口,这时候没有办法来知道那个接口数据放回是最后一次的,只能取消之前发起的相同接口,所以就有了取消接口。

    var CancelToken = axios.CancelToken;
    var source = CancelToken.source();
    
    axios.get('/user/12345', {
      cancelToken: source.token
    }).catch(function(thrown) {
      if (axios.isCancel(thrown)) {
        console.log('Request canceled', thrown.message);
      } else {
        // 处理错误
      }
    });
    
    // 取消请求(message 参数是可选的)
    source.cancel('Operation canceled by the user.');
  • 相关阅读:
    MFC 控件RadioButton和CheckBox区别
    python的传递实参
    python的返回值
    Machine Learning的定义
    pythion的定义函数和传递实参
    python的用户输入和while循环
    python的字典
    python的if语句
    python的元组及其书写规矩
    python中操作列表
  • 原文地址:https://www.cnblogs.com/kyrie1/p/13304259.html
Copyright © 2020-2023  润新知