• ajax和comet


    一,XMLHttpRequest对象
      IE5是最早引入XHR对象的浏览器,XHR对象是通过MSXML库中的一个ActiveX对象实现的
      使用MSXML库中的XHR对象,编写一个函数如下

      function ceateXHR(){
        if(typeof argument.callee.activeXString != "String"){
          var versions = ["MSXML2.XMLHttp.6.0","MSXML2.XMLHttp.3.0","MSXML2.XMLHttp"],i,len;
          for(i=0;len=versions.length;i<len;i++){
            try{
              new ActiveXObject(versions[i]);
              arguments.callee.activeXString = versions[i];
              break;
            }catch(ex){
              //跳过
            }
          }
        }
        return new ActiveXObject(argument.callee.activeXString);
      }

      IE7+,Firefox,Opera,CHrome和safari中支持原生XHR对象,
      var xhr = new XMLHttpRequest();
      为了兼容以上所有,使用下列函数

      function createXHR(){
        if(typeof XMLHttpRequest != "undefined"){
          return new XMLHttpRequest();
        }else if(typeof ActiveXobject != "undefined"){
          if(typeof argument.callee.activeXString != "String"){
            var versions = ["MSXML2.XMLHttp.6.0","MSXML2.XMLHttp.3.0","MSXML2.XMLHttp"],i.len;
            for(i=0,len=versions.length;i<len;i++){
              try{
                new ActiveXObject(versions[i]);
                argument.callee.activeXString = versions[i];
                break;
              }catch(ex){
                //跳过
              }
            }  
          }
          return new ActiveXObject(argument.callee.activeXString);
        }else{
          throw new Error("No XHR object available");
        }
      }

      创建对象,var xhr = createXHR();
    1,XHR的用法
      open(请求的类型(get或post),请求的URL,是否异步发送的布尔值),
      要点:URL相对于执行代码的当前页面(可以使用绝对路径),open方法不会真的发送请求只是启动一个请求以备发送
      send(作为请求主体发送的数据),发送特定的请求,可以传人null,调用函数后请求会被派送到服务器
      如果请求时同步的,js代码会等到服务器响应之后执行,响应的数据会自动填充XHR对象的属性
        responseText:作为响应主体被返回的文本
        responseXML:如果响应的内容类型是text/xml,application/xml,这个属性会保存包含响应数据的XML DOM文档
        status:响应的http状态
        statusText:http状态的说明
      响应后,首先检查status属性,http状态200表示成功,此时responseText属性的内容就绪,内容类型正确的同时,responseXML也可以访问了
      状态304表示请求的资源没有被修改,可以直接使用浏览器中缓存的版本,
      xhr.open("get","example.text",false);
      xhr.send(null);
      if((xhr.status >= 200 && xhr.status <= 300) || xhr.status == 304){
        alert(xhr.responseText);
      }else{
        alert("Request was unsuccessful:" + xhr.status);
      }
      一般情况下使用异步请求,此时js会继续执行不必等待服务器的响应,此时检测XHR的readyState属性,属性值如下
      0:未初始化,尚未调用open方法
      1:启动,调用了open方法,未调用send方法
      2,发送,调用了send方法,未接受到响应
      3,接收,接收到部分响应数据
      4,完成,接收到全部的响应数据,而且在客户端使用了
      只要readyState属性的值发生变化就会触发readystatechange事件,可以利用这个事件检测每次状态后的readyState的值
      必须在open()之前指定onreadystatechange事件处理程序才能保证跨浏览器兼容

      var xhr = createXHR();
      xhr.onreadystatechange = function(){
        if(xhr.readyState == 4){
          if((xhr.status >= 200 && xhr.status <= 300) || xhr.status == 304{
            alert(xhr.responseText);
          }else{
            alert("Request was unsuccessful:" + xhr.status);
          }
        }
      };
      xhr.open("get","example",true);
      xhr.send(null);

      响应之前调用xhr.abort(),可以取消异步请求,由于内存原因,不建议重用XHR对象
    2,HTTP头部信息
      每个http请求和响应都会带有相应的头部信息,XHR对象也提供了操作这两种头部信息的方式
      发送XHR请求的同时,会发送下列头部信息
        Accept:浏览器能够处理的内容类型
        Accept-Charset:浏览器能够显示的字符集
        Accept-Encoding:浏览器能够处理的压缩编码
        Accept-Language:浏览器当前设置的语言
        Connection:浏览器与服务器之间连接的类型
        Cookie:当前页面设置的任何cookie
        Host:发送请求的页面所在的域
        Referer:发送请求的页面的URL
        User-Agent:浏览器的用户代理字符串
        setRequestHeader(头部字段的名称,头部字段的值)方法可以自定义设置头部信息,在open和send方法之前调用此方法

        var xhr = createXHR();
        xhr.onreadystatechange = function(){
          if(xhr.readyState == 4){
            if((xhr.status >= 200 && xhr.status <= 300) || xhr.status == 304){
              alert(xhr.reponseText);
            }else{
              alert("Request was unsuccessful :" + xhr.status);
            }
          }
        };
        xhr.open("get","example.php",true);
        xhr.setRequestHeader("MyHeader","MyValue");
        xhr.send(null);

      调用XHR对象的getResponseHeader()方法,传人头部字段名称,可以返回相应的响应头部信息
      getAllResponseHeaders()方法,取得一个包含所有头部信息的长字符串
      var myheader = xhr.getResponseHeader("myHeader");
      var allheader = xhr.getAllResponseHeader();
      服务器端,可以利用头部信息向浏览器发送额外的,结构化的数据,没有自定义的情况下,getAllResponseHeader()方法会返回如下
        Date:sun,14 Nov 2004 18:04:03 GMT
        Server: Apache/1.3.29(Unix)
        Vary:Accept
        X-Powered-By:PHP/4.3.8
        Connection:close
        Content-Type:text/html;charset=iso-8859-1
    3,GET请求
      用于向服务器查询某些信息,可以将查询字符串追加到URL末尾,
      查询字符串,每个参数的名称和值使用encodeURIComponent()进行编码,而且所有名值对使用&分隔
      xhr.open("get","example.php?name1=value&name2=value2",true);
      使用下面函数向现有的URL末尾添加查询字符串参数
      function addURLParam(url,name,value){
        url += (url.indexof("?") == -1 ? "?" : &);
        url += encodeURIComponent(name) + "=" + encodeURIComponent(value);
        return url;
      }
      使用下面函数来构建请求URL示例
      var url = "example.php";
      url = addURLParam(url,"name","Nicholas");
      xhr.open("get",url,true);
    4,POST请求
      用于向服务器发送应该被保存的数据,
      post请求将数据作为请求的主体提交,请求的主体可以包含非常多的数据,格式不限
      发送post请求,要向send()方法中传人某些数据,可以是XML DOM文档,可以使字符串
      XHR模仿表单提交,首先将Content-type头部信息设置为application/x-www-form-urlencoded,然后以适当的格式创建一个字符串,可以使用serialize()     函数来创建字符串
      function submitData(){
        var xhr = createXHR();
        xhr.onreadystatechange = function(){
          if(xhr.readystate == 4){
            if((xhr.status >= 200 && xhr.status <= 300) || xhr.status == 304){
              alert(xhr.responseText);
            }else{
              alert("Request was unsuccessful:" + xhr.status);
            }
          }
        };
      xhr.open("post","postexample.php",true);
      xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");
      var form = document.getElementById("user-info");
      xhr.send(serialize(form));
      }
      php文件通过$_POST取得提交的数据
        <?php
          header("Content-type:text/plain");
          echo <<<EOF
          Name:{$_POST["user-name"]}
          Email:{$_POST['user-email']}
          EOF;
        ?>
      如果不设置Content-type头部信息,那么发送给服务器的数据不会出现在$_POST超级全局变量中,
      要访问同样的数据,使用$HTTP_RAW_POST_DATA
    二,XMLHttpRequest2级
    1,FormData
      为了实现表单数据的序列化,FormData类型为序列化表单以及创建与表单格式相同的数据
      var data = new FormData();
      data.append("name","Nicholas");
      append()方法接收两个参数,键值对,
      通过向FormData构造函数中传人表单元素,可以用表单元素的数据预先向其中填入键值对
      var data = new FormData(document.forms[0]);
      创建了FormData实例,直接传给XHR的send()方法

      var xhr = createXHR();
      xhr.onreadystatechange = function(){
        if(xhr.readyState == 4){
          if((xhr.status >= 200 && xhr.status <= 300) || xhr.status == 304){
            alert(xhr.responseText);
          }else{
            alert("Request was unsuccessful :" + xhr.status);
          }    
        }
      };
      xhr.open("post","postexample.php",true);
      var form = document.getElementById("user-info");
      xhr.send(new FormData(form));

      XHR对象能够识别传人的数据类型是FormData的实例,并配置适当的头部信息
    2,超时设定
      XHR对象的timeout属性,表示请求在等待响应多少时间之后就终止,给timeout属性设置一个值,在超过了这个值后,
      就会调用ontimeout事件处理程序

      var xhr = createXHR();
      xhr.onreadystatechange = function(){
        if(xhr.readyState == 4){
          try{
            if((xhr.status >= 200 && xhr.status <= 300) || xhr.status == 304){
              alert(xhr.responseText);
            }else{
              alert("Request was unsuccessful:" + xhr.status);
            }
          }catch(ex){
            //假设由ontimeout事件处理程序处理
          }
        }
      };
      xhr.open("get","timeout.php",true);
      xhr.timoeout = 1000;
      xhr.ontimeout = function(){
        alert("Request did not return in a second.");
      };
      xhr.send(null);

    3,overrideMimeType()方法
      Firefox最早引入了overrideMimeType()方法,用于重写XHR响应的MIME类型,可以把响应当做XML而非纯文本来处理
      var xhr = createXHR();
      xhr.open("get","text.php",true);
      xhr.overrideMimeType("text/XML");
      xhr.send(null);
    三,进度事件
      Progress Events规范定义了与客户端服务器通信有关的事件
      loadstart:在接收到响应数据的第一个字节时触发
      progress:在接收响应期间持续不断的触发
      error:在请求发生错误时触发
      abort:因为调用abort()方法而终止连接时触发
      load:在接收到完整的响应数据时触发
      loadend:在通信完成,或者触发了error,abort,load事件后触发(尚未被任何浏览器支持)
    1,load事件
      firefox引入了load事件,用于替代readystatechange事件
      响应接收完毕后触发load事件,没有必要去检测readyState属性了,
      onload会接收到event对象,target属性就指向XHR对象,可以访问到XHR对象的所有方法和属性
      对于不支持的浏览器

      var xhr = createXHR();
      xhr.onload = function(){
        if((xhr.status >=200 && xhr.status <= 300) || xhr.status == 304){
          alert(xhr.responseText);
        }else{
          alert("Request was unsuccessful:" + xhr.status);
        }
      };
      xhr.open("get","alertEvents.php",true);
      xhr.send(null);

    2,progress事件
    事件在浏览器接收数据期间周期性触发,
    onprogress事件处理程序会接收到一个evnet对象,其target属性是XHR对象
    包含额外的三个属性:lengthComputable(进度信息是否可用的布尔值),position(已经接收的字符数),totalSize(根据Content-Length响应头部确定的预期字节数)

    var xhr = createXHR();
    xhr.onload = function(){
      if((xhr.status >= 200 && xhr.status <= 300) || xhr.status == 304){
        alert(xhr.responsText);
      }else{
        alert("Request was unsuccessful" + xhr.status);
      }
    };
    xhr.onprogress = function(event){
      var divStatus = document.getElementById("status");
      if(event.lengthComputable){
        divStatus.innerHTML = "Recevied " + event.position + "Of" + event.totalSize + "bytes";
      }
    };
    xhr.open("get","alertEvent.php",true);
    xhr.send(null);

    四,跨源资源共享(CORS)
      思想是使用自定义的http头部,让浏览器与服务器进行沟通,从而决定请求或响应的成功与否
      请求的origin头部,包含请求页面的源信息(协议,域名,端口),
      如果服务器认为是可接受的,在Origin-Control-Allow-Origin头部中回发相同的源信息,如果是公共资源,可以回发*
    1,IE对CORS的实现
      IE8引入XDR类型,类似XHR对象,但能实现安全可靠的跨域通信,区别如下
      cookie不会随请求发送,也不会随响应返回
      只能设置请求头部信息中的Content-type字段
      不能访问响应头部信息
      只支持GET和POST请求
      XDR使用方法,创建一个XDomainRequest的实例,调用open()方法,再调用send()方法,
      open()方法只接收两个参数,请求类型的UR,XDR请求都是异步的,
      请求返回之后会触发load事件,响应的数据保存在responseText属性中
      var xdr = new XDomainRequest();
      xdr.onload = function(){
        alert(xdr.responseText);
      };
      xdr.open("get","http://www.somewhere-else.com/page/");
      xdr.send(null);
      接收到响应后,只能访问响应的原始文本,没有办法确定响应的状态代码,只要响应有效就会会触发load事件,
      失败就会触发error事件,遗憾的是,除了错误本身外,没有其他信息可用,因此唯一能够确定的就是请求未成功,
      检测错误可以指定一个onerror事件处理程序
      var xdr = new XDomainRequest();
      xdr.onload = function(){alert(xdr.responseText)};
      xdr.onerror = function(){alert("An error occurred.")};
      xdr.open("get","http://www.somewhere-else.com/page/");
      xdr.send(null);
      返回请求前调用abrot()方法会终止请求
      同样支持timeout属性和ontimeout事件
      var xdr = new XDomainRequest();
      xdr.onload = function(){alert(xdr.responseText);};
      xdr.onerror = function(){alert("An Error Occurred");};
      xdr.timeout = 1000;
      xdr.ontimeout = function(){alert("Request took too long.");};
      xdr.open("get","http://www.somewhere-else.com/page/");
      xdr.send(null);
      为了支持POST请求,XDR对象提供了contenType属性,用来表示发送数据的格式
      var xdr = new XDomainRequest();
      xdr.onload = function(){};
      xdr.onerror = function(){};
      xdr.open("post","http://www.somewhere-else.com/page/");
      xdr.contentType = "application/x-www-form-urlencoded";
      xdr.send("name1=value1&name2=value2");
    2,其他浏览器对CORS的实现
      通过XMLHttpRequest对象实现对CORS的原生支持,请求另一个域中的资源,使用标准的XHR对象并在open方法中传人绝对的URL

      var xhr = createXHR();
      xhr.onreadystatechange = function(){
        if(xhr.readyState == 4){
          if((xhr.status >= 200 && xhr.status <= 300) || xhr.status == 304){
            alert("xhr.responseText");
          }else{
            alert("Request was unsuccessful:" + xhr.status);
          }
        }
      };
      xhr.open("get","htt://www.somewhere-else.com/page/",true);
      xhr.send(null);

      跨域XHR对象有限制,
      不能使用setRequestHeader()设置自定义头部
      不能发送和接受cookie
      调用getAllResponseHeader()方法总会返回空字符串
    3,Preflighted Request
      透明服务器验证机制支持开发人员使用自定义的头部,
      使用下列高级选项来发送请求时,就会向服务器发送一个Preflight请求,使用OPTIONS方法,发送下列头部
        Origin:与简单的请求相同
        Access-Control-Request-Method:请求自身使用的方法
        Access-Control-Request-Header:自定义的头部信息,多个头部用逗号分隔
        Origin:http://www.nczonline.net
        Access-Control-Request-Method:POST
        Access-Control-Request-Header:NCZ
      发送这个请求后,服务器可以决定是否允许这种类型的请求,服务器通过在响应中发送如下头部与浏览器进行沟通
        Access-Control-Allow-Origin:与简单请求相同
        Access-Control-Allow-Method:允许的方法,多个方法以逗号分隔
        Access-Control-Allow-Header:允许的头部,多个头部以逗号分隔
        Access-Control-Max-Age:应该将这个Prefight请求缓存多长时间(秒)
        Access-Control-Allow-Origin:http://www.nczonline.net
        Access-Control-Allow-Method:POST,GET
        Access-COntrol-Allow-Header:NZC
        Access-Control-Max-Age:1728000
      第一次发送这种请求时会多一个http请求
    4,带凭据的请求
      默认情况下跨源请求不会提供凭据(cookie,HTTP认证,客户端SSL证明)
      通过将withCredentials属性设置为true,可以指定某个请求应该发送凭据
      如果服务器端接收带凭据的请求,会使用Access-Control-Allow-Credentials:true来响应
      如果服务器的响应中没有这个头部,浏览器就不会把响应交给javascript,responseText中将是空字符串status的值为0,调用onerror事件处理程序
    5,跨浏览器的CORS

      function createCORSRequest(method,url){
        var xhr = new XMLHttpRequest();
        if("withCredentials" in xhr){
          xhr.open(method,url,true);
        }else if(typeof XDomainRequest != "undefined"){
          vxhr = new XDomainRequest();
          xhr.open(method,url);
        }else{
          xhr = null;
        }
        return xhr;
      }
      var request = createCORSRequest("get","http://www.somewhere-else.com/page/");
      if(request){
        request.onload = function(){
        //
      };
      request.send();
      }

      firefox,safari,chrome中的XMLHttpRequest对象和IE中XDomainRequest对象类似,共有属性方法如下
      abort(),用于停止正在进行的请求
      onerror(),用于替代onreadystatechange()检测错误
      onload(),用于替代onreadystatechange()检测成功
      responseText(),用于取得响应内容
      send(),用于发送请求
    五,其他跨域技术
    1,图像Ping
      使用img标签,动态的创建图像,使用onload和onerror事件处理程序来确定是否接收到了响应
      图形Ping是与服务器进行简单,单向,的跨域通信的一种方式
      请求的数据通过查询字符串形式发送的,响应的是任意内容,通常是像素图或204响应
      var img = new Image();
      img.onload = img.onerror = function(){};
      img.src = "http://www.example.com/test?name=Nicholas";
      请求从设置src属性那一刻开始,请求中发送了name参数
      图像Ping用于跟踪用户点击页面或动态广告曝光次数,
      两个缺点,只能发送get请求,无法访问服务器的响应文本,只能单向通信
    2,JSONP
      被包含在函数调用中的JSON,
      callback({"name":"Nicholas"});
      JSONP包含两部分,回调函数和数据,回调函数是当响应到来时应该在页面中调用的函数,函数名一般在请求中指定
      数据就是传人回调函数中的JSON数据,例如:http://freegeoip.net/json/?callback=handleResponse,指定的函数名为handleResponse()
      JSONP是通过动态script元素使用的,使用时可以为src属性指定一个跨域URL
      function handleResponse(response){
        alert("you're at IP address" + resopnse.ip + ",which is in" + response.city + "," + response.region_name);
      }
      var script = document.createElement(script);
      script.src="http://freegeoip.net/json/?callback=handleResponse";
      document.body.insertBefore(script,document.body.firstChild);
      可以直接访问响应文本,支持在浏览器与服务器之间双向通信,但不能保证安全,确定JSONP请求是否失败不是很容易
      html5给script提供onerror事件处理程序,尚未得到任何任何浏览器支持,为此常通过设置计时器检测指定时间内是否收到了响应
    3,Comet
      Ajax是一种从页面向服务器请求数据的技术,Comet是一种服务器向页面推送数据的技术
      Comet能够让信息近乎实时的被推送到页面上,非常适合处理体育比赛的分数和股票报价
      两种实现Comet的方式:长轮询和流
        长轮询:页面发起一个到服务器的请求,然后服务器一直保持连接打开,直到有数据可发送,
        发送完数据之后,浏览器关闭连接,随即又发起一个到服务器的新请求,这一过程在页面打开期间持续不断
        长轮询是服务器等待发送响应后发送数据
      HTTP流:浏览器向服务器发送一个请求,服务器保持连接打开,然后周期性向浏览器发送数据
      DOM浏览器中,通过监听readystatechange事件及检测readyState的值是否为3,利用XHR对象实现流

      function createStreamingClient(url,progress,finished){
        var xhr = new XMLHttpRequest(),received = 0;
        xhr.open("get",url,true);
        xhr.onreadystatechange = function(){
          var result;
          if(xhr.readyState == 3){
            //只取得最新数据并整理计数器
            result = xhr.responseText.substring(received);
            received += result.length;
            //调用progress回调函数
            progress(result);
          }else if(xhr.readyState == 4){
            finished(xhr.responseText);
          }
        };
        xhr.send(null);
        return xhr;
      }
      var client = createStreamingClient("streaming.php",function(data){
        alert("Received:" + data);},function(data){
        alert("Done!");
      });

    4,服务器发送事件
    1)SSE API
      创建到服务器的单向连接,服务器通过这个连接可以发送任意数量的数据,
      服务器响应的MIME类型必须是text/event-stream,支持长短轮询,和http流
      首先创建一个新的EventSource对象,并传进一个入口点:
      var source = new EventSource("Myevent.php");url要与创建对象的页面同域
      EventSource实例有个readyState属性,0表示正连接到服务器,1表示打开了连接,2表示关闭了连接
      还有open事件,在建立连接时触发,message事件,在服务器接收到新事件时触发,error事件,在无法建立连接时触发
      服务器发回的数据以字符串形式保存在event.data中
      默认情况下,EventSource对象会保持与服务器的活动连接,连接断开会重新连接
      强制断开使用close()方法,source.close()
    2)事件流
      服务器事件会通过一个持久的http响应发送,这个响应的MIME类型为text/event-stream
      响应的格式为纯文本,最简单的情况是每个数据项前有data
      对于多个连续的以data开头的数据行,将作为多段数据解析,每个值之间有一个换行分隔符
      只有包含data:的数据行后有空行,才会触发messge事件,
      通过id:前缀可以给特定的事件指定一个关联的ID,位于data:行前面和后面皆可
      设置了ID,EventSource对象会跟踪上一次触发的事件,连接断开时,会向服务器发送一个包含名为last-Event-ID的特殊HTTP头部的请求,以便服务器知道     下一次触发的事件
    5,Web Socket
      在一个单独的持久的连接上提供全双工,双向通信
      一个http请求发送到服务器已发起连接,取得服务器响应后,建立的连接使用http升级从http协议交换为web Socket协议
      ws://;wss://
      1)Web Socket API
      首先实例一个WebSocket对象并传人要连接的URL
      var socket = new WebSocket("ws://www.example.com/sever.php");
      必须给websocket构造函数传人绝对的URL,
      浏览器在实例化websocket对象后,马上尝试创建连接,readyState属性
        WebSocket.OPENING(0):正在建立连接
        WebSocket.OPEN(1):已经建立连接
        WebSocket.CLOSING(2):正在关闭连接
        WebSocket.CLOSE(3):已经关闭连接
      没有readystatechange事件,readyState永远是从0开始的
      关闭WebSocket,使用close方法
      2)发送和接收数据
      send()方法
      var socke = new WebSocket("ws://www.example.com/server.php");
      socket.send("hello world");
      websocket只能发送纯文本数据,对于复杂的结构,将数据序列化为JSON字符串,使用stringify()方法
      当服务器向客户端发来消息时,WebSocket对象会触发message事件,这个message事件与其他传递消息的协议类似,
      也是把返回的数据保存在event.data属性中
      socket.onmessage = function(event){
        var data = event.data;
        //处理数据
      }
      3)其他事件
        open:在成功建立连接时触发
        error:在发生错误时触发,连接不能持续
        close:在连接关闭时触发
      WebSocket对象不支持DOM2级事件监听器,必须使用DOM0级语法分别定义每个事件处理程序
      var socket = new WebSocket("ws://www.example.com/server.php");
      socket.onopen = function(){};
      socket.onerror = function(){};
      socket.onclose = function(){};
      只有close事件对象包含额外的三个属性
        wasClean:布尔值,表示连接是否已经明确的关闭
        code:服务器返回的数据状态码
        reason:字符串包含服务器发回的消息
    6,SSE与Web Socket
      SSE,支持单向的从服务器读取数据
      WebSocket,支持双向通信
      SEE和XHR组合也可以实现双向通信
    六,安全
      确保XHR访问的URl安全,通行的做法是验证发送请求者是否有权限访问相应的资源
      要求以SSL连接来访问可以通过XHR请求的资源
      要求每一次请求都附带经过相应算法计算得到的验证码
      要求发送POST而不是GET请求(对CSRF攻击没有作用)
      检查来源URL以确保是否可信,(对CSRF攻击没有作用,来源记录很容易伪造)
      基于cookie信息进行验证(对CSRF攻击没有作用,容易被伪造)

  • 相关阅读:
    nginx缓存实战
    单机编排之Docker Compose
    NGINX镜像的制作
    k8s的kube-proxy
    k8s应用环境
    k8s ansible部署部署文档
    部署docker镜像仓库及高可用
    openstack高可用集群20-openstack计算节点宕机迁移方案
    openstack 租户控制台修改虚拟机账户密码
    如何修改openstack虚拟机密码
  • 原文地址:https://www.cnblogs.com/b0xiaoli/p/3659658.html
Copyright © 2020-2023  润新知