分类 编程语言 下的文章

Yii事件机制分析

在Yii中使用事件需要三个步骤:1、定义事件;2、定义事件回调函数;3、将回调函数添加到事件中4、触发事件。

Yii事件机制的实现是在其底层基类CComponent类里,这是所有组件的基类。

1、如何定义事件

很简单,事件以on开头的命名方式定义,如以下定义了一个onEcho的事件:

public function onEcho($event)
{
     $this->raiseEvent('onEcho', $event);
}

这样就定义了一个事件,其中$event参数是一个CEvent或其子类的实例(但其实用到的并不多,下面再说)。

2、定义事件回调函数

回调函数可以是一个全局函数或者一个类中的函数(说白了就是一个函数而已),其需要一个参数$event,如下:

public function huidiao($event)
{
     //TODO
}

$event参数其实是接收定义事件时传入的那个参数,也就是上面第一步定义的事件函数的参数,它最终会传到这个回调函数里来。然后回调函数根据业务逻辑使用这个参数,但一般情况下我们不怎么使用到这个参数,所以意义也不是很大,不过如果你需要参数的情况下可以通过这样方式定义,因为传入的参数并不一定限制是CEvent的实例或其子类,也可以是其他类型(待会下面继续解释)。

3、将回调函数添加到事件中

为啥还要将回调函数添加到事件中呢?因为当事件触发后总得要有程序逻辑(也就是一段php代码)去处理业务是吧。所以光光的定义一个事件而不往里面添加回调函数是没有意义的,而且即使定义了回调函数却添加到事件里也是没有意义的。就如同你只买了个电脑主机有啥意义呢?也或者你只买了一台显示器也同样没有意义,要将两者结合才是他们真正的用处。

那么如何将回调函数添加到事件中?

一种方法:$this->onEcho=array($this, 'huidiao');
另一种方法:$this->attachEventHandler('onEcho', array($this, 'huidiao'));
第一种方法利用了Yii组件中setter原理,看代码:

public function __set($name,$value)
{
	$setter='set'.$name;
	if(method_exists($this,$setter))
		return $this->$setter($value);
	elseif(strncasecmp($name,'on',2)===0 && method_exists($this,$name))
	{
		// duplicating getEventHandlers() here for performance
		$name=strtolower($name);
		if(!isset($this->_e[$name]))
			$this->_e[$name]=new CList;
		return $this->_e[$name]->add($value);
	}
	elseif(is_array($this->_m))
	{
		foreach($this->_m as $object)
		{
			if($object->getEnabled() && (property_exists($object,$name) || $object->canSetProperty($name)))
				return $object->$name=$value;
		}
	}
	if(method_exists($this,'get'.$name))
		throw new CException(Yii::t('yii','Property "{class}.{property}" is read only.',
			array('{class}'=>get_class($this), '{property}'=>$name)));
	else
		throw new CException(Yii::t('yii','Property "{class}.{property}" is not defined.',
			array('{class}'=>get_class($this), '{property}'=>$name)));
}

