ホームページ >バックエンド開発 >PHPチュートリアル >PHPカーネルで探る変数 (5) - セッションの基本原則

PHPカーネルで探る変数 (5) - セッションの基本原則

WBOY
WBOYオリジナル
2016-08-08 09:30:29975ブラウズ

今回はセッションについてお話しましょう

セッションはインターネット上で最もよく言われる用語の一つと言えます。その意味は非常に幅広く、完全なトランザクション インタラクション (セッション) を指します。たとえば、HTTP リクエストの送信と応答の受信、SQL ステートメントの実行はセッションと見なされます。特に指定がない限り、この記事で言及されているセッションは HTTP セッションのみを指します。

この記事は、PHP カーネルの探索に関する 5 番目の記事です。主に次の側面が含まれます。

    背景知識とセッションの基本
  1. PHPにおけるセッションの原理
  2. 参考文献
  3. 1. 背景知識、セッションの基本

1. HTTP はステートレスです

HTTP プロトコルはもともと匿名の

ステートレス リクエスト/レスポンス プロトコルであったことがわかっています。このようなシンプルな設計により、HTTP プロトコルはリソースの送信に集中できるようになり (HTTP はハイパーテキスト転送プロトコルです)、パフォーマンスが向上します。ただし、このステートレスなデザインは、インタラクティブな Web アプリケーションの開発を妨げることも証明されています。典型的な例には、電子商取引 Web サイトでは注文、ショッピング カート、トランザクションなどの機能を実装するためにユーザー情報を取得する必要があり、SNS Web サイトではユーザーを取得する必要があります。本当の「ソーシャル ネットワーク」を構築するには、映画や CD のレンタル Web サイトでもユーザー情報を取得して、パーソナライズされた推奨を提供する必要があり、それによってより良いメリットがもたらされます。これは、ユーザー情報を識別して管理するために何らかのテクノロジーを使用する必要があることを意味します。Cookie とセッションのテクノロジーはこの文脈で生まれました。 2. セッションとクッキー

セッションと言えば、セッションの良き友人である Cookie について言及する必要があります。多くの場合、セッションは session_id を保存するために Cookie に依存しているためです。セッションとクッキーの違いについて話したい場合は、次のような共通の違いを簡単に暗唱できる生徒もいると思います:

(1) したがって、Cookie はクライアント側に保存され、Session は状態を維持するためのテクノロジーです。サーバー側に保存されます。

(2). ほとんどの場合、セッションは session_id を保存するためのキャリアとして Cookie を使用する必要があるため、Cookie が無効になっている場合は、session_id を他の手段 (get または post で session_id を渡すなど) で取得する必要があります。 )

(3). Cookie の有効期限と削除はクライアントの接続を確実に無効にするだけであり、サーバーのセッションはクリアされません

(4). デフォルトでは、Session と Cookie の両方がファイルを書き込みます (Session はデータベースまたは memcached などの他のメモリ キャッシュに書き込むこともできます)。ただし、Cookie はブラウザの設定によって異なります。たとえば、IE6 には下限があります。 Cookie はドメイン名あたり最大 20 個で、多くのブラウザでは Cookie のサイズが 4096 バイト以下に制限されています。

でも

深い理解。 3. phpでのSessionの基本操作 php では、セッション関連の操作は

拡張機能

(ソース コード ディレクトリ: PHPSRC/ext/session/) の形式で提供されます。 PHP は、セッションを操作するための豊富な API を多数提供しています:

(1).セッション開始

リーリー

セッションを開始するには、Session_start()

を使用します。一般的に、$_SESSIONを使用するときは、最初にsession_startを呼び出す必要があります(または、php.iniでsession.auto_startが設定されています)。それでは、

session.auto_start=false

