思考PHP语言一:控制结构

思考PHP语言一:控制结构
【概述】
命令式程序设计语言的实质是赋值语句占主导地位。 赋值语句的目的是改变变量的值,因此在所有的命令式程序设计语言中,其共同部分就是不停的变幻变量的值,最后达到我们的目的。
然而我们的程序并不是仅仅是由赋值语句组成的。至少还需要两种额外的语言机制:
1、控制路径选择的方法。
2、控制某些语句重复执行的方法。
这些方法或语言机制我们称之为控制语句。
大量的控制语句可以提高程序语言的可写性,但是同时又会降低程序语言的可读性,为此,经常需要在这二者之间进行权衡。
【PHP中的选择语句】
1、双向选择语句
示例:

1
2
3
4
5
if (expr) {
	statement
}else{
	statement
}

PHP选择语句中的逻辑表达式可以是布尔表达式,也可以是结果为数字的表达式,但是在PHP内部只有一种变量zval,于是这些表达式结果类型对于PHP来说没有多大的意义

在PHP中,then子句和else子句可以是简单语句也可以是复合语句,如果是复合语句需要使用大括号将所有的复合语句括起来。

关于else的归属问题,以就近原则为准,如果需要在一个then子句中包含一个if语句,建议将其以复合语句处理,使用大括号将if语句括起来。如下所示代码:

1
2
3
4
5
6
7
if (expr) {
	if (expr2) {
		statement
	}
}else{
	statement
}

2、多向选择语句
多向选择语句允许在任意数目的语句或语句组中进行一种选择。多向选择器与双向选择器可以互相构造,都可以实现对方的功能。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
switch(expr) {
case 常量表达式1:
	statement 1;
	break;
......
 
case 常量表达式n:
	statement n;
	break;
[default: statement n + 1;]
 
}

PHP的switch语句使用了C的switch语句的语法,但是却有一些差别,PHP的case 表达式可以是数字,字符串或浮点数这些简单类型。
与双向选择语句一样,多向选择语句在选择的路径上可以是简单语句,也可以是复合语句,只是这里我们不需要使用大括号,此时我们可以叫它语句序列。
当所有的情况都不满足时,执行default路径,如果没有所谓的default情况,请将default子句也写上,并且报错吧。
但是,当出现了switch选择语句时,此时可以想想我们的代码是否可以重构了。
当某个case下没有 break语句时,请写下注释说明原因。

【PHP中的循环语句】
循环语句的作用是将一条或一组语句执行n(n>=0)的一种语句
在函数式程序设计语言中一般是通过递归完成循环语句的功能
1、计数器循环控制语句
这种形式的循环控制语句一般包括初始值,终值,步长,这些统称为循环参数
PHP的计数器循环控制语句是类似于C语言的。
示例如下:

1
2
3
for (表达式1; 表达式2;表达式3) {
	循环体
}

这就是我们所说的for语句,只是在PHP中,for语句的使用较少,用得最多的还是在后面会提到的基于数据结构的循环—foreach
for语句中的三个表达式都是可选的,如果缺少第二个表达式,则认为其结果为真,如果在循环体中没有退出操作的话,这将是一个死循环。并且每个表达式都是可以由多条语句组成,语句与语句之间用逗号隔开,此时表达式的值为最后一条语句的值。
虽然for语句属于计数器循环控制语句,但是它并没有明显的循环变量或循环参数,可以在循环体中改变所有的所有变量,这就导致了for语句的灵活性,同时,如果用得不好也将导致程序逻辑的混乱
2、逻辑控制循环语句
逻辑控制循环语句是基于布尔表达式的,所有的计数循环都可以使用逻辑循环来实现,反过来则不一定。
示例:

1
2
3
4
5
6
7
while (控制表达式) {
    循环体
}
 
do {
   循环体
}while(控制表达式);

逻辑控制语句分为先测试和后测试两种情况,这个在上面的示例中有所体现。与for语句一样,PHP的逻辑循环控制语句也是类似于C语言的实现。
对于while语句,当控制表达式的条件为真时执行循环体,对于do-while语句,先执行循环体,再判断控制表达式是否为真。while语句总会比do-while语句少执行一次。
3、基于数据的循环语句
顾名思义,基于数据的循环语句的循环是由数据结构中的元素的数目来控制的。一般来说,基于数据的循环语句会使用一种称之为迭代器的函数来实现元素的遍历。
PHP中可以使用预定义的一些函数来实现对数组的迭代访问,如current,next,reset等等。
然而我们使用得最多的还是foreach语句,foreach可以直接迭代访问数组,如果用户自己定义的对象需要使用此语句进行迭代访问,必须实现SPL的迭代器。
之前写过两篇关于迭代器的文章:
PHP中迭代器的简单实现及Yii框架中的迭代器实现
PHP源码阅读笔记二十四 :iterator实现中当值为false时无法完成迭代的原因分析

PHP源码阅读笔记三十八:base64_encode实现

PHP源码阅读笔记三十八:base64_encode实现
【什么是base64编码】
Base64是一种使用64基的位置计数法。它使用2的最大次方来代表仅可打印的ASCII 字符。这使它可用来作为电子邮件的传输编码。在Base64中的变量使用字符A-Z、a-z和0-9 ,这样共有62个字符,用来作为开始的64个数字,最后两个用来作为数字的符号在不同的系统中而不同。一些如uuencode的其他编码方法,和之后binhex的版本使用不同的64字符集来代表6个二进制数字,但是它们不叫Base64。

