zoukankan      html  css  js  c++  java
  • node.js初探


    惭愧惭愧,node.js已经火了好几年了,最近才开始接触……主要是工作中也基本用不到,但是我觉得一个东西火肯定自有道理,很有必要接触了解,甚至深入学习里面的精髓~
    官网:https://nodejs.org
    API查询:https://nodejs.org/api

    简介

    JavaScript是一种运行在浏览器的脚本,它简单,轻巧,易于编辑,这种脚本通常用于浏览器的前端编程,但是一位开发者Ryan有一天发现这种前端式的脚本语言可以运行在服务器上的时候,一场席卷全球的风暴就开始了。

    Node.js是一个基于Chrome JavaScript运行时建立的平台, 用于方便地搭建响应速度快、易于扩展的网络应用。Node.js 使用事件驱动, 非阻塞I/O 模型而得以轻量和高效,非常适合在分布式设备上运行的数据密集型的实时应用。

    Node是一个Javascript运行环境(runtime)。实际上它是对Google V8引擎进行了封装。V8引 擎执行Javascript的速度非常快,性能非常好。Node对一些特殊用例进行了优化,提供了替代的API,使得V8在非浏览器环境下运行得更好。


    Node.js中代码是单进程、单线程执行,但是底层其实是有线程池的。
    NodeJS适合运用在高并发、I/O密集、少量业务逻辑的场景,例如RESTful API,消息推送,聊天服务等

    安装

    由于我使用的是Ubuntu Kylin,所以安装方式如下:

    1.curl -sL https://deb.nodesource.com/setup_5.x | sudo -E bash
    2.sudo apt-get install -y nodejs

    顺便下载安装一个Atom编辑器来配合敲代码~

    curl -sL https://atom.io/download/deb -o atom.deb
    #由于atom安装依赖于git,还要先安装git
    sudo apt-get install git
    sudo dpkg –install atom.deb

    安装完成输入atom运行即可(插件什么的自行https://atom.io/packages寻找)。

    Hello World

    1. var http = require('http');
    2. http.createServer(function(request, response){
    3. // 发送 HTTP 头部
    4. // HTTP 状态值: 200 : OK
    5. // 内容类型: text/plain
    6. response.writeHead(200,{'Content-Type':'text/plain'});
    7. // 发送响应数据 "Hello World"
    8. response.end('Hello World ');
    9. }).listen(8888);
    10. // 终端打印如下信息
    11. console.log('Server running at http://127.0.0.1:8888/');

    node server.js
    浏览器浏览127.0.0.1:8888

    npm

    NPM是随同NodeJS一起安装的包管理工具,能解决NodeJS代码部署上的很多问题,常见的使用场景有以下几种:

    • 允许用户从NPM服务器下载别人编写的第三方包到本地使用。
    • 允许用户从NPM服务器下载并安装别人编写的命令行程序到本地使用。
    • 允许用户将自己编写的包或命令行程序上传到NPM服务器供别人使用。

    全局安装与本地安装

    npm 的包安装分为本地安装(local)、全局安装(global)两种,从敲的命令行来看,差别只是有没有-g而已,比如

    npm install express # 本地安装
    npm install express -g # 全局安装

    本地安装

    • 将安装包放在 ./node_modules 下(运行 npm 命令时所在的目录),如果没有 node_modules 目录,会在当前执行 npm 命令的目录下生成 node_modules 目录。
    • 可以通过 require() 来引入本地安装的包。

    全局安装

    • 将安装包放在 /usr/local 下。
    • 可以直接在命令行里使用。
    • 不能通过 require() 来引入本地安装的包。

    使用npm help可查看所有命令。

    Node.js REPL(交互式解释器)

    Node.js REPL(Read Eval Print Loop:交互式解释器) 表示一个电脑的环境,类似 Window 系统的终端或 Unix/Linux shell,我们可以在终端中输入命令,并接收系统的响应。

    使用变量

    你可以将数据存储在变量中,并在你需要的使用它。
    变量声明需要使用 var 关键字,如果没有使用 var 关键字变量会直接打印出来。
    使用 var 关键字的变量可以使用 console.log() 来输出变量。

    $ node
    > x = 10
    10
    > var y = 10
    undefined
    > x + y
    20
    > console.log(“Hello World”)
    Hello World
    undefined
    > console.log(“www.runoob.com”)
    www.runoob.com
    undefined

    多行表达式

    Node REPL 支持输入多行表达式,这就有点类似 JavaScript。接下来让我们来执行一个 do-while 循环:

    $ node
    > var x = 0
    undefined
    > do {
    … x++;
    … console.log(“x: ” + x);
    … } while ( x < 5 );
    x: 1
    x: 2
    x: 3
    x: 4
    x: 5
    undefined
    >

    … 三个点的符号是系统自动生成的,你回车换行后即可。Node 会自动检测是否为连续的表达式。

    下划线(_)变量

    你可以使用下划线(_)获取表达式的运算结果:

    $ node
    > var x = 10
    undefined
    > var y = 20
    undefined
    > x + y
    30
    > var sum = _
    undefined
    > console.log(sum)
    30
    undefined
    >

    Node.js 回调函数

    Node.js 异步编程的直接体现就是回调。
    异步编程依托于回调来实现,但不能说使用了回调后程序就异步化了。
    回调函数在完成任务后就会被调用,Node 使用了大量的回调函数,Node 所有 API 都支持回调函数。
    例如,我们可以一边读取文件,一边执行其他命令,在文件读取完成后,我们将文件内容作为回调函数的参数返回。这样在执行代码时就没有阻塞或等待文件 I/O 操作。这就大大提高了 Node.js 的性能,可以处理大量的并发请求。

    1. var fs = require("fs");
    2. //阻塞代码实例
    3. var data = fs.readFileSync('input.txt');
    4. 非阻塞代码实例
    5. fs.readFile('input.txt',function(err, data){
    6. if(err)return console.error(err);
    7. console.log(data.toString());
    8. });

    Node.js 事件循环

    Node.js 是单进程单线程应用程序,但是通过事件和回调支持并发,所以性能非常高。
    Node.js 的每一个 API 都是异步的,并作为一个独立线程运行,使用异步函数调用,并处理并发。
    Node.js 基本上所有的事件机制都是用设计模式中观察者模式实现。
    Node.js 单线程类似进入一个while(true)的事件循环,直到没有事件观察者退出,每个异步事件都生成一个事件观察者,如果有事件发生就调用该回调函数.

    事件驱动程序

    Node.js 使用事件驱动模型,当web server接收到请求,就把它关闭然后进行处理,然后去服务下一个web请求。
    当这个请求完成,它被放回处理队列,当到达队列开头,这个结果被返回给用户。
    这个模型非常高效可扩展性非常强,因为webserver一直接受请求而不等待任何读写操作。(这也被称之为非阻塞式IO或者事件驱动IO)
    在事件驱动模型中,会生成一个主循环来监听事件,当检测到事件时触发回调函数。

    1. // 引入 events 模块
    2. var events = require('events');
    3. // 创建 eventEmitter 对象
    4. var eventEmitter =new events.EventEmitter();
    5. // 创建事件处理程序
    6. var connectHandler =function connected(){
    7. console.log('连接成功。');
    8. // 触发 data_received 事件
    9. eventEmitter.emit('data_received');
    10. }
    11. // 绑定 connection 事件处理程序
    12. eventEmitter.on('connection', connectHandler);
    13. // 使用匿名函数绑定 data_received 事件
    14. eventEmitter.on('data_received',function(){
    15. console.log('数据接收成功。');
    16. });
    17. // 触发 connection 事件
    18. eventEmitter.emit('connection');
    19. console.log("程序执行完毕。");

    我们执行以上代码:

    $ node main.js
    连接成功。
    数据接收成功。
    程序执行完毕。

    Node.js 所有的非阻塞I/O I/O 操作在完成时都会发送一个事件到事件队列。
    Node.js里面的许多对象都会分发事件:一个net.Server对象会在每次有新连接时分发一个事件, 一个fs.readStream对象会在文件被打开的时候发出一个事件。 所有这些产生事件的对象都是 events.EventEmitter 的实例。
    EventEmitter 的核心就是事件触发与事件监听器功能的封装。

    EventEmitter 的每个事件由一个事件名和若干个参数组成,事件名是一个字符串,通常表达一定的语义。对于每个事件,EventEmitter 支持 若干个事件监听器。
    当事件触发时,注册到这个事件的事件监听器被依次调用,事件参数作为回调函数参数传递。

    1. var events = require('events');
    2. var emitter =new events.EventEmitter();
    3. emitter.on('someEvent',function(arg1, arg2){
    4. console.log('listener1', arg1, arg2);
    5. });
    6. emitter.on('someEvent',function(arg1, arg2){
    7. console.log('listener2', arg1, arg2);
    8. });
    9. emitter.emit('someEvent','arg1 参数','arg2 参数');

    error 事件

    EventEmitter 定义了一个特殊的事件 error,它包含了错误的语义,我们在遇到 异常的时候通常会触发 error 事件。
    当 error 被触发时,EventEmitter 规定如果没有响 应的监听器,Node.js 会把它当作异常,退出程序并输出错误信息。
    我们一般要为会触发 error 事件的对象设置监听器,避免遇到错误后整个程序崩溃。

    继承 EventEmitter

    大多数时候我们不会直接使用 EventEmitter,而是在对象中继承它。包括 fs、net、 http 在内的,只要是支持事件响应的核心模块都是 EventEmitter 的子类。
    为什么要这样做呢?原因有两点:
    首先,具有某个实体功能的对象实现事件符合语义, 事件的监听和发射应该是一个对象的方法。
    其次 JavaScript 的对象机制是基于原型的,支持 部分多重继承,继承 EventEmitter 不会打乱对象原有的继承关系。

    Node.js Buffer(缓冲区)

    JavaScript 语言自身只有字符串数据类型,没有二进制数据类型。
    但在处理像TCP流或文件流时,必须使用到二进制数据。因此在 Node.js中,定义了一个 Buffer 类,该类用来创建一个专门存放二进制数据的缓存区。
    在 Node.js 中,Buffer 类是随 Node 内核一起发布的核心库。Buffer 库为 Node.js 带来了一种存储原始数据的方法,可以让 Node.js 处理二进制数据,每当需要在 Node.js 中处理I/O操作中移动的数据时,就有可能使用 Buffer 库。原始数据存储在 Buffer 类的实例中。一个 Buffer 类似于一个整数数组,但它对应于 V8 堆内存之外的一块原始内存。

    utf-8 是默认的编码方式,此外它同样支持以下编码:”ascii”, “utf8”, “utf16le”, “ucs2”, “base64” 和 “hex”。

    Node.js Stream(流)

    • Stream 是一个抽象接口,Node 中有很多对象实现了这个接口。例如,对http 服务器发起请求的request 对象就是一个 Stream,还有stdout(标准输出)。
    • Node.js,Stream 有四种流类型:
    • Readable - 可读操作。
    • Writable - 可写操作。
    • Duplex - 可读可写操作.
    • Transform - 操作被写入数据,然后读出结果。

    所有的 Stream 对象都是 EventEmitter 的实例。常用的事件有:

    • data - 当有数据可读时触发。
    • end - 没有更多的数据可读时触发。
    • error - 在接收和写入过程中发生错误时触发。
    • finish - 所有数据已被写入到底层系统时触发。

    Node.js模块系统

    为了让Node.js的文件可以相互调用,Node.js提供了一个简单的模块系统。
    模块是Node.js 应用程序的基本组成部分,文件和模块是一一对应的。换言之,一个 Node.js 文件就是一个模块,这个文件可能是JavaScript 代码、JSON 或者编译过的C/C++ 扩展。

    Node.js 提供了exports 和 require 两个对象,其中 exports 是模块公开的接口,require 用于从外部获取一个模块的接口,即所获取模块的 exports 对象。
    接下来我们就来创建hello.js文件,代码如下:

    1. exports.world =function(){
    2. console.log('Hello World');
    3. }

    在以上示例中,hello.js 通过 exports 对象把 world 作为模块的访问接口,在 main.js 中通过 require(‘./hello’) 加载这个模块,然后就可以直接访 问 hello.js 中 exports 对象的成员函数了。
    有时候我们只是想把一个对象封装到模块中
    例如:

    1. //hello.js
    2. functionHello(){
    3. var name;
    4. this.setName =function(thyName){
    5. name = thyName;
    6. };
    7. this.sayHello =function(){
    8. console.log('Hello '+ name);
    9. };
    10. };
    11. module.exports =Hello;

    这样就可以直接获得这个对象了:

    1. //main.js
    2. varHello= require('./hello');
    3. hello =newHello();
    4. hello.setName('BYVoid');
    5. hello.sayHello();

    模块接口的唯一变化是使用 module.exports = Hello 代替了exports.world = function(){}。 在外部引用该模块时,其接口对象就是要输出的 Hello 对象本身,而不是原先的 exports。

    由于Node.js中存在4类模块(原生模块和3种文件模块),尽管require方法极其简单,但是内部的加载却是十分复杂的,其加载优先级也各自不同。如下图所示:
    nodejs-require

    从文件模块缓存中加载

    尽管原生模块与文件模块的优先级不同,但是都不会优先于从文件模块的缓存中加载已经存在的模块。

    从原生模块加载

    原生模块的优先级仅次于文件模块缓存的优先级。require方法在解析文件名之后,优先检查模块是否在原生模块列表中。以http模块为例,尽管在目录下存在一个http/http.js/http.node/http.json文件,require(“http”)都不会从这些文件中加载,而是从原生模块中加载。
    原生模块也有一个缓存区,同样也是优先从缓存区加载。如果缓存区没有被加载过,则调用原生模块的加载方式进行加载和执行。

    从文件加载

    当文件模块缓存中不存在,而且不是原生模块的时候,Node.js会解析require方法传入的参数,并从文件系统中加载实际的文件,加载过程中的包装和编译细节在前一节中已经介绍过,这里我们将详细描述查找文件模块的过程,其中,也有一些细节值得知晓。
    require方法接受以下几种参数的传递:

    • http、fs、path等,原生模块。
    • ./mod或../mod,相对路径的文件模块。
    • /pathtomodule/mod,绝对路径的文件模块。
    • mod,非原生模块的文件模块。

    Node.js 全局对象

    JavaScript 中有一个特殊的对象,称为全局对象(Global Object),它及其所有属性都可以在程序的任何地方访问,即全局变量。
    在浏览器 JavaScript 中,通常 window 是全局对象, 而 Node.js 中的全局对象是 global,所有全局变量(除了 global 本身以外)都是 global 对象的属性。
    在 Node.js 我们可以直接访问到 global 的属性,而不需要在应用中包含它。

    全局对象与全局变量

    global 最根本的作用是作为全局变量的宿主。按照 ECMAScript 的定义,满足以下条 件的变量是全局变量:

    • 在最外层定义的变量;
    • 全局对象的属性;
    • 隐式定义的变量(未定义直接赋值的变量)。
      当你定义一个全局变量时,这个变量同时也会成为全局对象的属性,反之亦然。需要注 意的是,在 Node.js 中你不可能在最外层定义变量,因为所有用户代码都是属于当前模块的, 而模块本身不是最外层上下文。
      注意: 永远使用 var 定义变量以避免引入全局变量,因为全局变量会污染 命名空间,提高代码的耦合风险。

    Node.js 多进程

    我们都知道 Node.js 是以单线程的模式运行的,但它使用的是事件驱动来处理并发,这样有助于我们在多核 cpu 的系统上创建多个子进程,从而提高性能。
    每个子进程总是带有三个流对象:child.stdin, child.stdout 和child.stderr。他们可能会共享父进程的 stdio 流,或者也可以是独立的被导流的流对象。
    Node 提供了 child_process 模块来创建子进程,方法有:

    • exec - child_process.exec 使用子进程执行命令,缓存子进程的输出,并将子进程的输出以回调函数参数的形式返回。
    • spawn - child_process.spawn 使用指定的命令行参数创建新线程。
    • fork - child_process.fork 是 spawn()的特殊形式,用于在子进程中运行的模块,如 fork(‘./son.js’) 相当于 spawn(‘node’, [‘./son.js’]) 。与spawn方法不同的是,fork会在父进程与子进程之间,建立一个通信管道,用于进程之间的通信。

    阻塞与非阻塞,同步与异步

    我们听到Node.js时,我们常常会听到异步,非阻塞,回调,事件这些词语混合在一起。其中,异步与非阻塞听起来似乎是同一回事。从实际效果的角度说,异步和非阻塞都达到了我们并行I/O的目的。但是从计算机内核I/O而言,异步/同步和阻塞/非阻塞实际上时两回事。

    I/O的阻塞与非阻塞
    阻塞模式的I/O会造成应用程序等待,直到I/O完成。同时操作系统也支持将I/O操作设置为非阻塞模式,这时应用程序的调用将可能在没有拿到真正数据时就立即返回了,为此应用程序需要多次调用才能确认I/O操作完全完成。
    I/O的同步与异步
    I/O的同步与异步出现在应用程序中。如果做阻塞I/O调用,应用程序等待调用的完成的过程就是一种同步状况。相反,I/O为非阻塞模式时,应用程序则是异步的。

    异步I/O与轮询技术

    当进行非阻塞I/O调用时,要读到完整的数据,应用程序需要进行多次轮询,才能确保读取数据完成,以进行下一步的操作。
    轮询技术的缺点在于应用程序要主动调用,会造成占用较多CPU时间片,性能较为低下。现存的轮询技术有以下这些:

    • read
    • select
    • poll
    • epoll
    • pselect
    • kqueue

    read是性能最低的一种,它通过重复调用来检查I/O的状态来完成完整数据读取。select是一种改进方案,通过对文件描述符上的事件状态来进行判断。操作系统还提供了poll、epoll等多路复用技术来提高性能。
    轮询技术满足了异步I/O确保获取完整数据的保证。但是对于应用程序而言,它仍然只能算时一种同步,因为应用程序仍然需要主动去判断I/O的状态,依旧花费了很多CPU时间来等待。

    上一种方法重复调用read进行轮询直到最终成功,用户程序会占用较多CPU,性能较为低下。而实际上操作系统提供了select方法来代替这种重复read轮询进行状态判断。select内部通过检查文件描述符上的事件状态来进行判断数据是否完全读取。但是对于应用程序而言它仍然只能算是一种同步,因为应用程序仍然需要主动去判断I/O的状态,依旧花费了很多CPU时间等待,select也是一种轮询。

    理想的异步I/O模型

    理想的异步I/O应该是应用程序发起异步调用,而不需要进行轮询,进而处理下一个任务,只需在I/O完成后通过信号或是回调将数据传递给应用程序即可。

    幸运的是,在Linux下存在一种这种方式,它原生提供了一种异步非阻塞I/O方式(AIO)即是通过信号或回调来传递数据的。
    不幸的是,只有Linux下有这么一种支持,而且还有缺陷(AIO仅支持内核I/O中的O_DIRECT方式读取,导致无法利用系统缓存。参见:http://forum.nginx.org/read.php?2,113524,113587#msg-113587
    以上都是基于非阻塞I/O进行的设定。另一种理想的异步I/O是采用阻塞I/O,但加入多线程,将I/O操作分到多个线程上,利用线程之间的通信来模拟异步。Glibc的AIO便是这样的典型http://www.ibm.com/developerworks/linux/library/l-async/。然而遗憾在于,它存在一些难以忍受的缺陷和bug。可以简单的概述为:Linux平台下没有完美的异步I/O支持。
    所幸的是,libev的作者Marc Alexander Lehmann重新实现了一个异步I/O的库:libeio。libeio实质依然是采用线程池与阻塞I/O模拟出来的异步I/O。
    那么在Windows平台下的状况如何呢?而实际上,Windows有一种独有的内核异步IO方案:IOCP。IOCP的思路是真正的异步I/O方案,调用异步方法,然后等待I/O完成通知。IOCP内部依旧是通过线程实现,不同在于这些线程由系统内核接手管理。IOCP的异步模型与Node.js的异步调用模型已经十分近似。
    以上两种方案则正是Node.js选择的异步I/O方案。由于Windows平台和*nix平台的差异,Node.js提供了libuv来作为抽象封装层,使得所有平台兼容性的判断都由这一层次来完成,保证上层的Node.js与下层的libeio/libev及IOCP之间各自独立。Node.js在编译期间会判断平台条件,选择性编译unix目录或是win目录下的源文件到目标程序中。

    下文我们将通过解释Windows下Node.js异步I/O(IOCP)的简单例子来探寻一下从JavaScript代码到系统内核之间都发生了什么。

    Node.js的异步I/O模型

    很多同学在遇见Node.js后必然产生过对回调函数究竟如何被调用产生过好奇。在文件I/O这一块与普通的业务逻辑的回调函数不同在于它不是由我们自己的代码所触发,而是系统调用结束后,由系统触发的。下面我们以最简单的fs.open方法来作为例子,探索Node.js与底层之间是如何执行异步I/O调用和回调函数究竟是如何被调用执行的。

    1. fs.open =function(path, flags, mode, callback){
    2. callback = arguments[arguments.length -1];
    3. if(typeof(callback)!=='function'){
    4. callback = noop;
    5. }
    6. mode = modeNum(mode,438/*=0666*/);
    7. binding.open(pathModule._makeLong(path),
    8. stringToFlags(flags),
    9. mode,
    10. callback);
    11. };

    fs.open的作用是根据指定路径和参数,去打开一个文件,从而得到一个文件描述符,是后续所有I/O操作的初始操作。

    在JavaScript层面上调用的fs.open方法最终都透过node_file.cc调用到了libuv中的uv_fs_open方法,这里libuv作为封装层,分别写了两个平台下的代码实现,编译之后,只会存在一种实现被调用。

    请求对象

    在uv_fs_open的调用过程中,Node.js创建了一个FSReqWrap请求对象。从JavaScript传入的参数和当前方法都被封装在这个请求对象中,其中回调函数则被设置在这个对象的oncomplete_sym属性上。

    req_wrap->object_->Set(oncomplete_sym, callback);

    对象包装完毕后,调用QueueUserWorkItem方法将这个FSReqWrap对象推入线程池中等待执行。

    QueueUserWorkItem(&uv_fs_thread_proc, req, WT_EXECUTELONGFUNCTION)

    QueueUserWorkItem接受三个参数,第一个是要执行的方法,第二个是方法的上下文,第三个是执行的标志。当线程池中有可用线程的时候调用uv_fs_thread_proc方法执行。该方法会根据传入的类型调用相应的底层函数,以uv_fs_open为例,实际会调用到fs__open方法。调用完毕之后,会将获取的结果设置在req->result上。然后调用PostQueuedCompletionStatus通知我们的IOCP对象操作已经完成。

    PostQueuedCompletionStatus((loop)->iocp, 0, 0, &((req)->overlapped))

    PostQueuedCompletionStatus方法的作用是向创建的IOCP上相关的线程通信,线程根据执行状况和传入的参数判定退出。
    至此,由JavaScript层面发起的异步调用第一阶段就此结束。

    事件循环

    在调用uv_fs_open方法的过程中实际上应用到了事件循环。以在Windows平台下的实现中,启动Node.js时,便创建了一个基于IOCP的事件循环loop,并一直处于执行状态。

    uv_run(uv_default_loop());

    每次循环中,它会调用IOCP相关的GetQueuedCompletionStatus方法检查是否线程池中有执行完的请求,如果存在,poll操作会将请求对象加入到loop的pending_reqs_tail属性上。 另一边这个循环也会不断检查loop对象上的pending_reqs_tail引用,如果有可用的请求对象,就取出请求对象的result属性作为结果传递给oncomplete_sym执行,以此达到调用JavaScript中传入的回调函数的目的。 至此,整个异步I/O的流程完成结束。其流程如下:

    事件循环和请求对象构成了Node.js的异步I/O模型的两个基本元素,这也是典型的消费者生产者场景。在Windows下通过IOCP的GetQueuedCompletionStatus、PostQueuedCompletionStatus、QueueUserWorkItem方法与事件循环实。对于*nix平台下,这个流程的不同之处在与实现这些功能的方法是由libeio和libev提供。

    参考

    http://www.runoob.com/nodejs/nodejs-tutorial.html
    http://www.infoq.com/cn/articles/nodejs-asynchronous-io/





  • 相关阅读:
    常用的 Javascript 操作汇总 (一)
    SQL SERVER 2005 新特性CTE
    屏幕上创建页签
    81条生活小常识大放送 看看哪些你不知道的 生活至上,美容至尚!
    这样保养让你皮肤变水嫩 生活至上,美容至尚!
    头发一周洗几次才适宜? 生活至上,美容至尚!
    人体排毒规律以及排毒食物介绍 生活至上,美容至尚!
    养肝粥,用电脑过度人群必备! 生活至上,美容至尚!
    想睡觉却睡不着,失眠烦恼10招解决 生活至上,美容至尚!
    雪碧的N种新潮喝法雪碧的N种新潮喝法 生活至上,美容至尚!
  • 原文地址:https://www.cnblogs.com/leestar54/p/5160119.html
Copyright © 2011-2022 走看看