Archive for PHP

笔记:新MacOS Sierra 系统使用brew安装php、mysql(mariadb)、nginx等开发环境,也适合小白用户

安装brew

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

安装完毕后,把源改成中科大的

cd "$(brew --repo)"
git remote set-url origin https://mirrors.ustc.edu.cn/brew.git

cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core"
git remote set-url origin https://mirrors.ustc.edu.cn/homebrew-core.git

cd "$(brew --repo)"/Library/Taps/caskroom/homebrew-cask
git remote set-url origin https://mirrors.ustc.edu.cn/homebrew-cask.git

# 2选1
# for bash user
echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.ustc.edu.cn/homebrew-bottles' >> ~/.bash_profile
source ~/.bash_profile
# for zsh user
echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.ustc.edu.cn/homebrew-bottles' >> ~/.zshrc
source ~/.zshrc

安装 MySQL(MariaDB)

brew install mariadb

# 设置下,否则mysql_的命令不会出现
# ls /usr/local/Cellar/mariadb/ 看下是不是10.2.8,如果是别的版本记得更换下下面的路径
echo 'export PATH="$PATH:/usr/local/Cellar/mariadb/10.2.8/bin"' >> ~/.bash_profile

修改完必要的 /usr/local/etc/my.cnf 配置后(其实本地测试没什么好修改的,我就用的默认的)执行 mysql_install_db 命令初始化数据库(如果 /usr/local/var/mysql/ 目录已经有了,可以忽略这个)

启动服务器:

brew services start mariadb

下次开机会自动启动,看所有服务列表:

brew services list

如果要修改MySQL管理员密码,可以使用

mysql_secure_installation

命令(先把服务器启动起来)

安装PHP

先执行下面

brew install homebrew/php/php-install

这个命令,然后

brew search php

你会发现就有好多不同版本的 php 了,比如你要装 php7.1 版本的话,可以

brew search php71

,然后选择你需要的扩展模块安装就可以了,比如:

brew install homebrew/php/php71 homebrew/php/php71-redis homebrew/php/php71-swoole homebrew/php/php71-yaml

你可能没找到 mysql,php-fpm 等这些扩展,这个是 homebrew/php/php71 里就包含了的。

启动 php-fpm

在启动 php-fpm 前建议修改下配置

vim /usr/local/etc/php/7.1/php-fpm.d/www.conf

将 group 改成 staff,也可以将 user 改成自己的账户名。这样做可能会存在一点点安全风险,但是如果你用到php需要写一些文件的话,就会方便很多,否则连自己操作这个文件都没有权限,略麻烦。

启动的话非常简单,此时使用命令

brew services list

会看到 php71 是 stopped 的,执行

brew services start php71

就可以了。

安装nginx

执行执行

brew install nginx

就可以快速的安装好了,brew 安装好的 nginx 默认值监听 8080 端口的,这显然太麻烦了,果断改到80端口,但是由于 Mac 系统的限制,低于1000的端口不能由用户启动,所以需要再配置下。

执行

sudo vim /Library/LaunchDaemons/homebrew.mxcl.nginx.plist

输出密码,后把下面的内容写进去

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Label</key>
    <string>homebrew.mxcl.nginx</string>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <false/>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/opt/nginx/bin/nginx</string>
        <string>-g</string>
        <string>daemon off;</string>
    </array>
    <key>WorkingDirectory</key>
    <string>/usr/local</string>
  </dict>
</plist>

保存,然后执行下面代码加载 nginx

sudo launchctl load -w /Library/LaunchDaemons/homebrew.mxcl.nginx.plist

这样重启nginx

sudo launchctl stop homebrew.mxcl.nginx
sudo launchctl start homebrew.mxcl.nginx

看看有没有启动成功:

ps -ef | grep nginx

,如果有进程就表示启动成功了。

小建议,brew 安装的 nginx 默认是8个线程,个人觉得测试有点浪费了,所以可以自己设置下,

vim /usr/local/etc/nginx/nginx.conf

将 worker_processes 改成你要的值(我的是4)

安装phpMyAdmin

本人比较懒,直接使用

brew install homebrew/php/phpmyadmin

安装了个 phpMyAdmin,然后将下列配置文件写入

/usr/local/etc/nginx/servers/phpmyadmin.conf

重启 nginx 即可

upstream php-handler {
    server 127.0.0.1:9000;
}
server {
    listen 80;
    server_name 127.0.0.1;
    root /usr/local/share/phpmyadmin/;
    charset utf-8;

    gzip off;
    proxy_buffering off;
    index index.html index.php;

    location ~ .php$ {
        fastcgi_pass php-handler;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        #fastcgi_buffering off;
        fastcgi_buffers 10 256k;
        fastcgi_buffer_size 512k;
        fastcgi_keep_conn on;
        #fastcgi_request_buffering off;
        client_max_body_size 128m;
        fastcgi_temp_file_write_size 512k;
        include fastcgi_params;
    }
}