【base64编码产生的历史原因】
在Email的传送过程中,由于历史原因,Email只被允许传送ASCII字符,即一个8位字节的低7位。因此,如果您发送了一封带有非ASCII字符(即字节的最高位是1)的Email通过有”历史问题“的网关时就可能会出现问题。网关可能会把最高位置为0!由于以上原因,产生了Base64编码。

【base64_encode的PHP内部实现】
base64_encode是PHP的标准函数,它存在于标准扩展中,在ext/standard/base64.c 210行,以标准的PHP_FUNCTION(base64_encode)实现。如下所示代码:

210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
/* {{{ proto string base64_encode(string str)
   Encodes string using MIME base64 algorithm */
PHP_FUNCTION(base64_encode)
{
	char *str;
	unsigned char *result;
	int str_len, ret_length;
 
	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &str, &str_len) == FAILURE) {
		return;
	}
	result = php_base64_encode((unsigned char*)str, str_len, &ret_length);
	if (result != NULL) {
		RETVAL_STRINGL((char*)result, ret_length, 0);
	} else {
		RETURN_FALSE;
	}
}
/* }}} */

第218行 函数的参数输入,base64_encode仅有一个参数,字符串数据类型;
第221行 调用php_base64_encode函数实现base64编码;
第222~226行 返回编码后的值,如果编码成功,返回编码后的字符串,如果失败返回FALSE

[PHP_FUNCTION(base64_encode) -> php_base64_encode()]

56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
PHPAPI unsigned char *php_base64_encode(const unsigned char *str, int length, int *ret_length) /* {{{ */
{
	const unsigned char *current = str;
	unsigned char *p;
	unsigned char *result;
 
	if ((length + 2) < 0 || ((length + 2) / 3) >= (1 << (sizeof(int) * 8 - 2))) {
		if (ret_length != NULL) {
			*ret_length = 0;
		}
		return NULL;
	}
 
	result = (unsigned char *)safe_emalloc(((length + 2) / 3) * 4, sizeof(char), 1);
	p = result;
 
	while (length > 2) { /* keep going until we have less than 24 bits */
		*p++ = base64_table[current[0] >> 2];
		*p++ = base64_table[((current[0] & 0x03) << 4) + (current[1] >> 4)];
		*p++ = base64_table[((current[1] & 0x0f) << 2) + (current[2] >> 6)];
		*p++ = base64_table[current[2] & 0x3f];
 
		current += 3;
		length -= 3; /* we just handle 3 octets of data */
	}
 
	/* now deal with the tail end of things */
	if (length != 0) {
		*p++ = base64_table[current[0] >> 2];
		if (length > 1) {
			*p++ = base64_table[((current[0] & 0x03) << 4) + (current[1] >> 4)];
			*p++ = base64_table[(current[1] & 0x0f) << 2];
			*p++ = base64_pad;
		} else {
			*p++ = base64_table[(current[0] & 0x03) << 4];
			*p++ = base64_pad;
			*p++ = base64_pad;
		}
	}
	if (ret_length != NULL) {
		*ret_length = (int)(p - result);
	}
	*p = '\0';
	return result;
}
/* }}} */

第62~67行 输入判断,程序的健壮性处理,如果长度小于-2,或者长度大于2的(机器的int型长度 * 8 – 2)次方
第69行 按需分配内存
第72~80行 处理所给字符串的能被3整除的部分,在这里需要说明的是Base64编码要求把3个8位字节(3*8=24)转化为4个6位的字节(4*6=24),之后在6位的前面补两个0,形成8位一个字节的形式。
第83~94行 处理以3个字节划分后剩余的数据,分成只有一个字节,和两个字节的情况,分别在其后面添加一个或两个base64_pad,在这里base64_pad定义为:static const char base64_pad = ‘=’;
最后是添加’\0′,返回结果

【base64的URL应用】
Base64编码可用于在HTTP环境下传递较长的标识信息。例如,在Java持久化系统Hibernate中,就采用了Base64来将一个较长的唯一标识符(一般为128-bit的UUID)编码为一个字符串,用作HTTP表单和HTTP GET URL中的参数。在其他应用程序中,也常常需要把二进制数据编码为适合放在URL(包括隐藏表单域)中的形式。此时,采用Base64编码不仅比较简短,同时也具有不可读性,即所编码的数据不会被人用肉眼所直接看到。
然而,标准的Base64并不适合直接放在URL里传输,因为URL编码器会把标准Base64中的「/」和「+」字符变为形如「%XX」的形式,而这些「%」号在存入数据库时还需要再进行转换,因为ANSI SQL中已将「%」号用作通配符。
为解决此问题,可采用一种用于URL的改进Base64编码,它不在末尾填充’=’号,并将标准Base64中的「+」和「/」分别改成了「*」和「-」,这样就免去了在URL编解码和数据库存储时所要作的转换,避免了编码信息长度在此过程中的增加,并统一了数据库、表单等处对象标识符的格式。
另有一种用于正则表达式的改进Base64变种,它将「+」和「/」改成了「!」和「-」,因为「+」,「*」以及前面在IRCu中用到的「[」和「]」在正则表达式中都可能具有特殊含义。
此外还有一些变种,它们将「+/」改为「_-」或「._」(用作编程语言中的标识符名称)或「.-」(用于XML中的Nmtoken)甚至「_:」(用于XML中的Name)。
以上部分来源于维基百科http://zh.wikipedia.org/zh/Base64

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的一些原理,值得学习一下。