标签php-resque下的文章

Jerry Bendy 发布于 04月06, 2015

后台任务和PHP-Resque的使用(五) 创建任务

到目前为止已经让 Worker 运行了,我们需要创建并添加任务。这一节主要了解什么是任务(Job),以及如何使用任务。

简单的说,任务就是传递给 Worker 要执行的内容。我们需要把 Job 依次添加到 Queue 来执行。

要把任务添加到队列,程序必须要包含 php-resque 库以及 Redis。

使用 require_once '/path/to/php-resque/lib/Resque.php';包含 php-resque 的库文件,它会自动连接到 Redis 服务器,如果你的 Redis 服务器不是默认的localhost:6379,你需要使用Resque::setBackent('192.168.1.56:3680'); 这样的格式来设置你的 Redis 服务器的地址,同样 setBackent 支持可选的第二个参数为使用的 Redis 数据库名,默认为 0 。

现在 php-resque 已经准备好了,使用以下代码添加一个任务到队列:

Resque::enqueue('default', 'Mail', array('dest@mail.com', 'hi!', 'this is a test content'));
  • 第一个参数,'default'是指队列的名字,示例中将会把任务推送到名为 default 的队列中
  • 第二个参数是 Job 的类名,表示要执行哪个 Job
  • 第三个参数是要发送给 Job 的参数也可以使用关联数组的形式

传递给 Job 的参数(上面第三个参数)可以是普通数组、关联数组的形式,也可以是一个字符串,但使用数组可以很方便的传递更多的信息给 Job。所有的参数在推送到队列前都会经过 json_encode 处理。

创建一个 Job

如上面的例子中,第一个参数是队列的名字(还记得上一节里面启动 php resque.php 时传递的 QUEUE 环境变量吗?),第二个参数是 Job 的类名,即要执行的 Job。Mail 类就是一个 Job 类。

所有的Job类都应该包含一个 perform() 方法,使用 Resque::enqueue() 传递的第三个参数可以在 perform() 方法中使用 $this->args 来得到。一个典型的 Job 类如下所示:

class Mail{
    public function perform(){
        var_dump($this->args);
    }
}

Job 类也可以包含 setUp()tearDown() 方法,可选的这两个方法分别会在 perform() 方法之前和之后运行。

class Mail{
    public function setUp(){
        # 这个方法会在perform()之前运行,可以用来做一些初始化工作
        # 如连接数据库、处理参数等
    }

    public function perform(){
        # 执行Job
    }

    public function tearDown(){
        # 会在perform()之后运行,可以用来做一些清理工作
    }
}

包含 Job 类

在实例化 Job 类之前,必须让 Worker 找到并包含这个类。有很多种方法可以做到。

使用 include_path

当 PHP 运行于 Apache model 方式的时候可以使用 .htaccess 设置包含:

php_value include_path ".:/already/existing/path:/path/to/job-classes"

或者通过 php.ini

include_path = ".:/php/includes:/path/to/job-classes"

使用 APP_INCLUDE 包含

上一节说了使用 APP_INCLUDE 指定 Worker 执行时要包含的PHP文件的路径,如:

QUEUE=default APP_INCLUDE=/path/to/loader.php php resque.php

loader.php 的内容可以是下面的那样:

include '/path/to/Mail.php';
include '/path/to/AnotherJobClass.php';
include '/path/to/somewhere/AnotherJobClass.php';
include '/JobClass.php';

当然也可以使用 PHP 的 autoloader 方法 —— sql_autoloader

在你的项目中使用后台任务

以下面的代码为例,把耗时较多的工作交给后台任务来做。

class User{
    # functions(){}  // 其它函数

    public function updateLocation($location) {
        $db->updateUserTable($this->userId, 'location', $location);
        $this->recomputeNewFriends(); # 此操作耗时较长
    }

    public function recomputeNewFriends() {
        # 查找新的朋友
    }
}

把以上代码改成:

class User {
    # functions(){}  // 其它函数

    public function updateLocation($location) {
        $db->updateUserTable($this->userId, 'location', $location);
        # 把任务添加到队列
        # 这里的队列名为 'queueName'
        # 任务名为 'FriendRecommendator'
        Resque::enqueue('queueName', 'FriendRecommendator', array('id' => $this->userId));
    }
}

以下是任务 FriendRecommendator 类的实现代码:

