>  기사  >  백엔드 개발  >  PHP 커널에서 탐색되는 변수(5) - 세션의 기본 원칙

PHP 커널에서 탐색되는 변수(5) - 세션의 기본 원칙

WBOY
WBOY원래의
2016-08-08 09:30:29902검색

이번에는 세션에 대해 이야기해보겠습니다.

세션은 인터넷에서 가장 많이 언급되는 용어 중 하나라고 할 수 있습니다. 그 의미는 매우 광범위하며 HTTP 요청을 보내고 응답을 받거나 세션으로 간주될 수 있는 SQL 문을 실행하는 등 완전한 트랜잭션 상호 작용(세션)을 나타낼 수 있습니다. 별도로 지정하지 않는 한, 이 문서에서 언급된 세션은 HTTP 세션만을 의미합니다.

이 글은 PHP 커널 탐구에 관한 다섯 번째 글입니다. 주로 다음과 같은 내용을 담고 있습니다.

  1. 배경 지식 및 세션 기본
  2. PHP의 세션 원리
  3. 참고자료

1. 배경 지식, 세션 기본

1. HTTP는 상태 비저장입니다

우리는 HTTP 프로토콜이 원래 익명의 상태 비저장 요청/응답 프로토콜이라는 것을 알고 있습니다. 이러한 단순한 설계를 통해 HTTP 프로토콜은 리소스 전송(HTTP는 하이퍼텍스트 전송 프로토콜)에 집중할 수 있으므로 더 나은 성능을 얻을 수 있습니다. 그러나 이러한 상태 비저장 디자인은 대화형 웹 애플리케이션의 개발을 방해하는 것으로도 입증되었습니다. 일반적인 예는 다음과 같습니다. 전자 상거래 웹사이트는 주문, 장바구니, 거래 등의 기능을 구현하기 위해 사용자 정보를 얻어야 하며, SNS 웹사이트는 사용자 정보를 얻어야 합니다. 실제 '소셜 네트워크'를 구축하려면 영화, CD 대여 사이트에서도 사용자 정보를 얻어 개인화된 추천을 제공해야 하므로 더 나은 혜택을 누릴 수 있습니다. 이는 사용자 정보를 식별하고 관리하기 위해 어떤 종류의 기술을 사용해야 함을 의미합니다. 쿠키 및 세션 기술은 이러한 맥락에서 탄생했습니다.

2. 세션과 쿠키

세션에 관해 말하자면, 세션의 좋은 친구인 쿠키를 언급해야 합니다. 왜냐하면 많은 경우 세션은 쿠키를 사용하여 session_id를 저장하기 때문입니다. 세션과 쿠키의 차이점에 대해 이야기하려면 누구나 익숙해져야 한다고 생각합니다. 일부 학생들은 다음과 같은 공통 차이점을 쉽게 외울 수도 있습니다.

(1) 쿠키는 클라이언트 측에서 상태를 유지하기 위한 솔루션인 반면, 세션은 서버 측에서 상태를 유지하기 위한 기술이므로 쿠키는 클라이언트 측에 저장되고 세션은 다음 위치에 저장됩니다. 서버 측.

(2) 대부분의 경우 세션은 session_id를 저장하기 위한 전달자로 쿠키를 사용해야 합니다. 따라서 쿠키가 비활성화된 경우 다른 방법(예: get 또는 post를 통해)을 통해 session_id를 얻어야 합니다. session_id를 서버에 전달하는 방법)

(3) 쿠키 만료 및 삭제는 클라이언트 연결의 무효화만 보장할 수 있으며 서버측 세션은 삭제되지 않습니다

(4) 기본적으로 세션과 쿠키는 모두 파일을 작성하지만(세션은 데이터베이스나 memcached와 같은 다른 메모리 캐시에도 쓸 수 있음) 쿠키는 브라우저 설정에 따라 다릅니다. 예를 들어 IE6은 도메인 이름당 쿠키는 최대 20개이며, 많은 브라우저에서는 쿠키 크기를 4096바이트 이하로 제한합니다.

쿠키에 대한 추가 논의는 이 기사의 범위를 벗어납니다. 더 자세히 알고 싶은 학생은 "HTTP 권위 있는 가이드" "JavaScript 고급 프로그래밍" 을 참조하세요. 이 두 권의 책을 통해 쿠키에 대한 더 깊은 이해를 가지실 수 있을 거라 믿습니다.

3. PHP에서 세션의 기본 동작

PHP에서는 세션의 동작을 확장 형태로 제공합니다(소스 디렉터리: PHPSRC/EXT/Session/). PHP는 세션을 운영하기 위해 풍부한 API를 다수 제공합니다:

(1) 세션_시작

bool <span>session_start</span> ( void )

session_start()는 세션을 시작하는 데 사용됩니다. 일반적으로 $_SESSION을 사용할 때 먼저 session_start를 호출해야 합니다(또는 session.auto_start가 php.ini에 구성되어 있습니다). 그러면 session.auto_start=false의 경우 session_start가 반드시 세션 작업을 위해 호출되어야 하는 첫 번째 함수인가요? 대답은 '아니요'입니다. 일반적인 상황에서는 세션을 운영해야 할 경우 기본적으로 스크립트의 첫 번째 줄에 session_start()를 넣습니다. 실제로 session_start가 호출되면 이후에는 Session과 관련된 모든 매개변수가 초기화됩니다. 세션 매개변수 정보는 session_name, session_set_cookie_params, session_save_path 등의 함수를 통해 변경할 수 있습니다. 따라서 세션의 해당 매개변수를 변경해야 하는 경우 ini 파일에서 변경(또는 ini_set을 통해 변경)하는 것 외에 session_name, session_save_path, session_set_cookie_params 등의 함수를 통해서도 수정할 수 있으며, 이러한 함수는 반드시 session_start 전에 호출되어야 합니다. 예:

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

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

session_start() 호출 후 Session의 기본 매개변수 설정과 함께 Session의 GC도 일정 확률로 시작됩니다.

(2).세션_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 슈퍼 전역 변수 유지 관리

이것들은 하나씩 설명되지 않습니다. 관심 있는 학생들은 소스 코드 구현을 추적할 수 있습니다.

시간이 촉박하고 개인 수준의 한계로 인해 기사에 필연적으로 오류가 있을 수 있으니 지적과 소통을 환영합니다. 마지막으로 기사를 재인쇄해도 좋지만 개인적인 업적을 존중하고 출처를 표시해 주시기 바랍니다.

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으로 문의하세요.