HTTP-进阶知识

2020/7/11 HTTP
// 跨域
1. 跨域只是浏览器拦截掉了,实际发了请求也返回了数据
2. 同源策略: 协议、域名、端口必须一致
3. Cookie、LocalStorage 和 IndexDB 无法读取
4. DOMJS 对象无法获取
5. Ajax请求发送不出去
6. 允许跨域访问的内容
  1. 跨域写操作一般被允许,如:a标签,重定向,表单提交
  2. 跨域资源嵌入一般被允许,如 script,link,img,video,object,embed,iframe 标签
  3. 不同源窗口直接的访问有限制,只能访问以下属性
    1. frames,只读
    2. length,只读
    3. closed,只读
    4. opener,只读
    5. parent,只读
    6. self,只读
    7. top,只读
    8. window,只读
    9. location,读写
      1. replace()
      2. href,只写
    10. blur()
    11. close()
    12. focus()
    13. postMessage()
----------------------------------------------------------------------------------------------
// 跨域解决方案
1. 通过jsonp跨域
2. 跨域资源共享(CORS3. document.domain + iframe跨域
4. location.hash + iframe
5. window.name + iframe跨域
6. postMessage跨域
7. nginx代理跨域
8. nodejs中间件代理跨域
9. WebSocket协议跨域
----------------------------------------------------------------------------------------------
// JSONP,在src中增加查询参数callback=函数名
1. <script>可以绕过同源限制
2. 服务器可以任意动态拼接数据返回
3. <script>就可以获得跨域的数据,只要服务端愿意返回一个函数执行,函数名在前端中定义
4. 通过函数参数获得跨域内容
5. link、img等也可以绕过同源限制
6. JSONP只支持GET请求
7. jQuery 的 $.ajax 如果要请求 jsonp,dataType 需要传 'jsonp',因为正常请求使用的是 XMLHTTPRequest
    而 jsonp,是用 script 标签请求
8. 示例:
    前端
      var script = document.createElement('script');
      script.type = 'text/javascript';
      // 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
      script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback';
      document.head.appendChild(script);
      // 回调执行函数
      function handleCallback(res) {
          alert(JSON.stringify(res));
      }
    后端
      handleCallback({"status": true, "user": "admin"})
----------------------------------------------------------------------------------------------
// CORS(一)
1. 响应头
  1. Access-Control-Allow-Origin,允许访问该资源的外域 URL
  2. Access-Control-Max-Age,预检请求的结果能被缓存多久
  3. Access-Control-Expose-Headers,服务器把允许浏览器访问的响应头放入白名单
  4. Access-control-Allow-Methods,服务器支持的所有跨域请求的方法
  5. Access-control-Allow-Headers,请求头所有支持的首部字段列表
  5. Access-control-Allow-Credentials,是否可以使用 credentials
2. 请求头
  1. Origin,预检请求或实际请求的网址,不包含路径信息,只有服务器名称
  2. Access-Control-Request-Method,将实际使用的 HTTP 方法告诉服务器,用于预检请求
  3. Access-Control-Request-Headers,将实际请求携带的 header 字段告诉服务器,用于预检请求
3. 简单请求(以下条件如有一个不满足,则是复杂请求)
  1. 条件一(使用下列方法之一)
    1. GET
    2. HEAD
    3. POST
  2. 条件二(只允许 Header 中有以下字段)
    1. Accept
    2. Accept-Language
    3. Content-Language
    4. Content-Type
      1. text/plain
      2. multipart/form-data
      3. application/x-www-form-urlencoded
    5. range
  3. 条件三(请求中 XHR 对象没有注册任何事件监听器,可以使用 XHR.upload 属性访问)
  4. 条件四(请求中没有使用 ReadableStream 对象)
4. 复杂请求
  1. 会有一个预检请求
  2. 需要预检的请求,必须使用 OPTIONS 方法发起一个预检请求到服务器,查看服务器是否允许发生实际请求
5. 注意,如果服务器设置了允许携带身份凭证即 Access-control-Allow-Credentials 为 true1. Access-Control-Allow-Origin 不能设置为 '*',应该设置为特定的域
  2. Access-Control-Expose-Headers 不能设置为 '*',应该设置为首部名称的列表
  3. Access-control-Allow-Methods 不能设置为 '*',应该设置为特定请求方法名称的列表
  4. 另外响应首部中如果携带了 Set-Cookie 字段,尝试去对 Cookie 进行修改,如果操作失败就会抛出异常
----------------------------------------------------------------------------------------------
// CORS(二)(Cross-Origin Resource Sharing)跨域资源共享,IE10+,有简单请求和非简单请求
1. 服务端设置http header: Access-Control-Allow-Headers: '*'
2. HTTP的头信息不超出以下几种字段:
    Accept
    Accept-Language
    Content-Language
    Last-Event-ID
    Content-Type
3. 允许CORS跨域的类型: content-Type: text/plain、multipart/form-data、application/x-www-form-urlencoded
4. 允许CORS跨域的方法: GETPOSTHEAD
5. 如果满足 234 的条件则是简单请求,其他则是非简单请求
6. 简单请求不需要预请求,非简单请求需要预请求
7. 简单请求:
    简单请求只需要设置Access-Control-Allow-Origin即可
    对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段
      GET /cors HTTP/1.1
      Origin: http://api.bob.com
      Host: api.alice.com
      Accept-Language: en-US
      Connection: keep-alive
      User-Agent: Mozilla/5.0...
    Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
    如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含
    Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror
    回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200
    如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段:
      Access-Control-Allow-Origin: http://api.bob.com
      Access-Control-Allow-Credentials: true
      Access-Control-Expose-Headers: FooBar
      Content-Type: text/html; charset=utf-8
    上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头
      Access-Control-Allow-Origin // 该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求
      Access-Control-Allow-Credentials // 该字段可选
      // 它的值是一个布尔值,表示是否允许发送Cookie,默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可
      // Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可
      Access-Control-Expose-Headers // 该字段可选
      // CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language
      // Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定
    CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials
    Access-Control-Allow-Credentials: true
    另一方面,开发者必须在AJAX请求中打开withCredentials属性
    否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理
    但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials
    xhr.withCredentials = false
    需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名
    同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传
    且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie
8. 非简单请求:
    非简单请求会发出一次预检测请求,返回码是204,预检测通过才会真正发出请求,这才返回200
    非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)
    浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段
    只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错
      :
        var url = 'http://api.alice.com/cors';
        var xhr = new XMLHttpRequest();
        xhr.open('PUT', url, true);
        xhr.setRequestHeader('X-Custom-Header', 'value');
        xhr.send();
      上面代码中,HTTP请求的方法是PUT,并且发送一个自定义头信息X-Custom-Header
      浏览器发现,这是一个非简单请求,就自动发出一个"预检"请求,要求服务器确认可以这样请求。下面是这个"预检"请求的HTTP头信息
        OPTIONS /cors HTTP/1.1
        Origin: http://api.bob.com
        Access-Control-Request-Method: PUT
        Access-Control-Request-Headers: X-Custom-Header
        Host: api.alice.com
        Accept-Language: en-US
        Connection: keep-alive
        User-Agent: Mozilla/5.0...
      预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源
      除了Origin字段,"预检"请求的头信息包括两个特殊字段
      Access-Control-Request-Method // 该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT。
      Access-Control-Request-Headers // 该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header。
    服务器收到"预检"请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后
    确认允许跨源请求,就可以做出回应:
      HTTP/1.1 200 OK
      Date: Mon, 01 Dec 2008 01:15:39 GMT
      Server: Apache/2.0.61 (Unix)
      Access-Control-Allow-Origin: http://api.bob.com
      Access-Control-Allow-Methods: GET, POST, PUT
      Access-Control-Allow-Headers: X-Custom-Header
      Content-Type: text/html; charset=utf-8
      Content-Encoding: gzip
      Content-Length: 0
      Keep-Alive: timeout=2, max=100
      Connection: Keep-Alive
      Content-Type: text/plain
    上面的HTTP回应中,关键的是Access-Control-Allow-Origin字段,表示http://api.bob.com可以请求数据。该字段也可以设为星号,表示同意任意跨源请求
    如果服务器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定
    服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息
      XMLHttpRequest cannot load http://api.alice.com.
      Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.
    服务器回应的其他CORS相关字段如下:
      Access-Control-Allow-Methods: GET, POST, PUT
      Access-Control-Allow-Headers: X-Custom-Header
      Access-Control-Allow-Credentials: true
      Access-Control-Max-Age: 1728000
    Access-Control-Allow-Methods // 该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求
    Access-Control-Allow-Headers // 如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段
    Access-Control-Allow-Credentials // 该字段与简单请求时的含义相同
    Access-Control-Max-Age // 该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求
    一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段
    下面是"预检"请求之后,浏览器的正常CORS请求:
      PUT /cors HTTP/1.1
      Origin: http://api.bob.com
      Host: api.alice.com
      X-Custom-Header: value
      Accept-Language: en-US
      Connection: keep-alive
      User-Agent: Mozilla/5.0...
      上面头信息的Origin字段是浏览器自动添加的
    下面是服务器正常的回应:
      Access-Control-Allow-Origin: http://api.bob.com
      Content-Type: text/html; charset=utf-8
      上面头信息中,Access-Control-Allow-Origin字段是每次回应都必定包含的