class FriendRecommendator {
    function perform() {
        # 这里没有User类,需要创建一个User类对象
        $user = new User($this->args['id']);
        # 查找新朋友的操作
    }
}

简单的说,你只需要把你的执行任务的代码放到 Job 类中并改名为perform()即可,只要你愿意甚至可以将普通类改成 Job 类,但并不推荐这样做。

perform() 方法有个缺点,即一个 Job 类只能包含一个 perform() 方法,也就是说一个 Job 类只能执行一种后台任务。例如你有一个发送通知信息的后台任务,但又有发送给用户和发送给管理员两个不同的需求,一般来说就得需要两个 Job 类才能实现。不过这里有个小小的 Hack 可以使一个 Job 能执行多个类型的任务。

首先就是给你的 Job 分类,把相似工作的 Job 放在同一个 Job 类中,因为完全不相关的 Job 即使放在同一个类中也没有任何意义。然后通过给 Resque::enqueue() 方法传递一个表示不同 Job 的参数过去。

# Job类中的写法
class Notification{
    function sentToUser(){
        # Code..
    }

    function sentToAdmin{
        # code..
    }

    function perform(){
        $action = $this->{array_shift($this->args)};
        if(method_exists($this, $action)){
            $this->$action();
        }
    }
}

# 添加任务时的写法
Resque::enqueue('default', 'Notification', array('sendToAdmin', 'this is content'));

也可以使用其它类继承 Job 类以获取相同的 perform() 方法,但要注意必须同时包含这些类文件。

另外需要注意的是使用这种 Hack 的方法 Resque::enqueue() 的第三个参数必须是一个数组,并且它的第一个元素是要执行的任务的方法名,并且这个元素会在执行时从 $args 数组中移除。

必须在每次修改 Job 类后重新启动你的Worker


本文由冰翼翻译自Kamisama.me

阅读全文 »

Jerry Bendy 发布于 04月05, 2015

后台任务和PHP-Resque的使用(四) 使用Worker

注意,这篇教程仅适用于 Linux 和 OS X 的系统,Windows 并不适用。

理解 Worker 的本质

技术上讲一个 Worker 就是一个不断运行的PHP进程,并且不断监视新的任务并运行。

一个简单的 Worker 的代码如下:

while (true) {
    $jobs = pullData(); // 从队列中拉取任务

    foreach ($jobs as $class => $args) { // 循环每个找到的任务
        $job = new $class();
        $job->perform($args); // 执行任务
    }
    sleep(300); // 等待5分钟后再次尝试拉取任务
}

以上这些代码的具体实现都可以交给 php-resque。创建一个 Worker,php-resque 需要以下参数:

  • QUEUE: 需要执行的队列的名字
  • INTERVAL:在队列中循环的间隔时间,即完成一个任务后的等待时间,默认是5秒
  • APP_INCLUDE:需要自动载入 PHP 文件路径,Worker 需要知道你的 Job 的位置并载入 Job
  • COUNT:需要创建的 Worker 的数量。所有的 Worker 都具有相同的属性。默认是创建1个Worker
  • REDIS_BACKEND:Redis 服务器的地址,使用 hostname:port 的格式,如 127.0.0.1:6379,或 localhost:6379。默认是 localhost:6379
  • REDIS_BACKEND_DB:使用的 Redis 数据库的名称,默认是 0
  • VERBOSE:啰嗦模式,设置 1 为启用,会输出基本的调试信息
  • VVERBOSE:设置“1”启用更啰嗦模式,会输出详细的调试信息
  • PREFIX:前缀。在 Redis 数据库中为队列的 KEY 添加前缀,以方便多个 Worker 运行在同一个Redis 数据库中方便区分。默认为空
  • PIDFILE:手动指定 PID 文件的位置,适用于单 Worker 运行方式

以上参数中只有QUEUE是必须的。如果让 Worker 监视执行多个队列,可以用逗号隔开多个队列的名称,如:queue1,queue2,queue3,队列执行是有顺序的,如上 queue2queue3 总是会在 queue1 后面被执行。

也可以设置QUEUE*让 Worker 以字母顺序执行所有的队列。

Worker 必须以CLI方式启动。你不可以从浏览器启动 Worker,因为:

  • 你无法从浏览器执行后台任务
  • PCNTL 扩展只能运行在 CLI 模式