这是一个魔术方法__set,6-13行是给事件添加回调函数的步骤。如果类里面存在以on开头的函数,那么将事件放到私有变量$_e里,$_e是个数组,其键名是事件名称,其值是一个CList实例(也可以理解为一个数组,因为CList实现了一个整数索引的集合类,可以用数组的形式往里面添加元素 ,可详见CList),再使用CList中add方法往数组形式的值里面添加回调函数,也等同于$_e['onecho'][]=array($this,'huidiao')。那么按我上面添加一个事件来说,添加一个回调函数huidiao后$_e的内容应该是array('onecho'=>array(array($this,'huidiao'))

第二种方法使用了类中的attachEventHandler函数,代码如下:

public function attachEventHandler($name,$handler)
{
	$this->getEventHandlers($name)->add($handler);
}

public function getEventHandlers($name)
{
	if($this->hasEvent($name))
	{
		$name=strtolower($name);
		if(!isset($this->_e[$name]))
			$this->_e[$name]=new CList;
		return $this->_e[$name];
	}
	else
		throw new CException(Yii::t('yii','Event "{class}.{event}" is not defined.',
			array('{class}'=>get_class($this), '{event}'=>$name)));
}

public function hasEvent($name)
{
	return !strncasecmp($name,'on',2) && method_exists($this,$name);
}

attachEventHandler函数首先用getEventHandler函数获取需要添加回调函数的事件,用hasEvent判断是否存在这个事件函数名,如果存在的话(也就是定义事件函数的话)就和第一种方法原理一样,获得$_e['onecho']的值(CList实例),使用其方法add往数组形式的值里添加回调函数。

至此,回调函数都已经添加到事件中去了。

4、触发事件

这个比较简单,在你需要执行这个事件的地方调用第1步定义的事件,如$this->onEcho($event),函数执行里面的一句话$this->raiseEvent('onEcho'.$event),raiseEvent这个方法代码如下:

public function raiseEvent($name,$event)
{
	$name=strtolower($name);
	if(isset($this->_e[$name]))
	{
		foreach($this->_e[$name] as $handler)
		{
			if(is_string($handler))
				call_user_func($handler,$event);
			elseif(is_callable($handler,true))
			{
				if(is_array($handler))
				{
					// an array: 0 - object, 1 - method name
					list($object,$method)=$handler;
					if(is_string($object))	// static method call
						call_user_func($handler,$event);
					elseif(method_exists($object,$method))
						$object->$method($event);
					else
						throw new CException(Yii::t('yii','Event "{class}.{event}" is attached with an invalid handler "{handler}".',
							array('{class}'=>get_class($this), '{event}'=>$name, '{handler}'=>$handler[1])));
				}
				else // PHP 5.3: anonymous function
					call_user_func($handler,$event);
			}
			else
				throw new CException(Yii::t('yii','Event "{class}.{event}" is attached with an invalid handler "{handler}".',
					array('{class}'=>get_class($this), '{event}'=>$name, '{handler}'=>gettype($handler))));
			// stop further handling if param.handled is set true
			if(($event instanceof CEvent) && $event->handled)
				return;
		}
	}
	elseif(YII_DEBUG && !$this->hasEvent($name))
		throw new CException(Yii::t('yii','Event "{class}.{event}" is not defined.',
			array('{class}'=>get_class($this), '{event}'=>$name)));
}

这个方法会获得$_e['onecho']值,那么这个值是数组形式对吧,而且数组的每个元素都是一个回调函数,那么用foreach对其进行遍历(代码第6行),循环部分的代码最终都是调用了call_user_func,把回调函数和$event传入进去,那么回调函数就被执行了,并且该回调函数得到的参数是触发事件时$this->onEcho($event)的这个$event参数。所以我在第2步中说道,这个$event参数并不一定要CEvent的实例或其子类,因为要看你回调函数需要什么参数。这么一看,通过foreach后,注册到这个事件onEcho中的所有回调函数都会被执行一遍。

那么就这样完成了整个事件了,还比较容易理解吧?有任何疑问可以留言,一起探讨

 

关于swfupload插件无法上传文件的问题

因为项目上所使用的flash上传图片插件名叫swfupload,但里面的js写法好像另一个上传插件sapload,所以我也搞不清到底是哪个,但最终了解到这两个插件都是有共同的一个问题,就是对需要进行身份才能上传文件的方式会失效。

我们都知道服务器端要对识别当前用户登录状态需要客户端cookie的支持,请求URL时会带着cookie,服务器端才能找到对应的session。而使用这个flash插件上传文件请求接口时,如果被请求的接口需要登陆才能传文件的话,那很可能就会失败。

一种原因是说:因为该插件会忽略部分浏览器下的cookie,IE和chrome下是正常的,像火狐等浏览器上传文件时,服务器端会发现请求带过来的cookie是空的(建议尝试打印一下cookie)

另一种可能的说法:该插件是通过soket套接字进行通信,相当于建立了一次新对话,即使请求带着cookie过去了,但打印出来发现存储的sessionid并不是当前登录时的,因此用户身份又验证失败。(建议多打印cookie并查看)

归根到底,无法上传文件的原因就是无法验证身份。

解决方法有两种:

1、在初始化flash插件时,在js代码中设置一下传递参数(可参考其插件手册)。swfupload插件中增加一个post_params选项,格式如post_params:{"PHPSESSID": <?php echo session_id(); ?>} ,sapload插件中增加一个args选项,格式如args:"PHPSESSID=<?php echo session_id(); ?>" 。这两个意思都是在post文件时,带上参数PHPSESSID(注:后端是PHP脚本)。

咱用不了cookie中的值,那就用POST过来值,然后在PHP脚本中处理一下:

if(isset($_POST['PHPSESSID'])){
    session_id($_POST['PHPSESSID']);
    session_start()
}

这样就可以使用当前登录正确的session id进行一系列操作了。

2、取消上传URL接口的登陆验证。(有点蛋疼,非必要情况下还是使用第一种方法吧)

 

 

附(这是给我自己看的,swfupload好像格式有所不同,如本文开头提到的,不知道是不是同一个插件):

flash 接口文档:

upUrl :后台文件的地址,必须要绝对路径

etmsg:每次文件上传成功后需不需要回调函数(0|1),默认回调函数为sapLoadMsg(x)函数。

ltmsg:最后一个文件上传成功后需不需要回调函数(0|1),默认回调函数为sapLoadMsg(x)函数。

types:上传文件的类型(如*.apk;*.jpg)用分号分隔各个类型

args:需要传递的参数(如apkName=123;apkName1=1234)用分号分隔各个类型

fileName:此参数可选,允许你自定义文件域名称,默认是Filedata。

maxNum:当最大上传数为1的时候自动切换到单文件上传模式,用户将不能在使用多选选取文件。

pear安装

说到安装pear,我是因为给一个变量起一个名字,联想到了如何起名才规范,于是又联想到了PHPDocument,要安装PHPDocument可以通过pear安装和手动下载安装,于是乎我就折腾起了这个pear的安装。

根据:http://pear.php.net/manual/en/installation.getting.php中的内容翻译得来

点此http://pear.php.net/go-pear.phar下载文件到本地并命名为go-pear.phar进行保存。在cmd中输入php go-pear.phar 开始安装。

Are you installing a system-wide PEAR or a local copy?
(system|local) [system] :

 选择系统级别安装还是安装本地,默认是system,直接按回车继续。

Below is a suggested file layout for your new PEAR installation.  To
change individual locations, type the number in front of the
directory.  Type 'all' to change all of them or simply press Enter to
accept these locations.

 1. Installation base ($prefix)                   : D:\php5
 2. Temporary directory for processing            : D:\php5\tmp
 3. Temporary directory for downloads             : D:\php5\tmp
 4. Binaries directory                            : D:\php5
 5. PHP code directory ($php_dir)                 : D:\php5\pear
 6. Documentation directory                       : D:\php5\docs
 7. Data directory                                : D:\php5\data
 8. User-modifiable configuration files directory : D:\php5\cfg
 9. Public Web Files directory                    : D:\php5\www
10. Tests directory                               : D:\php5\tests
11. Name of configuration file                    : C:\Windows\pear.ini
12. Path to CLI php.exe                           : D:\php5

1-12, 'all' or Enter to continue:

以上是默认的pear的临时、数据、配置、测试、执行目录的设置,第11项默认的话会显示错误,我所以还是改到了D:\php5目录下(输入需要改的数字项,会提示让你输入新的目录地址,不改的话啥也不用输入直接回车)。然后回车。

然后一系列安装,其中有以下这个警告:

WARNING!  The include_path defined in the currently used php.ini does not
contain the PEAR PHP directory you just specified:
<D:\php5\pear>
If the specified directory is also not in the include_path used by
your scripts, you will have problems getting any PEAR packages working.

Would you like to alter php.ini<D:\php5\php.ini>?[Y/n]

需要将pear配置目录D:\php5\pear加入php.ini的include_path指令中。输入Y后自动修改php.ini中的路径。

php.ini <D:\php5\php.ini> include_path updated.

Current include path           : .;C:\php\pear
Configured directory           : D:\php5\pear
Currently used php.ini (guess) : D:\php5\php.ini
Press Enter to continue:

 到这里也没啥了,按回车基本安装成功了。会在D:\php5下面生成一个PEAR_ENV.reg的文件,双击运行进行注册表注册即可。

通过命令pear install package安装程序包,如果出现failed to mkdir的错误,只要以管理员身份重新运行cmd即可。

HTTP协议是如何工作的

浏览网页时HTTP协议的主要应用,但是这并不代表HTTP协议就只能应用于网页的浏览,只要通信的双方都遵守HTTP协议,其就有用武之地。比如腾讯QQ、迅雷等软件都是用HTTP协议(当然还包括其他的协议)。

那么HTTP协议是如何工作的呢?

首先,客户端发送一个请求给服务器,服务器在接收到这个请求后将生成响应返回给客户端。一次HTTP操作成为一个事务,其工作过程可分为四步:

1、客户机与服务器需要建立连接,单击某个超链接,HTTP协议的工作开始。

2、建立连接后,客户机发送一个请求给服务器。格式为:前边是统一资源标识符(URL)、中间是协议版本号,后边是MIME信息(包括请求修饰符、客户机信息和可能的内容)

3、服务器接收到请求后,给予相应的相应信息。格式为:首先是一个状态行(包括信息的协议版本号、一个成功或错误的代码),然后是MIME信息(包括服务器信息、实体信息和可能的内容)

4、客户端接收服务器返回的信息并显示在用户的显示屏上,然后客户机与服务器断开连接。

 

如果以上过程中的某一步出现错误,产生错误的信息将返回到客户端,有显示屏输出。对于用户来说,这些过程是由HTTP协议自己完成的,用户只要用鼠标点击等待信息显示就可以了。

请求

HTTP请求由三部分组成:请求行、消息报头、请求正文。请求行以一个请求方式符号开头,以空格分开,后面跟着请求的URI和协议的版本,格式为:Method Request-URI HTTP-Version CRLF

 

比如我请求本站某页面:http://www.veitor.net/cate/bianchengyuyan

请求行如图

QQ截图20150106220345

参数说明:

  • Method:请求方法。截图中为GET。
  • Request-URI:一个统一资源标识符,截图中为/cate/bianchengyuyan
  • HTTP-Version:请求的HTTP协议版本,截图中为1.1
  • CRLF:回车和换行符
响应

在接收和解释请求消息后,服务器返回一个HTTP响应消息。HTTP相应也由三个部分组成。分别是:状态行、消息报头、响应正文。状态行格式为:HTTP-Version Status-Code Reason-Phrase CRLF

比如我请求本站某页面:http://www.veitor.net/cate/bianchengyuyan

QQ截图20150106221255

 

参数说明如下:

  • HTTP-Version:服务器HTTP协议的版本,截图中为1.1
  • Status-Code:服务器发回的响应状态码,截图中为200
  • Reason-Phrase:状态代码的文本描述,截图中为OK

其中状态代码由三位数字组成,第一个数字定义了响应的类别,有五种可能取值:

  1. 1xx:指示信息——请求已接收,继续处理。
  2. 2xx:成功——请求已被成功接收、理解、接受
  3. 3xx:重定向——要完成请求必须进行更进一步的操作
  4. 4xx:客户端错误——请求有语法错误或请求无法实现
  5. 5xx:服务器错误——服务器未能实现合法的请求

常见的状态码、状态描述和说明如下:

  • 200 OK:客户端请求成功
  • 400 Bad Request:客户端请求有语法错误,不能被服务器所理解
  • 401 Unauthorize:请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用
  • 403 Forbidden:服务器收到请求,但是拒绝提供服务
  • 404 Not Found:请求资源不存在,例如输入了错误的URL
  • 500 Internal Server Error:服务器发生不可预期的错误
  • 503 Server Unavailable:服务器当前不能处理客户端的请求,一段时间后可能恢复正常

 

报头

HTTP消息报头包括普通报头、请求报头、响应报头、实体报头。每个报头域组成形式如下:

  1. 普通报头中有少数报头域用于所有的请求和响应消息,但并不用于被传输的实体,只用于传输的消息(如缓存控制、连接控制)
  2. 请求报头允许客户端向服务器端传递请求的附加信息以及客户端自身的信息(如UA头、Accept等)
  3. 响应报头允许服务器传递不能放在状态行中的附加响应信息,以及关于服务器的信息和对Request-URL所标的资源进行下一步访问的信息(如Location)
  4. 实体报头定义了关于实体正文和请求所标识的资源的元信息,例如有无实体正文。

 

比较重要的几个报头如下(可以结合着上面的截图看):

  • Host:头域指定请求资源的Internet主机和端口号,必须表示请求URL的原始服务器或网关的位置。HTTP1.1请求必须包含主机头域,否则系统会以400状态码返回。
  • User-Agent:简称UA,内容包含发出请求的用户信息。通常UA包含浏览者的信息,主要是浏览器的名称版本和所用的操作系统。在上面的截图中可以看到,我的客户端电脑使用的是Gecko渲染引擎的浏览器,我用的Chrome,操作系统为Windows NT6.1的内核,即Windows 7操作系统(内核版本号和操作系统代号不是一一对应的)。这个UA头不仅仅是使用浏览器才存在,只要使用了基于HTTP协议的客户端软件都会发送这个请求,无论是手机端还是PDA等,这个UA头是辨别客户端所用设备的重要依据
  • Accept:告诉服务器可以接受的文件格式,通常这个值在各种浏览器中都差不多。不过Wap浏览器所能接受的格式要少一点,这也是用来区分WAP和计算机浏览器的主要依据之一。随着WAP浏览器的升级,其已经和计算机浏览器越来越接近,因此这个判断所起的作用越来越弱。
  • Cookie:Cookie分两种,一种是客户端向服务器端发送的,使用Cookie报头,用来标记一些信息;另一种是服务器发送给浏览器的,报头为Set-Cookie。两者的区别是Cookie报头的value里可以有多个cookie值,并且不需要显式指定domain等。而set-Cookie报头里一条记录只能有一个Cookie的value,需要指明domain、path等
  • Cache-Control:指定请求和响应遵循的缓存机制。在请求消息或响应消息中设置Cache-Control并不会选择另一个消息处理过程中的缓存处理过程。请求时的缓存指令包括no-cache、no-store、max-age、max-stale、min-fresh、only-if-cached;响应消息中的指令包括public、private、no-cache、no-store、no-transform、must-revalidate、proxy-revalidate、max-age
  • Referer:头域允许客户端指定请求URI的资源地址,这可以允许服务器生成回退链表,可以用来登录、优化缓存等。也允许废除的或错误的连接由于维护的目的而被追踪。如果请求的URI没有自己的URI地址,Referer不能被发送。如果指定的是部分URI地址,则此地址应该是一个相对地址。Referer通常是流量统计系统来记录来访者地址的参数。
  • Content-Length:内容长度
  • Content-Range:响应的资源范围。可以在每次请求中标记请求的资源范围,在连接断开重连时,客户端只请求该资源未下载的部分,而不是重新请求整个资源,实现断点续传迅雷就是基于这个原理,使用多线程分段读取网络上的资源,最后再合并。
  • Accept-Encoding:指定所能接受的编码方式。通常服务器会对页面进行GZIP压缩后再输出以减少流量,一般浏览器均支持对这种压缩后的数据进行处理。但对于我们来说,如果不想接受到这些看似“乱码”的数据,可以指定不接受任何服务器端压缩处理,要求其原样返回
  • 自定义报头:在HTTP消息中,也可以使用一些HTTP1.1正式规范里没有定义的头字段,这些头字段统称为自定义的HTTP头或者扩展头。比如上面截图中X-Powered-By字段,这是由服务器发送的。在PHP里,使用header函数即可实现。

 

参考:《PHP核心技术与最佳实践》

Python乱码问题解决方法

最近要写个python脚本,遇到各种乱码问题,后来一查原来python乱码问题还真是层出不穷,让人头疼啊。

我主要是用的sublime进行脚本编写,使用cmd或python自带的GUI运行脚本调试查看。

比如下列的脚本代码在运行时会产生乱码

#coding:utf-8
print "的是"

QQ截图20150106092343因为编写代码时保存的编码为UTF-8,而在Windows中运行读取脚本时以系统的编码GBK去执行,因此GBK与UTF-8冲突导致编码错误。

解决方法有三种:

1、我们往往在代码顶部加入#coding:utf-8 这样指明解码时的字符集,而指明utf-8还是会出现所说的乱码,还是因为运行时采用的GBK与我们指定的UTF-8冲突。所以要指明编码的话就只好指明为和系统编码一样的字符集,如这样指明#coding:gbk ,不写coding的话默认为与系统一样的编码

QQ截图20150106094006

2、在打印的字符前加上u,如print u"是" ,这样就指明了使用头部coding指定的字符集解码,当然前提是要在头部添加coding,并且coding所指的的字符集与操作系统用的字符集一样(刚才第一条说明了)

QQ截图20150106094135

3、输出时指定使用的字符集,如print '是'.decode('utf-8')

QQ截图20150106094339

当然保存脚本时的编码要与coding一样。

另外据说还有种方法是加入这三行代码:

import sys
reload(sys)
sys.setdefaultencoding('utf-8')

这种方法我没试过,看起来好像是从系统编码进行下手的,将系统编码临时改掉,这样的话你们懂的。

关于使用PDO无法执行两次查询的问题

今天在使用PDO查询数据时遇到这么一个问题,使用实例化的PDO类无法执行两次查询,即第一次查询是正常的,第二次查询是无效的。

举个栗子:

$pdo = new PDO('mysql:host=127.0.0.1;dbname=test;', 'root', '');

$stmt1 = $pdo->query('select count(*) from table');

$stmt2 = $pdo->query('select * from table limit 0,5');

实例化PDO类,调用类中方法query查询一条语句“select count(*) from table”,返回的$stmt1变量是一个PDOStatement对象,而此时没有使用$stmt1这个实例做任何操作(或者只是使用了$stmt1->fetchColumn()获得了数据数量)。接着再调用PDO实例中的方法query(使用prepare后再execute也一样)再一次查询数据,这是返回给$stmt2变量的值就不是一个PDOStatement对象了,而是false。使用$stmt2->errorInfo();打印出结果可以看到这么个错误“Cannot execute queries while other unbuffered queries are active.  Consider using PDOStatement::fetchAll().  Alternatively, if your code is only ever going to run against mysql, you may enable query buffering by setting the PDO::MYSQL_ATTR_USE_BUFFERED_QUERY attribute”

大致意思是说,当一个未缓存查询正在活动时不能再执行查询操作。解决方式有:

1、可以考虑使用前一个查询返回的PDOStatement对象(本例子中是$stmt1)中fetchAll方法把数据处理完毕

2、亦或者是将前一个查询PDOStatement对象设为null:$stmt1=null

3、再或者是连接数据库后使用PDO实例中方法$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true); 设置mysql启用缓冲查询。