9. CORSJSONP的使用目的相同,但是比JSONP更强大JSONP只支持GET请求,CORS支持所有类型的HTTP请求
   JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据
10. options 请求是跨域请求之前的预检查
    是浏览器自行发起的,无需我们干预
    不会影响实际的功能
----------------------------------------------------------------------------------------------
// document.domain + iframe跨域
// 此方案仅限主域相同,子域不同的跨域应用场景
// 实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域
1. 父窗口:http://www.domain.com/a.html
    <iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
    <script>
      document.domain = 'domain.com';
      var user = 'admin';
    </script>
2. 子窗口:http://child.domain.com/b.html
    <script>
      document.domain = 'domain.com';
      // 获取父窗口中变量
      alert('get js data from parent ---> ' + window.parent.user);
    </script>
----------------------------------------------------------------------------------------------
// location.hash + iframe跨域
// 实现原理: a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信
// 具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信
// b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象
1. a.html:http://www.domain1.com/a.html
    <iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
    <script>
        var iframe = document.getElementById('iframe');

        // 向b.html传hash值
        setTimeout(function() {
            iframe.src = iframe.src + '#user=admin';
        }, 1000);

        // 开放给同域c.html的回调方法
        function onCallback(res) {
            alert('data from c.html ---> ' + res);
        }
    </script>
