PHP源码分析-流的实现之parse_url的源码分析

PHP源码分析-流的实现之parse_url的源码分析

<scheme>://<user>:<password>@<host>:<port>/<path>;<params>?<query>#<frag>

此处是一个完整的url的组成,这里只对每个段做简单的说明,
scheme:协议部分,例如:http
user和password:用户名和密码,最原始的协议规定的用户名和密码,以http为例如果服务器设置了此选项而在访问时不输入此项时将返回401的状态.
host:域名 例如baidu.com.
port:端口号例如http的80
path:访问的文件路径.例如aa/bb.php
params:忽略(现在很少有统一定位资源符使用这个了,php中也没有关于这个的解析)
query:查询参数例如a=1&b=2
frag:锚点
其他详细的内容就不多描述了, 可以百度其他人的文章或者去看下<<http权威详解>>中的内容.
举例说明几个url的实例

http://www.ori.com/index.html
http://www.yahoo.com/images/logo.gif
http://www.zous.com/inven-check.cgi?item=12731
ftp://joe:tools4U@ftp.zous.com/looking.gif

首先根据标准定义来分析URL的组成,固定的可见字符中只有:/@;?#号,如果要得到其中的每个字段根据其中出现次数最多的:号来处理url是最合适的(可以进行相关的数学证明,但不是本次讨论的重点).
例如:
取到第一个分号的位置,假如其后两个字符紧跟着//部分,则可以认为分号前的是协议部分<scheme>
再从//下一个位置往后取如果匹配到@字符则认为中间部分是<user><password>,如果中间没有:号则认为这个是<user>没有password.
接着@下一个位置往后取到第一个/如果中间有:则证明其中有<host><port>如果没有则认为是只有host.
再往后取/的下一个位置.处理剩余的字符串#号之后的是<frag>
去掉<frag>后?号之后的是<query>
去掉<frag><query>之后的是<path>+<params>
这对url进行的初步的分析.
然后一起来看下php中的parse_url函数是如何实现的

PHP_FUNCTION(parse_url)
{
	//省略一些初始化的量
	//底层的核心调用就是这个函数
	resource = php_url_parse_ex(str, str_len);
	//省略另一些初始化的量
}

这里是调用的php_url_parse_ex函数,这个函数接受两个入参,url和url的长度
首先是

char port_buf[6];
php_url *ret = ecalloc(1, sizeof(php_url));
char const *s, *e, *p, *pp, *ue;

s = str;
ue = s + length;

这里定义port的临时缓存和五个指向字符串的指针const可以忽略(可根据简单的左定值右定向原则判断出来为什么可以忽略)
然后s指向str的第一个字符所在的地址.
ue的赋值有个特别的地方需要注意ue=s+length而不是ue = s+length-1 这也是为什么必须用const修饰的原因之一
ue指向字符串末尾的’\0’
接着往下看第一个判断

	/* parse scheme –解析协议简单明了的优秀注释*/
if ((e = memchr(s, ':', length)) && e != s) {
		/* validate scheme */
		p = s;
		while (p < e) {
			/* scheme = 1*[ lowalpha | digit | "+" | "-" | "." ] */
//这决定冒号只能是password和port之前的冒号逻辑有点绕得多读几遍代码
			if (!isalpha(*p) && !isdigit(*p) && *p != '+' && *p != '.' && *p != '-') {
				if (e + 1 < ue && e < s + strcspn(s, "?#")) {
					goto parse_port;
				} else if (s + 1 < ue && *s == '/' && *(s + 1) == '/') { /* relative-scheme URL */
					s += 2;
					e = 0;
					goto parse_host;
				} else {
					goto just_path;
				}
			}
			p++;
		}

		if (e + 1 == ue) { /* only scheme is available */
			ret->scheme = zend_string_init(s, (e - s), 0);
			php_replace_controlchars_ex(ZSTR_VAL(ret->scheme), ZSTR_LEN(ret->scheme));
			return ret;
		}

		/*
		 * certain schemas like mailto: and zlib: may not have any / after them
		 * this check ensures we support those.
		 */
		if (*(e+1) != '/') {
			/* check if the data we get is a port this allows us to
			 * correctly parse things like a.com:80
			 */
			p = e + 1;
			while (p < ue && isdigit(*p)) {
				p++;
			}

			if ((p == ue || *p == '/') && (p - e) < 7) {
				goto parse_port;
			}

			ret->scheme = zend_string_init(s, (e-s), 0);
			php_replace_controlchars_ex(ZSTR_VAL(ret->scheme), ZSTR_LEN(ret->scheme));

			s = e + 1;
			goto just_path;
		} else {
			ret->scheme = zend_string_init(s, (e-s), 0);
			php_replace_controlchars_ex(ZSTR_VAL(ret->scheme), ZSTR_LEN(ret->scheme));

			if (e + 2 < ue && *(e + 2) == '/') {
				s = e + 3;
				if (zend_string_equals_literal_ci(ret->scheme, "file")) {
					if (e + 3 < ue && *(e + 3) == '/') {
						/* support windows drive letters as in:
						   file:///c:/somedir/file.txt
						*/
						if (e + 5 < ue && *(e + 5) == ':') {
							s = e + 4;
						}
						goto just_path;
					}
				}
			} else {
				s = e + 1;
				goto just_path;
			}
		}
	}