另外,如果要用 phpMyAdmin 执行超长的任务需要注意,它默认是最大300秒的执行时限的,请修改

/usr/local/etc/phpmyadmin.config.inc.php

文件,在最后加入如下内容即可:

$cfg['ExecTimeLimit'] = 0;  # 或你认为的最大值

另外,关于php.ini的一些设置,

vim /usr/local/etc/php/7.1/php.ini

,可以根据自己的需要调节,比如:

max_execution_time 30  # 最大执行时间,可以根据自己的需要修改
memory_limit = 128M    # 内存限制
post_max_size = 8M     # 将会影响上传文件大小
date.timezone          # 默认时区,可以改成 PRC(中华人民共和国缩写),即 date.timezone = PRC

改好后,使用

brew services restart php71

重启 php-fpm 即可。

至此,php,nginx,mysql都已经安装好了。如果你是用PhpStorm的开发者,推荐我调的代码风格和配色,预览和下载地址:https://www.queyang.com/blog/archives/638

see

奉上自己的 PhpStorm 的黑色风格的配色和代码格式

二话不说先看配色,自己调的,整体风格是 Sublime 的御用配色样子,附上下载地址(内有说明) PhoStorm代码风格和配色

使用 xdebug 在 phpStorm 进行 php 本地和远程 swoole 调试设置

其实类似 xdebug 在 phpStorm 的调试文件一搜一大把,这里简单的总结下,主要是适合使用 swoole 开发的人。

本地调试的设置方法

第1步

在 Run 菜单中点击 Edit Configurations,然后点击最左侧 + 按钮,选择 PHP Script。

第2步

Configuration 的 File 参数就选择 swoole 的启动文件,Arguments 输入你这个启动文件的参数,没有的话就留空,名字自己设定

完成

然后点击保存,如果 phpStorm 提示找不到php命令的话你设置下路径就好了。

这个时候在窗口的右上角就会有一个可以调试的按钮,这个时候在要断点的位置打上打断,启动调试就可以了。


远程调试设置

当脚本在服务器上运行时,就需要用到远程调试了(也适用于本机调试)远程调试设置方法如下:

第1步

打开设置面板,找到 Languages & Frameworks -> PHP -> Servers。点击 + 号添加,Host 填需要调试的服务器的ip,端口填 xdebug 的 remote_port (ini中默认为 9000,这个值远程服务器启动命令上是可以设置的)

如果服务器上代码路径和本地路径不一致的话,需要勾选 “Use path mappings”,然后主项目路径后的 Absolute path on the server 填一下对应服务器的路径,设置好后保存。

第2步

设置时候继续设置,同本地是设置,在 Run 菜单中点击 Edit Configurations 点击添加按钮,选择 php remote debug。servers 就选择刚刚添加的那个。网上文档都有说 ide key 要填个 PHPSTORM 什么的,其实可以不用填,因为 swoole 起的服务器并不是传统的 web 服务器。

第3步

设置好了后,记得在swoole的启动命令上加上参数:

-dxdebug.remote_autostart=1 -dxdebug.remote_host=192.168.1.2 -dxdebug.remote_port=9100 -dxdebug.remote_enable=1

, 例如:

php -dxdebug.remote_autostart=1 -dxdebug.remote_host=192.168.1.2 -dxdebug.remote_port=9100 -dxdebug.remote_enable=1 server.php -vvv

其中 remote_host 是服务器 ip,remote_port 是 xdebug 的调试端口,千万别暴露给外网。当然,这些参数是可以在 php.ini 中直接设置的,如果设置好了,可以不带这些参数直接启动的。


参阅

开源了一个好用又简单的大数据日志数据实时统计服务功能 Easy Total

因为我们公司游戏的日志越来越多,普通的数据光插入数据就会让数据库挂掉,后来我们用 Hadoop 集群来处理日志,但是问题是需要越来越多的服务器,为了解决这个问题,用了半年时间开发了一个简单好用的实时统计服务器:EasyTotal。此服务还在测试,感兴趣的同学可以去点赞或贡献代码,项目地址 https://github.com/xindong/easy-total。在我们线上测试环境(监听了20多条统计的SQL语句)单机每分钟处理300万+的日志量cpu负载在 3 – 5之间,可持续处理1000万/分钟的日志量,峰值可达1300万/分钟,这样的性能堪称无敌,因为这样的数据光插入 10 台机器组成的 Elasticsearch 集群用不了多久集群就要挂了。

下面是项目介绍:

EasyTotal 是一个通过监听预先添加好的SQL统计查询语句,对汇入的数据进行实时统计处理后不间断的将统计结果导出的服务解决方案,它解决了日志数据量非常巨大的情况下,数据库无法承载巨大的插入和查询请求的问题,并且可以满足业务统计的需求。程序的网络层是采用c写的swoole扩展进行处理,具有极高的性能,网络处理能力和 nginx 相当,处理数据模块采用 php 开发则可以方便团队根据自己的需求进行二次开发。