の場合、session_startはセッション操作のために呼び出さなければならない最初の関数である必要があるのでしょうか?答えは否定的です。通常、セッションを操作する必要がある場合は、基本的にスクリプトの最初の行に session_start() を記述しますが、実際には session_start が呼び出された時点で、セッションに関連するすべてのパラメーターが初期化されます。セッションパラメータ情報は、session_namesession_set_cookie_paramssession_save_pathなどの関数を通じて変更できます。したがって、セッションの関連パラメータを変更する必要がある場合は、ini ファイル内で変更する (または ini_set を使用して変更する) だけでなく、session_name、session_save_path、session_set_cookie_params などの関数を使用して変更することもできます。これらの関数は、 session_startの前に呼び出されます。例: リーリー session_start()が呼び出された後、セッションの基本パラメータの設定に加えて、一定の確率でセッションの

GC

も開始されます。

(2).session_id()

  如同数据库中每条记录需要一个主键一样,Session也需要一个id值用于唯一标识一个Client,这个标识便是session_id。函数session_id()用于设置或者更改当前会话的session_id,例如:

session_save_path('/root/xiaoq/phpCode/session');
session_start();                                   

$_SESSION['index'] = "this is desc";
$_SESSION['int']   = 123;

print_r( session_id());//5rdhbe4k8k73h5g1fsii01iau5

在设置了session.save_handler=files的情况下,服务器端是以sess_{session_id}的命名方式来储存Session数据文件的:

 

  正常情况下,不同会话的session_id是不会重复的。在已知session_id的情况下,我们可以通过传递session_id的方法来获取Session数据,从而避开Cookie的限制:

session_save_path('/root/xiaoq/phpCode/session');
session_id("5rdhbe4k8k73h5g1fsii01iau5");
session_start();

print_r($_SESSION);
/* Array
(
    [index] => this is desc
    [int] => 123
) */

  Session文件存储会有很多问题和瓶颈,关于这一点,之后也会有详细的说明和解释。

(4).  session_write_close/session_commit

  默认情况下,session数据是在当前会话结束时(一般就是指脚本执行完毕时)才会写入文件的,这样会带来一些问题。例如,如果当前脚本执行过长,那么当其他脚本访问同一session_id下的session数据时便会阻塞(这实际上会涉及到文件锁flock,之后会有说明),直到前一脚本执行完毕并写入session文件。可以用sleep来简单模拟这一情况:

session_save_path('/root/xiaoq/phpCode/session');
session_start();

$_SESSION['index'] = "this is desc";
$_SESSION['int']   = 123;

sleep(15);

  避免这一情况的一种方法是:在session数据使用完毕之后,调用session_commit或者session_write_close函数通知服务器写入session数据并关闭文件(释放flock的锁):

session_save_path('/root/xiaoq/phpCode/session');
session_start();

$_SESSION['index'] = "this is desc";
$_SESSION['int']   = 123;
session_commit();

sleep(15);

注意session_commit和session_write_close只是同一函数的不同别名。

(5).  session_destroy

       很多同学在会话结束的时候,都是通过unset($_SESSION)的方式来删除会话数据(这与session_unset()的作用类似)。实际上这样并不是稳妥的做法,原因是:unset($_SESSION)只是重置$_SESSION这个全局变量,并不会将session数据从服务器端删除。较为稳妥的做法是,在需要清除当前会话数据的时候调用session_destroy删除服务器端Session数据(同时,最好使Cookie也过期):

session_save_path('/root/xiaoq/phpCode/session');
session_start();

$_SESSION['index'] = "this is desc";
$_SESSION['int']   = 123;

unset($_SESSION);
session_destroy();