memchr是C 库函数.
原型:extern void *memchr(void *buf, char ch, unsigned count);
用法:#include <string.h>
功能:从buf所指内存区域的前count个字节查找字符ch。
说明:当第一次遇到字符ch时停止查找。如果成功,返回指向字符ch的指针;否则返回NULL。

这里是在url种寻找:号第一次出现的位置找到了(这里有个坑,这里有个赋值操作,赋值之后冒号的开始位置在下边的else等等中也是可以使用的,在一般的代码中不推荐这种写法,除了晦涩难懂外没有任何用途)
e != s
假如找到了而且冒号不是在开头的位置出现
开始检验是否是有效的协议,
将p也指向开头的位置
开始循环处理过滤掉非字母非数字非+非.非-号的字符(检查没有协议的情况)

while (p < e) {
	/* scheme = 1*[ lowalpha | digit | "+" | "-" | "." ] */
	if (!isalpha(*p) && !isdigit(*p) && *p != '+' && *p != '.' && *p != '-') {
//省略其他
}
}

然后下边

if (!isalpha(*p) && !isdigit(*p) && *p != '+' && *p != '.' && *p != '-') {
				if (e + 1 < ue && e < s + strcspn(s, "?#")) {
					goto parse_port;
				} else if (s + 1 < ue && *s == '/' && *(s + 1) == '/') { /* relative-scheme URL */
					s += 2;
					e = 0;
					goto parse_host;
				} else {
					goto just_path;
				}
			}
			p++;

第一个判断遇到另一个函数
关函数 strspn
表头文件 #inclued<string.h>
定义函数 size_t strcspn ( const char *s,const char * reject);
函数说明 strcspn()从参数s字符串的开头计算连续的字符,而这些字符都完全不在参数reject 所指的字符串中。简单地说,若strcspn()返回的数值为n,则代表字符串s开头连续有n个字符都不含字符串reject内的字符。
返回值 返回字符串s开头连续不含字符串reject内的字符数目。
这个函数在这里的作用就是返回从字符串开头开始最长的不包含?号和#号的长度.
而这个条件的意思就是e不是最后一个字符串而且e在开始的字符和?号或者#号之前.
e所需指向的:号只能使password之前或者port之前的:号.然后跳转解析port.
如果s是双斜线开头直接将s后移两位e重置跳转host解析
如果都不是则被认为是path,跳转path解析.

再往下这里正常跳出循环的条件时:号之前全部是字母数字或者+.-组成的字符串
跳出循环之后

//如果这里:号之后再无字符认为只有协议,赋值并返回
if (e + 1 == ue) { /* only scheme is available */
	ret->scheme = zend_string_init(s, (e - s), 0);
	php_replace_controlchars_ex(ZSTR_VAL(ret->scheme), ZSTR_LEN(ret->scheme));
	return ret;
}

再往下

/*
		 * certain schemas like mailto: and zlib: may not have any / after them
		 * this check ensures we support those.
		 */
//此处是支持协议之后不是/字符的url
		if (*(e+1) != '/') {
			/* check if the data we get is a port this allows us to
			 * correctly parse things like a.com:80
			 */
			p = e + 1;
			while (p < ue && isdigit(*p)) {
				p++;
			}

			if ((p == ue || *p == '/') && (p - e) < 7) {
				goto parse_port;
			}

			ret->scheme = zend_string_init(s, (e-s), 0);
			php_replace_controlchars_ex(ZSTR_VAL(ret->scheme), ZSTR_LEN(ret->scheme));

			s = e + 1;
			goto just_path;
		} else {
			ret->scheme = zend_string_init(s, (e-s), 0);
			php_replace_controlchars_ex(ZSTR_VAL(ret->scheme), ZSTR_LEN(ret->scheme));

			if (e + 2 < ue && *(e + 2) == '/') {
				s = e + 3;
				if (zend_string_equals_literal_ci(ret->scheme, "file")) {
					if (e + 3 < ue && *(e + 3) == '/') {
						/* support windows drive letters as in:
						   file:///c:/somedir/file.txt
						*/
						if (e + 5 < ue && *(e + 5) == ':') {
							s = e + 4;
						}
						goto just_path;
					}
				}
			} else {
				s = e + 1;
				goto just_path;
			}
		}

