月度归档:2010年12月

PHP源码阅读笔记三十七:PHP中的SESSION实现

PHP源码阅读笔记三十七:PHP中的SESSION实现
源码版本:php5.3.1
环境:VS2008
本文包括PHP中SESSION用到的COOKIE管理,缓存限制,序列化
【COOKIE管理】
在浏览器未关闭cookie的情况下,我们可以看到当有session id生成并返回给发送请求的客户端时,会有一些cookie信息写入到浏览器。其实现代码在session.c 1191行开始。

1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
/* *********************
   * Cookie Management *
   ********************* */
 
#define COOKIE_SET_COOKIE "Set-Cookie: "
#define COOKIE_EXPIRES	"; expires="
#define COOKIE_PATH	"; path="
#define COOKIE_DOMAIN	"; domain="
#define COOKIE_SECURE	"; secure"
#define COOKIE_HTTPONLY	"; HttpOnly"
 
static void php_session_send_cookie(TSRMLS_D) /* {{{ */
{
	smart_str ncookie = {0};
	char *date_fmt = NULL;
	char *e_session_name, *e_id;
 
	if (SG(headers_sent)) {
		char *output_start_filename = php_get_output_start_filename(TSRMLS_C);
		int output_start_lineno = php_get_output_start_lineno(TSRMLS_C);
 
		if (output_start_filename) {
			php_error_docref(NULL TSRMLS_CC, E_WARNING, "Cannot send session cookie - headers already sent by (output started at %s:%d)", output_start_filename, output_start_lineno);
		} else {
			php_error_docref(NULL TSRMLS_CC, E_WARNING, "Cannot send session cookie - headers already sent");
		}
		return;
	}
 
	/* URL encode session_name and id because they might be user supplied */
	e_session_name = php_url_encode(PS(session_name), strlen(PS(session_name)), NULL);
	e_id = php_url_encode(PS(id), strlen(PS(id)), NULL);
 
	smart_str_appends(&ncookie, COOKIE_SET_COOKIE);
	smart_str_appends(&ncookie, e_session_name);
	smart_str_appendc(&ncookie, '=');
	smart_str_appends(&ncookie, e_id);
 
	efree(e_session_name);
	efree(e_id);
 
	if (PS(cookie_lifetime) > 0) {
		struct timeval tv;
		time_t t;
 
		gettimeofday(&tv, NULL);
		t = tv.tv_sec + PS(cookie_lifetime);
 
		if (t > 0) {
			date_fmt = php_format_date("D, d-M-Y H:i:s T", sizeof("D, d-M-Y H:i:s T")-1, t, 0 TSRMLS_CC);
			smart_str_appends(&ncookie, COOKIE_EXPIRES);
			smart_str_appends(&ncookie, date_fmt);
			efree(date_fmt);
		}
	}
 
	if (PS(cookie_path)[0]) {
		smart_str_appends(&ncookie, COOKIE_PATH);
		smart_str_appends(&ncookie, PS(cookie_path));
	}
 
	if (PS(cookie_domain)[0]) {
		smart_str_appends(&ncookie, COOKIE_DOMAIN);
		smart_str_appends(&ncookie, PS(cookie_domain));
	}
 
	if (PS(cookie_secure)) {
		smart_str_appends(&ncookie, COOKIE_SECURE);
	}
 
	if (PS(cookie_httponly)) {
		smart_str_appends(&ncookie, COOKIE_HTTPONLY);
	}
 
	smart_str_0(&ncookie);
 
	/*	'replace' must be 0 here, else a previous Set-Cookie
		header, probably sent with setcookie() will be replaced! */
	sapi_add_header_ex(ncookie.c, ncookie.len, 0, 0 TSRMLS_CC);
}
/* }}} */

第1195~1200行 定义常量宏,这些定义的内容我们可以通过查看http协议的应答头中可以看到,Set-Cookie是字段名,其余的为此字段中可以包含的变量,我们可以通过与PHP自带的setcookie操作函数的参数比对进行学习,此函数的定义如下:

1
bool setcookie ( string $name [, string $value [, int $expire = 0 [, string $path [, string $domain [, bool $secure = false [, bool $httponly = false ]]]]]] )

