PHP Wrapper在SAE上的应用

PHP Wrapper在SAE上的应用

一、PHP Wrapper是什么
自PHP 4.3开始,PHP开始允许用户通过stream_wrapper_register()自定义URL风格的协议。用户使用fopen(), copy()等文件系统函数对封装协议进行操作时,PHP会调用注册协议时所提供的类中相应的函数。
PHP手册中给了一个例子,它将VariableStream类注册为var://协议,通过这个协议,用户可以使用文件系统函数直接读写全局变量。例如,用户可以通过 “var://foo” 读写 $GLOBALS[‘foo’] 。

二、SAE为什么需要PHP Wrapper
出于性能和安全方面的考虑,SAE平台上禁用了本地文件读写和对外的数据抓取。相应的,我们提供了对应的服务来做同样的事情。
由于新服务的接口和PHP本身的接口不太一样,专门为我们平台开发的程序当然不会存在问题,但是大量已有的程序和开源项目,就面临着繁杂的迁移工作。而使用PHP Wrapper对我们的服务的接口进行封装之后,用户就可以更方便地将程序迁移到SAE平台。

三、如何写PHP Wrapper
要通过PHP Wrapper封装一个协议,首先,我们需要写一个 streamWrapper 类,类名可自定义,类的格式为:

streamWrapper {
public resource $context ;
__construct ( void )
public bool dir_closedir ( void )
public bool dir_opendir ( string $path , int $options )
public string dir_readdir ( void )
public bool dir_rewinddir ( void )
public bool mkdir ( string $path , int $mode , int $options )
public bool rename ( string $path_from , string $path_to )
public bool rmdir ( string $path , int $options )
public resource stream_cast ( int $cast_as )
public void stream_close ( void )
public bool stream_eof ( void )
public bool stream_flush ( void )
public bool stream_lock ( mode $operation )
public bool stream_open ( string $path , string $mode , int $options , string &$opened_path )
public string stream_read ( int $count )
public bool stream_seek ( int $offset , int $whence = SEEK_SET )
public bool stream_set_option ( int $option , int $arg1 , int $arg2 )
public array stream_stat ( void )
public int stream_tell ( void )
public int stream_write ( string $data )
public bool unlink ( string $path )
public array url_stat ( string $path , int $flags )
}

类中各方法说明:
streamWrapper::__construct — 构造函数,仅在stream_open前被调用
streamWrapper::dir_closedir — 关闭目录句柄,响应closedir()函数
streamWrapper::dir_opendir — 打开目录句柄,响应opendir()函数
streamWrapper::dir_readdir — 从目录句柄读取条目,响应readdir()函数
streamWrapper::dir_rewinddir — 倒回目录句柄,响应rewinddir()函数
streamWrapper::mkdir — 创建目录,响应mkdir()函数
streamWrapper::rename — 目录或文件重命名,响应rename()函数
streamWrapper::rmdir — 删除目录,响应rmdir()函数
streamWrapper::stream_cast — 检索基础资源,响应stream_select()函数
streamWrapper::stream_close — 关闭资源,响应fclose()函数
streamWrapper::stream_eof — 检查文件指针是否已经在文件末尾,响应feof()函数
streamWrapper::stream_flush — 清除输出缓存,响应fflush()函数
streamWrapper::stream_lock — 咨询文件锁定,响应flock()函数
streamWrapper::stream_open — 打开文件或URL为流,响应fopen()函数
streamWrapper::stream_read — 从流中读取内容,响应fread(), fgets()函数
streamWrapper::stream_seek — 在流中定位指针,响应fseek()函数
streamWrapper::stream_set_option — 改变流设置
streamWrapper::stream_stat — 检索文件资源的信息,响应fstat()函数
streamWrapper::stream_tell — 检索流中指针的位置,响应ftell()函数
streamWrapper::stream_write — 向流中写入内容,响应fwrite(), fputs()函数
streamWrapper::unlink — 删除文件,响应unlink()函数
streamWrapper::url_stat — 检索文件的信息,响应所有stat()相关的函数,例如file_exists(), is_dir(), is_file(), filesize(), fileinode()等等