3.  session的ini配置

       由于Session的很多操作依赖于ini中的参数配置,因此我们有必要对此做一个比较全面的了解。php.ini比较重要的Session参数配置包括:

       (1).  session.save_handler

       这个参数用于指定Session的存储方式(实际上是指定了一个处理Session的句柄)。可以是files(文件存储,默认), user( 用户自定义存储 ),或者其他的扩展方式(如memcache)。

       (2).  session.save_path

       在使用session.save_handler=files的情况下,session.save_path用于指定Session文件存储的目录,如session.save_path= “/tmp”;这种配置下,所有的session文件都是写入一个目录的。这在某些情况下是有问题的(如有的系统单目录下支持的文件数是有限制的,而且,同一目录下文件过多,会造成读取变慢)。session.save_path还支持多级目录hash的方式:session.save_path = "N;/path"; 这种配置方式会将session文件分散到不同的子目录中,避免单目录文件文件过多。同样,这种配置方式也有较大的问题:如Session的GC是无效的,而且,PHP并不会自动为你创建子目录,需要手动创建或者通过脚本创建。

       (3).  session.name

       在使用Cookie为载体的情况下,session.name指定存储session_id的Cookie的key( cookie中也是基于key=>value)。默认的情况下,session.name= PHPSESSID

,可以更改为任何合法的其他名称。同样,也可以通过session_name函数,在调用session_start之前设置这个key的名称:

session_name("NEW_SESSION");
session_start();

$_SESSION['index'] = "this is desc";
$_SESSION['int']   = 123;

抓包可以看到,现在,Cookie中是以新的session.name来传递session_id了,而第一次服务器端的响应中,也会发送Set-Cookie:

 

       (4).  session.auto_start

       这个参数用于指定是否需要自动开启session,在设置为true的情况下,不需要在脚本中显式的调用session_start(). 如果不是特殊需要,我们并不建议开启session.auto_start.

       (5).  session.gc_*

       主要用于配置session GC的相关参数。关于这点,我们在后面会有详细讲解,这里暂时搁置

       (6).  session.cookie_*

       主要用于配置session的载体cookie的相关参数信息,如cookie的path, lifetime, 域domain等。

       关于Session的更多配置,可以参考:

http://cn2.php.net/manual/zh/session.configuration.php

二、  under the hood  - PHP中session的原理

       现在,我们对Session已经有了一个基本的认识,接下来,我们将更深入的去探讨和挖掘Session的更多细节。这一部分的内容比较枯燥乏味,对于不需要了解Session内部细节的同学,完全可以略过。接下来的部分,如果没有特殊说明,都是指session.save_handler=files的情况。

1.      session模块的初始化MINIT

       前面我们提到,在php中,Session是以扩展的形式加载的,因此,它也会经历扩展的MINIT -> RINIT -> RSHUTDOWN -> MSHUTDOWN等阶段。PHP_MINIT_FUNCTION和PHP_RINIT_FUNCTION是php启动过程中两个关键点:在php启动时,会依次调用各个扩展模块的PHP_MINIT_FUNCTION来完成各个扩展模块的初始化工作,而PHP_RINIT_FUNCTION则在对模块的请求到来时作一些准备性工作。对于Session而言,PHP_MINIT_FUNCTION主要完成的初始化工作包括(注:不同版本的PHP具体处理过程并不完全相同,如PHP 5.4+提供了SessionHandlerInterface,这样可以通过session_set_save_handler ( SessionHandlerInterface $sessionhandler )的方式自定义Session的处理机制,而不必像之前一样使用冗长的bool session_set_save_handler ( callable $open , callable $close , callable $read , callable $write , callable $destroy , callable $gc [, callable $create_sid ] )):

(1).  注册$_SESSION超全局变量:

zend_register_auto_global("_SESSION", sizeof("_SESSION")-1, NULL TSRMLS_CC);

也就是说,$_SESSION超全局变量实际上是在session的MINIT阶段被注册的。

(2).  读取ini文件中的相关配置。

REGISTER_INI_ENTRIES();

 REGISTER_INI_ENTRIES();实际上是一个宏定义:

#define REGISTER_INI_ENTRIES() zend_register_ini_entries(ini_entries, module_number TSRMLS_CC)

因此,实际上是调用zend_register_ini_entries(ini_entries, module_number TSRMLS_CC)。关于ini文件的解析和配置,已经超出了本文的范畴,可以参考这篇文章:http://www.cnblogs.com/driftcloudy/p/4011954.html 。

   扩展中读取和设置ini的相关配置位于PHP_INI_BEGIN和PHP_INI_END宏之间。对于session而言,实际上包括:

PHP_INI_BEGIN()

       STD_PHP_INI_BOOLEAN("session.bug_compat_42",    "1",         PHP_INI_ALL, OnUpdateBool,   bug_compat,         php_ps_globals,    ps_globals)
       STD_PHP_INI_BOOLEAN("session.bug_compat_warn",  "1",         PHP_INI_ALL, OnUpdateBool,   bug_compat_warn,    php_ps_globals,    ps_globals)
       STD_PHP_INI_ENTRY("session.save_path",          "",          PHP_INI_ALL, OnUpdateSaveDir,save_path,          php_ps_globals,    ps_globals)
       STD_PHP_INI_ENTRY("session.name",               "PHPSESSID", PHP_INI_ALL, OnUpdateString, session_name,       php_ps_globals,    ps_globals)
       PHP_INI_ENTRY("session.save_handler",           "files",     PHP_INI_ALL, OnUpdateSaveHandler)
       STD_PHP_INI_BOOLEAN("session.auto_start",       "0",         PHP_INI_ALL, OnUpdateBool,   auto_start,         php_ps_globals,    ps_globals)
       STD_PHP_INI_ENTRY("session.gc_probability",     "1",         PHP_INI_ALL, OnUpdateLong,   gc_probability,     php_ps_globals,    ps_globals)
       STD_PHP_INI_ENTRY("session.gc_divisor",         "100",       PHP_INI_ALL, OnUpdateLong,   gc_divisor,         php_ps_globals,    ps_globals)
       STD_PHP_INI_ENTRY("session.gc_maxlifetime",     "1440",      PHP_INI_ALL, OnUpdateLong,   gc_maxlifetime,     php_ps_globals,    ps_globals)
       PHP_INI_ENTRY("session.serialize_handler",      "php",       PHP_INI_ALL, OnUpdateSerializer)
       STD_PHP_INI_ENTRY("session.cookie_lifetime",    "0",         PHP_INI_ALL, OnUpdateLong,   cookie_lifetime,    php_ps_globals,    ps_globals)
       STD_PHP_INI_ENTRY("session.cookie_path",        "/",         PHP_INI_ALL, OnUpdateString, cookie_path,        php_ps_globals,    ps_globals)
       STD_PHP_INI_ENTRY("session.cookie_domain",      "",          PHP_INI_ALL, OnUpdateString, cookie_domain,      php_ps_globals,    ps_globals)
       STD_PHP_INI_BOOLEAN("session.cookie_secure",    "",          PHP_INI_ALL, OnUpdateBool,   cookie_secure,      php_ps_globals,    ps_globals)
       STD_PHP_INI_BOOLEAN("session.cookie_httponly",  "",          PHP_INI_ALL, OnUpdateBool,   cookie_httponly,    php_ps_globals,    ps_globals)
       STD_PHP_INI_BOOLEAN("session.use_cookies",      "1",         PHP_INI_ALL, OnUpdateBool,   use_cookies,        php_ps_globals,    ps_globals)
       STD_PHP_INI_BOOLEAN("session.use_only_cookies", "1",         PHP_INI_ALL, OnUpdateBool,   use_only_cookies,   php_ps_globals,    ps_globals)
       STD_PHP_INI_ENTRY("session.referer_check",      "",          PHP_INI_ALL, OnUpdateString, extern_referer_chk, php_ps_globals,    ps_globals)
       STD_PHP_INI_ENTRY("session.entropy_file",       "",          PHP_INI_ALL, OnUpdateString, entropy_file,       php_ps_globals,    ps_globals)
       STD_PHP_INI_ENTRY("session.entropy_length",     "0",         PHP_INI_ALL, OnUpdateLong,   entropy_length,     php_ps_globals,    ps_globals)
       STD_PHP_INI_ENTRY("session.cache_limiter",      "nocache",   PHP_INI_ALL, OnUpdateString, cache_limiter,      php_ps_globals,    ps_globals)
       STD_PHP_INI_ENTRY("session.cache_expire",       "180",       PHP_INI_ALL, OnUpdateLong,   cache_expire,       php_ps_globals,    ps_globals)
       PHP_INI_ENTRY("session.use_trans_sid",          "0",         PHP_INI_ALL, OnUpdateTransSid)
       PHP_INI_ENTRY("session.hash_function",          "0",         PHP_INI_ALL, OnUpdateHashFunc)
       STD_PHP_INI_ENTRY("session.hash_bits_per_character", "4",    PHP_INI_ALL, OnUpdateLong,   hash_bits_per_character, php_ps_globals, ps_globals)