相关参数说明请参考:http://docs.php.net/manual/zh/function.setcookie.php
第1208~1218行 关于是否已经发送了cookie应答的情况处理
第1221~1222行 使用PHP中的函数urlencode的C程序实现,以防止人为添加的session name和session id出现混乱
第1224~1227行 将Set-Cookie字段名,session name和session id的值添加到将要发送的cookie字符串中。
第1232~1245行 cookie的过期时间处理
第1247~1250行 cookie的路径处理
第1252~1255行 cookie的域名处理
第1257~1259行 cookie是否仅仅通过安全连接(https)发送cookie。
第1261~1263行 cookie是否在cookie中添加httpOnly标志(仅允许HTTP协议访问)的处理
最后通过sapi_add_header_ex函数将生成的字符串添加到应答头中。
【缓存限制器】
缓存限制器的结构定义:

1041
1042
1043
1044
typedef struct {
	char *name;	
	void (*func)(TSRMLS_D);
} php_session_cache_limiter_t;

如上所示代码,一个限制器包括一个名称和一个处理函数。
缓存限制器的实现原理
从其代码实现看,所谓的缓存限制器是通过返回不同的http请求的缓存控制字段内容达到缓存控制的目的。
默认情况下,session.cache_limiter = nocache
即使用CACHE_LIMITER_FUNC(nocache)
其中关于缓存控制器总共有4种方案,分别为:

1154
1155
1156
1157
1158
1159
1160
static php_session_cache_limiter_t php_session_cache_limiters[] = {
	CACHE_LIMITER_ENTRY(public)
	CACHE_LIMITER_ENTRY(private)
	CACHE_LIMITER_ENTRY(private_no_expire)
	CACHE_LIMITER_ENTRY(nocache)
	{0}
};

其调用代码在session.c1180 行,如下:

1180
1181
1182
1183
1184
1185
	for (lim = php_session_cache_limiters; lim->name; lim++) {
		if (!strcasecmp(lim->name, PS(cache_limiter))) {
			lim->func(TSRMLS_C);
			return 0;
		}
	}

以上代码为一个遍历缓存控制器所在数组,并判断其方案名与PS(cache_limiter)是否一致,如果一致则执行此缓存控制,并返回。

【序列化实现】
PHP的session存放的都是序列化后的数据。
默认情况下使用session.serialize_handler = php
在源码中默认给出了两种序列化的实现。其最多可以有11种序列化的方法,相关代码如下:

982
983
984
985
986
987
988
#define MAX_SERIALIZERS 10
#define PREDEFINED_SERIALIZERS 2
 
static ps_serializer ps_serializers[MAX_SERIALIZERS + 1] = {
	PS_SERIALIZER_ENTRY(php),
	PS_SERIALIZER_ENTRY(php_binary)
};

ps_serializer结构定义如下 :

161
162
163
164
165
166
167
168
#define PS_SERIALIZER_ENCODE_ARGS char **newstr, int *newlen TSRMLS_DC
#define PS_SERIALIZER_DECODE_ARGS const char *val, int vallen TSRMLS_DC
 
typedef struct ps_serializer_struct {
	const char *name;
	int (*encode)(PS_SERIALIZER_ENCODE_ARGS);
	int (*decode)(PS_SERIALIZER_DECODE_ARGS);
} ps_serializer;

序列化通过调用php_session_register_serializer函数实现序列化的注册过程
php_session_register_serializer函数遍历ps_serializers数组,并判断数组元素的name属性是否为NULL,如果为NULL,则将新的序列化函数添加到ps_serializers数组。

【其它维度的文章】
PHP5 Session 浅析I
PHP5 Session 浅析II
作者从另外一个维度分析了session的一些原理,值得学习一下。

PHP源码阅读笔记三十六:PHP中的SESSION实现之常规操作

PHP源码阅读笔记三十六:PHP中的SESSION实现之常规操作
源码版本:php5.3.1
环境:VS2008
本笔记包括PHP中SESSION用到的全局变量,session_id的生成算法,初始化及session的清除操作
【全局变量】
/ext/session/php_session.h 文件

