Laravel系列7.3Session与响应

Posted 码农老张Zy

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Laravel系列7.3Session与响应相关的知识,希望对你有一定的参考价值。

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

以上是关于Laravel系列7.3Session与响应的主要内容,如果未能解决你的问题,请参考以下文章

Laravel表单篇----request请求,session存储,response响应以及中间件

在 jBoss 6.2 与 jBoss 7.3 中部署应用程序时的不同响应

Laravel 会话 ID 随每个请求而变化

Servlet之会话(Session)

Session是啥?

在 Laravel 项目文件夹中将 PHP 7.3 升级到 7.4