PHP_INI_END()

       如果在ini文件中没有配置相关的参数项,在session的MINIT阶段,参数会被初始化为默认的值。

(3).  自php 5.4起,php提供了SessionHandlerSessionHandlerInterface这两个Class, 因此还需要对这两个Class做相关的初始化工作。这是通过:

INIT_CLASS_ENTRY(ce, PS_IFACE_NAME, php_session_iface_functions);

INIT_CLASS_ENTRY(ce, PS_CLASS_NAME, php_session_class_functions);

来实现的,有兴趣的同学可以查看具体的实现过程,这里不再赘述。

2.      session请求时的准备RINIT

PHP_RINIT_FUNCTION(session) 用于完成session请求之时的准备工作,主要包括:

(1).  初始化session相关的全局变量,这是通过php_rinit_session_globals来完成的:

static inline void php_rinit_session_globals(TSRMLS_D)
{
    PS(id) = NULL;//session的id
    PS(session_status) = php_session_none;//初始化session_status
    PS(mod_data) = NULL;//session data
    PS(mod_user_is_open) = 0;
    /* Do NOT init PS(mod_user_names) here! */
    PS(http_session_vars) = NULL;
}

(2). 根据ini的配置查找session.save_handler,从而确定是使用files还是user( 或者是其他的扩展方式)来处理session:

if (PS(mod) == NULL) {
    char *value;

    value = zend_ini_string("session.save_handler", sizeof("session.save_handler"), 0);
    if (value) {
        PS(mod) = _php_find_ps_module(value TSRMLS_CC);
    }
}

  确定是user还是files来处理session的逻辑是由_php_find_ps_module来完成的,这个函数会依次查找ps_modules中预定义的module, 一旦查找成功,立即返回:

PHPAPI ps_module *_php_find_ps_module(char *name TSRMLS_DC)
{
       ps_module *ret = NULL;
       ps_module **mod;
       int i;
      
      for (i = 0, mod = ps_modules; i < MAX_MODULES; i++, mod++) {
              if (*mod && !strcasecmp(name, (*mod)->s_name)) {
                     ret = *mod;
                     break;
              }
       }
       return ret;
}

ps_modules的定义:

#define MAX_MODULES 10

static ps_module *ps_modules[MAX_MODULES + 1] = {
    ps_files_ptr,// &ps_mod_files
    ps_user_ptr//&ps_mod_user
};

而每一个ps_module,实际上是一个struct:

typedef struct ps_module_struct {
    const char *s_name;
    int (*s_open)(PS_OPEN_ARGS);
    int (*s_close)(PS_CLOSE_ARGS);
    int (*s_read)(PS_READ_ARGS);
    int (*s_write)(PS_WRITE_ARGS);
    int (*s_destroy)(PS_DESTROY_ARGS);
    int (*s_gc)(PS_GC_ARGS);
    char *(*s_create_sid)(PS_CREATE_SID_ARGS);
} ps_module;

  这意味着,每一个处理session的mod,不管是files, user还是其他扩展的模块,都应该包含ps_module中定义的字段,分别是:module的名称(s_name), 打开句柄函数(s_open), 关闭句柄函数(s_close), 读取函数(s_read) , 写入函数(s_write), 销毁函数(s_destroy), gc函数(s_gc),生成session_id的函数(s_create_sid)。例如,对于session.save_handler=files而言,实际上是:

{
       "files",
       ps_open_files,
       ps_close_files,
       ps_read_files,
       ps_write_files,
       ps_delete_files,
       ps_gc_files,
       php_session_create_id
}

  很多模块都是以PS_MOD(module_name)的方式定义,上述files的ps_module结构,便是PS_MOD(files)宏展开后的结果:

#define PS_MOD(x) \
    #x, ps_open_##x, ps_close_##x, ps_read_##x, ps_write_##x, \
     ps_delete_##x, ps_gc_##x, php_session_create_id

       上述宏定义我们也可以看出,session.save_handler不管是files, user,还是其他的session处理的handler(如memcache, redis等) 生成session_id的算法都是使用php_session_create_id函数来实现的。

       我们花费了大量的精力来说session.save_handler, 其实是想说明:原则上,session可以存储在任何可行的存储中的(例如文件,数据库,memcache和redis),如果你自己开发了一个存储系统,比memcache的性能更好,那么OK, 你只要按照session存储的规范,设置好session.save_handler,不管是你在脚本中提供接口还是使用扩展,可以很方便的操作session数据,是不是很方便?

       接着说RINIT的过程。

       确定完session的save_handler之后。需要确定serializer, 这个也是必须的。Serializer用于完成session数据的序列化和反序列化,我们在session.save_handler=files的情况下可以看到,session数据并不是直接写入文件的,而是通过一定的序列化机制序列化之后存储到文件的,在读取session数据时需要对文件的内容进行反序列化:

session_save_path('/root/xiaoq/phpCode/session');
session_start();

$_SESSION['key'] = 'value';
session_write_close();

则相应session文件的内容是:

<span>key</span>|s:5:"value"

查找serializer的过程与查找PS(mod)的方式类似:

if (PS(serializer) == NULL) {
    char *value;

    value = zend_ini_string("session.serialize_handler", sizeof("session.serialize_handler"), 0);

    if (value) {
        PS(serializer) = _php_find_ps_serializer(value TSRMLS_CC);
    }
}

_php_find_ps_serializer也是在预定义的ps_serializers数组中查找:

PHPAPI const ps_serializer *_php_find_ps_serializer(char *name TSRMLS_DC) {
    const ps_serializer *ret = NULL;
    const ps_serializer *mod;

    for (mod = ps_serializers; mod->name; mod++) {
        if (!strcasecmp(name, mod->name)) {
            ret = mod;
            break;
        }
    }
    return ret;
}

static ps_serializer ps_serializers[MAX_SERIALIZERS + 1] = {
    PS_SERIALIZER_ENTRY(php_serialize),
    PS_SERIALIZER_ENTRY(php),
    PS_SERIALIZER_ENTRY(php_binary)
};

同样,每一个serializer都是一个struct:

typedef struct ps_serializer_struct {
    const char *name;
    int (*encode)(PS_SERIALIZER_ENCODE_ARGS);
    int (*decode)(PS_SERIALIZER_DECODE_ARGS);
} ps_serializer;

       这时,如果mod不存在(设置的session.save_handler错误)或者serializer不存在,那么直接标记session_status为php_session_disabled,并返回,后面的代码不再执行。否则,确定了mod和serializer,如果设置了session.auto_start,那么就自动开启session:

if (auto_start) {
    php_session_start(TSRMLS_C);
}

由于session_start()时,也是调用php_session_start开启session,因此我们捎带着把session_start也一并分析。

3.      session_start

   session_start用于开启或者重用现有的会话,在底层,其实现为:

static PHP_FUNCTION(session_start)
{
    php_session_start(TSRMLS_C);

    if (PS(session_status) != php_session_active) {
        RETURN_FALSE;
    }
    RETURN_TRUE;
}

  内部是调用php_session_start完成session相关上下文的设置, 其基本步骤是:

(1).  检查当前会话的session状态。

php_session_status用于标志所有可能的会话状态,它是一个enum:

typedef enum {      
    php_session_disabled,
    php_session_none,
    php_session_active
} php_session_status;

那么可能的情况有:

  (a). session_status = php_session_active

  表明已经开启了session。那么忽略本次的session_start(), 但同时会产生一条警告信息:

A session had already been started - ignoring session_start()

  (b). session_status = php_session_ disabled

这种情况可能发生在RINIT的过程中,前面我们看到:

if (PS(mod) == NULL || PS(serializer) == NULL) {
    /* current status is unusable */

    PS(session_status) = php_session_disabled;
    return SUCCESS;
}

如果session_status = php_session_ disabled, 无法确定session是否真不可用(比如我们在脚本中设置了session_set_save_handler),还要做进一步的分析。查找mod和serializer的过程与RINIT的类似。

  (c). session_status = php_session_none

  在session_status= php_session_ disabled和php_session_none的情况下,都会继续向下执行。

(2).  如果session_id不存在,那么内核会依次尝试下列方法获取session_id(为了方便起见,我们直接使用了$_COOKIE, $_GET, $_POST,实际上这样是不严谨的,因为这些超级全局变量是php内核生成并提供给应用程序的,内核实际上是在全局的symbol_table中查找)

a.    $_COOKIE中

b.    $_GET中

c.    $_POST中

任何一此查找成功都会设置PS(id),不再继续查找。

(3).  执行php_session_initialize完成session的初始化工作。  

       注意此时PS(id)依然可能是NULL,这通常发生在第一次访问页面的时候。php_session_initialize完成的主要工作包括:

  a.  安全性检查

  正常情况下,生成的session_id不会包含html标签,单双引号和空白字符的,如果session_id中包含了这些非法的字符,那么很有可能session_id是伪造的。对于这种情况,处理很简单,释放session_id的空间,并标志为NULL,这样与第一次访问页面时的逻辑就基本一致了:

if (PS(id) && strpbrk(PS(id), "\r\n\t <>'\"\\")) {
    efree(PS(id));
    PS(id) = NULL;
}

  b.  为了稳妥起见,这里再次验证PS(mod)是否存在,如果不存在则返回错误。

  在PS(mod)存在的情况下,尝试打开句柄(对于session.save_handler=files而言,实际上是打开文件)。

  c.  session_id

  如果session_id不存在,那么会调用相应模块的s_create_sid方法创建相应的session_id。实际上,不管是user, files还是memcache,创建session_id时都是调用的PHPAPI char *php_session_create_id(PS_CREATE_SID_ARGS);有兴趣的同学可以看看生成session_id的算法,比较复杂,由于篇幅问题,这里并不跟踪。

  d.  尝试读取数据

  如果读取失败,则可能原因是session_id是无效的,那么重新尝试c中的步骤,直到读取成功。

if (PS(mod)->s_read(&PS(mod_data), PS(id), &val, &vallen TSRMLS_CC) == SUCCESS) {
    php_session_decode(val, vallen TSRMLS_CC);
    efree(val);
} else if (PS(invalid_session_id)) { /* address instances where the session read fails due to an invalid id */
    PS(invalid_session_id) = 0;
    efree(PS(id));
    PS(id) = NULL;
    goto new_session;
}

在这之前,其实还有一个逻辑:php_session_track_init,用于清除PHP中已经存在的$_SESSION数组(可能是垃圾数据):

static void php_session_track_init(TSRMLS_D)
{
    zval *session_vars = NULL;

    /* Unconditionally destroy existing array -- possible dirty data */
    zend_delete_global_variable("_SESSION", sizeof("_SESSION")-1 TSRMLS_CC);

    if (PS(http_session_vars)) {
        zval_ptr_dtor(&PS(http_session_vars));
    }

    MAKE_STD_ZVAL(session_vars);
    array_init(session_vars);
    PS(http_session_vars) = session_vars;

    ZEND_SET_GLOBAL_VAR_WITH_LENGTH("_SESSION", sizeof("_SESSION"), PS(http_session_vars), 2, 1);
}

4.      session的基本流程

到这里,session_start的流程基本走完了。我们据此总结一下在session.save_handler=files情况下,session的基本流程:

  • php启动的时候,完成session模块的初始化,其中包含对ini中session参数的处理。
  • 用户请求到达,完成模块的RINIT。如果ini中配置了session.auto_start,或者用户调用session_start,便开启session。
  • 尝试从Cookie, Get, Post中获取session_id, 如果没有获取到,说明这是一个新的session,则调用相应的算法生成session_id。打开对应的session文件。
  • 用户的业务逻辑,大多数情况下会包含对$_SESSION全局变量的操作。这些session数据并不是直接写入文件,而是存在内存中。
  • 调用session_commit或者脚本执行完毕时,session数据写入文件,关闭打开的session文件句柄。如果session_id是以Cookie存储的,那么在服务器端的响应中,还应该发送Set-Cookie的HTTP头,通知客户端存储session_id,之后的每次请求都应该携带这个session_id.

5.  session文件存储的问题

让我们回到之前提出的问题:在session.save_handler=files的情况下,会有哪些性能问题和瓶颈?

  a.  文件锁带来的性能问题

  前面我们已经提到,如果一个脚本的处理时间过程,且其中包含session的相关操作,那么其他脚本在访问session数据时便会阻塞,直到前一脚本执行完毕,这是为什么呢?在session/mod_files.c中ps_files_open函数中追踪到这样一句:

flock(data->fd, LOCK_EX);

由于是LOCK_EX(互斥锁),因而在文件锁定期间,即使是读取文件的数据也是不允许的。这就造成要写入或读取的进程必须等待,直到前一进程释放锁(这通常发生在脚本执行完毕或者用户调用session_commit/session_write_close)。

  b.  分布式服务器环境下session共享的问题

session文件存储实际上是存储在服务器的磁盘上的,这样在分布式服务器环境下会造成一定的问题:假如你有a,b,c三台服务器。则用户的多次请求可能按照负载均衡策略定向到不同的服务器,由于服务器之间并没有共享session文件,这在表象看来便发生了session丢失。这虽然可以通过用户粘滞会话解决,但会带来更大的问题:无法服务器的负载均衡,增加了服务器的复杂性

  c.  高并发场景下session,大量磁盘I/O

上記の理由に基づいて、実際のアプリケーションでは、多くは分散メモリキャッシュ memcache または redis を使用してセッションを保存および共有します。フルメモリ操作により、セッション操作のパフォーマンスが大幅に向上します。

セッションの探索は基本的にこれで終了ですが、解決する必要のある問題がまだたくさんあります:

  1. セッションの有効期限
  2. セッションGC
  3. Session_id 生成アルゴリズム
  4. セッションのシリアル化と逆シリアル化のメカニズム
  5. Memcache、redis、その他のセッションのサポート
  6. $_SESSION スーパーグローバル変数のメンテナンス

これらは 1 つずつ説明されているわけではありません。興味のある学生はソース コードの実装を追跡できます。

時間が急で個人レベルが限られているため、記事には必然的に間違いが含まれる可能性がありますので、ご指摘やご連絡をお待ちしております。最後に、記事の転載は自由ですが、個人の業績を尊重し、出典を明記してください。

4.参考文献

1. http://www.tuicool.com/articles/26Rrui

2. 「HTTP の決定版ガイド」

3. http://www.cnblogs.com/shiyangxt/archive/2008/10/07/1305506.html

4. http://blog.163.com/lgh_2002/blog/static/4401752620105246517509/

5. http://www.cnblogs.com/driftcloudy/p/4011954.html

上記は、PHP カーネル探索における変数 (5) セッションの基本原則を、関連する内容も含めて紹介しています。PHP チュートリアルに興味のある友人に役立つことを願っています。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。