标题:【Laravel系列7.3】Session与响应
Session与响应
Session 这个东西还需要多说?学 PHP 或者任何 Web 开发语言的入门课好不好!既然这么说,那么看来你是没用过 Laravel 自带的 Session 呀,Laravel 的 Session 可不是用得 PHP 默认的那个 Session 哦。今天我们就来一起看看 Laravel 自己实现的这个 Session 是啥样的。另外,请求流程我们在最早的时候就已经讲过了,但是响应一直都没怎么提过,咱们也一起来看一看。
Session
默认情况下对于普通的 Session 来说,我们的 PHP 会生成一个临时文件存储到系统的 /tmp 目录下,里面的内容实际上是一个序列化的对象结构。这个大家可以自己去试试,如果需要改成使用 memcached 或者 redis 的话,需要修改 php.ini 文件,还是比较麻烦的。Laravel 框架没有使用默认的 PHP 的 Session 来进行存储,而是自己实现了一套。其实功能差不多,默认同样是存储成文件,但如果需要修改存储方式的话,框架就会方便很多,毕竟不用去动 php.ini 文件。
Route::get('session/test', function(){
\Illuminate\Support\Facades\Session::put('a', 'aaaaaaa');
echo request()->session()->get('a'); // aaaaaaa
echo session()->get('a'); // aaaaaaa
echo \Illuminate\Support\Facades\Session::getId(); // SZL5LXKfJTm9ZRotUZRxM59qXO4IcmdKollBMFW9
});
在这里我们展示了三种操作 Session 的方式,一个是通过门面的 Session 对象,另一个则是通过请求对象 Request 的 session() 方法,最后还可以通过 session() 这个全局辅助函数来操作。put() 添加数据,get() 获取数据,getId() 获得 Session ID ,都非常简单。
这个时候,你可以去 storage/framework/sessions 目录下面找到对应 Session ID 名称的缓存文件,它的内容是下面这个样子的。
a:5:{s:6:"_token";s:40:"EZOEx7QwZNKbibc9hHOqJRo07Srd5RT9q5mYVmVC";s:1:"a";s:7:"aaaaaaa";s:6:"_flash";a:2:{s:3:"new";a:0:{}s:3:"old";a:1:{i:0;s:1:"b";}}s:9:"_previous";a:1:{s:3:"url";s:28:"http://laravel8/session/test";}s:1:"b";s:3:"bbb";}
后面的 _flash 是我们马上要测试的另外一个功能保存的数据,暂时可以忽略掉。是不是和 PHP 默认的 Session 没啥区别,都是保存了一个序列化对象。另外需要注意的是,和普通的 Session 一样,我们在使用普通的 Session 前需要 session_start() 一下,在 Laravel 中,则是需要保证 app/Http/Kernel.php 中 StartSession 中间件没有被注释掉,当然,默认它是打开的。
闪存
闪存是什么东西?这可不是我们的 U盘 呀,而是一种一次性的 Session 机制。它的作用就是一个一次性的 Session 数据,当数据被放到缓存后,下一次或者别的请求只能从 Session 中取一次这个数据,再次取数据就没有了。注意,一定是别的请求,而不是当前这个请求。
Route::get('session/test', function(){
\Illuminate\Support\Facades\Session::flash('b', 'bbb');
echo request()->session()->get('b');
echo request()->session()->get('b');
});
Route::get('session/test2', function(){
echo request()->session()->get('b');
});
使用 flash() 方法就可以保存一条闪存数据,它在数据中的格式就是我们上面看到的 Session 内容中的 _flash 字段所保存的内容,可以看到在这个对象中还包含了 _previous 字段,其中就有我们保存数据时的链接地址。
在保存数据时的地址访问时,可以一直访问,但当你使用别的链接访问数据时,就只能访问一次了,不信你可以试试多访问几次 session/test2 这条链接。这时,再查看保存的 Session 数据,我们会发现 b 的数据内容已经没有了。
a:4:{s:6:"_token";s:40:"5I8GP7LNzygNcYeTKer8kINqdxMGY1ArTqAXd13n";s:1:"a";s:7:"aaaaaaa";s:6:"_flash";a:2:{s:3:"new";a:0:{}s:3:"old";a:0:{}}s:9:"_previous";a:1:{s:3:"url";s:29:"http://laravel8/session/test2";}}
这个就是闪存的作用。如果你想要将这个数据保存到其它的请求,而不是在 test2 中使用的话,可以使用 reflash() 。或者你想要将这个数据转换成正常的 Session 数据,那么可以使用 keep() 方法。这两个方法大家可以自己测试一下,官方文档上都有代码演示。
切换成 redis
默认情况下,我们的 Session 走的是文件存储,这个上面我们已经看到了,而且也很方便地能够找到生成的 Session 文件。对于正式的开发环境来说,稍微上一点规模的项目多少都会需要进行多台服务器的分布式布局,这个时候,如果 Session 还是以文件形式分布在不同的服务器,就会出现很尴尬的局面,那就是用户的请求可能并不一定每次都会落在同一台服务器。于是,使用外部的 公共硬盘 或者使用 Redis 或者 Memcached 之类的缓存框架来进行 Session 的保存就是非常常见的做法了。相对于 公共硬盘 来说,肯定是缓存服务效率更好,而且也更便于维护。
Laravel 中使用 Redis 或 Memcached 来进行 Session 保存非常简单,只需要修改 .env 配置文件就可以了,这里我们就以 Redis 为例。
SESSION_DRIVER=redis
SESSION_CONNECTION=default
直接修改 SESSION_DRIVER 驱动为 redis 即可,下面的 SESSION_CONNECTION 则是指定要使用的连接,也就是我们在 config/db.php 中配置的连接。
通过设置之后,我们再次访问测试页面,然后直接在 redis 中就可以看到一个 laravel_database_laravel_cache:SZL5LXKfJTm9ZRotUZRxM59qXO4IcmdKollBMFW9 键的缓存数据,里面的内容就是我们的 Session 信息。这个 key 使用的依然是 Laravel 生成的那个 Session ID 。
阻塞
默认情况下,Laravel 是允许使用同一 Session 的请求并发执行的。但是一小部分应用程序中可能会丢失 Session ,比如两个请求同时到达,其中一个设置另一个读取,这时候,读取的请求可能就是无法读取到内容的,或者两个请求同时写入同一个 Session 。其实这就是一个并发的问题,一般情况下,我们在 Swoole 或者 Java 中会加锁来实现,而 Laravel 框架则是提供了一个阻塞的能力。
Route::get('session/test', function(){
\Illuminate\Support\Facades\Session::flash('b', 'bbb');
echo request()->session()->get('b');
echo request()->session()->get('b');
sleep(10);
})->block($lockSeconds = 10, $waitSeconds = 10);
Route::get('session/test2', function(){
echo request()->session()->get('b');
})->block($lockSeconds = 10, $waitSeconds = 10);
在这段代码中,我们设置了一个闪存数据,同一个请求中,闪存可以无限次访问。然后我们让代码停顿 10秒 用于测试。接下来就是使用了一个 block() 方法来进行阻塞,它有两个参数,一个是 lockSeconds 表示加锁时间,另一个 waitSeconds 表示等待时间。
加锁时间也就是阻塞时间,如果请求的执行时间长,则在阻塞时间内会锁住请求,另一个请求的等待时间则是有锁情况下会一直等待会话锁的完成,如果超过了设置的 10秒 则会返回 LockTimeoutException 异常。大家可以先运行行一个请求,在等待的时候再运行第二个请求,当 sleep() 结束后,两个请求的结果才会返回。
Session 实现
相信大家对于如何找到源码实现内容已经非常熟悉了,那么我也就不多说了,直接去找到 vendor/laravel/framework/src/Illuminate/Session/SessionManager.php 就可以了。在这个类中,我们可以看到许多的 Session 驱动,依然还是以 Redis 的来看一看。
protected function createRedisDriver()
{
$handler = $this->createCacheHandler('redis');
$handler->getCache()->getStore()->setConnection(
$this->config->get('session.connection')
);
return $this->buildSession($handler);
}
protected function buildSession($handler)
{
return $this->config->get('session.encrypt')
? $this->buildEncryptedSession($handler)
: new Store($this->config->get('session.cookie'), $handler);
}
protected function createCacheHandler($driver)
{
$store = $this->config->get('session.store') ?: $driver;
return new CacheBasedSessionHandler(
clone $this->container->make('cache')->store($store),
$this->config->get('session.lifetime')
);
}
在 bulidSession 中,我们获得的是一个 Store 对象,传递进去的 handler 是在上面的 createRedisDriver() 中通过 createCacheHandler() 方法中定义的,其实在这个方法中,就是通过 服务容器 获得了一个 Cache 对象。当你使用 SESSION 门面或者 session() 辅助函数调用 Session 的操作函数时,其实是在 SessionManager 继承的 Manager 对象中,它实现了 __call 方法的调用,实际上最后调用的都是 Store 对象。
public function __call($method, $parameters)
{
return $this->driver()->$method(...$parameters);
}
接下来,在 vendor/laravel/framework/src/Illuminate/Session/Store.php 类中,我们就可以看到各种 Session 方法,在这里比较有意思的是,它是以这个对象进行保存的,也就是说,在执行 put()、get() 之类的方法时,其实操作的是 Store 中的数组
public function get($key, $default = null)
{
return Arr::get($this->attributes, $key, $default);
}
public function put($key, $value = null)
{
if (! is_array($key)) {
$key = [$key => $value];
}
foreach ($key as $arrayKey => $arrayValue) {
Arr::set($this->attributes, $arrayKey, $arrayValue);
}
}
那么 Session 是在什么时候保存的呢?这个就要看 startSession 这个中间件了。
// vendor/laravel/framework/src/Illuminate/Session/Middleware/StartSession.php
protected function handleStatefulRequest(Request $request, $session, Closure $next)
{
// If a session driver has been configured, we will need to start the session here
// so that the data is ready for an application. Note that the Laravel sessions
// do not make use of PHP "native" sessions in any way since they are crappy.
$request->setLaravelSession(
$this->startSession($request, $session)
);
$this->collectGarbage($session);
$response = $next($request);
$this->storeCurrentUrl($request, $session);
$this->addCookieToResponse($response, $session);
// Again, if the session has been configured we will need to close out the session
// so that the attributes may be persisted to some storage medium. We will also
// add the session identifier cookie to the application response headers now.
$this->saveSession($request);
return $response;
}
在 startSession 中间件中,handle() 最后会调用 handleStatefulRequest() 这个方法,可以看出,这个方法是一个后置中间件,在请求操作结束后,调用了 saveSession() 方法,它实际上调用的是 Manager 中的 save() 方法。关于这一块,大家可以自己尝试一下,让一个请求暂停然后看 Session 文件里数据有没有变化,然后暂停完成之后再看一下就明白了。当然,我们也可以手动调用 save() 方法实时保存。
响应
对于请求流程,大家已经非常熟悉了,也了解过在控制器或者路由中,想要返回响应的内容,直接 retrun 就可以了,不过对于具体的响应操作我们还是没有进行过深入的学习。今天就一起来学习一下响应的具体内容。
添加响应头及 Cookie
如果要返回响应内容,直接 return 数据就可以了,但如果想为响应增加头和 Cookie 信息的话,最简单的就是使用 response() 辅助函数。
Route::get('response/test1', function(){
return response('Hello test1', 200)
->header('Content-type', 'application/json')
->withHeaders([
'A'=>'A info',
'B'=>'B info'
])
->cookie('oppo', 'o', );
});
header() 方法可以指定单个响应头,而 withHeaders() 则可以以数组的方式设置多个响应头。cookie() 方法则是设置 Cookie 的方法,它的参数和我们普通的 Cookie 操作函数的参数是一致的,后面的可选参数中也可以设置过期时间、HttpOnly 等内容。
重定向与文件下载
对于重定向来说,我们可以看成是跳转至某个页面,可以直接写路由、使用路由别名,也可以直接跳转到某个控制器方法。
Route::get('response/test2', function(){
// return redirect('response/test1');
// return redirect('response/test1',301);
// return redirect()->route('rt3');
return redirect()->action([\App\Http\Controllers\TestController::class, 'test2'], ['id'=>1]);
});
Route::name('rt3')->get('response/test3', function(){
echo 111;
});
上面的测试代码中,第二行注释起来的测试代码我们还可以指定重定向的状态码。默认情况下走的是 302 跳转,在这里我们可以设置成 301 跳转。关于 302 和 301 的区别我就不再多说了,一个是临时重定向,一个是永久重定向,如果有不明白的小伙伴可以去查询一下相关的资料。不过更推荐的是好好学习一下 HTTP 相关的知识。
Route::get('response/test4', function(){
return response()->download(\Illuminate\Support\Facades\Storage::path('public/8cb3c505713a1e861169aa227ee1c37c.jpg'));
});
文件下载有一个非常简单的函数就是直接使用 download() 函数,里面指定文件路径就可以了。同时还有别的方式可以实现文件的下载,文档中写得很详细了,这里就不多说了。
响应流程
对于响应来说,通过查阅 response() 方法的实现就可以发现它返回的是一个 vendor/laravel/framework/src/Illuminate/Http/Response.php 对象,而这个对象又是继承自 Symfony 的 vendor/symfony/http-foundation/Response.php 。就和请求一样,它的底层实现依然是 Symfony 框架中的响应实现。
首先到 public/index.php 入口文件中,我们会发现这样的一段代码。
$response = tap($kernel->handle(
$request = Request::capture()
))->send();
在这里,Kernel 的 Handle() 方法实际上返回的就是一个 Response 对象。
public function handle($request)
{
try {
$request->enableHttpMethodParameterOverride();
$response = $this->sendRequestThroughRouter($request);
} catch (Throwable $e) {
$this->reportException($e);
$response = $this->renderException($request, $e);
}
$this->app['events']->dispatch(
new RequestHandled($request, $response)
);
return $response;
}
sendRequestThroughRouter() 方法在之前的 中间件 以及 服务容器 和 管道 相关的文章中都接触过,他就是我们请求处理的核心流程,在请求的最后,就会返回响应结果。在 index.php 中,Kernel 执行完 handle() 之后,会再调用一个 send() 方法。这个方法存在于 vendor/symfony/http-foundation/Response.php 中。
public function send()
{
$this->sendHeaders();
$this->sendContent();
if (\function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
} elseif (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) {
static::closeOutputBuffers(0, true);
}
return $this;
}
接下来我们再进入到 sendHeaders() 和 sendContent() 中。
public function sendHeaders()
{
// headers have already been sent by the developer
if (headers_sent()) {
return $this;
}
// headers
foreach ($this->headers->allPreserveCaseWithoutCookies() as $name => $values) {
$replace = 0 === strcasecmp($name, 'Content-Type');
foreach ($values as $value) {
header($name.': '.$value, $replace, $this->statusCode);
}
}
// cookies
foreach ($this->headers->getCookies() as $cookie) {
header('Set-Cookie: '.$cookie, false, $this->statusCode);
}
// status
header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode);
return $this;
}
public function sendContent()
{
echo $this->content;
return $this;
}
嗯,还需要继续解释吗?相信大家已经明白最后的输出就在这里完成了吧!
总结
今天学习了两块内容,不过其实都和请求响应有关,Session 是非常常用的功能,响应也是所有请求必不可少的。Session 之所以框架要重写一套而不用原生的,也是为了灵活起见,我们不需要去 php.ini 配置文件修改 Session 相关的功能。而响应则走的依然是 Symfony 的底层框架功能。就像 Laravel 的口号一样,让实现的代码更优雅,从而对这些功能又重新进行更适合自己的封装。
参考文档:
https://learnku.com/docs/laravel/8.x/session/9373
https://learnku.com/docs/laravel/8.x/responses/9370