if这里支持了协议:号之后不是/号的协议.
如果连续的数字则认为是端口号跳转host解析.
解析:号之后不是/号的协议
else内接正常的协议格式(http://xxxx)
这里有个判断用来解析file:///的格式url
最后跳转path解析

好了,现在回过头来看看第一个判断
((e = memchr(s, ‘:’, length)) && e != s)
当不满足这个条件
e是存在时候即e是第一个字符且是:号的时候进入端口分析
端口号这块的解析还是比较简单的需要注意的是

if (s + 1 < ue && *s == '/' && *(s + 1) == '/') { /* relative-scheme URL */
		s += 2;
}

这个地方是承接上边if (e + 1 < ue && e < s + strcspn(s, “?#”))的判断当s是//开头的字符串的时候处理,这里跳转host解析
其余跳转path解析

host解析开始置空e
将e指向/?#中的最后一个字符所在的位置
这块也比较简单
其中zend_memrchr可以认为是memrchr的zend版本
这里主要是根据@来解析user和password

再往下就比较简单了就不做过多的冗述了.

2个小时的时间终于写完了,可能写的林林总总的有些有问题的地方欢迎大家指正和讨论(^^)
最后附上php的php_url_parse_ex源代码

/* {{{ php_url_parse
 */
PHPAPI php_url *php_url_parse_ex(char const *str, size_t length)
{
	char port_buf[6];
	php_url *ret = ecalloc(1, sizeof(php_url));
	char const *s, *e, *p, *pp, *ue;

	s = str;
	ue = s + length;

	/* parse scheme */
	if ((e = memchr(s, ':', length)) && e != s) {
		/* validate scheme */
		p = s;
		while (p < e) {
			/* scheme = 1*[ lowalpha | digit | "+" | "-" | "." ] */
			if (!isalpha(*p) && !isdigit(*p) && *p != '+' && *p != '.' && *p != '-') {
				if (e + 1 < ue && e < s + strcspn(s, "?#")) {
					goto parse_port;
				} else if (s + 1 < ue && *s == '/' && *(s + 1) == '/') { /* relative-scheme URL */
					s += 2;
					e = 0;
					goto parse_host;
				} else {
					goto just_path;
				}
			}
			p++;
		}

		if (e + 1 == ue) { /* only scheme is available */
			ret->scheme = zend_string_init(s, (e - s), 0);
			php_replace_controlchars_ex(ZSTR_VAL(ret->scheme), ZSTR_LEN(ret->scheme));
			return ret;
		}

		/*
		 * certain schemas like mailto: and zlib: may not have any / after them
		 * this check ensures we support those.
		 */
		if (*(e+1) != '/') {
			/* check if the data we get is a port this allows us to
			 * correctly parse things like a.com:80
			 */
			p = e + 1;
			while (p < ue && isdigit(*p)) {
				p++;
			}

			if ((p == ue || *p == '/') && (p - e) < 7) {
				goto parse_port;
			}

			ret->scheme = zend_string_init(s, (e-s), 0);
			php_replace_controlchars_ex(ZSTR_VAL(ret->scheme), ZSTR_LEN(ret->scheme));

			s = e + 1;
			goto just_path;
		} else {
			ret->scheme = zend_string_init(s, (e-s), 0);
			php_replace_controlchars_ex(ZSTR_VAL(ret->scheme), ZSTR_LEN(ret->scheme));

			if (e + 2 < ue && *(e + 2) == '/') {
				s = e + 3;
				if (zend_string_equals_literal_ci(ret->scheme, "file")) {
					if (e + 3 < ue && *(e + 3) == '/') {
						/* support windows drive letters as in:
						   file:///c:/somedir/file.txt
						*/
						if (e + 5 < ue && *(e + 5) == ':') {
							s = e + 4;
						}
						goto just_path;
					}
				}
			} else {
				s = e + 1;
				goto just_path;
			}
		}
	} else if (e) { /* no scheme; starts with colon: look for port */
		parse_port:
		p = e + 1;
		pp = p;

		while (pp < ue && pp - p < 6 && isdigit(*pp)) {
			pp++;
		}

		if (pp - p > 0 && pp - p < 6 && (pp == ue || *pp == '/')) {
			zend_long port;
			memcpy(port_buf, p, (pp - p));
			port_buf[pp - p] = '\0';
			port = ZEND_STRTOL(port_buf, NULL, 10);
			if (port > 0 && port <= 65535) {
				ret->port = (unsigned short) port;
				if (s + 1 < ue && *s == '/' && *(s + 1) == '/') { /* relative-scheme URL */
				    s += 2;
				}
			} else {
				php_url_free(ret);
				return NULL;
			}
		} else if (p == pp && pp == ue) {
			php_url_free(ret);
			return NULL;
		} else if (s + 1 < ue && *s == '/' && *(s + 1) == '/') { /* relative-scheme URL */
			s += 2;
		} else {
			goto just_path;
		}
	} else if (s + 1 < ue && *s == '/' && *(s + 1) == '/') { /* relative-scheme URL */
		s += 2;
	} else {
		goto just_path;
	}

	parse_host:
	/* Binary-safe strcspn(s, "/?#") */
	e = ue;
	if ((p = memchr(s, '/', e - s))) {
		e = p;
	}
	if ((p = memchr(s, '?', e - s))) {
		e = p;
	}
	if ((p = memchr(s, '#', e - s))) {
		e = p;
	}

	/* check for login and password */
	if ((p = zend_memrchr(s, '@', (e-s)))) {
		if ((pp = memchr(s, ':', (p-s)))) {
			ret->user = zend_string_init(s, (pp-s), 0);
			php_replace_controlchars_ex(ZSTR_VAL(ret->user), ZSTR_LEN(ret->user));

			pp++;
			ret->pass = zend_string_init(pp, (p-pp), 0);
			php_replace_controlchars_ex(ZSTR_VAL(ret->pass), ZSTR_LEN(ret->pass));
		} else {
			ret->user = zend_string_init(s, (p-s), 0);
			php_replace_controlchars_ex(ZSTR_VAL(ret->user), ZSTR_LEN(ret->user));
		}

		s = p + 1;
	}

	/* check for port */
	if (s < ue && *s == '[' && *(e-1) == ']') {
		/* Short circuit portscan,
		   we're dealing with an
		   IPv6 embedded address */
		p = NULL;
	} else {
		p = zend_memrchr(s, ':', (e-s));
	}

	if (p) {
		if (!ret->port) {
			p++;
			if (e-p > 5) { /* port cannot be longer then 5 characters */
				php_url_free(ret);
				return NULL;
			} else if (e - p > 0) {
				zend_long port;
				memcpy(port_buf, p, (e - p));
				port_buf[e - p] = '\0';
				port = ZEND_STRTOL(port_buf, NULL, 10);
				if (port > 0 && port <= 65535) {
					ret->port = (unsigned short)port;
				} else {
					php_url_free(ret);
					return NULL;
				}
			}
			p--;
		}
	} else {
		p = e;
	}

	/* check if we have a valid host, if we don't reject the string as url */
	if ((p-s) < 1) {
		php_url_free(ret);
		return NULL;
	}

	ret->host = zend_string_init(s, (p-s), 0);
	php_replace_controlchars_ex(ZSTR_VAL(ret->host), ZSTR_LEN(ret->host));

	if (e == ue) {
		return ret;
	}

	s = e;

	just_path:

	e = ue;
	p = memchr(s, '#', (e - s));
	if (p) {
		p++;
		if (p < e) {
			ret->fragment = zend_string_init(p, (e - p), 0);
			php_replace_controlchars_ex(ZSTR_VAL(ret->fragment), ZSTR_LEN(ret->fragment));
		}
		e = p-1;
	}

	p = memchr(s, '?', (e - s));
	if (p) {
		p++;
		if (p < e) {
			ret->query = zend_string_init(p, (e - p), 0);
			php_replace_controlchars_ex(ZSTR_VAL(ret->query), ZSTR_LEN(ret->query));
		}
		e = p-1;
	}

	if (s < e || s == ue) {
		ret->path = zend_string_init(s, (e - s), 0);
		php_replace_controlchars_ex(ZSTR_VAL(ret->path), ZSTR_LEN(ret->path));
	}

	return ret;
}
/* }}} */
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值