支持常用的运算统计功能,比如 count, sum, max, min, avg, first, last, dist,支持 group by、where 等,后续将会增加 join 的功能。

特点:

  • 实时处理,定时汇出统计结果;
  • 对巨大的日志量进行清洗汇总成1条或多条输出,可以成万倍的缩小数据体量,可在汇总结果中进行二次统计;
  • 特别适用于对大量log的汇入统计,免去了先入库再统计这种传统方式对系统造成的负担;
  • 分布式水平扩展,支持随时增删统计服务器;
  • 不需要特别的技术、使用简单;
  • 可以二次开发;

使用场景

当需要对业务数据进行统计分析时,传统的做法是把数据或日志导入到数据库后进行统计,但是随着数据量的增长,数据库压力越来越大甚至插入数据都成问题更不用说是进行数据统计了,此时只能对数据进行分库、分片等处理或者是用 hadoop、spark 等离线统计,然后就需要分布式架构,这在技术角度上来说是可行的做法,但这带来的问题就是:

  • 需要很多服务器,增加巨额托管费用;
  • 需要能够hold住这些服务器和技术的高级开发及运维人员;
  • 架构调整导致的研发难度和周期增加;

EasyTotal 正是为了解决这些问题而诞生,可以用极少的服务器以及简单的技术就能处理巨大的数据并满足业务统计的需求,对你来说一切都是那么的easy。

上两张管理界面:

admin01

admin02

swoole_process 正常退出却 zm_deactivate_swoole: worker process is terminated by exit()/die().

不废话,先上一段代码

<?php

error_reporting(7);

$serv = new swoole_server("0.0.0.0", 9500, SWOOLE_PROCESS);
$serv->set([
    'worker_num' => 1,
    'task_worker_num' => 1,
]);

$serv->on('Receive', function(swoole_server $server, $fd, $from_id, $data)
{
    $server->task('a');
});


$serv->on('workerstart', function(swoole_server $server, $id)
{
});

//$serv->set([]);

$serv->on('Task', function($serv, $task_id, $from_id, $data)
{
    $arr = ['a','b','c','d'];
    $processes = [];
    foreach ($arr as $v)
    {
        echo "-------". (microtime(1))."\n";

        $process = new swoole_process(function(swoole_process $worker) use ($v)
        {
            echo $v."======". ($t = microtime(1))."\n";
            sleep(3);
            echo $v."++++++". (microtime(1) - $t)."\n";
            // $worker->daemon(true);
        }, false);
        $process->start();
        $processes[] = $process;
    }

    while(true)
    {
        # 等待回收,如果不回收进程会变成僵死进程,很可怕的
       if (false === swoole_process::wait())
        {
            break;
        }
    }

    echo "done\n";
});

$serv->on('Finish', function()
{
});


$serv->start();

(TO小白:保存成test.php文件,然后php test.php这样运行,再telnet 127.0.0.1 9500 上,随便输入个内容回车就可以执行了)

这个代码成功执行后,在控制台上会输出4个WARN的错误,类似

zm_deactivate_swoole: worker process is terminated by exit()/die().

经过测试发现,如果 new swoole_process 时第二个参数设置成 true(定向子进程的标准输入和输出),则不会报这样的错误。
但是我的程序就是需要接受输出那么怎么办呢?

在测试代码里有一个注释了的代码

// $worker->daemon(true);

,把注释去掉这样就解决了,不再报那个错误了。
daemon 方法可见文档,它是把子进程蜕变成一个守护进程用的。

最后,再强调一下,一定要:

    while(true)
    {
        # 等待回收,如果不回收进程会变成僵死进程,很可怕的
       if (false === swoole_process::wait())
        {
            break;
        }
    }

这样进行回收,因为测试代码里开了4个子进程,如果只回收1次,还会有3个进程得不到回收,变成僵死进程的。

Fluentd 的PHP客户端,支持ACK方式以及批量推送log的类库

fluentd是一个非常好用的日志分发处理的程序,网站 http://www.fluentd.org/

使用官方的php程序自己改了一个单文件的Fluent的类库,并且加入了更多的支持,比如 tcp 方式支持 require_ack_response 了,这个参数主要是用来请求是否推送成功用的,避免因为网络问题没有推送成功而无法知道。
但是这个功能我实测如果每条log都去ask一下本来推送10w条记录只需要3-5秒,用这个后可能要几分钟时间,所以性能上会大打折扣。
所以我加入了add()方法,可以加入n多条log后一次性push到服务器进行1次ack,这样性能就会很好了。
另外,官网的php程序我在使用中发现在大量日志的推送后会突然出现“incoming chunk is broken”这样的错误,这个应该也是解决了,具体我再多测几天看看情况。

[2016-03-20] 更新http协议支持更多内容的提交

使用方法:

$option = array
(
    'require_ack_response' => 1,    //开启ACK
    'max_buffer_length' => 2000     //每2000条add会自动push一次
);