启动Worker

可以从resque.php启动 Worker,这个位置位于 php-resque/bin 目录下(也可能不带.php后缀)。

在终端中执行:

cd /path/to/php-resque/bin/

php resque.php

很显然 Worker 不会被启动,因为缺少必须的参数 QUEUE,程序将会返回如下错误:

Set QUEUE env var containing the list of queues to work.

php-resque 通过getenv获取参数,所以在启动 Worker 的时候应该传递环境变量过去。所以应该以下面的方式启动 Worker:

QUEUE=notification php resque.php

如果启用VVERBOSE模式:

QUEUE=notification VVERBOSE=1 php resque.php

终端将会输出:

*** Starting worker KAMISAMA-MAC.local:84499:notification
** [23:48:18 2012-10-11] Registered signals
** [23:48:18 2012-10-11] Checking achievement
** [23:48:18 2012-10-11] Checking notification
** [23:48:18 2012-10-11] Sleeping for 5
** [23:48:23 2012-10-11] Checking achievement
** [23:48:23 2012-10-11] Checking notification
** [23:48:23 2012-10-11] Sleeping for 5
... etc ...

Worker 会自动被命名为KAMISAMA-MAC.local:84499:notification,命名的规则是hostname:process-id:queue-names

如果觉得这种启动方式太麻烦且难记,可以自己手动写一个 bash 脚本来帮助你启动 Resque,如:

EXPORT QUEUE=notifacation
EXPORT VERBOSE=1

php resque.php

后台运行Worker

通过上面的方法成功启动了 Worker,但只有在终端开启的状态下,关闭终端或按下 Ctrl+C 时 Worker 就会停止运行。我们可以在命令后面添加一个 & 来使其后台运行。

QUEUE=notification php resque.php &

这样就可以让 resque 在后台运行。但如果你开启了 VERBOSE 模式,所有的输出信息将会丢失。所以我们需要在 resque 后台运行时把输出的信息保存起来。

我们可以使用 nohup 来保持 resque 后台运行,即使是在用户登出后。

nohup QUEUE=notification php resque.php &

当然,如果安装了 node 和 pm2 也可以使用 pm2 启动来保证 resque 后台的运行。

记录下 Worker 的输出

可以使用管道操作的方式重定向输出到文件:

nohup QUEUE=notification php resque.php >> /path/to/your/logfile.log 2>&1 &

这样一来所有的标准及错误输出都会被写入到 logfile.log 文件中。如果需要监视这个文件的内容:

tail -f /path/to/your/logfile.log

Worker 的执行权限

无论何时你在终端中执行命令都是以当前登录用户的权限来执行。如果你登录的 jerry 的账户,php-resque将会运行于 jerry 的权限下。以 root 用户登录时也一样。

如果需要避开当前登录账户以其它用户的权限运行,如 Apache 通常运行在 www-data 用户下,让 php-resque 运行于 www-data 账户:

nohup sudo -u www-data QUEUE=notification php resque.php >> /path/to/your/logfile.log 2>&1 &

操作执行权限时需要注意:

  • 通过 Worker 生成的文件无法被其它用户的php代码读取
  • Worker 没有权限创建或编辑其它用户的文件

Let's play

前面已经讲了如何启动、如何后台运行、以及记录运行日志,下面就用一些例子结束本节的内容。

创建一个执行 default 队列的 Worker,并且每隔 10 秒检索一次任务:

INTERVAL=10 QUEUE=default php resque.php

创建5个执行 default 队列的 Worker,每隔 5 秒检索一次任务:

QUEUE=default COUNT=5 php resque.php

INTERVAL 参数没有被指定,因为默认值是 5 秒。

创建一个执行 achievementnotification 队列的 Worker(需要注意队列名的顺序):

QUEUE=achievement,notification php resque.php

创建一个执行所有队列的 Worker:

QUEUE=* php resque.php

如果你的 Redis 服务器在别的地址:

QUEUE=default REDIS_BACKENT=192.168.1.56:6380 php resque.php

使用自动载入 php 文件:

QUEUE=default APP_INCLUDE=/path/to/autoloader.php php resque.php

确认你的 Worker 成功运行了

通过管道操作无法知道 Worker 是否成功启动,当前通过查看log文件中有没有输出 *** Starting worker ..... 的内容也可以知道是否启动。

也可以通过查看系统进程的方法确认 Worker 是否正在运行。

ps -ef|grep resque.php

将会输出名称中包含resque.php的进程,其中第二列是进程的 PID。

使用这个方法可以很好的知道 Worker 是否正在运行,以及有没有意外终止。

暂停和停止 Worker

要停止一个Worker,直接 kill 掉它的进程就行了。可以通过 ps -ef|grep resque.php 查看 Worker进程的 PID。当然通过这个命令你无法知道哪个 PID 代码的哪个 Worker。

如果要结束一个 PID 是 86681 的进程:

kill 86681

这个命令将会立即结束掉 PID 为 86681 的进程及子进程。如果 Worker 正在执行一个任务也不会等待任务执行完成(未完成的部分将会丢失)。

有一个可以平滑的停止 Worker 的方法,可以通过给 kill 命令发送一个 SIGSPEC 信号来告诉 kill 应该怎么做,这需要 PCNTL 扩展的支持。

当然下面所讲述的所有命令都需要 PCNTL 扩展支持。

通过 PCNTL 扩展,Worker 可以支持以下信号:

  • QUIT - 等待子进程结束后再结束
  • TERM / INT - 立即结束子进程并退出
  • USR1 - 立即结束子进程,但不退出
  • USR2 - 暂停Worker,不会再执行新任务
  • CONT - 继续运行Worker

当没有信号发出时默认是 TERM / INT 信号。

如果想在所有当前正在运行的任务都完成后再停止,使用 QUIT 信号:

kill -QUIT YOUR-WORKER-PID

结束所有子进程,但保留 Worker:

kill -USR1 YOUR-WORKER-PID

暂停和继续执行 Worker:

kill -USR2 YOUR-WORKER-PID

kill -CONT YOUR-WORKER-PID

本文由冰翼翻译自Kamisama.me

阅读全文 »

Jerry Bendy 发布于 03月31, 2015

后台任务和PHP-Resque的使用(三) 安装

第二部分我们使用 php-resque 作为队列系统,这一节讲如何安装 php-resque。

PHP-Resque 是依赖 Redis 的,所以需要先安装 Redis 及 PHP 的 Redis 扩展。以下是所有需要安装的组件:

  • Redis
  • PHP 的 Redis 扩展(php-redis)
  • php-resque
  • PHP 的 PCNTL 扩展

Redis

Redis 是一个开源的 KV 数据库,数据是保存在电脑 RAM 中的,速度非常快,所以通常可以使用 Redis 来做缓存,或保存 Session 等。可以在 Redis 的官方网站下载最新稳定版本。Redis 的安装方法本文不再赘述,安装完成后不要忘记启动。

PHP-Resque

php-resque 是 resque 的 PHP 版本,很多特性都和原版相似或相同。

下载 最新版本的 zip 压缩包,或克隆它的仓库:

git clone git://github.com/chrisboulton/php-resque.git

以上下载的只是 php-resque 的库,只需要把文件夹放在任何你项目需要的位置即可。也可以使用 Composer 安装 php-resque。

composer require chrisboulton/php-resque

PHPredis

PHPredis扩展 相当于是 Redis 的 PHP API,但它不是 PHP 使用 Redis 的唯一接口,类似的库还有redisentrediskapredis、redisentwrap 等。但 phpredis 是其中最快也是最流行的。关于phpredis 扩展的安装方法网上也有很多,也就不再赘述了。

PHP PCNTL 扩展

PCNTL(进程控制扩展)依赖于 Unix 系列系统的进程管理,所以 php-resque 只能运行在UNIX架构的电脑上,如 Linux。

一般可以通过编译安装 PHP 的时候启用 PCNTL 扩展,如果没有安装也可以:

  • 下载 对应版本的PHP源码
  • 解压文件 tar -zxvf php-x.x.x.tar.gz
  • 进入 ext/pcntl 目录 cd php-x.x.x/ext/pcntl/
  • 配置、编译、安装
    `sudo phpize && ./configure && make install`
    
  • 添加 extension=pcntl.so 到 php.ini
  • 重新启动 Apache/Nginx

本文由冰翼翻译自Kamisama.me

阅读全文 »

Jerry Bendy 发布于 03月30, 2015

后台任务和PHP-Resque的使用(二) 队列系统

接着第一部分,Queue 需要保存 Jobs,Worker 需要按照指定的时间间隔在 Queue 中轮循并执行 Jobs。

queue-system1

这个系统包含以下三个部分:

  • 推送者:推送任务到 Queue,可以是任何过程,甚至是 Worker;
  • Queue:按顺序保存 Jobs;
  • Worker:从 Queue 中拉取 Jobs 并执行。

注意,这里使用了推送(Push)和拉取(Pull)来代替添加(Add)和获取(Get)。

Push: 把数据添加到栈的尾部的操作。

**_Pull(or Pop):_**拉取(或弹出)。弹出并删除栈顶部的数据。

Push操作通常会把数据添加到栈的尾部。这种数据类型会保证先添加到队列的项目总是先被读取和删除,即“先进先出”(FIFO, First-in-first-out)的队列。

如果 Jobs 按照1,2,3这样的顺序被加入到Queue,那么它也会按照同样的顺序被执行。

什么是 Job

Job 就是准备被执行的任务,它告诉 Worker 应该做什么。Worker在执行前并不会知道 Job 是什么,如果Job 是发送 Email,它也不会知道需要发给谁、为什么要发、发送时的上下文环境是什么等。

如果你从你程序的主流程中执行发送邮件的函数,发送者就是登录的用户。你还需要额外为发送邮件提供更多信息,如收件人、邮件内容等。

就像你需要在家里安装电话线,电信公司就会来给你安装,而他们不需要知道你为什么需要安装电话线,他们要做的仅仅是你要求的(安装电话线)。

保存Jobs

任务必须被保存在队列中,这个队列:

  • 必须有一个队列类型的数据结构(支持推送和拉取任务)
  • 必须非常快
  • 可以在多个服务端共享
  • 是否持久化

你可以选择满足以上条件的队列系统,或使用第三方的程序,像 RabbitMQ、Gearman、Redis 等。我们将会使用 Redis 存储队列,并且使用 PHP-Resque 作为队列系统。

PHP-Resque 算是 Resque 的 PHP 版本,Resque 是开源在 Github 上的一个使用 Ruby 编写的队列系统。下一节将会讲如何安装 PHP-Resque。


本文由冰翼翻译自Kamisama.me

阅读全文 »

Jerry Bendy 发布于 03月27, 2015

后台任务和PHP-Resque的使用(一) 介绍

什么是后台任务?

后台任务就是运行在程序流程以外的任务,毕竟 PHP 作为网站后台语言是需要在执行后立即返回数据的,而且一般服务器都有设置执行的超时时间,所以使用 PHP 去完成一些比较耗时的后台操作就有些问题了。

通常在做一些比较耗时的操作时都会想到使用一个后台任务以及任务队列,在流程外执行这些操作,并立即返回给前台一个正在执行的提示。

我们来看一个实例:一个社交网站,某用户修改了他的的个人资料中的所在地。

一般的流程是这样的:

workflow1

PHP 并不是多线程的,所以所有任务必须在前一个任务完成后再能开始,这就会致使用户等待较长的时间。上面例子中,大约3.7秒后用户才能获取到来自服务器的反馈,用户体验较差。

需要注意的是在整个流程中最重要的仅仅是第一个步骤:Update Database(更新数据库)。如果这个过程失败了,其它的流程都不会被执行;如果成功就可以发送一条执行成功的消息给用户,如:“您的所在地信息已经更新成功”,因为最终的结果是要反馈信息给用户,在失败时同样会返回一条“未能成功更新所在地”的信息。

其它流程(如刷新缓存、发送邮件等)对于返回给用户的页面来说并不是必须的,那么为什么要让用户等待这些时间去得到非必需的结果?如果仅仅响应用户请求的操作并且立即返回结果,让那些不重要的流程稍后再去进行岂不是更好?

理想的工作流程

workflow2

这样会大大提升用户体验,因为用户仅需 0.3 秒的等待便可以得到响应。那些不重要的流程全部交给主流程之外的后台任务去执行。

一个简单的类比

如果你仍不明白究竟发生了什么:

  • 用户 Lambda 告诉 Mr.Server(服务器)更新他的所在地信息
  • Mr.PHP 是为 Mr.Server 工作的,现在他接收了这个任务
  • Mr.PHP 为 Lambda 更新了数据库
  • Mr.PHP 在公司便签上留言:“我已经更新了 Lambda 的所在地信息,请更新缓存、发送通知邮件,并为他推荐些好友”
  • Mr.PHP 告诉用户 Lambda:“已经给您更新好了!”
  • Mr.PHP 现在空闲下来了,准备接收下一个任务

但是 Mr.PHP 在便签上留言之后发生了什么?

  • Mr.PHP#2,另一个给 Mr.Server 打工的小伙子取下了这张便签
  • 他回到自己的办公桌上并按照便签上的内容开始工作
  • 当他完成工作后休息一会儿并去取走下一个便签,周而复始

在上面的例子中,Mr.PHP#2 就是worker,一张张便签就是queue(队列)。便签纸上的任务就是jobs

Mr.PHP 以及和他做同样工作的人都可以往墙上贴便签,即往队列中添加任务。Worker 的主要工作就是每隔一段时间去查看墙上有没有便签,并取下便签去执行上面所写的工作。

这些任务(jobs)就是后台任务,“前台任务”是指被直接处理的流程(即Mr.PHP的工作)。而后者并不知道(或者说不需要知道)这些任务是由如何、由谁以及何时被执行的。

一旦添加了 Jobs,只有 Worker 可以操作这 Jobs,而“前台任务”只知道这些 Jobs 等会儿会执行。

Worker 和 Queue 不一定是一个。多个 Worker 也可以执行同一个 Queue,一个 Worker 也可以执行多个 Queue。Worker 在队列中轮循的间隔时间也不固定,可以是 5 秒,也可以是 15 秒、1 分钟,等。

一个Queue System(队列系统) 被用来管理所有的 Jobs,包括保存、排序(优先级)等。

后台任务的重要性

后台任务在生活中随处可见。例如你打电话购买一台虚拟主机:

  • 接线员在电话里向你询问信用卡信息
  • 他向银行核对信息,银行批准交易
  • 他为你创建账户,并给你一个登录口令
  • 他让你等 15 分钟,15 分钟后主机将可用

优点

如果没有后台任务,你可能需要等很长时间。因为银行需要付款给主机供应商,在这里使用后台任务只需要告诉银行付款给主机供应商,接线员只需要确认你的信用卡是有效的,同时主机供应商确信银行会付款给他们,并为你创建账户。

同样,你可能需要继续等待 15 分钟,因为接线员那边需要让技术人员为你开通主机并等待开通成功。这样不仅浪费了你的时间,也浪费了接线员的时间。

从编程的角度想,后台任务由另一个进程启动,这就意味着无论后台任务发生了什么都不会影响到主流程,无论是异常还是错误。

缺点

后台任务虽然节省了大量时间,但你却失去了对这些任务的控制。一旦你把任务发送到队列,也许只有上帝知道它们怎么样了~你还有 Worker 在工作吗?它们有没有被释放?它们在执行正确的 Queue 吗?(不知道)

确信你发送到队列的 Jobs 有被 Worker 正确的执行将会是你的责任。经常会出现的错误如:

  • 发送 Jobs 到一个名为“achievement”的Queue,而Worker执行的是名为“achivement”的 Queue
  • 重新启动了服务,却忘了重新启动 Worker
  • 没有监视你的 Worker 工作状态,致使一个致命错误发生并终止了 Worker 的运行

以前面虚拟主机的例子,接线员告诉你主机将会在 15 分钟内上线。他怎么会知道?因为手册上是这么写的,他也不能保证主机就一定会在 15 分钟内上线。

Worker 执行自己的工作(技术人员为主机安装软件)不与外面的环境沟通,如果他现在有大量的工作,你的应该会在工作(Queue)的最下面。

为了获取 Worker 的工作状态,你不得不让 Worker 发送大量的报告,如何时开始工作、发送邮件/通知给管理员等,Worker 一旦开始你将不能再命令它们做什么,但它们可以告诉你它们在做什么。

但仍然有些错误是不能获取到的,如致命错误(出现致命错误时程序会终止,也就不会发送任何信息出去了)。唯一能发现它们的方法就是监视服务器的PHP错误日志。这个相当重要,一旦没有任何Worker在处理工作,这将是一件麻烦事。


本文由冰翼翻译自Kamisama.me

阅读全文 »