详细说明请参考PHP手册:http://cn2.php.net/manual/en/class.streamwrapper.php

写好streamWrapper类之后,使用 stream_wrapper_register () 将这个类注册到Wrapper中,就可以开始使用了。函数使用方法为:
bool stream_wrapper_register ( string $protocol , string $classname [, int $flags = 0 ] )
例如:
stream_wrapper_register(“saemc”, “SaeMemcacheWrapper”);

由于SAE平台不支持对本地文件的写操作,因此Smarty之类的一些需要在本地写文件的开源项目就没办法直接在SAE平台上使用,而有了saemc Wrapper,用户就可以将Smarty编译的模板保存在MC中,很方便的将Smarty迁移到SAE平台上来。
在附件中我们为大家提供了SAE上Memcache Wrapper的实现代码,大家可以下载此附件进行测试。
在测试之前,需要先在本地启动一个端口为22222的Memcached服务:
memcached -m 10 -p 22222 -u nobody -l 127.0.0.1
然后使用下面代码就可以测试了:

//包含附件代码,注册saemc Wrapper
include_once(‘wrapper.php’);

//测试 saemc Wrapper
$fp = fopen( “saemc://test.txt”, “w+” ) or die(“fopen faild!”);

fwrite( $fp, “line1\n” ) or die(“fwrite line1 faild!”);
fwrite( $fp, “line2\n” ) or die(“fwrite line2 faild!”);
fwrite( $fp, “line3\n” ) or die(“fwrite line3 faild!”);

var_dump(ftell($fp));
fseek( $fp, 0 );
while ( !feof( $fp ) ) {
$c = fgets( $fp ) or die(“fgets faild!”);
var_dump($c);
}
fclose( $fp );

var_dump(file_get_contents(“saemc://test.txt”));
var_dump(file_put_contents(“saemc://path/test.txt”, “hello world!\n”));
var_dump(file_put_contents(“saemc://path/test.txt”, “hello world!\n”, FILE_APPEND));
var_dump(file_get_contents(“saemc://path/test.txt”));
var_dump(copy(“saemc://path/test.txt”, “saemc://path/test_new.txt”));
var_dump(file_get_contents(“saemc://path/test_new.txt”));
var_dump(unlink(“saemc://path/test.txt”));
var_dump(file_get_contents(“saemc://path/test.txt”));
var_dump(rename(“saemc://path/test_new.txt”, “saemc://path/test.txt”));
var_dump(file_get_contents(“saemc://path/test.txt”));
echo “====test include====\n”;
include_once(“saemc://path/test.txt”);

测试页面的输出结果:
int(18)
string(6) “line1

string(6) “line2

string(6) “line3

string(18) “line1
line2
line3

int(13)
int(13)
string(26) “hello world!
hello world!

bool(true)
string(26) “hello world!
hello world!

bool(true)
bool(false)
bool(true)
string(26) “hello world!
hello world!

====test include====
hello world!
hello world!

我们提供的 Memcache Wrapper并没有实现目录操作的一些方法和Memcache的Timeout,大家可以参考PHP手册,尝试实现目录操作,或者通过context使这个Wrapper支持Memcache的Timeout。
另外,大家可以到下面这个地址查看SAE Stdlib中sae_include的源码,在其中还有我们为Storage服务封装的saestor Wrapper和为Fetchurl服务重新封装的http Wrapper的实现:
http://stdlib.sinaapp.com/?f=sae_include.function.php

四、写Wrapper时的一些注意事项
1. 构造函数
streamWrapper 类很特别,它的构造函数并不是每次都调用的。只有在你的操作触发了stream_open相关的操作时才会调用,比如你用file_get_contents()了。而当你的操作触发和stream无关的函数时,比如file_exists会触发url_stat方法,这个时候构造函数是不会被调用的。

2. 读实现
Wrapper里边有Position和Seek等概念,但是很多服务其实是一次性就读取全部数据的,这个可以在stream_open的时候一次性读回,放到一个属性中,以后seek和tell的时候直接操作属性里边存放的数据就可以了。

3. 追加写实现
有很多服务是一次性写入所有数据,不支持追加写的功能(比如Memcache),这就需要我们自己在Wrapper中来实现追加写。可以将整个value一次性读取出来,将需要追加写的数据追加在读取出来的内容后面之后,再一次性写回。
但是这种追加写的实现方式性能会比较差,尤其是内容体积较大之后,一次性读取所有内容会非常消耗资源,因此在某些服务中我们不得不舍弃对追加写的支持。

4. url_stat的实现
在streamWrapper类的实现中,url_stat的实现是个难点。必须正确的实现url_stat才能使is_writable和is_readable等查询文件元信息的函数正常工作。
而我们需要为我们的虚设备伪造这些值。以mc为例,我们给大家一些参考数据:

url_stat应该返回一个数组,分13个项,内容如下:
dev 设备号 - 写0即可;
ino inode号 - 写0即可;
mode 文件mode - 这个是文件的权限控制符号,稍后详细说明;
nlink link - 写0即可;
uid uid - Linux上用posix_get_uid可以取到,windows上为0;
gid gid - Linux上用posix_get_gid可以取到,windows上为0;
rdev 设备类型 - 当为inode设备时有值;
size - 文件大小;
atime - 最后读时间 格式为unix时间戳;
mtime - 最后写时间;
ctime - 创建时间;
blksize - blocksize of filesystem IO 写零即可;
blocks - number of 512-byte blocks allocated 写零即可;

其中mode的值必须写对:
如果是文件,其值为:
0100000 + 文件权限,如 0100000 + 0777。
如果是目录,其值为:
040000 + 目录权限,如 0400000 + 0777。

5. 关于stat的缓存
PHP会在同一个页面的执行过程中对文件的元信息进行缓存。
根据PHP文档对 clearstatcache() 这个方法的说明得知:在使用 stat(), lstat(), file_exists(), is_writable(), is_readable(), is_executable(), is_file(), is_dir(), is_link(), filectime(), fileatime(), filemtime(), fileinode(), filegroup(), fileowner(), filesize(), filetype(), 或 fileperms() 方法查询文件信息时,PHP会将文件的stat的缓存以提高性能。 clearstatcache()方法可以用来清除这个缓存,当unlink()会自动清除stat缓存。
而实际上,PHP只有在对本地文件进行unlink, rename和rmdir操作时会清除stat缓存,而在通过其他的wrapper进行unlink, rename和rmdir操作时,并不会清除stat缓存。因此在写wrapper时我们要自己在unlink等方法中通过clearstatcache()来清除stat缓存。

附件下载地址: http://apidoc-demo.stor.sinaapp.com/wrapper.php

< ?php

class SaeMemcacheWrapper {

	public $dir_mode = 16895 ; //040000 + 0222;
	public $file_mode = 33279 ; //0100000 + 0777;

	public function __construct() {
		//初始化MC连接
		$this->mc();
	}

	//初始化MC连接
	public function mc() {
		if ( !isset( $this->mc ) ) $this->mc = memcache_connect('127.0.0.1', '22222');
		return $this->mc;
	}

	public function stream_open( $path , $mode , $options , &$opened_path) {
		$this->position = 0;	//初始化文件指针位置
		$this->mckey = trim(substr($path, 8));	//将$path开头的saemc://去掉
		$this->mode = $mode;
		$this->options = $options;

		//根据$mode以不同的方式打开
		if ( in_array( $this->mode, array( 'r', 'r+', 'rb' ) ) ) {
			//以只读模式打开
			if ( $this->mccontent = memcache_get( $this->mc, $this->mckey ) ) {
				$this->get_file_info( $this->mckey );
				$this->stat['mode'] = $this->stat[2] = $this->file_mode;
			} else {
				trigger_error("fopen({$path}): failed to read from Memcached: No such key.", E_USER_WARNING);
				return false;
			}
		} elseif ( in_array( $this->mode, array( 'a', 'a+', 'ab' ) ) ) {
			//以追加写模式打开
			if ( $this->mccontent = memcache_get( $this->mc , $this->mckey ) ) {
				$this->get_file_info( $this->mckey );
				$this->stat['mode'] = $this->stat[2] = $this->file_mode;
				$this->position = strlen($this->mccontent);
			} else {
				$this->mccontent = '';
				$this->stat['ctime'] = $this->stat[10] = time();
			}
		} elseif ( in_array( $this->mode, array( 'x', 'x+', 'xb' ) ) ) {
			//创建并以写入方式打开,如果文件已存在,返回false;
			if ( !memcache_get( $this->mc , $this->mckey ) ) {
				$this->mccontent = '';
				$this->statinfo_init();
				$this->stat['ctime'] = $this->stat[10] = time();
			} else {
				trigger_error("fopen({$path}): failed to create at Memcached: Key exists.", E_USER_WARNING);
				return false;
			}
		} elseif ( in_array( $this->mode, array( 'w', 'w+', 'wb' ) ) ) {
			//创建并以写入方式打开
			$this->mccontent = '';
			$this->statinfo_init();
			$this->stat['ctime'] = $this->stat[10] = time();
		} else {
			$this->mccontent = memcache_get( $this->mc , $this->mckey );
		}

		return true;
	}

	public function stream_read($count) {
		//判断打开模式是否可读
		if (in_array($this->mode, array('w', 'x', 'a', 'wb', 'xb', 'ab') ) ) {
			return false;
		}

		//读取内容并将文件指针向后移
		$ret = substr( $this->mccontent , $this->position, $count);
		$this->position += strlen($ret);

		//修改文件属性中的访问时间和访问者
		$this->stat['atime'] = $this->stat[8] = time();
		$this->stat['uid'] = $this->stat[4] = 0;
		$this->stat['gid'] = $this->stat[5] = 0;

		return $ret;
	}

	public function stream_write($data) {
		//判断打开模式是否可写
		if ( in_array( $this->mode, array( 'r', 'rb' ) ) ) {
			return false;
		}

		//写入内容,移动文件指针,修改文件大小和修改时间
		$left = substr($this->mccontent, 0, $this->position);
		$right = substr($this->mccontent, $this->position + strlen($data));
		$this->mccontent = $left . $data . $right;
		if ( memcache_set( $this->mc , $this->mckey , $this->mccontent ) ) {
			$this->stat['mtime'] = $this->stat[9] = time();
			$this->position += strlen($data);
			return $this->stat['size'] = $this->stat[7] = strlen( $data );
		}
		else return false;
	}

	public function stream_close() {
		//将文件属性信息写入MC
		memcache_set( $this->mc , $this->mckey.'.meta' ,  serialize($this->stat)  );
	}


	public function stream_eof() {
		//判断文件指针是否在文件末尾
		return $this->position >= strlen( $this->mccontent  );
	}

	public function stream_tell() {
		//返回文件指针位置
		return $this->position;
	}

	//移动文件指针位置
	public function stream_seek($offset , $whence = SEEK_SET) {
		switch ($whence) {
			case SEEK_SET:
				if ($offset < strlen( $this->mccontent ) && $offset >= 0) {
					$this->position = $offset;
					return true;
				} else {
					return false;
				}
				break;

			case SEEK_CUR:
				if ($offset >= 0) {
					$this->position += $offset;
					return true;
				} else {
					return false;
				}
				break;

			case SEEK_END:
				if (strlen( $this->mccontent ) + $offset >= 0) {
					$this->position = strlen( $this->mccontent ) + $offset;
					return true;
				}
				else
					return false;
				break;

			default:
				return false;
		}
	}

	//返回文件属性信息
	public function stream_stat() {
		return $this->stat;
	}

	// ============================================
	// 创建目录
	public function mkdir($path , $mode , $options) {
		$path = trim(substr($path, 8));

		$path  = rtrim( $path  , '/' );

		$this->stat = $this->get_file_info( $path );
		$this->stat['ctime'] = $this->stat[10] = time();
		$this->stat['mode'] = $this->stat[2] = $this->dir_mode;

		memcache_set( $this->mc() , $path.'.meta' ,  serialize($this->stat)  );
		memcache_close( $this->mc );

		return true;
	}

	//文件或目录重命名
	public function rename($path_from , $path_to) {
		$path_from = trim(substr($path_from, 8));
		$path_to = trim(substr($path_to, 8));

		memcache_set( $this->mc() , $path_to , memcache_get( $this->mc() , $path_from ) );
		memcache_set( $this->mc() , $path_to . '.meta' , memcache_get( $this->mc() , $path_from . '.meta' ) );
		memcache_delete( $this->mc() , $path_from );
		memcache_delete( $this->mc() , $path_from.'.meta' );
		
		//清除文件信息缓存
		clearstatcache( true );

		return true;
	}

	//删除目录
	public function rmdir($path , $options) {
		$path = trim(substr($path, 8));
		$path  = rtrim( $path  , '/' );

		memcache_delete( $this->mc() , $path .'.meta'  );

		//清除文件信息缓存
		clearstatcache( true );

		return true;
	}

	//删除文件
	public function unlink($path) {
		$path = trim(substr($path, 8));
		$path  = rtrim( $path  , '/' );

		memcache_delete( $this->mc() , $path );
		memcache_delete( $this->mc() , $path . '.meta' );

		//清除文件信息缓存
		clearstatcache( true );

		return true;
	}

	//查询文件元信息
	public function url_stat($path , $flags) {
		$path = trim(substr($path, 8));
		$path = rtrim( $path  , '/' );

		if ( !$this->is_file_info_exists( $path ) ) {
			return false;
		} else {
			$this->get_file_info( $path );
			return $this->stat;
		}
	}


	// ============================================

	//通过从Memcache中查询文件属性信息判断文件是否存在
	public function is_file_info_exists( $path ) {
		$d = memcache_get( $this->mc() , $path . '.meta' );
		return $d;
	}

	//查询文件属性信息
	public function get_file_info( $path ) {
		if ( $stat = memcache_get( $this->mc() , $path . '.meta' ) )
			return $this->stat =  unserialize($stat);
		else $this->statinfo_init();
	}

	//初始化文件属性信息
	public function statinfo_init( $is_file = true ) {
		$this->stat['dev'] = $this->stat[0] = 0;
		$this->stat['ino'] = $this->stat[1] = 0;

		if( $is_file )
			$this->stat['mode'] = $this->stat[2] = $this->file_mode;
		else
			$this->stat['mode'] = $this->stat[2] = $this->dir_mode;

		$this->stat['nlink'] = $this->stat[3] = 0;
		$this->stat['uid'] = $this->stat[4] = 0;
		$this->stat['gid'] = $this->stat[5] = 0;
		$this->stat['rdev'] = $this->stat[6] = 0;
		$this->stat['size'] = $this->stat[7] = 0;
		$this->stat['atime'] = $this->stat[8] = 0;
		$this->stat['mtime'] = $this->stat[9] = 0;
		$this->stat['ctime'] = $this->stat[10] = 0;
		$this->stat['blksize'] = $this->stat[11] = 0;
		$this->stat['blocks'] = $this->stat[12] = 0;

	}

	public function dir_closedir() {
		return false;
	}

	public function dir_opendir($path, $options) {
		return false;
	}

	public function dir_readdir() {
		return false;
	}

	public function dir_rewinddir() {
		return false;
	}

	public function stream_cast($cast_as) {
		return false;
	}

	public function stream_flush() {
		return false;
	}

	public function stream_lock($operation) {
		return false;
	}

	public function stream_set_option($option, $arg1, $arg2) {
		return false;
	}

}

//注册 SaeMemcacheWrapper 类为 Wrapper
if ( ! in_array("saemc", stream_get_wrappers()) )
	stream_wrapper_register("saemc", "SaeMemcacheWrapper");

About 智足者富

http://chenpeng.info

发表评论

电子邮件地址不会被公开。 必填项已用*标注

您可以使用这些HTML标签和属性:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>