分类: PHP

用PHP编写daemon process

没有评论

2013 年 07 月 30 日 at 下午 10:26分类:Linux | PHP

php从5开始,他的使用场景早已不限于处理web请求,而可以实现诸多服务化的使用场景。
从php的架构体系来说,php分为三个层次:sapi、php core和zend engine。
php core本身和web没有任何耦合,php通过sapi与其它应用程序通信,例如mod_php就是为apache编写的sapi实现,同样,fpm是一个基于fastcgi协议的sapi实现,这些sapi都是与web server配合用于处理web请求的。但是也有许多sapi与web无关,例如cli sapi可以使得在命令行环境下直接执行php,embed sapi可以将php嵌入其它语言(如Lua)那样。这里我并不打算详细讨论php的架构体系和sapi的话题,只是说明从架构体系角度目前的php早已被设计为支持各种环境,而非为web独有。

除了架构体系的支持外,php丰富的扩展模块也为php在不同环境发挥作用提供了后盾,例如本文要提到的pcntl(进程)模块和posix(信号)模块配合可以实现基本的进程管理、信号处理等操作系统级别的功能,而sockets模块可以使php具有socket通信的能力。因此php完全可以用于编写类似于shell或perl或python常做的工具性脚本,甚至是具有server性质的daemon process。

为了展示php如何编写daemon server,我用php编写了一个简单的http server,这个server以daemon process的形式运行。当然,为了把重点放在如何使用php编写daemon,我没有为这个http server实现具体业务逻辑,但它可以监听指定端口,接受http请求并返回给客户端一条固定的文本,整个过程通过socket实现,全部由php编写而成。

<?php
function handle_http_request($address, $port)
{
    $max_backlog = 16;
    $res_content = "HTTP/1.1 200 OK
        Content-Length: 15
        Content-Type: text/plain; charset=UTF-8

        PHP HTTP Server";
    $res_len = strlen($res_content);

    //Create, bind and listen to socket
    if(($socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) === FALSE)
    {
        echo "Create socket failed!\n";
        exit;
    }   

    if((socket_bind($socket, $address, $port)) === FALSE)
    {
        echo "Bind socket failed!\n";
        exit;
    }

    if((socket_listen($socket, $max_backlog)) === FALSE)
    {
        echo "Listen to socket failed!\n";
        exit;
    }

    //Loop
    while(TRUE)
    {
        if(($accept_socket = socket_accept($socket)) === FALSE)
        {
            continue;
        }
        else
        {
            socket_write($accept_socket, $res_content, $res_len);   
            socket_close($accept_socket);
        }
    }
}

//以daemon方式运行
function run()
{
    if(($pid1 = pcntl_fork()) === 0)
    //第一个子进程
    {
        posix_setsid(); //子进程1变为session leader,让子进程2与其祖先分离

        if(($pid2 = pcntl_fork()) === 0)
        //第二个子进程,以daemon方式跑他的父进程托管给了init
        {
            //此段代码调用个可以替换成你的代码 让你的代码也运行以daemon
            handle_http_request('www.codinglabs.org', 9999); 
        }
        else
        {
            //子进程退出
            exit;
        }
    }
    else
    {
        //等待子进程处理状态
        pcntl_wait($status);
    }
}

run();

?>

上面的代码分为两段,第一段是socket的过程,无非就是一些监听端口,接收客户端连接,处理连接,返回数据的过程;第二段是进程处理的过程,run函数负责将整个程序变为daemon process,方法和Unix环境下C的方法很类似,通过两次fork,第一次fork后调用setsid将子进程1变为session leader,这样就可以让子进程2与其祖先detach,即使祖先进程结束了它也会继续运行(托孤给init进程)。这个run函数可以用在我们自己的其他功能中,只需要修改其中的一行代码即可。

php为函数设置超时限制

没有评论

2013 年 07 月 25 日 at 下午 1:52分类:PHP

如何防止一个函数执行时间过长呢?在PHP里可以用pcntl时钟信号+异常来实现。

<?php
declare(ticks = 1);
function a()
{
    sleep(10);
    echo "a finishi\n";
}
function b()
{
    echo "Stop\n";
}
function c()
{
    usleep(100000);
}
 