2. b.html:http://www.domain2.com/b.html
    <iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
    <script>
        var iframe = document.getElementById('iframe');

        // 监听a.html传来的hash值,再传给c.html
        window.onhashchange = function () {
            iframe.src = iframe.src + location.hash;
        };
    </script>
3. c.html:http://www.domain1.com/c.html
    <script>
    // 监听b.html传来的hash值
    window.onhashchange = function () {
        // 再通过操作同域a.html的js回调,将结果传回
        window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
    };
    </script>
----------------------------------------------------------------------------------------------
// window.name + iframe跨域
// window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)
1. a.html:http://www.domain1.com/a.html
    var proxy = function(url, callback) {
    var state = 0;
    var iframe = document.createElement('iframe');

    // 加载跨域页面
    iframe.src = url;

    // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
    iframe.onload = function() {
        if (state === 1) {
            // 第2次onload(同域proxy页)成功后,读取同域window.name中数据
            callback(iframe.contentWindow.name);
            destoryFrame();

        } else if (state === 0) {
            // 第1次onload(跨域页)成功后,切换到同域代理页面
            iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
            state = 1;
        }
    };

    document.body.appendChild(iframe);

    // 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
    function destoryFrame() {
        iframe.contentWindow.document.write('');
        iframe.contentWindow.close();
        document.body.removeChild(iframe);
    }
  };

  // 请求跨域b页面数据
  proxy('http://www.domain2.com/b.html', function(data){
      alert(data);
  });
2. proxy.html:http://www.domain1.com/proxy....
    中间代理页,与a.html同域,内容为空即可
3. b.html:http://www.domain2.com/b.html
    <script>
      window.name = 'This is domain2 data!';
    </script>
4. 总结:通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域
   这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作
----------------------------------------------------------------------------------------------
// iframe + postMessage跨域
1. postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:
    页面和其打开的新窗口的数据传递
    多窗口之间消息传递
    页面与嵌套的iframe消息传递
    上面三个场景的跨域数据传递
2. 用法:postMessage(data,origin)方法接受两个参数
    data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。
    origin: 协议+主机+端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"