$f = new Fluent('tcp://27x004.xd.com:2600', $option);
$t = microtime(1);
for($i = 1; $i<= 10000; $i++)
{
    // 批量加入,到达2000条会自动推送
    $f->add('test.abc', array('i'=>$i), time());  // 第3个time()参数可以不传
}
// 推送剩余的
var_dump($f->push('test.abc'));

echo "\n\n", microtime(1) - $t , "\n";

下面是源代码

<?php
/**
 * Fluent日志处理核心类
 *
 * 配置根目录 `$config['log']['fluent'] = 'tcp://127.0.0.1:24224/'` 后
 * 使用 `self::log('myapp.test.debug', $_SERVER)` 默认就可以调用本方法
 *
 *
 *      Fluent::instance('tcp://127.0.0.1:24224/')->push('xd.game.test', ["test"=>"hello"]);
 *
 *      Fluent::instance('unix:///full/path/to/my/socket.sock')->push('xd.game.test', ["test"=>"hello"]);
 *
 *
 * @see        https://github.com/fluent/fluent-logger-php
 * @author     呼吸二氧化碳 <jonwang@myqee.com>
 * @category   Core
 * @package    Classes
 * @copyright  Copyright (c) 2008-2016 myqee.com
 * @license    http://www.myqee.com/license.html
 */

class Fluent
{
    const CONNECTION_TIMEOUT = 3;
    const SOCKET_TIMEOUT     = 3;
    const MAX_WRITE_RETRY    = 5;

    /* 1000 means 0.001 sec */
    const USLEEP_WAIT = 1000;

    /**
     * 是否开启ACK
     *
     * @var bool
     */

    const REQUIRE_ACK_RESPONSE = true;

    /**
     * backoff strategies: default usleep
     *
     * attempts | wait
     * 1        | 0.003 sec
     * 2        | 0.009 sec
     * 3        | 0.027 sec
     * 4        | 0.081 sec
     * 5        | 0.243 sec
     **/

    const BACKOFF_TYPE_EXPONENTIAL = 0x01;
    const BACKOFF_TYPE_USLEEP      = 0x02;

    /**
     * 服务器
     *
     * 例如 `tcp://127.0.0.1:24224`
     *
     * @var string
     */

    protected $transport;

    /* @var resource */
    protected $socket;

    protected $is_http = false;

    protected $data = [];

    protected $options = array
    (
        'socket_timeout'       => self::SOCKET_TIMEOUT,
        'connection_timeout'   => self::CONNECTION_TIMEOUT,
        'backoff_mode'         => self::BACKOFF_TYPE_USLEEP,
        'backoff_base'         => 3,
        'usleep_wait'          => self::USLEEP_WAIT,
        'persistent'           => true,
        'retry_socket'         => true,
        'max_write_retry'      => self::MAX_WRITE_RETRY,
        'require_ack_response' => self::REQUIRE_ACK_RESPONSE,
        'max_buffer_length'    => 1000,
    );

    /**
     * @var Fluent
     */

    protected static $instance = array();

    function __construct($server, array $option = array())
    {
        $this->transport = $server;

        if (($pos = strpos($server, '://')) !== false)
        {
            $protocol = substr($server, 0, $pos);

            if (!in_array($protocol, array('tcp', 'udp', 'unix', 'http')))
            {
                throw new Exception("transport `{$protocol}` does not support");
            }

            if ($protocol === 'http')
            {
                # 使用HTTP推送
              $this->is_http = true;
                $this->transport = rtrim($this->transport, '/ ');
            }
        }
        else
        {
            throw new Exception("fluent config error");
        }

        if ($option)
        {
            $this->options = array_merge($this->options, $option);
        }
    }

    /**
     * destruct objects and socket.
     *
     * @return void
     */

    public function __destruct()
    {
        if ($this->data)
        {
            # 把遗留的数据全部推送完毕
          foreach (array_keys($this->data) as $tag)
            {
                $this->push($tag);
            }
        }

        if (!$this->get_option('persistent', false) && is_resource($this->socket))
        {
            @fclose($this->socket);
        }
    }

    /**
     * 返回Fluent处理对象
     *
     * @return Fluent
     */

    public static function instance($server)
    {
        if (!isset(Fluent::$instance[$server]))
        {
            Fluent::$instance[$server] = new Fluent($server);
        }

        return Fluent::$instance[$server];
    }

    /**
     * 添加数据,添加完毕后并不直接推送
     *
     * 当开启ack后,推荐先批量 add 后再push,当超过 max_buffer_length 后会自动推送到服务器
     *
     *      $fluent = new Fluent('tcp://127.0.0.1:24224/');
     *      $fluent->add('debug.test1', array('a' => 1));
     *      $fluent->add('debug.test2', array('a' => 2));
     *      $fluent->add('debug.test1', array('a' => 1));
     *
     *      var_dump($fluent->push('debug.test1'));
     *      var_dump($fluent->push('debug.test2'));
     *
     * @param string $tag tag内容
     * @param array $data 数据内容
     * @param int $time 标记日志的时间戳,不设置就是当前时间
     */

