nginx 支持 pathinfo 模式和去掉 index.php

我们在使用ThinkPHP框架的时候,默认的时候,都是喜欢用PATHINFO的URL模式,这种框模式对SEO很友好。我们的Apache默认下都是支持PATHINFO的,但是,在 nginx 下,默认是不支持的,我们要开启nginx的PATHINFO支持。

其实配置很简单,只要增加下面几行加数代码,就可以了,注意,这里是nginx1.6以上,如果版本太低没有做测试。不过如果您的 nginx版本太低还是建议升级一下。现在最高稳定版本是1.81了。

location ~ \.php {
        root           /study/study;
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;

        #支持 pathinfo 模式
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_param  SCRIPT_FILENAME  /stucdy/study$fastcgi_script_name;
        fastcgi_param  PATH_INFO $fastcgi_path_info;
        fastcgi_param  PATH_TRANSLATED  $document_root$fastcgi_path_info;

        include        fastcgi_params;
}

配置好之后,我们重启一下nginx,重启可以用下面这行命令

kill -HUP 主进程号,如:kill -HUP cat /usr/local/nginx/log/nginx.pid

重启后,我们就可以 https://www.yduba.com/index.php/Index/index.html 访问网站了。

这时,我们一般不希望URL里带着index.php,不友好。我们可以再配置一下,把 index.php 也去掉。

location / {
    if (!-e $request_filename){
        rewrite ^/(.*)$ /index.php?s=/$1 last;
    }
}

其实原理很简单,就是当访问文件不存在时,就会重写到 /index.php 文件上,并把 / 之后的当做参数加在 /index.php 之后

配置好之后,再重启一下 nginx,然后,我们就可以 https://www.yduba.com/Index/index.html 访问网站了。

PHP验证邮箱地址是否合法

背景

PHP校验邮箱地址的方法很多, 比较常用的就是自己写正则了, 不过正则多麻烦, 用PHP自带了方法做校验多方便;

filter_var

filter_var是PHP内置的一个变量过滤的方法,提供了很多实用的过滤器,可以用来校验整数、浮点数、邮箱、ULR、MAC地址等等。使用格式如:
mixed filter_var ( mixed $variable [, int $filter = FILTER_DEFAULT [, mixed $options ]] )

参数说明:
$variable 待过滤的变量
$filter 应用的筛选器的标识。具体可以查看https://php.net/manual/zh/filter.filters.sanitize.php
$options

一个选项的关联数组,或者按位区分的标示。如果过滤器接受选项,可以通过数组的 "flags" 位去提供这些标示。 对于回调型的过滤器,应该传入 callable。这个回调函数必须接受一个参数,即待过滤的值,并且 返回一个在过滤/净化后的值。
filter_var如果返回false, 说明变量无法通过过滤器, 也就是不合法了。

$email = "ningpanyun@@yeah.net";
var_dump(filter_var($email,  FILTER_VALIDATE_EMAIL));
$email = "asb";
var_dump(filter_var($email,  FILTER_VALIDATE_EMAIL));

$email = "1@a.com";
var_dump(filter_var($email,  FILTER_VALIDATE_EMAIL));
输出:

string(21) "lastchiliarch@163.com"
bool(false)
string(7) "1@a.com"

对于asb这种非法邮箱格式返回了false, 但对于1@a.com则通过了,还是略有瑕疵啊。不过一般的正则也通过会认为1@a.com是一个合法的邮箱, 那有啥办法可以更精准的验证呢?

checkdnsrr

checkdnsrr其实是用来查询指定的主机的DNS记录的,我们可以借用它来验证邮箱是否存在。对于1@abc.com 肯定是MX记录不存在的。checkdnsrr函数的使用可以查看https://php.net/manual/zh/function.checkdnsrr.php

$email = "ningpanyun@@yeah.net";
$arr = explode("@", $email);
$str = array_pop( $arr );
var_dump( checkdnsrr($str, 'MX') );

$email = "1@a.com";
$arr = explode("@", $email);
$str = array_pop( $arr );
var_dump( checkdnsrr($str, 'MX') );

输出:
bool(true)
bool(false)
filter_var+checkdnsrr

我们可以接合filter_var 和checkdnsrr做校验, 对于绝大多数的非法邮箱肯定会在filter_var的时候就挂掉了, 剩下的再用checkdnsrr进一步判断。

$email = "ningpanyun@@yeah.net";
if (filter_var($email) === false) 
{
    echo "invalid email: $email \n";
    continue;
}
if(checkdnsrr(array_pop(explode("@",$email)), "MX") === false) 
{
    echo "invalid email: $email \n";
    continue;
}
输出: invalid email: 1@a.com

但要注意的是, 由于只是检查MX记录, 所以只能判断163.com是存在的, 但不能说明ningpanyun这个用户是存在的。想要更精确的判断邮箱存在, 那只能连接到smtp服务器去验证了。

php魔术方法详解

PHP 将所有以 (两个下划线)开头的类方法保留为魔术方法。所以在定义类方法时,除了上述魔术方法,建议不要以 为前缀。方术方法包括:(加组表示常用)

__construct(),类的构造函数

__destruct(),类的析构函数

__call(),在对象中调用一个不可访问方法时调用

__callStatic(),用静态方式中调用一个不可访问方法时调用

__get(),获得一个类的成员变量时调用

__set(),设置一个类的成员变量时调用

__isset(),当对不可访问属性调用isset()或empty()时调用

__unset(),当对不可访问属性调用unset()时被调用。

__clone(),当对象复制完成时调用

__autoload(),尝试加载未定义的类

__toString(),类被当成字符串时的回应方法

__sleep(),执行serialize()时,先会调用这个函数

__wakeup(),执行unserialize()时,先会调用这个函数

__invoke(),以调用函数的方式调用一个对象时会行此方法

__set_state(),调用var_export()导出类时,此静态方法会被调用。

__debugInfo(),打印所需调试信息

范例

下面让我们以实例的形式向大家讲解下这几个魔术方法是如何使用的。

1、__construct(),类的构造函数

说明:php中构造方法是对象创建完成后第一个被对象自动调用的方法。在每个类中都有一个构造方法,如果没有显示地声明它,那么类中都会默认存在一个没有参数且内容为空的构造方法。

作用: 通常构造方法被用来执行一些有用的初始化任务,如对成员属性在创建对象时赋予初始值。

格式: void __construct()

注意: 在同一个类中只能声明一个构造方法,原因是,PHP不支持构造函数重载。

<?php
class Demo
{
    private $prop;

    public function __construct($val) 
    {
        $this->prop = $val;
    }

    public function abc()
    {
        echo $this->prop;
    }
}
$demo = new Demo(1); // 会执行 __construct 方法
$demo->abc(); // 输出 1

2、__destruct(),类的析构函数

说明:析构方法允许在销毁一个类之前执行的一些操作或完成一些功能,比如说关闭文件、释放结果集等。

析构方法是PHP5才引进的新内容。析构函数不能带有任何参数

析造方法的声明格式与构造方法 __construct() 比较类似,也是以两个下划线开始的方法 __destruct() ,这种析构方法名称也是固定的

<?php
class Demo
{
    private $prop;

    public function __construct($val) 
    {
        echo "实例化的时候,会首先调用这个方法<br><br>";
        $this->prop = $val;
    }

    public function __destruct()
    {
        echo "在销毁时调用";
    }
}

$demo = new Demo(1); // 会执行 __construct 方法

// 这里会输出“在销毁时调用”, 默认的时候,我们不需要手动去 unset 一个对象实例,当PHP执行完会自动销毁
3、__call()和__callStatic()方法

说明:该方法有两个参数,第一个参数$functionName会自动接收不存在的方法名,第二个 $arguments 则以数组的方式接收不存在方法的多个参数

格式:public function __call(string $functionName, array $arguments)

格式:public static function __callStatic( string $functionName, array $arguments )

作用:为了避免当调用的方法不存在时产生错误,而意外的导致程序中止,可以使用 __call() 方法来避免。该方法在调用的方法不存在时会自动调用,程序仍会继续执行下去。

<?php
class Demo
{
    public function __call($functionName, $arguments)
    {
        var_dump( $functionName );
        var_dump( $arguments );
    }

    public static function __callStatic($functionName, $arguments)
    {
        var_dump( $functionName );
        var_dump( $arguments );
    }
}

$demo = new Demo();
$demo->test(123); // 会调用 __call() 方法
$demo::abc(123); // 会调用 __callStatic() 方法

4、__get()和__set()方法

说明:在 php 面向对象编程中,类的成员属性被设定为 private 后,如果我们试图在外面调用它则会出现“不能访问某个私有属性”的错误。那么为了解决这个问题,我们可以使用魔术方法 __get()

同样给一个未定义的属性赋值时,此方法会被触发,传递的参数是被设置的属性名和值

作用:__get 可以在必要的时候获取私有成员属性的值,__set 可以给未定义属性赋值

格式:public function __get($propertyName)

格式:public function __set($propertyName, $propertyValue)

<?php
class Demo
{
    private $prop = 123;

    public function __get($propertyName)
    {
        if( isset($this->$propertyName) )
        {
            return $this->$propertyName;
        }
    }

    public function __set($propertyName, $propertyValue)
    {
        $this->$propertyName = $propertyValue;
    }
}

$demo = new Demo();
echo $demo->prop;// 调用 __get() 返回 123
$demo->test = 123; // 调用 __set() 给 $demo 对象设置一个 test 属性

5、__isset()方法,当对不可访问属性调用isset()或empty()时调用

说明:在看这个方法之前我们看一下isset()函数的应用,isset()是测定变量是否设定用的函数,传入一个变量作为参数,如果传入的变量存在则传回true,否则传回false。如果变量的值是 NULL, isset() 也是返回 false

那么如果在一个对象外面使用isset()这个函数去测定对象里面的成员是否被设定可不可以用它呢?

分两种情况,如果对象里面成员是公有的,我们就可以使用这个函数来测定成员属性,如果是私有的成员属性,这个函数就不起作用了,原因就是因为私有的被封装了,在外部不可见。那么我们就不可以在对象的外部使用isset()函数来测定私有成员属性是否被设定了呢?当然是可以的,但不是一成不变。你只要在类里面加上一个__isset()方法就可以了,当在类外部使用isset()函数来测定对象里面的私有成员是否被设定时,就会自动调用类里面的__isset()方法了帮我们完成这样的操作。

__isset()的作用:当对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用。

<?php
class Demo
{
    private $prop = 123;

    public function __isset($propertyName)
    {
        var_dump( $propertyName );

        return isset($this->$propertyName);
    }
}

$demo = new Demo();
var_dump( isset($demo->prop) ); // 这里会调用 __isset() 方法,返回 __isset() 的返回值true

6、__unset()方法,当对不可访问属性调用unset()时被调用

看这个方法之前呢,我们也先来看一下 unset() 函数,unset()这个函数的作用是删除指定的变量且传回true,参数为要删除的变量。

那么如果在一个对象外部去删除对象内部的成员属性用unset()函数可以吗?

这里自然也是分两种情况:

1、 如果一个对象里面的成员属性是公有的,就可以使用这个函数在对象外面删除对象的公有属性。

2、 如果对象的成员属性是私有的,我使用这个函数就没有权限去删除。

虽然有以上两种情况,但我想说的是同样如果你在一个对象里面加上__unset()这个方法,就可以在对象的外部去删除对象的私有成员属性了。在对象里面加上了__unset()这个方法之后,在对象外部使用“unset()”函数删除对象内部的私有成员属性时,对象会自动调用__unset()函数来帮我们删除对象内部的私有成员属性。

<?php
class Demo
{
    private $prop = 123;

    public function __unset($propertyName)
    {
        var_dump( $propertyName );
        unset( $this->$propertyName );
    }
}

$demo = new Demo();
unset($demo->prop); // 这里会调用 __unset() 方法,删除私有属性 prop
print_r( $demo ); // 已经没有了 prop 属性

7、__clone()方法,当复制一个对象的时候会被调用

说明:在多数情况下,我们并不需要完全复制一个对象来获得其中属性。但有一个情况下确实需要:如果你有一个 GTK 窗口对象,该对象持有窗口相关的资源。你可能会想复制一个新的窗口,保持所有属性与原来的窗口相同,但必须是一个新的对象(因为如果不是新的对象,那么一个窗口中的改变就会影响到另一个窗口)。还有一种情况:如果对象 A 中保存着对象 B 的引用,当你复制对象 A 时,你想其中使用的对象不再是对象 B 而是 B 的一个副本,那么你必须得到对象 A 的一个副本

格式:void __clone( void )

<?php
class Demo
{
    private $prop = 123;
    private $link = 'abc';

    public function __clone()
    {
        $this->prop = 345;
    }
}

$demoa = new Demo();
$demob = clone $demoa;

var_dump( $demoa ); // demoa 的prop为123
var_dump( $demob ); // demob 的prop为345

8、__autoload() 函数,尝试加载未定义的类

说明:这个函数和其它方法有所不同,这个函数不需要放到类里面。是在类外调用

格式:function void __autoload( string $className );

作用:可以通过这个函数,来做自动加载

<?php
function __autoload($className)
{
    var_dump( $className );
    include '这里要加载 $className 这个类的文件';
}

$demoa = new Demo(); // 如果没有Demo这个类,就会调用 __autoload 函数

以上就是PHP中常用的魔术方法了,以下几个不常用。但是最好了解下,当看到别人的代码用到时候,最好知道什么意思

1、__sleep() 和 __wakeup() 方法

格式:public array __sleep ( void )

格式: void __wakeup ( void )

serialize() 函数会检查类中是否存在一个魔术方法 __sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。如果该方法未返回任何内容,则 NULL 被序列化,并产生一个 E_NOTICE 级别的错误

__sleep() 方法常用于提交未提交的数据,或类似的清理操作。同时,如果有一些很大的对象,但不需要全部保存,这个功能就很好用。

与之相反,unserialize() 会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup 方法,预先准备对象需要的资源。__wakeup() 常用在反序列化操作中,例如重新建立数据库连接,或执行其它初始化操作

<?php
class Connection
{
    private $link = 111;
    private $connection = 'abc';
    
    public function __sleep()
    {
        echo "当调用serialize函数时,先调用此方法", "<br/>";
        return array('link', 'connection');
    }
    
    public function __wakeup()
    {
        echo "当调用unserialize函数时,先调用此方法", "<br/>";
    }
}

$mylink = new Connection('localhost', 'root', 'root', 'test');
$serialize =  serialize($mylink);// 输出 "当调用serialize函数时,先调用此方法"
unserialize( $serialize ); // 输出 "当调用unserialize函数时,先调用此方法"

2、__toString() 方法

格式:public string __toString ( void )

__toString() 方法用于一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么。此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR 级别的致命错误。

<?php
class TestClass
{
    public $foo;

    public function __construct($foo) 
    {
        $this->foo = $foo;
    }

    public function __toString() 
    {
        return $this->foo;
    }
}

$class = new TestClass('Hello');
echo $class; // Hello

特别说明:在 PHP 5.2.0 之前,__toString() 方法只有在直接使用于 echo 或 print 时才能生效。PHP 5.2.0 之后,则可以在任何字符串环境生效(例如通过 printf(),使用 %s 修饰符),但不能用于非字符串环境(如使用 %d 修饰符)。自 PHP 5.2.0 起,如果将一个未定义 __toString() 方法的对象转换为字符串,会产生 E_RECOVERABLE_ERROR 级别的错误

3、__invoke() 方法

格式:public mixed __invoke ([ $... ] )

当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用。本特性只在PHP5.3.0及以上版本有效。

<?php
class CallableClass 
{
    public function __invoke($x) 
    {
        var_dump($x);
    }
}

$obj = new CallableClass;
$obj(5); // int 5
var_dump(is_callable($obj)); // boolean true

4、__set_state() 方法

格式:public static object __set_state( array $properties )

说明:自 PHP 5.1.0 起当调用 var_export() 导出类时,此静态 方法会被调用。本方法的唯一参数是一个数组,其中包含按 array('property' => value, ...) 格式排列的类属性

<?php
class A
{
    public $var1;
    public $var2;

    public static function __set_state($an_array) 
    {
        $obj = new A;
        $obj->var1 = $an_array['var1'];
        $obj->var2 = $an_array['var2'];
        return $obj;
    }
}

$a = new A;
$a->var1 = 5;
$a->var2 = 'foo';
eval('$b = ' . var_export($a, true) . ';');
var_dump( $b ); 
// 此时 $b == A::_set_state(array('var1'=>5, 'var2'=>'foo'))

5、__debugInfo()方法

格式:public array __debugInfo ( void )

说明:这个方法在PHP 5.6.0以后支持, 当在用 var_dump 函数输出类的时候,会调用这方法

<?php
class C 
{
    private $prop;

    public function __construct($val) 
    {
        $this->prop = $val;
    }

    public function __debugInfo() 
    {
        echo "会先调用这个方法";
        return array(1, 2, 3);
    }
}
var_dump(new C(42)); // 会先调用 __debugInfo()

本文参考内容:

https://php.net/manual/zh/language.oop5.magic.php

PHP分段读取大文件并统计

有时候,我们经常对大文件进行操作和分析,比如:去统计日志里某个IP最近访问网站的情况。nginx 的 assess.log 的文件就记录了访问日志,但是这个文件一般的情况下都是特别大的。用PHP的话,怎么去统计里面的信息呢?这里自己做一个学习总结。
理论方法:
1、把文件一次性读到内存中,然后一行一行分析,如:函数 file_get_contents、fread、file 这些都可以操作
2、分段读取内容到内存里,一段一段的分析,如:函数 fgets

这两种方法都可以现实我们的操作。首先说一说第一种,一次性读取内容。这种方法比较简单,就是把日志读到内存之后,转换成数组,然后分析分析数组的每一列。代码如下:

<?php
defined('READ_FILE')  or define('READ_FILE', './assess.log');
defined('WRITE_FILE_A') or define('WRITE_FILE_A', './temp1');

// 方法 file_get_contents
function writeLogA()
{
    $st = microtime( true );
    $sm = memory_get_usage();

    $content         = file_get_contents(READ_FILE);
    $content_arr     = explode("\n", $content);
    $writeres         = fopen(WRITE_FILE_A, 'a+');

    // 锁定 WRITE_FILE
    flock($writeres, LOCK_EX);

    foreach($content_arr as $row)
    {
    if( strpos($row, "192.168.10.10") !== false )
    {
            fwrite($writeres, $row  . "\n");
    }
    }

    $em = memory_get_usage();
    flock($writeres, LOCK_UN);
    fclose($writeres);

    $et = microtime( true );
    echo "使用 file_get_contents: 用时间:" . ($et - $st) . "\n";
    echo "使用 file_get_contents: 用内存:" . ($em - $sm) . "\n";
}

writeLogA();

以上代码运行之后,就可以把IP为192.168.10.10的用户访问日志给写到一个临时文件中,但是这时候,有一个问题是,代码运行的时间和消耗的内存特别大。特别说明:我的 assess.log 并不大,10万行左右,这里只为做演示 。请输入图片描述

现在这个access.log文件并不大,已经用了这么长时间和消耗这么大的内存了,如果更大的文件呢,所以,这种方法并不实用。
再看看另一个函数 fread, fread是读取一个文件的内容到一个字符串中。
string fread ( resource $handle , int $length )
第一个参数是文件系统指针,由 fopen 创建并返回的值,第二个参数是要读取的大小。返回值为正确读取的内容。
我们现在把上面的代码中的 file_get_contents 替换成 fread。代码如下:

<?php
defined('READ_FILE')  or define('READ_FILE', './error.log');
defined('WRITE_FILE_B') or define('WRITE_FILE_B', './temp1');

// 方法 fread
function writeLogB()
{
    $st = microtime( true );
    $sm = memory_get_usage();

    $fopenres = fopen(READ_FILE, "r");
    $content  = fread($fopenres, filesize(READ_FILE));
    $content_arr     = explode("\n", $content);

    $writeres         = fopen(WRITE_FILE_B, 'a+');

    // 锁定 WRITE_FILE
    flock($writeres, LOCK_EX);

    foreach($content_arr as $row)
    {
        if( strpos($row, "[error]") !== false )
    {
        fwrite($writeres, $row  . "\n");
    }
    }

    $em = memory_get_usage();
    flock($writeres, LOCK_UN);
    fclose($writeres);
    fclose($fopenres);

    $et = microtime( true );
    echo "使用 fread: 用时间:" . ($et - $st) . "\n";
    echo "使用 fread: 用内存:" . ($em - $sm) . "\n";
}
writeLogB();

如果不出什么特别的情况下,内存消耗会比上一个代码更大。结果图如下:请输入图片描述
这一点,在PHP的官方网站也有说明:如果只是想将一个文件的内容读入到一个字符串中,用 file_get_contents(),它的性能比 fread 好得多, 具体查看https://php.net/manual/zh/function.fread.php

file 函数也可以做到,想要的结果,这里就不做演示了。file 函数是把一个文件读到一个数组中。其实上面三个函数的原理都一样,就是把一个大文件一次性读到内存里,显然这样的方式很消耗内存,在处理大文件的时候,这个方法并不可取。

我们再看看理论的第二种方法,分段读取。这个方法是把一个大文件分成若干小段,每次读到一段分析,最后整合在一起再分析统计。fgets 是每次读到一行,好像正是我们想要的。

string fgets ( resource $handle [, int $length ] )
第一个参数是文件系统指针,由 fopen 创建并返回的值,第二个参数是要读取的大小。如果没有指定 length,则默认为 1K,或者说 1024 字节,返回值为正确读取的内容。

现在把上面的代码再做一次修改。如下:

<?php
defined('READ_FILE')  or define('READ_FILE', './error.log');
defined('WRITE_FILE_C') or define('WRITE_FILE_C', './temp1');

// 方法 fgets
function writeLogC()
{
    $st = microtime( true );
    $sm = memory_get_usage();

    $fileres  = fopen(READ_FILE, 'r');
    $writeres = fopen(WRITE_FILE_C, 'a+');

    // 锁定 WRITE_FILE
    flock($writeres, LOCK_EX);

    while( $row = fgets($fileres) )
    {
    if( strpos($row, "[error]") !== false )
    {
            fwrite($writeres, $row);
    }
    }

    $em = memory_get_usage();
    flock($writeres, LOCK_UN);
    fclose($writeres);
    fclose($fileres);

    $et = microtime( true );
    echo "使用 fgets: 用时间:" . ($et - $st) . "\n";
    echo "使用 fgets: 用内存:" . ($em - $sm) . "\n";
}
writeLogC();

运行之后,发现内存一下降了好多,但是,时间好像还是一样的。运行结果如下:

请输入图片描述

为什么为这样的呢,其实很简单,因为现在是每次读取一行到内存,所以,内存并不会太高。但是,不管怎么样,以上三种方法,都是要循环一遍整个文件(10万行),所以时间的话,三种方法并不会相差太多。那有没有更好的方法呢,有。就是采用多线程,把大文件分成小文件,每一个线程处理一个小文件,这样的话,时间肯定会小很多。

说明一下,PHP默认情况下,并没有安装多线程模块,这里要自己安装。没有安装的,请查看 PHP, 多线程开发的配置

现在就按上面的方法把代码换成如下的方法:

<?php
defined('READ_FILE')  or define('READ_FILE', './error.log');
defined('WRITE_FILE_D') or define('WRITE_FILE_D', './temp1');

// 使用多线程
class Mythread extends Thread
{
    private $i = null;

    public function __construct( $i )
    {
    $this->i = $i;
    }

    public function run()
    {
    $filename = "temp_log_" . Thread::getCurrentThreadId();
    $cutline = ($this->i - 1) * 40000 + 1 . ", " . ($this->i * 40000);
    exec("sed -n '" . $cutline . "p' " . READ_FILE . " > " . $filename);

    $this->_writeTemp( $filename );
    }

    private function _writeTemp( $readfile = '' )
    {
    if( !$readfile || !file_exists($readfile) ) return;

        $fileres  = fopen($readfile, 'r');
    $writeres = fopen(WRITE_FILE_D, 'a+');

    // 锁定 WRITE_FILE
    flock($writeres, LOCK_EX);

    while( $row = fgets($fileres) )
    {
        if( strpos($row, "[error]") !== false )
        {
            fwrite($writeres, $row);
        }
    }

    flock($writeres, LOCK_UN);
    fclose($fileres);
    fclose($writeres);
    unlink( $readfile );
    }
}

function writeLogd()
{
    $st = microtime( true );
    $sm = memory_get_usage();

    $count_line = 0;

    //获取整个文件的行数
    $content         = exec('wc -l ' . READ_FILE);
    $content_arr     = explode(" ", $content);
    $count_line     = $content_arr[0];

    //线程数
    $count_thread     = ceil($count_line / 40000);

    $worker = array();
    for($i=1; $i<=$count_thread; $i++)
    {
        $worker[$i] = new Mythread( $i );
        $worker[$i]->start();
    }
    $em = memory_get_usage();
    $et = microtime( true );
    echo "使用 多线程: 用时间:" . ($et - $st) . "\n";
    echo "使用 多线程: 用内存:" . ($em - $sm) . "\n";
}
writeLogd();

运行一下,发现时间直线下降。内存也有所减少。运行结果图如:

请输入图片描述

特别说明一点,线程数并不是越多超好。线程数量多的话,在每一个线程处理的时间少了,但是在创建线程的时候,也是浪费时间的,我这里每个线程处理4万条记录,你可以把这个数减少或增加,测试一下结果

终于分析完了,这里只是自己想到的方法,做一个笔记。如果要会C语言的话,完全可以用C来写一个PHP模块,这里不再记录,我也在学习中。

PHP多线程开发的配置

PHP 5.3 以版本,使用 pthreads PHP扩展,可以使PHP真正地支持多线程。多线程在处理重复性的循环任务,能够大大缩短程序执行时间。

多数网站的性能瓶颈不在PHP服务器上,因为它可以简单地通过横向增加服务器或CPU核数来轻松应对(对于各种云主机,增加VPS或CPU核数就更方便了,直接以备份镜像增加VPS,连操作系统、环境都不用安装配置),而是在于MySQL数据库。如果用 MySQL 数据库,一条联合查询的SQL,也许就可以处理完业务逻辑,但是,遇到大量并发请求,就歇菜了。如果用 NoSQL 数据库,也许需要十次查询,才能处理完同样地业务逻辑,但每次查询都比 MySQL 要快,十次循环NoSQL查询也许比一次MySQL联合查询更快,应对几万次/秒的查询完全没问题。如果加上PHP多线程,通过十个线程同时查询NoSQL,返回结果汇总输出,速度就要更快了。我们实际的APP产品中,调用一个通过用户喜好实时推荐商品的PHP接口,PHP需要对BigSea NoSQL数据库发起500~1000次查询,来实时算出用户的个性喜好商品数据,PHP多线程的作用非常明显。

PHP扩展下载:https://pecl.php.net/package/pthreads

PHP手册文档:https://php.net/manual/zh/book.pthreads.php

特别说明一点,在安装PHP的时候,一定要指定 --enable-maintainer-zts 参数。这是必选项,安装代码如下:(以下安装过程在 centos 6.5 环境中,此安装过程仅供参考,你在实例应用中,PHP的安装目录不一定要/www中)

安装PHP

tar zxvf php-7.0.0.tar.gz
cd ../php-7.0.0
./configure --prefix=/www/php --enable-fpm --with-fpm-user=www --with-fpm-group=www --with-openssl --with-libxml-dir --with-zlib --enable-mbstring --with-mysql=/www/mysql --with-mysqli=mysqlnd --enable-mysqlnd --with-pdo-mysql=/www/mysql --with-gd --with-jpeg-dir --with-png-dir --with-zlib-dir --with-freetype-dir --enable-sockets --with-curl --enable-maintainer-zts
make && make install

安装扩展

wget https://pecl.php.net/get/pthreads-3.1.6.tgz
tar zxvf pthreads-3.1.6.tgz
cd pthreads-3.1.6
phpize
./configure --with-php-config=/www/php/bin/php-config
make && make install

修改php.ini

vim /www/php/lib/php.ini

在php.ini中添加

extension = "pthreads.so"

重启

kill -USR2 `cat /www/php/var/run/php-fpm.pid`

运行 /www/php/bin/php -m | grep pthreads 发现 pthreads 已经安装成功

这里用一个最简单的代码来说明一下多线程的实用方法

<?php
//这里用一个函数,表示操作日志,每操作一次花1秒的时间

function doThings( $i )
{
    //    Write log file
    sleep(1);
}

$s = microtime(true);
for($i = 1; $i <= 10; $i ++)
{
    doThings( $i );
}
$e = microtime(true);
echo "For循环:" . ($e - $s) . "\n";  

#############################################
class MyThread extends Thread
{
    private $i = null;

    public function __construct( $i )
    {
    $this->i = $i;
    }

    public function run()
    {
        doThings( $this->i );
    }
}

$s = microtime(true);
$work = array();
for($i = 1; $i <= 10; $i ++)
{
    $work[$i] = new MyThread( $i );
    $work[$i]->start();
}
$e = microtime(true);
echo "多线程:" . ($e - $s) . "\n";

运行此文件之后,发现多线程的效率会远远高于 for 循环