function sig()
{
    throw new Exception;
}
 
try
{
    //设置一个闹钟信号为一秒钟执行一次
    pcntl_alarm(1);
    //安装闹钟信号,并绑定callback
    pcntl_signal(SIGALRM, "sig");
    a();
    //取消闹钟信号
    pcntl_alarm(0);
}
catch(Exception $e)
{
    echo "timeout\n";
}
 
b();
a();
b();

原理是在函数执行前先设定一个时钟信号,如果函数的执行超过规定时间,信号会被触发,信号处理函数(sig)会抛出一个异常,被外层代码捕获。这样就跳出了原来函数的执行,接着执行下面的代码。如果函数在规定的时间内,时钟信号不会触发,在函数结束后清除时钟信号,不会有异常抛出。

读取PDF文件的总页数

没有评论

2013 年 07 月 10 日 at 下午 10:03分类:PHP

最近有个小需求,需要批量分析一批pdf文件的总页数,于是找了个小函数:

function getPageTotal($path){
    ini_set("auto_detect_line_endings",true);
 
    if (!$fp = @fopen($path, 'r')) {
        return false;
    }
    $max=0;
    while(!feof($fp)) {
        $line = fgets($fp,255);
        if (preg_match('/\/Count [0-9]+/', $line, $matches)){
            preg_match('/[0-9]+/',$matches[0], $matches2);
            if ($max<$matches2[0]) $max=$matches2[0];
        }
    }
    fclose($fp);
    return $max;
}

echo getPageTotal('./test.pdf');

sublime安装格式化php代码插件(PHP_Beautifier)

没有评论

2013 年 04 月 22 日 at 下午 12:59分类:PHP | 其他技术

首先你的环境需要安装pear;
pear的下载:http://pear.php.net/go-pear.phar
下载好在命令行执行:php go-pear.phar;
安装perl成功后,要在d:\www\pear 目录下找到 PEAR_ENV.reg文件,执行导入即可。注意这个目录因人而已,选择不同的pear安装目录
那么这个文件所在地址也不一样。
perl安装成功,那么现在可以安装PHP_Beautifier,即然上面是通过perl安装的。那我在这也通过perl安装吧。

pear install PHP_Beautifier-0.1.15 

如果这里你有报什么错的话,那么就去问问百度和谷歌大神吧。哈哈~~
现在我们来安装sublime text的PhpBeautifier插件
通过sublime的package控制器直接安装 或者在这里 下载PhpBeautifier,解压到sublime text/data/package目录下
之后我们需要做些简单的修改:
1、打开你通过pear安装好的PHP_Beautifier的这个路径D:/www/pear/PHP/Beautifier/Filter 目录,

# 打开下面两个文件
 - Pear.filter.php  
 - phpBB.filter.php  
    
# 注释屌下面这句话
require_once ('PEAR/Config.php');  

然后修改sublime的插件文件:php_beautifier.py,找到滴26行代码

#修改这个地址为你的php_beautifier.bat所在文件目录
cmd = "D:\\www\\pear\\php_beautifier.bat"#"php_beautifier"

最好还要修改上面的那个bat文件:

//修改为你的bat文件所在的目录(应该是当前目录)
SET BEAUTIFY="D:\www\pear"\php_beautifier

OK。这样子就可以了
我们只要在php文件中使用ctrl+alt+f就可以格式化php的代码了
目前个人认为有个人缺点就是把默认的空行全部都去掉了。还有就是那个过滤的规则暂时还
不知道怎么写,还需要慢慢研究。。。

利用libevent来实现异步操作

没有评论

2013 年 04 月 16 日 at 下午 1:26分类:PHP

libevent是php的一个网络异步方面的拓展,利用他我们可以实现一些网络异步访问方面的工作。比如抓去远程网页的内容等,若是在从前,一个比较靠谱的办法是利用curl_multi_exec或者其他函数来串行的请求好几个接口,但是这个办法需要用一个do … while循环来完成请求,很是坑爹,必须等待上一次执行完了才会执行下一次的操作,那么要是用libevent呢,可不是酱紫的哦。下面直接晒代码:

<?php
function httpGet($host, $base) {

    global $index;
	
	//使用fsockopen会导致阻塞
    //$fd = fsockopen($host, 80, $errno, $errstr, 3);
    $fd = stream_socket_client("{$host}:80", $errno, $errstr, 3, STREAM_CLIENT_ASYNC_CONNECT | STREAM_CLIENT_CONNECT); 
    $index[$fd] = 0;
	$event = event_new();
	//设置一个事件
	//回调函数有两个默认参数,第三个为传递值
    event_set($event, $fd, EV_WRITE | EV_PERSIST, function($fd, $events, $arg) use($host) {

		global $times, $limit, $index;
		
		if (!$index[$fd]) {
            $index[$fd] = 1;
            $out = "GET / HTTP/1.1\r\n";
            $out .= "Host: $host\r\n";
            $out .= "Connection: Close\r\n\r\n";
            fwrite($fd, $out);
        } else {
			//这里没有显示全部内容,这里获取的内容按照自己的需要去处理
            echo substr(fread($fd, 4096), 0, 20);
            if(feof($fd)) {
                fclose($fd);
                $times++;
                echo "done\n";
				//如果执行到最后一个事件了那么执行完毕退出
                if($times == $limit) {
                    event_base_loopexit($arg[1]);
                }
            }
		}
    }, array($event, $base));
    event_base_set($event, $base);
	//添加事件
    event_add($event);
}

$times = 0;
$limit = 2;
$index = array();

$base = event_base_new();

$urls = array('http://www.baidu.com', 'http://www.fbbin.com');

for($i = 0; $i < $limit; $i++) {
    echo "$i\n";
    httpGet($urls[$i], $base);
}

event_base_loop($base);

?>

代码中的 httpGet($urls[$i], $base);这行虽然是靠一个命令行顺序执行,但是不会阻塞后面的代码,直接就进行下一次请求了。在你运行的结果中你应该是先看到输出$i的值完了之后才会输出获取到的网页内容的。

Gearman的安装和使用

没有评论

2013 年 04 月 15 日 at 下午 1:17分类:Nginx | PHP | 其他技术

Gearman是一个用来把工作委派给其他机器、分布式的调用更适合做某项工作的机器、并发的做某项工作在多个调用间做负载均衡、或用来在调用其它语言的函数的系统。
通常,多语言多系统之间的集成是个大问题,一般来说,人们多半会采用 WebService 的方式来处理此类集成问题,但不管采用何种风格的 WebService,如 RPC 风格,或者 REST 风格,其本身都有一定的复杂性。相比之下,Gearman 也能实现类似的作用,而且更简单易用。
一个Gearman请求的处理过程涉及三个角色:Client -> Job -> Worker。
Client:请求的发起者,可以是 C,PHP,Perl,MySQL UDF 等等。
Job:请求的调度者,用来负责协调把 Client 发出的请求转发给合适的 Work。
Worker:请求的处理者,可以是 C,PHP,Perl 等等。
因为 Client,Worker 并不限制用一样的语言,所以有利于多语言多系统之间的集成。
甚至我们通过增加更多的 Worker,可以很方便的实现应用程序的分布式负载均衡架构。
详细的一些信息就去问谷歌大神吧。
Gearman的安装,可能有些麻烦 我一开始用 的CentOS5.4的机器 结果是各种问题啊,后来实在不行了 ,换了6.2的机器来安装。

# wget https://launchpad.net/gearmand/1.2/1.1.5/+download/gearmand-1.1.5.tar.gz
# tar xzvf gearmand-1.1.5.tar.gz
# cd gearmand-1.1.5
# ./configure --prefix=/usr/local/gearmand
#  make
#  make install

这里如果出现了相关configure的错误(Boost ,uuid,mysql)的话,那么到网上找资料解决就好了,基本上都能解决,没什么难度的。
这样子的话,那么gearmand的服务端就完成了,我们称之为Job Server。
那么我们现在启动它:

# usr/local/gearmand/sbin/gearmand -d -P /var/run/gearmand.pid -l /var/log/gearmand.log -u root

接下来我们来安装gearmand的php拓展,我们在http://pecl.php.net中找下是可以找到的,如果找不到那么去这里点击也可以下载到相应的拓展。

# wget http://pecl.php.net/get/gearman-1.1.1.tgz
# tar zxvf gearman-1.1.1.tgz
# cd gearman
# phpize
# ./configure --with-php-config=/usr/local/php/bin/php-config --with-gearman=/usr/local/gearmand
# make 
# make install

这里一定要加上–with-gearman参数,不然你又要为这个拓展单独去安装哥libgearmand了,没这个必要,网上有些文章就是这个错误引起的很多问题。
成功之后加入到php的配置文件中。
下面是我写的一个例子:

#worker.php
<?php
$worker=new GearmanWorker();
$worker->addServer("127.0.0.1", 4730);  //连接到Job server(gearmand) 上
$worker->addFunction("test", "testFunction"); 
while ($worker->work());
function testFunction($job) {
	$result = $job->workload();
	sleep(5);
	$data = unserialize($result);
	file_put_contents('./s.txt', var_export($data, true));
}
# client.php
<?php

$client=new GearmanClient();

$client->addServer("127.0.0.1", 4730); //连接到Job server(gearmand)上

$data = array('file'=>__FILE__, 'line'=>__LINE__);

$handler = $client->doBackground("test", serialize($data));//让任务在后台执行也就是异步

var_dump($handler);

$done = false;

echo "Running: .";

do {
   sleep(1);
   $stat = $client->jobStatus($handler);
   if (!$stat[0]) {
      $done = true;
   }
   echo " .";
} while(!$done);

echo "\n";

这个例子呢就是将客户端要执行的抛给gearmand的job server,让他去调度一个空闲的worker来执行我当前的这个任务。我们的woker可以有很多个,jobserver也可以有很多个,这样子就起到了负载均衡的作用了。这个东西在某些情况下应该比队列的效应来的更好些。
在实际使用时应该是运行gearmand -d 的 server 一台或者多台. [要装gearmand,运行gearmand];处理worker的机器若干[要装gearmand 及php so.但gearmand不需要运行].下达任务client若干[要装gearmand 及php so.但gearmand不需要运行]. worker 与 client 不会直接通信,都是通过gearmand来调度分发处理的.

参考:
1、http://www.it300.com/article-15263.html
2、http://blog.s135.com/dips
3、http://gearman.org/download

PHP中获取文件扩展名的几种方法比较

没有评论

2013 年 04 月 12 日 at 下午 1:19分类:PHP

第1种方法:

function get_extension($file) { 
    substr(strrchr($file, '.'), 1); 
} 

第2种方法:

 
function get_extension($file) { 
    return substr($file, strrpos($file, '.')+1); 
} 

第3种方法:

function get_extension($file) { 
    return end(explode('.', $file)); 
} 

第4种方法:

function get_extension($file) { 
   $info = pathinfo($file); 
   return $info['extension']; 
} 

第5种方法:

function get_extension($file) { 
   return pathinfo($file, PATHINFO_EXTENSION); 
} 

以上集中方法好像都行,特别是1、2种方法,在我不知道pathinfo有第二个参数之前也一直在用。
但是仔细考虑一下,前四种方法都有各种各样的毛病。要想完全正确获取文件的扩展名,必须要能处理以下三种特殊情况。
1、没有文件扩展名
2、路径中包含了字符.,如/home/test.d/test.txt
3、路径中包含了字符.,但文件没有扩展名。如/home/test.d/test

很明显:1、2不能处理第三种情况,3不能正确处理第一三种情况。4可以正确处理,但是在不存在扩展名时,会发出一个警告。
只有第5种方法才是最正确的方法。顺便看一下pathinfo方法。官网上介绍如下:

$file_path = pathinfo('/www/htdocs/your_image.jpg'); 
echo "$file_path ['dirname']\n"; 
echo "$file_path ['basename']\n"; 
echo "$file_path ['extension']\n"; 
echo "$file_path ['filename']\n"; // only in PHP 5.2+ 

它会返回一个数组,包含最多四个元素,但是并不会一直有四个,比如在没有扩展名的情况下,就不会有extension索引存在,所以第4种方法才会发现警告。但是phpinfo还支持第二个参数。可以传递一个常量,指定返回某一部分的数据:

PATHINFO_DIRNAME – 目录
PATHINFO_BASENAME – 文件名(含扩展名)
PATHINFO_EXTENSION – 扩展名
PATHINFO_FILENAME – 文件名(不含扩展名,PHP>5.2)

这四个常量的值分别是1、2、4、8,刚开始我还以为可以通过或运算指定多个:
pathinfo($file, PATHINFO_EXTENSION | PATHINFO_FILENAME);
后来发现这样不行,这只会返回几个进行或运算常量中最小的那个。也就是四个标志位中最小位为1的常量。

PHP用CURL伪造IP和来源

没有评论

2013 年 04 月 10 日 at 下午 10:19分类:PHP

今天群里一个朋友在问这个问题。
查了下,CURL确实很强悍的可以伪造IP和来源。
1.php 请求 index.php 。
1.php代码:

<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://localhost/index.php");
curl_setopt($ch, CURLOPT_HTTPHEADER, array('X-FORWARDED-FOR:8.8.8.8', 'CLIENT-IP:8.8.8.8'));  //构造IP
curl_setopt($ch, CURLOPT_REFERER, "http://www.sina.com.cn/ ");   //构造来路
curl_setopt($ch, CURLOPT_HEADER, 1);
$out = curl_exec($ch);
curl_close($ch);

2.php代码如下:

<?php
function getClientIp() {
    if (!empty($_SERVER["HTTP_CLIENT_IP"]))
        $ip = $_SERVER["HTTP_CLIENT_IP"];
    else if (!empty($_SERVER["HTTP_X_FORWARDED_FOR"]))
        $ip = $_SERVER["HTTP_X_FORWARDED_FOR"];
    else if (!empty($_SERVER["REMOTE_ADDR"]))
        $ip = $_SERVER["REMOTE_ADDR"];
    else
        $ip = "err";
    return $ip;
}

echo "IP: " . getClientIp() . "";
echo "referer: " . $_SERVER["HTTP_REFERER"];

echo "IP: " . getClientIp() . "";
echo "referer: " . $_SERVER["HTTP_REFERER"];

伪造成功,这是不是给“刷票”的朋友提供了很好的换IP的方案!!
哈哈。

结果:
HTTP/1.1 200 OK Date: Wed, 03 Apr 2013 06:20:42 GMT Server: Apache/2.2.22 (Win32) PHP/5.3.13 X-Powered-By: PHP/5.3.13 Content-Length: 44 Content-Type: text/html
IP: 8.8.8.8
referer: http://www.sina.com.cn/

Php session内部执行流程的再次剖析

没有评论

2013 年 03 月 25 日 at 下午 2:48分类:PHP

近期再次分析了php session内部的执行流程,我将在这篇文章中简要地概括出php内部关于session的执行步骤。
首先php中的session其实就是作为一个扩展载入到php内核中的。我们可以将它理解成一个扩展就可以了。当session扩展被载入时,php会调用内部核心函数来获取处理session的save_handler – 也就是存储读取session数据的接口类或者函数。 Php默认地是通过写或者读取文件来处理session数据的。但是,php也提供了user自定义的方式 – 也就是自定义处理session数据的接口,可以通过session_set_save_handler函数来注册。关于这方面,后续我会详细写一篇文章。同时,php会判断session.auto_start是否已经在配置中默认开启。如果开启了session.auto_start,PHP便会调用内部函数自动开启Session功能。以上,就是session扩展被载入时php内部所处理的两件事情。
接下来,php在启动session的时候,如果发现请求的Cookies,Get,Post中不存在session id,说明这是客户端的第一次访问,php会自动调用php_session_create_id函数创建一个唯一的session id,并且在http response中通过set-cookie头部发送给客户端保存(在客户端cookie被禁用的情况下,php也可以自动将session id添加到url参数中以及form的hidden 字段中,这需要将php.ini中的session.use_trans_sid设为开启,也可以在运行时调用ini_set来设置这个配置项)。相反,如果请求中已经携带了session id,那么php会做以下几件事情:
1、从cookie中获取Session ID
2、调用save_handler的open接口打开存储上下文
3、如果读取不到对应的session id, 生成新的Session Id
4、注册$_SESSION和$_HTTP_SESSION_VARS全局变量,$_SESSION和$_HTTP_SESSION_VARS会被注册为同一个数组
5、接着调用save_handler的read接口读取Session数据,如果是使用files方式存储的话,就从文件中读取Session数据,数据库方式的话,就从数据库中读取。 读取完毕后会把读到的数据写入到$_SESSION数组中
最后,当一个请求执行完毕时,php会调用内部函数获取$_SESSION数组中的值,然后调用php_session_encode将其系列化后,通过调用save_handler的write接口将session系列化数据存储起来。
以上大致按照顺序列出了php session的内部执行流程。

转载自:http://www.360weboy.com/php/session-cookie/php_session.html

同一域名对应多个IP时,PHP获取远程网页内容的函数[转载]

没有评论

2013 年 03 月 12 日 at 下午 9:03分类:PHP

PHP获取远程网页内容有多种方式,例如用自带的file_get_contents、fopen等函数。

<?php   
echo file_get_contents("http://www.fbbin.com/test.php");   
?>

但是,在DNS轮询等负载均衡中,同一域名,可能对应多台服务器,多个IP。假设www.fbbin.com被DNS解析到173.231.44.230、173.231.44.231、173.231.44.232三个IP,用户每次访问www.fbbin.com,系统会根据负载均衡的相应算法访问其中的一台服务器。
上周做一个视频项目时,就碰到这样一类需求:需要依次访问每台服务器上的一个PHP接口程序(假设为abc.php),查询这台服务器的传输状态。
这时就不能直接用file_get_contents访问http://www.fbbin.com/test.php了,因为它可能一直重复访问某一台服务器。
而采用依次访问http://173.231.44.230/test.php、http://173.231.44.231/test.php、http://173.231.44.232/test.php的方法,在这三台服务器上的Web Server配有多个虚拟主机时,也是不行的。通过设置本地hosts也不行,因为hosts不能设置多个IP对应同一个域名。
那就只有通过PHP和HTTP协议来实现:访问test.php时,在header头中加上www.fbbin.com域名。思路是这样子的:
如果那三台服务器有对外的IP,则做个host指向过去就可以定向抓取了~
程序实现的话,我觉得应该是这样的思路:
1) 用fsockopen建立与指定IP的80端口的连接;
2) 构造http头信息
GET status.html HTTP/1.1
Host: www.fbbin.com
发送到目标服务器
3) 获取服务器端返回的内容
4) 多IP轮询一遍就行了。
下面用PHP函数予以实现。

<?php
/************************
* 函数用途:同一域名对应多个IP时,获取指定服务器的远程网页内容
* 创建时间:2008-12-09
* 创建人:张宴(blog.s135.com)
* 参数说明:
*    $ip   服务器的IP地址
*    $host   服务器的host名称
*    $url   服务器的URL地址(不含域名)
* 返回值:
*    获取到的远程网页内容
*    false   访问远程网页失败
************************/
function HttpVisit($ip, $host, $url)   
{   
    $errstr = '';   
    $errno = '';
    $fp = fsockopen ($ip, 80, $errno, $errstr, 90);
    if (!$fp)   
    {   
         return false;   
    }   
    else  
    {   
        $out = "GET {$url} HTTP/1.1\r\n";
        $out .= "Host:{$host}\r\n";   
        $out .= "Connection: close\r\n\r\n";
        fputs ($fp, $out);   

        while($line = fread($fp, 4096)){
           $response .= $line;
        }
        fclose( $fp );

        //去掉Header头信息
        $pos = strpos($response, "\r\n\r\n");
        $response = substr($response, $pos + 4);
    
        return $response;   
    }   
}

//调用方法:
$server_info1 = HttpVisit("173.231.44.230", "www.fbbin.com", "/test.php");
$server_info2 = HttpVisit("173.231.44.231", "www.fbbin.com", "/test.php");
$server_info3 = HttpVisit("173.231.44.232", "www.fbbin.com", "/test.php");
?>