98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
typedef struct _php_ps_globals {
	char *save_path;	//	保存路径 
	char *session_name;	//	session名称 默认为PHPSESSID 这个在cookie中会看到
	char *id;		//	session ID
	char *extern_referer_chk;	//	请求头中的"Referer"字段不包含此处指定的字符串则会话ID将被视为无效
	char *entropy_file;	// 指定这里建立 session id
	char *cache_limiter;	// 可以设为{nocache,private,public},以决定 HTTP 的缓存问题
	long entropy_length;	//	 从文件中读取多少字节 
	long cookie_lifetime;	//	为按秒记的cookie的保存时间
	char *cookie_path;	//	cookie的有效路径
	char *cookie_domain;	//	cookie的有效域 
	zend_bool  cookie_secure;	//	是否仅仅通过安全连接(https)发送cookie。
	zend_bool  cookie_httponly;	//是否在cookie中添加httpOnly标志(仅允许HTTP协议访问)
	ps_module *mod;	//	session的处理方式
	void *mod_data;
	php_session_status session_status;	//	session的状态
	long gc_probability;	//	在每次 session 初始化的时候开始的可能性。 
	long gc_divisor;	// 收集概率计算公式:gc_probability/gc_divisor
	long gc_maxlifetime;	// 在这里数字所指的秒数后,保存的数据将被视为´碎片(garbage)´并由gc 进程清理掉。 
	int module_number;	//	存储方式编号
	long cache_expire;	//	过期时间,单位为分钟 
	union {
		zval *names[6];
		struct {
			zval *ps_open;
			zval *ps_close;
			zval *ps_read;
			zval *ps_write;
			zval *ps_destroy;
			zval *ps_gc;
		} name;
	} mod_user_names;	//	用户自定义函数 你懂的
	zend_bool bug_compat; /* Whether to behave like PHP 4.2 and earlier */
	zend_bool bug_compat_warn; /* Whether to warn about it */
	const struct ps_serializer_struct *serializer;	//	序列化处理函数
	zval *http_session_vars;	//	存放session变量
	zend_bool auto_start;	//	是否自动开始 默认为0
	zend_bool use_cookies;	//	是否使用cookies 
	zend_bool use_only_cookies;	//	是否仅仅使用cookie在客户端保存会话ID
	zend_bool use_trans_sid;	/* contains the INI value of whether to use trans-sid */
	zend_bool apply_trans_sid;	/* whether or not to enable trans-sid for the current request */
 
	long hash_func;	//	生成SID的散列算法。SHA-1的安全性更高一些
#if defined(HAVE_HASH_EXT) && !defined(COMPILE_DL_HASH)
	php_hash_ops *hash_ops;
#endif
	long hash_bits_per_character;	//指定在SID字符串中的每个字符内保存多少bit,
	int send_cookie;	//	是否发送cookie头
	int define_sid;		//	session常量id
	zend_bool invalid_session_id;	/* allows the driver to report about an invalid session id and request id regeneration */
} php_ps_globals;

【初始化】
在PHP_MINIT_FUNCTION方法中,注册全局变量_SESSION,将session_status设置为php_session_none,注册ini实体
[PHP_RINIT_FUNCTION方法]

2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
 
static PHP_RINIT_FUNCTION(session) /* {{{ */
{
	php_rinit_session_globals(TSRMLS_C);
 
	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);
		}
	}
 
	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);
		}
	}
 
	if (PS(mod) == NULL || PS(serializer) == NULL) {
		/* current status is unusable */
		PS(session_status) = php_session_disabled;
		return SUCCESS;
	}
 
	if (PS(auto_start)) {
		php_session_start(TSRMLS_C);
	}
 
	return SUCCESS;
}
/* }}} */

第2011行 调用php_rinit_session_globals函数初始化一些变量,其代码吓:

79
80
81
82
83
84
85
86
87
88
/* Dispatched by RINIT and by php_session_destroy */
static inline void php_rinit_session_globals(TSRMLS_D) /* {{{ */
{
	PS(id) = NULL;
	PS(session_status) = php_session_none;
	PS(mod_data) = NULL;
	/* Do NOT init PS(mod_user_names) here! */
	PS(http_session_vars) = NULL;
}
/* }}} */

PHP_RINIT_FUNCTION第2102~2109行 如果PS(mod)存在,则跳过,否则根据session.save_handle初始化PS(mod);
第2111~2118行 如果PS(serializer)存在,则跳过,否则根据session.serialize_handler,通过_php_find_ps_serializer函数初始化PS(serializer);
第2120~2124行 处理session不可用状态
第2126~2129行 根据PS(auto_start)(即php.ini中的session.auto_start)来判断是否自动开启session

【session id的默认生成算法】
session.c 350行开始定义的php_session_create_id实现了session_id的默认生成算法
从其实现看,session id的内容和REMOTE_ADDR、当前时间和一个随机数有关
通过PS(hash_func)获取生成session id的函数,此默认为0,即MD5算法,以MD5为例,此时会初始化MD5算法(PHP_MD5Init),把REMOTE_ADDR及当前时间组成的字符串添加(PHP_MD5Update)到算法的加密内容中
然后,判断PS(entropy_length) (从文件中读取多少字节)是否大于0,如果大于0,则需要从PS(entropy_file) (指定建立 session id的文件)中读取PS(entropy_length)长的字符串,添加(PHP_MD5Update)到MD5算法的加密内容中
最后执行PHP_MD5Final操作,生成session id