    public function add($tag, array $data, $time = null)
    {
        $this->_add($tag, $data, $time);

        if (count($this->data[$tag]) >= $this->options['max_buffer_length'])
        {
            return $this->push($tag);
        }

        return true;
    }

    protected function _add($tag, $data, $time)
    {
        if ($this->is_http)
        {
            if (!isset($data['time']))
            {
                $data['time'] = $time ? $time : time();
            }
            $this->data[$tag][] = $data;
        }
        else
        {
            $this->data[$tag][] = array($time ? $time : time(), $data);
        }
    }

    /**
     * 推送数据到服务器
     *
     * @param string $tag tag内容
     * @param array $data 数据内容
     * @param int $time 标记日志的时间戳,不设置就是当前时间
     * @return bool
     * @throws Exception
     */

    public function push($tag, $data = null, $time = null)
    {
        if ($data)
        {
            $this->_add($tag, $data, $time);
        }

        if (!isset($this->data[$tag]) || !$this->data[$tag])return true;

        if ($this->is_http)
        {
            $rs = $this->push_with_http($tag, $time);
        }
        else
        {
            $rs = $this->push_with_socket($tag);
        }

        if ($rs)
        {
            unset($this->data[$tag]);
        }

        return $rs;
    }

    protected function push_with_http($tag, $time)
    {
        $packed  = self::json_encode($this->data[$tag]);
        $opts = array(
            'http' => array
            (
                'method'  => 'POST',
                'content' => 'json='. urlencode($packed)
            )
        );
        $context = stream_context_create($opts);
        $url     = $this->transport .'/'. $tag;

        $ret = file_get_contents($url, false, $context);

        return ($ret !== false && $ret === '');
    }

    protected function push_with_socket($tag)
    {
        $data = $this->data[$tag];
       
        if ($ack = $this->get_option('require_ack_response'))
        {
            $ack_key = 'a'. (microtime(1) * 10000);
            $buffer = $packed = self::json_encode(array($tag, $data, array('chunk' => $ack_key)));
        }
        else
        {
            $ack_key = null;
            $buffer = $packed = self::json_encode(array($tag, $data));
        }

        $length = strlen($packed);
        $retry  = $written = 0;

        try
        {
            $this->reconnect();
        }
        catch (Exception $e)
        {
            $this->close();
            $this->process_error($tag, $data, $e->getMessage());

            return false;
        }

        try
        {
            // PHP socket looks weired. we have to check the implementation.
            while ($written < $length)
            {
                $nwrite = $this->write($buffer);

                if ($nwrite === false)
                {
                    // could not write messages to the socket.
                    // e.g) Resource temporarily unavailable
                    throw new Exception("could not write message");
                }
                else if ($nwrite === '')
                {
                    // sometimes fwrite returns null string.
                    // probably connection aborted.
                    throw new Exception("connection aborted");
                }
                else if ($nwrite === 0)
                {
                    if (!$this->get_option("retry_socket", true))
                    {
                        $this->process_error($tag, $data, "could not send entities");

                        return false;
                    }

                    if ($retry > $this->get_option("max_write_retry", self::MAX_WRITE_RETRY))
                    {
                        throw new Exception("failed fwrite retry: retry count exceeds limit.");
                    }

                    $errors = error_get_last();
                    if ($errors)
                    {
                        if (isset($errors['message']) && strpos($errors['message'], 'errno=32 ') !== false)
                        {
                            /* breaking pipes: we have to close socket manually */
                            $this->close();
                            $this->reconnect();

                            # 断开后重新连上后从头开始写,避免出现 incoming chunk is broken 的错误问题
                          $written = 0;
                            $buffer = $packed;
                            continue;
                        }
                        else if (isset($errors['message']) && strpos($errors['message'], 'errno=11 ') !== false)
                        {
                            // we can ignore EAGAIN message. just retry.
                        }
                        else
                        {
                            error_log("unhandled error detected. please report this issue to http://github.com/fluent/fluent-logger-php/issues: ". var_export($errors, true));
                        }
                    }

                    if ($this->get_option('backoff_mode', self::BACKOFF_TYPE_EXPONENTIAL) == self::BACKOFF_TYPE_EXPONENTIAL)
                    {
                        $this->backoff_exponential(3, $retry);
                    }
                    else
                    {
                        usleep($this->get_option("usleep_wait", self::USLEEP_WAIT));
                    }
                    $retry++;
                    continue;
                }

                $written += $nwrite;
                $buffer   = substr($packed, $written);
            }

            if ($ack)
            {
                $rs = @fread($this->socket, 25);
                if ($rs)
                {
                    $rs = @json_decode($rs, true);
                    if ($rs && isset($rs['ack']))
                    {
                        if ($rs['ack'] !== $ack_key)
                        {
                            $this->process_error($tag, $data, 'ack in response and chunk id in sent data are different');
                            return false;
                        }
                        else
                        {
                            return true;
                        }
                    }
                    else
                    {
                        return false;
                    }
                }
                else
                {
                    return false;
                }
            }
        }
        catch (Exception $e)
        {
            $this->close();
            $this->process_error($tag, $data, $e->getMessage());

            return false;
        }

        return true;
    }


