响应

响应,指的是 HTTP 通信过程中服务端对客户端应答的一种方式。

我们知道,这包括状态行,响应头和响应正文三部分。

举一个例子

test.php

1
2
3
4
5
6
7
8
9
10
11
12
echo "hello world";
curl发起请求,我们看一下响应的信息

$ curl -i -XGET http://test.dev/test.php
HTTP/1.1 200 OK
Date: Wed, 13 Dec 2017 12:27:52 GMT
Server: Apache/2.4.17 (Win32) OpenSSL/1.0.2d PHP/5.6.15
X-Powered-By: PHP/5.6.15
Content-Length: 11
Content-Type: text/html; charset=UTF-8

hello world

可以很清晰的看出,响应码等于200,多个响应头信息以及响应体内容 hello world。

假如我们想改变状态行的信息,比如200变404, OK变 Not Found,php脚本如何处理呢?

我们知道,只需要在输出hello world之前,调用header函数处理一下就好了。

修改test.php脚本如下

1
2
3
4
5
<?php

header("HTTP/1.1 404 Not Found");

echo "hello world";

依然是通过上面的curl命令发起请求,看结果

1
2
3
4
5
6
7
8
9
$ curl -i -XGET http://test.dev/test.php
HTTP/1.1 404 Not Found
Date: Wed, 13 Dec 2017 12:36:05 GMT
Server: Apache/2.4.17 (Win32) OpenSSL/1.0.2d PHP/5.6.15
X-Powered-By: PHP/5.6.15
Content-Length: 11
Content-Type: text/html; charset=UTF-8

hello world

有没有注意到,真的仅仅是状态行有所改变。

当然,还有很多cookie以及其他头信息,都可以通过header函数操作。

说白了,响应,其实就这么回事,并没有太多复杂的东西。

以上,算是我们引出的一点点知识。

下面我们看yii2中,响应是如何实现的。

在yii2中,用response组件来表示响应组件,即 yii\web\Response 。

其结构如下

1
2
3
4
5
6
7
class Response extends \yii\base\Response
{
}

class Response extends Component
{
}

对比下 Request 组件,Response组件同样也没有过于复杂的父类结构。

我们可以使用 Yii::$app->get(‘response’) 或者 Yii::$app->getResponse() 获取 Response 组件。

下面我们准备接着【应用的生命周期之执行请求】一文,看看yii2是如何构建响应以及发送数据给客户端的。

找到 yii\web\Application::handleRequest 方法,我们从 $this->runAction 方法的调用处开始分析。

$result = $this->runAction($route, $params);
这里调用的是 yii\base\Module::runAction,前面我们详细的分析过这一过程,希望你还没忘得彻底。

以 /index.php?r=site/index 的请求为例,假设是一个正常的web请求且 site/index 操作返回的是一段文本,比如return 123;再或者return 一段html内容。

那么这段文本,便是 yii\base\Module::runAction 的结果值,即 $result = ‘一段文本’(实际上$result 并不局限于一段文本,这里只是拿一段文本为例)。

所以,从下面的代码中可以看出,else 成立。

1
2
3
4
5
6
7
8
9
10
if ($result instanceof Response) {
return $result;
} else {
$response = $this->getResponse();
if ($result !== null) {
$response->data = $result;
}

return $response;
}

随后,获取 response 组件,为 yii\web\Response::data 赋值并返回 response 实例。这便开始构建响应了。

yii\web\Response::data 属性,是响应的原始数据,注意哦,这个属性的值,并不是最终要发送给客户端的数据。最终发送给客户端的数据是经过格式化处理之后的内容,即 yii\web\Response::content 属性,等会我们再做介绍。

回到 yii\web\Application::handleRequest 方法,看下面这句return。

1
return $response;

估计不少同学瞬间懵了,这 return 给谁了呢?

下面这行代码你肯定记得。

1
(new yii\web\Application($config))->run();

是不是恍然大悟,我们是在 yii\base\Application::run 方法内调用的yii\web\Application::handleRequest 。

接着 yii\base\Application::run 方法分析。

1
2
3
4
5
6
7
8
9
$response = $this->handleRequest($this->getRequest());

$this->state = self::STATE_AFTER_REQUEST;
$this->trigger(self::EVENT_AFTER_REQUEST);

$this->state = self::STATE_SENDING_RESPONSE;
$response->send();

$this->state = self::STATE_END

yii\web\Application::handleRequest 方法被调用之后,标记应用的状态为请求结束 yii\base\Application::STATE_AFTER_REQUEST,并触发yii\base\Application::EVENT_AFTER_REQUEST事件。触发该事件之后,才算是对请求的结束画上圆满句号。

下面主要看这两行代码

1
2
$this->state = self::STATE_SENDING_RESPONSE;
$response->send();

这包括两个步骤

标记应用当前请求状态为 yii\base\Application::STATE_SENDING_RESPONSE,即标记下发送请求前的一个状态
调用 yii\web\Response::send 方法向客户端发送响应信息
下面我们着重分析一下 yii\web\Response,在看 yii\web\Response::send 方法之前,我们有必要了解一下 yii\web\Response::init 初始化 Response 组件时做了哪些工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function init()
{
if ($this->version === null) {
if (isset($_SERVER['SERVER_PROTOCOL']) && $_SERVER['SERVER_PROTOCOL'] === 'HTTP/1.0') {
$this->version = '1.0';
} else {
$this->version = '1.1';
}
}
if ($this->charset === null) {
$this->charset = Yii::$app->charset;
}
$this->formatters = array_merge($this->defaultFormatters(), $this->formatters);
}

首先是 HTTP 协议版本和响应的字符集charset的确定,接着是转化响应的数据格式集,默认的核心格式被定义在 yii\web\Response::defaultFormatters 方法内,包括 html,xml,json等。

后面我们就需要 yii\web\Response::formatters 属性定义的格式转化映射类处理要发送给客户端的数据。

接着看 yii\web\Response::send 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
public function send()
{
if ($this->isSent) {
return;
}
$this->trigger(self::EVENT_BEFORE_SEND);
$this->prepare();
$this->trigger(self::EVENT_AFTER_PREPARE);
$this->sendHeaders();
$this->sendContent();
$this->trigger(self::EVENT_AFTER_SEND);
$this->isSent = true;
}

yii\web\Response::isSent 属性,标记下响应是否已经发送了,如果发送了再调用send就避免重复发送了。

我们注意到yii\web\Response::send 方法内有多达三个事件的触发,别着急,我们一个一个的介绍。

首先是 yii\web\Response::EVENT_BEFORE_SEND 事件的触发,这允许我们在真正发送之前做一些什么。

前几天还有一位同学,大胆留言,事件这东西,用是会用了,但就是搞不懂事件有什么好处。

这里我再啰嗦一下。假设没有事件,也就是说没有官方给我们预留的那么多后门,你说说,万一我们真的想在客户端发送之前做点什么,怎么办?动手改改官方的源码?这肯定不行。

事实上大家都知道,通过composer安装下来的源码我们都不建议改动。

同样,在我们自己写的代码中,也有着相同的道理。这不光光是程序的调用问题,它也是一种解耦的方式,值得体会一番。

说远了,回来看 yii\web\Response::EVENT_BEFORE_SEND 触发,事实上这个事件的触发机制,我们大有用途,卖个关子,等会再说。

我们知道,response响应阶段,大部分会通知客户端,我(服务器)给你(客户端)的数据是什么格式的,数据又是什么。 这便是 yii\web\Response::prepare 方法的实现。

以 index.php?r=site/index 且返回的内容是一段文本为例,yii\web\Response::prepare 方法内便会发起对 yii\web\HtmlResponseFormatter::format 的调用。

yii\web\HtmlResponseFormatter::format 方法做两件事

设置响应头的 Content-Type
将响应的原始数据转化为响应的最终数据转换
当然,还有其他格式的数据转换,比如json,参考 yii\web\JsonResponseFormatter::format 的实现。

回到 yii\web\Response::send方法,yii\web\Response::prepare 方法执行后,将会触发 yii\web\Response::EVENT_AFTER_PREPARE 事件。

一切准备就绪后,php脚本开始着手响应头的构建。

yii\web\Response::sendHeaders 方法中,有两句值得注意的代码,我们说一说。

1
2
$statusCode = $this->getStatusCode();
header("HTTP/{$this->version} {$statusCode} {$this->statusText}");

在这里,便通过header函数修改响应状态行的信息。

所以,如果我们想在应用程序内,指定响应的状态码,状态描述等信息,最好在调用 yii\web\Response::sendHeaders 方法之前操作。

怎么操作?动动手指改改源码?

跟改源码说拜拜吧,这是不可行的方案!

记得我们刚刚说过 yii\web\Response::send函数内触发的三个事件吗?其中有两个事件是在 yii\web\Response::sendHeaders 方法调用之前触发,所以,我们就可以在触发 yii\web\Response::EVENT_BEFORE_SEND事件或者yii\web\Response::EVENT_AFTER_PREPARE事件修改状态码等信息。

yii\web\Response 通过 getStatusCode 函数获取状态码,所以,这也告诉我们,应该有一个函数 setStatusCode 可以修改状态码,事实也是如此。

关于状态码以及状态码的描述,yii\web\Response::httpStatuses 属性有一些定义,但是,你也可以自定义状态码以及httpStatuses属性来改变这一现状。这便是在开发rest api的时候,很多人留言要的自定义状态码和状态码描述。

除此之外,yii\web\Response::sendHeaders 方法更多的是向客户端发送响应头信息以及cookie信息。

最后,调用 yii\web\Response::sendContent 函数发送响应正文。

响应正文发送之后,触发 yii\web\Response::EVENT_AFTER_SEND 事件,设置 yii\web\Respons::isSent为true,标识 send 结束。

回到 yii\base\Application::run 方法。

$response->send 方法被调用之后,标记当前应用的状态为 yii\base\Application::STATE_END,return yii\web\Response::exitStatus。

以上,便是整个应用的生命周期过程。当然,这是在一切都正常的情况下。比如不抛出异常等。