【清除过期session】
在php.ini文件中对于session的配置项有三个是与过期session的清除有关的,它们是session.gc_probability、session.gc_divisor和session.gc_maxlifetime。其中session.gc_probability默认为1,session.gc_divisor默认为100,session.gc_maxlifetime默认为1440秒
如下所示代码在session.c 1469行,为php_session_start函数的部分实现。

1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
	if (PS(mod_data) && PS(gc_probability) > 0) {
		int nrdels = -1;
 
		nrand = (int) ((float) PS(gc_divisor) * php_combined_lcg(TSRMLS_C));
		if (nrand < PS(gc_probability)) {
			PS(mod)->s_gc(&PS(mod_data), PS(gc_maxlifetime), &nrdels TSRMLS_CC);
#ifdef SESSION_DEBUG
			if (nrdels != -1) {
				php_error_docref(NULL TSRMLS_CC, E_NOTICE, "purged %d expired session objects", nrdels);
			}
#endif
		}
	}

以上代码段在php_session_start函数中,表示每次启动session时都会执行,只是它会依据 PS(gc_probability)和 PS(gc_divisor)以及一个生成的随机数判断是否执行垃圾清除操作。默认其概率为1/100
php_combined_lcg会生成一个(0,1)的随机数
PS(mod)->s_gc是根据选择的存储方式调用其定义的垃圾收集方法
以files存储方式为例,PS_GC_FUNC(files)函数调用ps_files_cleanup_dir遍历指定的目录,查找文件名为”sess_”的文件(这点在mod_files.c的51行定义#define FILE_PREFIX “sess_”),通过判断当前时间与最后修改时间之差是否大于maxlifetime(即在php.ini中配置的session.gc_maxlifetime文件)来决定是否删除文件。

PHP源码阅读笔记三十五:PHP中的SESSION实现之多种存储方式

PHP源码阅读笔记三十五:PHP中的SESSION实现之多种存储方式
源码版本:php5.3.1
环境:VS2008

在php.ini中,可以看到配置项session.save_handler = files
默认情况下,php.ini 中设置的 SESSION 保存方式是 files(session.save_handler = files),即使用读写文件的方式保存 SESSION 数据,而 SESSION 文件保存的目录由 session.save_path 指定,文件名以 sess_ 为前缀,后跟 SESSION ID,如:sess_sf26st2tfamqbarvnkfo2aftf2。文件中的数据即是序列化之后的 SESSION 数据了。如果访问量大,可能产生的 SESSION 文件会比较多,这时可以设置分级目录进行 SESSION 文件的保存,效率会提高很多,设置方法为:session.save_path=”N;/save_path”,N 为分级的级数,save_path 为开始目录。当写入 SESSION 数据的时候,PHP 会获取到客户端的 SESSION_ID,然后根据这个 SESSION ID 到指定的 SESSION 文件保存目录中找到相应的 SESSION 文件,不存在则创建之,最后将数据序列化之后写入文件。读取 SESSION 数据是也是类似的操作流程,对读出来的数据需要进行解序列化,生成相应的 SESSION 变量。如果需要对session文件设置mode,设置方法为:session.save_path = “N;MODE;/path”

同样我们可以使用session_set_save_handler函数设置session的处理方法。
bool session_set_save_handler ( callback $open , callback $close , callback $read , callback $write , callback $destroy , callback $gc )
那么此时可能需要思考一些问题,对于save_handler的设置是如何实现的?在内部存在怎样的结构存储这些处理方法?不同的存储方式之间是如何替换的?如果以一个扩展的方法添加,则需要实现哪些方法?这些方法在哪里添加到系统中的?

【结构】
/ext/session/php_session.h文件

32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#define PS_OPEN_ARGS void **mod_data, const char *save_path, const char *session_name TSRMLS_DC
#define PS_CLOSE_ARGS void **mod_data TSRMLS_DC
#define PS_READ_ARGS void **mod_data, const char *key, char **val, int *vallen TSRMLS_DC
#define PS_WRITE_ARGS void **mod_data, const char *key, const char *val, const int vallen TSRMLS_DC
#define PS_DESTROY_ARGS void **mod_data, const char *key TSRMLS_DC
#define PS_GC_ARGS void **mod_data, int maxlifetime, int *nrdels TSRMLS_DC
#define PS_CREATE_SID_ARGS void **mod_data, int *newlen TSRMLS_DC
 
/* default create id function */
PHPAPI char *php_session_create_id(PS_CREATE_SID_ARGS);
 
typedef struct ps_module_struct {
	const char *s_name;	// session存储方式的名字 如默认的files
	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;

第43到52行 定义ps_module,用于存放整个php_session结构,通过方法名应该可以很容易的识别出其作用意图。
【以扩展方式支持session】
如果一个扩展模块需要支持session,则在其PHP_MINIT_FUNCTION方法中调用php_session_register_module方法。
如在/ext/sqlite/sqlite.c 1395行,其调用了php_session_register_module(ps_sqlite_ptr);,说明在此扩展模块初始化时已经将其作为一个session的存储方式加载到内存中了。并且在此扩展中有sess_sqlite.c文件专门处理关于session中的应该定义的各方法,其分别以宏PS_OPEN_FUNC、PS_CLOSE_FUNC、PS_READ_FUNC、PS_WRITE_FUNC、PS_DESTROY_FUNC、PS_GC_FUNC、PS_CREATE_SID_FUNC等给出。这些宏的定义如下:

57
58
59
60
61
62
63
#define PS_OPEN_FUNC(x) 	int ps_open_##x(PS_OPEN_ARGS)
#define PS_CLOSE_FUNC(x) 	int ps_close_##x(PS_CLOSE_ARGS)
#define PS_READ_FUNC(x) 	int ps_read_##x(PS_READ_ARGS)
#define PS_WRITE_FUNC(x) 	int ps_write_##x(PS_WRITE_ARGS)
#define PS_DESTROY_FUNC(x) 	int ps_delete_##x(PS_DESTROY_ARGS)
#define PS_GC_FUNC(x) 		int ps_gc_##x(PS_GC_ARGS)
#define PS_CREATE_SID_FUNC(x)	char *ps_create_sid_##x(PS_CREATE_SID_ARGS)

php_session_register_module方法的实现在/ext/session/session.c 1021行开始。如下:

1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
#define MAX_MODULES 10
#define PREDEFINED_MODULES 2
 
static ps_module *ps_modules[MAX_MODULES + 1] = {
	ps_files_ptr,
	ps_user_ptr
};
 
PHPAPI int php_session_register_module(ps_module *ptr) /* {{{ */
{
	int ret = -1;
	int i;
 
	for (i = 0; i < MAX_MODULES; i++) {
		if (!ps_modules[i]) {
			ps_modules[i] = ptr;
			ret = 0;
			break;
		}
	}
	return ret;
}
/* }}} */

从上面的代码我们可能看到:PHP中的存储方式只能有10种,如果需要更多的方式,则需要修改MAX_MODULES的值,重新编译PHP。
存储方式的添加顺序与对应的扩展模块加载顺序有关。

【关于session_set_save_handler】
此函数用于将session的存储方式设置为用户自定义的函数,此函数的各参数对应ps_module结构的各个部分,并且会是新session.save_handler配置为user
在将用户定义的函数设置为session的存储方式时会提前判断这些用户函数是否可用。如果存在一个函数不可用,则警告并退出,表示设置失败。

【关于各方法的调用】
对于各存储方式定义的函数以类似于PS(mod)->s_open的方式调用。
对于用户自定义的方法,通过PS(mod_user_names)保存函数名,在mod_user.c中以类似于retval = ps_call_handler(PSF(open), 2, args TSRMLS_CC);的方式调用。其中#define PSF(a) PS(mod_user_names).name.ps_##a

到此处理我们可以回答之前的问题。
1、save_handler的设置是如何实现的?
在php.ini中设置session.save_handler,在请求初始化时(PHP_RINIT_FUNCTION)查找对应的ps_module,将其赋值到全局变量PS(mod),后续都是通过PS(mod)来处理session的存储操作,如果用户使用了session_set_save_handler方法设置存储方式,则此时的存储方式为user,通过user的模块方法中转调用用户指定的方法
2、在内部存在怎样的结构存储这些处理方法?
此问题可以参见上面关于结构的说明,ps_module
3、不同的存储方式之间是如何替换的?
可以在php.ini中设置session.save_handler修改,或者使用session_set_save_handler使用用户自定义的函数
4、如果以一个扩展的方法添加,则需要实现哪些方法?
需要实现PS_OPEN_FUNC、PS_CLOSE_FUNC、PS_READ_FUNC、PS_WRITE_FUNC、PS_DESTROY_FUNC、PS_GC_FUNC、PS_CREATE_SID_FUNC等宏,这些分别对应ps_module的各个方法
5、这些方法在哪里添加到系统中的?
通过php_session_register_module函数在模块初始化时加载