    /**
     * write data
     *
     * @param string $data
     * @return mixed integer|false
     */

    protected function write($buffer)
    {
        // We handle fwrite error on postImpl block. ignore error message here.
        return @fwrite($this->socket, $buffer);
    }

    /**
     * create a connection to specified fluentd
     *
     * @throws \Exception
     */

    protected function connect()
    {
        $connect_options = STREAM_CLIENT_CONNECT;
        if ($this->get_option("persistent", false))
        {
            $connect_options |= STREAM_CLIENT_PERSISTENT;
        }

        // could not suppress warning without ini setting.
        // for now, we use error control operators.
        $socket = @stream_socket_client($this->transport, $errno, $errstr, $this->get_option("connection_timeout", self::CONNECTION_TIMEOUT), $connect_options);

        if (!$socket)
        {
            $errors = error_get_last();
            throw new Exception($errors['message']);
        }

        // set read / write timeout.
        stream_set_timeout($socket, $this->get_option("socket_timeout", self::SOCKET_TIMEOUT));

        $this->socket = $socket;
    }

    /**
     * create a connection if Fluent Logger hasn't a socket connection.
     *
     * @return void
     */

    protected function reconnect()
    {
        if (!is_resource($this->socket))
        {
            $this->connect();
        }
    }

    /**
     * close the socket
     *
     * @return void
     */

    public function close()
    {
        if (is_resource($this->socket))
        {
            fclose($this->socket);
        }

        $this->socket = null;
    }

    /**
     * get specified option's value
     *
     * @param      $key
     * @param null $default
     * @return mixed
     */

    protected function get_option($key, $default = null)
    {
        $result = $default;
        if (isset($this->options[$key]))
        {
            $result = $this->options[$key];
        }

        return $result;
    }

    /**
     * backoff exponential sleep
     *
     * @param $base int
     * @param $attempt int
     */

    protected function backoff_exponential($base, $attempt)
    {
        usleep(pow($base, $attempt) * 1000);
    }

    /**
     * 处理错误
     *
     * @param $tag
     * @param $data
     * @param $error
     */

    protected function process_error($tag, $data, $error)
    {
        error_log(sprintf("%s %s: %s", $error, $tag, json_encode($data)));
    }

    protected static function json_encode(array $data)
    {
        try
        {
            // 解决使用 JSON_UNESCAPED_UNICODE 偶尔会出现编码问题导致json报错
            return defined('JSON_UNESCAPED_UNICODE') ? json_encode($data, JSON_UNESCAPED_UNICODE) : json_encode($data);
        }
        catch (Exception $e)
        {
            return json_encode($data);
        }
    }
}

榨干PHP性能之被遗忘的 if 中 == 和 === 判断的性能差异

对于

if ($a === $b)
{
    //coding...
}

if ($a == $b)
{
    //coding...
}

这2种最简单不过的判断,似乎很少有人关心他们的性能差别,包括我,写了快10年的php似乎从来没有在意过这些。
对我来说,只有在需要严格判断的情况下才会用 === 来判断,大多情况下都是用 == 来判断的。

今天闲来无事手贱测一下这些被遗忘的细节的性能差别,结果却有些让我出乎意料。

测试代码:

// 代码A, === 判断
$a = 'aaa';
$b = 'aaa';
$s = microtime(1);
for($i=0;$i<99999999;$i++) {
   if ($a===$b){$c=$a;}
}
echo microtime(1)-$s,"\n";
// 代码B, == 判断
$a = 'aaa';
$b = 'aaa';
$s = microtime(1);
for($i=0;$i<99999999;$i++) {
   if ($a==$b){$c=$a;}
}
echo microtime(1)-$s,"\n";

在我电脑上执行代码A和B的结果分别是:
=== 是 4.2317380905151
== 是 5.3882131576538
看来 === 胜出

千万别急着下结论,把代码$a, $b 的值换成 123 试试,结果却出乎我的意料

=== 是 3.3042049407959
== 是 3.091680765152
反而是 == 胜出了

那么再把 $a, $b 的值都赋值成一个 new stdClass(); 是什么结果呢,这个结果在我的意料之中不过也是意料之外
我把 99999999 次循环改成了 199999999 次,结果是:
=== 是 6.6931169033051
== 是 9.5787789821625
结果显示 === 比 == 快 1/3

以上似乎看不出什么差距,后来我又测了一个代码,发现问题来了

// 代码A, === 判断
$a = str_pad('', 99999);
$b = str_pad('', 99999);
$s = microtime(1);
for($i=0;$i<999999;$i++) {
   if ($a===$b){$c=$a;}
}
echo microtime(1)-$s,"\n";
// 代码B, == 判断
$a = str_pad('', 99999);
$b = str_pad('', 99999);
$s = microtime(1);
for($i=0;$i<999999;$i++) {
   if ($a==$b){$c=$a;}
}
echo microtime(1)-$s,"\n";