这时第二次查询时$stmt2就不会为false了。

Windows下安装python和pip

此前安装过python,但因为安装pip过程中遇到了问题,所以把python也一起卸载了。现在重新安装一下这两个。

这里选择了python2.7.8版本进行安装,因为我需要使用sqlmap这个工具,而这个工具对python版本有要求,最新版本的python会出错,所以我选择了2.7.8的版本。

首先上官网https://www.python.org/downloads 下载

QQ截图20141118141425

下载的是msi文件,直接打开像安装普通软件一样安装就行。但有一点一定要注意,如果你要使用pip,则安装python时选择的安装路径目录中不要有空格,如果安装到了Program Files这样的目录下,那么接下来安装的pip将无法正常运行,会报错。所以这里我直接选择了D盘下,安装前记得选择将python.exe添加到系统环境变量。

QQ图片20141118142038

 

安装完之后在CMD中输入python命令会看到欢迎界面。

接下来安装pip,上https://pip.pypa.io/en/latest/installing.html下载 get-pip.py脚本

在CMD下找到该文件目录并运行下列命令(需要管理员权限):

python get-pip.py

QQ截图20141118142644

安装好之后,会发现python安装目录下会多出一个Scripts目录(若之前没有)。

这时我们直接在命令行输入pip,会显示‘pip’不是内部命令,也不是可运行的程序。因为我们还没有添加环境变量。

只要在PATH变量中添加:D:\Python27\Scripts;(路径自己更换)就好了,输入pip list 查看安装情况

QQ截图20141118143306

 

如果出现下列错误,那么原因可能就是上面我所说的python安装路径目录名中有空格了

 

Fatal error in launcher: Unable to create process using '"D:\Program Files\Python27\python.exe" "D:\Program Files\Python27\Scripts\pip.exe" '

 

(完)