3. a.html:http://www.domain1.com/a.html
    <iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
    <script>
        var iframe = document.getElementById('iframe');
        iframe.onload = function() {
            var data = {
                name: 'aym'
            };
            // 向domain2传送跨域数据
            iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
        };

        // 接受domain2返回数据
        window.addEventListener('message', function(e) {
            alert('data from domain2 ---> ' + e.data);
        }, false);
    </script>
4. b.html:http://www.domain2.com/b.html
    `<script>
      // 接收domain1的数据
      window.addEventListener('message', function(e) {
          alert('data from domain1 ---> ' + e.data);
          var data = JSON.parse(e.data);
          if (data) {
            data.number = 16;
            // 处理后再发回domain1
            window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
          }
      }, false);
    </script>`
----------------------------------------------------------------------------------------------
// nginx代理跨域
// 浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外
// 此时可在nginx的静态资源服务器中加入以下配置
1. nginx配置解决iconfont跨域
    location / {
      add_header Access-Control-Allow-Origin *;
    }
2. nginx反向代理接口跨域
    跨域原理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议
    不会执行JS脚本,不需要同源策略,也就不存在跨越问题
    实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机
    反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录
    #proxy服务器
    server {
        listen       81;
        server_name  www.domain1.com;

        location / {
            proxy_pass   http://www.domain2.com:8080;  #反向代理
            proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
            index  index.html index.htm;

            # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
            add_header Access-Control-Allow-Origin http://www.domain1.com;  #当前端只跨域不带cookie时,可为*
            add_header Access-Control-Allow-Credentials true;
        }
    }
    前端
      var xhr = new XMLHttpRequest();
      // 前端开关:浏览器是否读写cookie
      xhr.withCredentials = true;
      // 访问nginx中的代理服务器
      xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
      xhr.send();
    后端
      var http = require('http');
      var server = http.createServer();
      var qs = require('querystring');

      server.on('request', function(req, res) {
          var params = qs.parse(req.url.substring(2));
          // 向前台写cookie
          res.writeHead(200, {
              'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'   // HttpOnly:脚本无法读取
          });
          res.write(JSON.stringify(params));
          res.end();
      });
      server.listen('8080');
      console.log('Server is running at port 8080...');
----------------------------------------------------------------------------------------------
// Nodejs中间件代理跨域
1. node中间件实现跨域代理,原理大致与nginx相同,都是通过启一个代理服务器,实现数据的转发
也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证
----------------------------------------------------------------------------------------------
// WebSocket协议跨域
1. WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。
2. 原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口
   也对不支持webSocket的浏览器提供了向下兼容
3. websocket 连接过程:
    1. 先发起一个 HTTP 请求
    2. 成功之后再升级到 websocket 协议,再通讯 // status code 101
4. websocket 和 http 长轮询 的区别
    1. HTTP 长轮询:客户端发起请求,服务端阻塞,不会立即返回,触发 settimeout 需要重新发起请求
    2. websocket:客户端可发起请求,服务端也可以发起请求
5. 示例:
    前端
      <div>user input:<input type="text"></div>
      <script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
      <script>
      var socket = io('http://www.domain2.com:8080');

      // 连接成功处理
      socket.on('connect', function() {
          // 监听服务端消息
          socket.on('message', function(msg) {
              console.log('data from server: ---> ' + msg);
          });

          // 监听服务端关闭
          socket.on('disconnect', function() {
              console.log('Server socket has closed.');
          });
      });

      document.getElementsByTagName('input')[0].onblur = function() {
          socket.send(this.value);
      };
      </script>
    后端
      var http = require('http');
      var socket = require('socket.io');

      // 启http服务
      var server = http.createServer(function(req, res) {
          res.writeHead(200, {
              'Content-type': 'text/html'
          });
          res.end();
      });

      server.listen('8080');
      console.log('Server is running at port 8080...');

      // 监听socket连接
      socket.listen(server).on('connection', function(client) {
          // 接收信息
          client.on('message', function(msg) {
              client.send('hello:' + msg);
              console.log('data from client: ---> ' + msg);
          });

          // 断开处理
          client.on('disconnect', function() {
              console.log('Client socket has closed.');
          });
      });
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
Last Updated: 2024/7/31 12:57:25
    飘向北方
    那吾克热-NW,尤长靖