没用 99999999 次循环是因为我等了几分钟都还没出结果

最终的结果是:
=== 是 4.435389995575
== 是 57.722193956375

足足有10倍性能的差距。

可见对于字符串来说,字符越长, === 的性能优势越大于 ==

结论:
如果php的if判断是字符串判断,一定要用 ===, 比 == 不知快多少,而对于整型的判断,== 比 === 快些但是优势不大。
所以判断尽量用 ===

PS:以上测试环境是 5.5.14 ,Mac OS 10.10.1, 2.3 GHz Intel Core i7,16G 1600 MHz DDR3

做了一个 FirePHP For Chrome 的插件

FirePHP(http://www.firephp.org/)是一个php里不错的用于调试的工具,我在MyQEE里内置了这个功能,在线调试非常有用,只可惜做这个的作者好久没更新版本了,官方也只出了一个在Firefox里的插件。

而Chrome里的插件都是第三方做的,实际上很难用。这些天下狠心改了别人一款,改到了基本自己满意了,媲美FireFox里的插件,掌声在哪~~~~

本来是要公开发布出来的,可无奈google的应用商店第一次发布必须要付$5才行,我尝试的去付了下,可是支付失败,好像是不支持中国的信用卡,唉~~~~ 于是只好发布成私有的应用,私有的应用和公开的应用的差别在于只能通过指定的URL访问到,在google的应用市场里搜索不到,也罢~~~

应用的地址是: https://chrome.google.com/webstore/detail/firephp/gkkcbhnljobgoehijcpenalbacdmhaff?hl=zh-CN&utm_source=chrome-ntp-launcher&authuser=1

注:这个地址可能无法打开(被墙,你懂的)浏览是可能需要借助翻墙软件。

附截图:

如果无法打开地址,可以在这下载:

gkkcbhnljobgoehijcpenalbacdmhaff_main.crx

下载后安装方法:
首先解压缩压缩包,得到 gkkcbhnljobgoehijcpenalbacdmhaff_main.crx 文件,然后打开Chrome的扩展程序界面(入下图操作):

然后把刚刚解压得到的crx文件拖拽到这个“扩展程序”里(注意,只能是拖到这个页面,拖到别的页面是没用的),如下图操作:

这样就可以安装上了。

提供一个自动下载射手网电影字幕的脚本

射手网突然停止下载了,现在看个电影都很费事,还好射手网的接口没有关闭。看了下接口文档,自己用php写了个脚本,可以直接根据影片文件下载匹配的字幕。

把代码方在/usr/local/bin/目录,文件名zimu,并chmod +x /usr/local/bin/zimu,然后这样用:

zimu 电影文件名

就可以自动下载字幕了
可以加参数“debug”, “all”
比如

zimu test.mkv all

则下载全部匹配的字幕

window用户的话,可以保存成zimu.php然后使用

php zimu.php 电影文件名

这样来下载

不多说,直接上代码

#!/usr/bin/env php
<?php

$file = $_SERVER['argv'][1];

if (!$file)
{
 echo "缺少参数\n";
 exit;
}
if (!is_file($file))
{
 echo "文件不存在\n";
    exit;
}
$ftotallen = filesize($file);
if ($ftotallen<8192)
{
    echo "文件太小\n";
 exit;
}





$offsets[] = 4096;
$offsets[] = floor($ftotallen / 3 * 2);
$offsets[] = floor($ftotallen / 3);
$offsets[] = $ftotallen - 8192;

$fp = fopen($file, "r");

foreach($offsets as $offset)
{
    fseek($fp, $offset, SEEK_SET);
    $data  = fread($fp, 4096);
  $md5[] = md5($data);
}

fclose($fp);

echo $md5_str = implode(';', $md5);

echo "\n";

function curl_post($url, array $post = NULL, array $options = array())
{
    $defaults = array(
        CURLOPT_POST => 1,
        CURLOPT_HEADER => 0,
        CURLOPT_URL => $url,
        CURLOPT_FRESH_CONNECT => 1,
        CURLOPT_RETURNTRANSFER => 1,
        CURLOPT_FORBID_REUSE => 1,
        CURLOPT_TIMEOUT => 4,
        CURLOPT_POSTFIELDS => http_build_query($post)
    );

    $ch = curl_init();
    curl_setopt_array($ch, ($options + $defaults));
    if( ! $result = curl_exec($ch))
    {
        trigger_error(curl_error($ch));
    }
    curl_close($ch);
    return $result;
}

function curl_get($url, array $get = NULL, array $options = array())
{
    $defaults = array(
        CURLOPT_URL => $url. (strpos($url, '?') === FALSE ? '?' : ''). ($get ? http_build_query($get): ''),
        CURLOPT_HEADER => 0,
        CURLOPT_RETURNTRANSFER => TRUE,
        CURLOPT_TIMEOUT => 4
    );

    $ch = curl_init();
    curl_setopt_array($ch, ($options + $defaults));
    if( ! $result = curl_exec($ch))
    {
        trigger_error(curl_error($ch));
    }
    curl_close($ch);
    return $result;
}


$data = array
(
    'filehash' => $md5_str,
   'pathinfo' => $file,
  'format'   => 'json',
   'lang'     => 'Chn',
);

$r = curl_post('https://www.shooter.cn/api/subapi.php', $data);

$rs = @json_decode($r, true);

if (!$rs)
{
  echo "获取字幕失败\n";
   exit;
}

if (in_array('debug', $_SERVER['argv']))
{
    print_r($rs);
}

$get_all = in_array('all', $_SERVER['argv']);

foreach ($rs as $key => $item)
{
    $url = $item['Files'][0];

    $content = curl_get($url['Link']);
    if (!strlen($content))
    {
     echo "获取失败 {$url['Link']}\n";
    }

    if (in_array('utf8', $_SERVER['argv']))
    {
        if (extension_loaded('mbstring'))
        {
            $content = mb_convert_encoding($content, 'UTF-8', 'GBK');
        }
        else
        {
            $content = iconv('UTF-8', 'gbk//IGNORE', $content);
        }
    }

    if (file_put_contents(substr($file, 0, strrpos($file, '.')).($key>0?'.'.$key:'').'.'.$url['Ext'], $content)>0)
    {
      echo "success {$url['Link']}\n";
    }
    else
    {
       echo "fail {$url['Link']}\n";
    }

    if (!$get_all)break;
}

echo "done.\n";

nginx+php-fpm环境下php输出图片、js、css等文件出现异常问题的原因及解决办法

【背景】
在我实际使用的环境中很少直接使用 nginx + php-fpm 方式搭建环境,大部分还都是使用apache,即便用到nginx,还只是用它监听80端口再代理apache的php做负载均衡器。
这次偶尔机会自己搭建了 nginx + php-fpm 环境,发现自己开发的MyQEE输出js、css以及image图片时会出现异常关闭的问题。

nginx配置了类似这样的rewrite

if (!-e $request_filename) {
    rewrite ^/.* /index.php last;
}

rewrite的意思是当请求的实际文件不存在时rewrite到index.php上

【那么问题来了】
由于有那个rewrite的存在,所以当请求类似 http://127.0.0.1/test.js 的URL,如果服务器上没有test.js,此时就会重定向导index.php,并不会直接返回404错误,而是由index.php来决定。

然后由这个index.php根据一些参数(例如uri=test.js)载入了另外一个js,并模拟页面输出,输出了一个200的头信息。
代码类似如下:

// index.php 文件,实际上的代码可能比这个复杂多,这里只是为了简单的说明问题
$file = 'my_other.js'; // 假设服务器上有另外一个my_other.js的文件
header('Content-Type: application/x-javascript');
header('Content-Length: '. filesize($file));
readfile($file);    // 把文件直接输出

这个看上去似乎没有任何问题,但是在nginx里却出现了奇怪问题。

浏览器再接受到部分内容输出后直接被关闭了。打开调试输出可以看到类似:
NET:ERR_CONTENT_LENGTH_MISMATCH
这样的错误。

如果在命令行里使用curl请求时,则会看到类似

curl: (18) transfer closed with 56097 bytes remaining to read

这样的错误。

【问题分析】
首先来看 nginx 的配置,类似如下:

location ~ \.php$ {
    fastcgi_pass   127.0.0.1:9000;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;
}

这个实际上是nginx通过代理php-fpm来实现的输出,当一个css、js、图片等正常请求因为rewrite的原因被rewrite到了php上,nginx内部会认为是一个文档处理,然后对文档进行压缩获得了压缩后的内容长度,在输出达到这个长度后就错误的关闭了tcp连接,但是返回的header头信息的长度却是压缩前的长度,这样就导致了之前的错误。

【问题解决】
我尝试过把gzip的功能关闭但是实际上没有启到任何作用,最后我是通过这样的方法解决的,在gzip的gzip_types参数把对应的文件都加上这样就解决问题了,代码如下:

location ~ \.php$ {
    fastcgi_pass   127.0.0.1:9000;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;
   
    # 以下解决用php输出js,css等文件导致出错的问题
    gzip on;
    gzip_min_length 1100;
    gzip_buffers 4 8k;
    gzip_types text/plain application/x-javascript text/css image;
}

2015-09-25更新
本人另外一个服务器实测也遇到这个问题,需要在加上才可以

proxy_buffering off

在nginx的默认配置里 gzip_types就只有 text/plain

【总结】
实际上的解决就是把对应输出的文件类型让nginx再压缩下即可,但是如果php输出的是图片或一些已经压缩过的二进制文件,那么这样做实际上是增加了服务器的负担。
所以如果有大量的图片、swf、zip等文件,建议还是通过sendfile的方式进行转发。
sendfile可参考: