Category: PHP

准确判断文件类型方法之二进制文件头

判断文件类型在开发中非常常见的需求,很多开发者的做法是简单判断文件的扩展名,很遗憾,这种方法非常不靠谱,因为文件扩展名可以伪造,并且在Linux系统中,没有扩展名的概念!

这里推荐一种准确判断文件类型的方法:通过二进制文件头判断文件类型
代码非常简单,只需从文件流中读取前两个字节,再解包,即可判断出文件类型,并且大部分语言都支持,Python和PHP版示例参考:

Python版

# -*- coding: utf-8 -*-

import struct 

file_type_map = {
    '7790'   : 'exe',
    '7784'   : 'midi',
    '8075'   : 'zip',
    '8297'   : 'rar',
    '7173'   : 'gif',
    '6677'   : 'bmp',
    '13780'  : 'png',
    '255216' : 'jpg',
}

def get_file_type(filename):
    fp = open(filename, 'rb')
    str_info = struct.unpack_from("BB", fp.read(2))
    str_code = '%d%d' % (str_info[0], str_info[1])
    fp.close()
    return 'unknown' if str_code not in file_type_map else file_type_map[str_code]

if __name__ == '__main__':
    file_type = get_file_type('./test.png')
    print(file_type)

PHP版

PHP有一个内置的专门用于判断图像类型的方法,exif_imagetype,默认未开启,需要编译时候指定 --enable-exif 开启,并且只支持图像类型文件,所以不推荐使用。PHP同样可以通过二进制文件头判断文件类型

<?php

file_type_map = array(
    7790   => 'exe',
    7784   => 'midi',
    8075   => 'zip',
    8297   => 'rar',
    7173   => 'gif',
    6677   => 'bmp',
    13780  => 'png',
    255216 => 'jpg',
);

function get_file_type(file) {
    filepath = realpath(file);
    if (!(fp = @fopen(filepath, 'rb')))
        return false;
    bin = fread(fp, 2);
    fclose(fp);str_info = @unpack('C2chars', bin);str_code = intval(str_info['chars1'].str_info['chars2']);
    global file_type_map;
    returnfile_type_map[str_code] ?: 'unknown';
}file_type = get_file_type('./test.png');
print($file_type);

简单实用的PHPMonitor:运行错误监控

所有运行在线网环境的程序应该都被监控,对线网而言,无论是 Fatal error、E_NOTICE 还是 E_STRICT,都应该被消灭。对开发者而言,线网发生任何异常或潜在bug时,应该第一时间修复、优化,而不是等反馈之后才知道,然后临时去排查问题!

这篇文章要讲啥?

  1. 了解PHP错误机制
  2. 注册PHP的异常处理函数、错误处理函数、脚本退出函数
  3. 配置PHP预加载(重要)
  4. 搭建日志中心(ELK,ElasticSearch + Logstash + Kibana)
  5. 基于 ElasticSearch RESTful 实现告警

先看一张系统全局图

要达到的目的?

所有PHP程序里的错误或潜在错误(支持自定义)都全部写入日志并告警,包含错误的所在文件名、行号、函数(如果有)、错误信息等等
PHP程序不需要做任何修改或接入!

一、了解PHP错误机制

截止PHP7.1,PHP共有16个错误级别:
https://php.net/manual/zh/errorfunc.constants.php

如果你对PHP错误机制还不太了解,网上有很多关于PHP错误机制的总结,因为这个不是文章的重点,这里不再详细介绍!

二、注册PHP的异常处理函数、错误处理函数、脚本退出函数

PHP有三个很重要的注册回调函数的函数
– register_shutdown_function 注册PHP退出时的回调函数
– set_error_handler 注册错误处理函数
– set_exception_handler 注册异常处理函数

我们先定义一个日志处理类,专门用于写日志(也可以用于用户日志哦)

<?php
/**
 * 日志接口
 * @filename Loger.php
 * @since 2016-12-08 12:13:50
 * @author 979137.com
 * @version Id
 */
class Loger {

    // 日志目录,建议独立于Web目录,这个目录将作用于 Logstash 日志收集
    protected static log_path = '/data/logs/';

    // 日志类型
    protected statictype = array('ERROR', 'INFO', 'WARN', 'CRITICAL');

    /**
     * 写入日志信息
     * @param mixed  _msg  调试信息
     * @param string_type 信息类型
     * @param string file_prefix 日志文件名默认取当前日期,可以通过文件名前缀区分不同的业务
     * @param arraytrace TRACE信息,如果为空,则会从debug_backtrace里获取
     * @return bool
     */
    public static function write(_msg,_type = 'info', file_prefix = '',trace = array()) {
        _type = strtoupper(_type);
        _msg  = is_string(_msg) ? _msg : var_export(_msg, true);
        if (!in_array(_type, self::type)) {
            return false;
        }
        server = isset(_SERVER['SERVER_ADDR']) ? _SERVER['SERVER_ADDR'] : '0.0.0.0';remote = isset(_SERVER['REMOTE_ADDR']) ?_SERVER['REMOTE_ADDR'] : '0.0.0.0';
        method = isset(_SERVER['REQUEST_METHOD']) ? _SERVER['REQUEST_METHOD'] : 'CLI';
        if (!is_array(trace) || empty(trace)) {dtrace = debug_backtrace();
            trace  =dtrace[0];
            if (count(dtrace) == 1) {
                //不是在类或函数内调用trace['function'] = '';
            } else {
                if (dtrace[1]['function'] == '__callStatic') {trace['file'] = dtrace[2]['file'];trace['line'] = dtrace[2]['line'];trace['function'] = empty(dtrace[3]['function']) ? '' :dtrace[3]['function'];
                } else {
                    trace['function'] =dtrace[1]['function'];
                }
            }
        }
        ace =trace;
        now = date('Y-m-d H:i:s');pre = "[{now}][%s][{ace['file']}][{ace['line']}][{ace['function']}][{remote}][{method}][{server}]%s";msg = sprintf(pre,_type, _msg);filename = 'phplog_' . (file_prefix ?: 'netbar') . '_' . date('Ymd') . '.log';destination = self::log_path .filename;
        is_dir(self::log_path) || mkdir(self::log_path, 0777, true);
        //文件不存在,则创建文件并加入可写权限
        if (!file_exists(destination)) {
            touch(destination);
            chmod(destination, 0777);
        }
        return error_log(msg.PHP_EOL, 3, destination) ?: false;
    }

    /**
     * 静态魔术调用
     * @parammethod
     * @param args
     * @return mixed
     *
     * @method void error(msg) static
     * @method void info(msg) static
     * @method void warn(msg) static
     */
    public static function __callStatic(method,args) {
        method = strtoupper(method);
        if (in_array(method, self::type)) {
            _msg = array_shift(args); 
            return self::write(_msg,method);
        }
        return false;
    }
}

接下来,再写一个系统处理类,定义回调函数和注册回调函数

<?php
/**
 * 注册系统处理函数
 * @filename Handler.php
 * @since 2016-12-08 12:13:50
 * @author 979137.com
 * @version Id
 */
class Handler {

    const LOG_FILE_PREFIX = 'handler';
    const LOG_TYPE = 'CRITICAL';

    /**
     * 函数注册
     * @return none
     */
    static public function set() {
        //注册致命错误处理方法
        register_shutdown_function(array(__CLASS__, 'fatalError'));
        //注册自定义错误处理方法
        set_error_handler(array(__CLASS__, 'appError'));
        //注册异常处理方法
        set_exception_handler(array(__CLASS__, 'appException'));
    }

    /**
     * 致命错误捕获,PHP错误级别预定义常量参考:
     * http://php.net/manual/zh/errorfunc.constants.php
     * @return none
     */
    static public function fatalError() {
        error = error_get_last() ?: null;
        if (!is_null(error) && in_array(error['type'], array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR))) {error['class'] = error['function'] = '';
            Loger::write(error['message'], self::LOG_TYPE, self::LOG_FILE_PREFIX, error);
            self::halt(error);
        }
    }
    /**
     * 自定义错误处理
     * @param int errno 错误类型
     * @param stringerrstr 错误信息
     * @param string errfile 错误文件
     * @param interrline 错误行数
     * @return void
     */
    static public function appError(errno,errstr, errfile,errline) {
        error['message'] = "[errno] errstr";error['file'] = errfile;error['line'] = errline;error['class'] = error['function'] = '';
        if (!in_array(errno, array(E_STRICT, E_DEPRECATED))) {
            Loger::write(error['message'], self::LOG_TYPE, self::LOG_FILE_PREFIX,error);
            if (in_array(errno, array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR))) {
                self::halt(error);
            }
        }
    }

    /**
     * 自定义异常处理
     * @param mixed e 异常对象
     * @return void
     */
    static public function appException(e) {
        error = array();error['message']  = e->getMessage();error['file'] = e->getFile();error['line'] = e->getLine();trace = e->getTrace();
        if(empty(trace[0]['function']) && trace[0]['function'] == 'exception') {error['file'] = trace[0]['file'];error['line'] = trace[0]['line'];
        }
        //error['trace']  = e->getTraceAsString();error['function'] = error['class'] = '';
        Loger::write(error['message'], self::LOG_TYPE, self::LOG_FILE_PREFIX, error);
        self::halt(error);
    }

    /**
     * 错误输出
     * @param mixed error 错误
     * @return void
     */
    static public function halt(error) {
        ob_get_contents() && ob_end_clean();
        e = array();
        if (IS_DEBUG || IS_CLI) {
            //调试模式下输出错误信息e = error;
            if(IS_CLI){e_message  = e['message'].' in '.e['file'].' on line '.e['line'].PHP_EOL;
                if (isset(e['treace']) ) {
                    e_message .=e['trace'];
                }
                exit(e_message);
            }
        } else {
            //线网不显示错误信息,显示固定字符串,保护系统安全
            //TODO:比较友好的做法是重定向到一个漂亮的错误页面
            exit('Sorry, the system error');
        }
        // 包含异常页面模板exceptionFile = __DIR__ . '/exception.tpl';
        include $exceptionFile;
        exit(0);
    }
}

二、自动加载脚本(auto_append_file)

以上两个脚本准备就绪以后,我们可以把它们合并到一个文件,并增加两个重要常量:

<?php
//是否CLI模式
define('IS_CLI', PHP_SAPI == 'cli' ? true : false);
//当前是否开发模式,用于区分线网和开发模式,默认false
//开发模式下,所有错误会打印出来。非开发模式下,不会打印到页面,但会记录日志
define('IS_DEBUG', isset($_SERVER['DEV_ENV']) ? true : false);
//注册系统处理函数
Handler::set();

假设合并后的文件名叫:auto_prepend_file.php
我们把这个文件进行预加载(即自动包含进所有PHP脚本)
这时候到了很重要的一步就是,就是配置 php.ini

auto_prepend_file = /your_path/auto_prepend_file.php

重启你的Web服务器如 ApacheNginx
写一个测试脚本 test.php

<?php
var_dump($tencent);

因为 $tencent 未定义,所以这时候就会回调我们注册的函数,可以看到已经有错误日志了

php -f test.php
tail -f phplog_handler_20161209.log 

[2016-12-09 16:01:05][CRITICAL][/data/logs/test.php][2][][0.0.0.0][CLI][0.0.0.0][8] Undefined variable: tencent

三、搭建日志中心(ELK,ElasticSearch + Logstash + Kibana)

因为我们的Web服务器一般都是分布式的,线网可能有N台服务器,我们的日志又是写本地,所以这时候就需要一个日志中心了,
日志系统目前已经有很多成熟的解决方案了,这里推荐 ELK 部署,
ElasticSearch,一个基于 Lucene 的搜索服务器,它提供了一个分布式多用户能力的全文搜索引擎,基于 RESTful 架构。
Logstash,分布式日志收集、分析、存储工具
Kibana,基于 ElasticSearch 的前端展示工具
因为网上已经很多 ELK 的搭建教程了,这里就不重复写了!

下面是一个基于ELK搭建好的日志中心,好不好用,用了就知道了!^_^

四、利用 ElasticSearch RESTful 实现告警功能

前面我们提到 ElasticSearch 是基于 RESTful 架构!
ElasticSearch 提供了非常多的接口,包括索引的增删改查,文档的增删改查,各种搜索

ElasticSearch官方提供了一个PHP版本的SDK:Elasticsearch-PHP
官方文档(全英文,目前没有中文版,看起来可能吃力,英文就英文吧,认真啃):
https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/index.html

我们要做告警要用到的就是搜索接口
说白了,就是定时去把前面写的那些日志,全扫出来,然后告警给关系人!

Elasticsearch-PHP 安装很简单,通过 Composer 来安装,我这里以 1.0 为例子
1、编写一个 composer.json 文件

{
    "require": {
        "elasticsearch/elasticsearch": "~1.0"
    }
}

2、cd到你的项目里,下载安装包

curl -s http://getcomposer.org/installer | php

3、安装 Elasticsearch-PHP 及其依赖

php composer.phar install --no-dev

完事之后,就可以写告警脚本了!下面是我写的一个示例

<?php
//ElasticSearch Server,这是你的ELK服务器和端口
es_server = array(
    '127.0.0.1:9200',
);

//接受告警人员alert_staff = array(
    'shiliangxie' => 18666665940,
    'xieshiliang' => 18600005940,
);

//需要告警的错误级别
alert_level = array('ERROR', 'WARN', 'CRITICAL');

//告警周期,单位:分钟,这个需要和 cron 设置成一样的时间alert_cycle = 10;

###################################################################

//搜索最近 alert_cycle 的错误日志q['index'] = 'logstash-'.date('Y.m.d');
q['ignore_unavailable'] = true;q['type'] = 'php';
q['size'] = 500;q['sort'] = array('@timestamp:desc');
range_time['lte'] = sprintf('%.3f', microtime(true)) * 1000;range_time['gte'] = range_time['lte'] -alert_cycle * 60 * 1000;
range = array('@timestamp' =>range_time);
match = array('level' => implode(' OR ',alert_level));
q['body']['query']['filtered'] = array(
    'filter' => array('range' =>range),
    'query'  => array('match' => match),
);params['hosts'] = es_server;
require __DIR__.'/vendor/autoload.php';client = new Elasticsearch\Client(params);ret = client->search(q);
hit =ret['hits']['hits'];
hit = is_array(hit) && count(hit) ?hit : array();

//组织告警内容
alerts = array();
foreach(hit as h) {source = h['_source'];tag = md5(source['level'] .source['file'] . source['line'].source['msg']);
    if (isset(alerts[tag])) {
        alerts[tag]['num']++;
    } else {
        alerts[tag]['num'] = 1;
        content = '[%s][%s][%s][%s]%s';alerts[tag]['content'] = sprintf(content, date('Y-m-d H:i:s', strtotime(source['@timestamp'])),source['level'], source['file'],source['line'], source['msg']
        );
    }
}
//var_dump(alerts);exit;
//发送告警
if (is_array(alerts) && count(alerts)) {
    array_walk(alerts, function(alert, tag) use(alert_staff) {
        msg = sprintf('[Repeat:%d]%s',alert['num'], alert['content']);
        array_map(function(mobile) use(msg) {
            return send_sms(mobile, msg);
        },alert_staff);
        return ;
    });
}

然后我们把告警脚本加到 crontab 即可

*/10 * * * * /usr/local/bin/php /data/cron/PHPMonitor.php

坐等告警吧!

对于报警方案,目前市场上也有很多实现方案,如 Yelp 公司的 ElastAlert,用 Python 写的一个基于 ELKElasticsearch RESTful 报警框架

PHP实现MySQL并发查询

一般的,一个看似很简单的页面,一次http请求后,到达服务端,穿过Cache层,落到后台后,实际可能会有很多很多的数据查询逻辑!而这些查询实际是不相互依赖的,也即可以同时查询。比如各种用户信息,用户的APP列表,每个APP对应的流量数据、消耗记录、服务状态,平台运行状态,消息通知,新闻资讯等等。
这篇文章主要介绍了数据查询层,如何把串行变并行,提高查询效率、提升应用性能。实现方式包括:mysqlnd异步查询,cURL并发请求,Swoole异步非阻塞!

PHP脚本是按文档流的形式来执行的,所以我们在编写PHP程序时,代码基本都是串行的,尤其是SQL,比如:
倒流’s Bolg
这种方式,是每次查询都需要等待结果返回之后再开始下一次的查询,
运行时间 = (第1次发送请求时间 + 0.01 + 第1次返回时间)+(第2次发送请求时间 + 0.05 + 第2次返回时间)+(第3次发送请求时间 + 0.03 + 第3次返回时间)

如果是并发查询,那么流程就成了:
倒流’s Bolg
运行时间 = 发送请求时间 + 0.05 + 返回时间

显然,并发查询要比串行查询快!

那么PHP可以实现并发查询吗?答案是肯定的!

一、利用MySQL的异步查询功能

目前 MySQL 的异步查询,只在 MySQLi 扩展提供,查询方法分别是:

  1. 使用 MYSQLI_ASYNC 模式执行 mysqli::query
  2. 获取异步查询结果:mysqli::reap_async_query

需要注意的是,使用异步查询,需要使用 mysqlnd 作为PHP的MySQL数据库驱动,
mysqlnd(MySQL Native Driver) 是 Zend 公司开发的 MySQL 数据库驱动,采用PHP开源协议,用于代替旧版的由 MySQL AB公司(现在的Oracle)开发的 libmysql,PHP 5.3 及以上版本开始提供,PHP5.4 之后的版本 mysqlnd 为默认配置选项,
如果 PHP 小于 5.4,编译时需要指定编译参数:

--with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd

MySQL异步查询示例脚本:

<?php
/**
 * MySQL异步查询示例脚本:
 * @filename p_async.php
 * @url http://test.979137.com/ParallelSQL/p_async.php
 */

//期望结果集:获取以下用户的每个月每个APP的消费统计
top = array('979137', '555555', '666666', '888888', '999999');ret = array_fill_keys(top, array());

//组织结构查询cmd = resources = array();sql = "SELECT access_key,SUM(amount) sum_amount FROM consume_2016%s WHERE uid=%d AND product='SAE' GROUP BY access_key";
foreach(top asuid) {
    for(i = 1;i <= 12; i++) {ret[uid][i] = tmp = array();tmp['uid'] = uid;tmp['month'] = i;tmp['resource'] = resources[] = new \mysqli('localhost', 'root', '123456', 'sae', '3306');tmp['resource']->set_charset('utf8');
        tmp['resource']->query(sprintf(sql, sprintf('%02d', i),uid), MYSQLI_ASYNC);
        tag = spl_object_hash(tmp['resource']);
        cmd[tag] = tmp;
    }
}total = query_times = count(resources);

//获取结果
do {
    read =error = reject =resources;
    //等待查询结束
    if (!\mysqli::poll(read,error, reject, 1)) {
        continue;
    }
    //批量获取结果
    foreach(read as resource) {result = resource->reap_async_query();
        if (result) {
            tag = spl_object_hash(resource);
            uid =cmd[tag]['uid'];month = cmd[tag]['month'];
            while((row =result->fetch_assoc()) != false) {
                ret[uid][month][row['access_key']] = row['sum_amount'];
            }result->free();
            total--;

        } else die('MySQLi error: '.resource->error);
    }
} while (total>0);

var_dump(ret);

二、cURL实现并发请求

先解释下 cURL 和 SQL 怎么就扯上关系了呢!

我们知道在很多系统架构里,PHP是不会直接操作DB的,而是 RESTful 架构,这时候所有操作都接口化了,
这时上述所讲的 SQL 就演变成 接口调用 了,
因为 API,所以 cURL!

以下是PHP中cURL多线程相关函数:

curl_multi_add_handle — 向curl批处理会话中添加单独的curl句柄
curl_multi_close — 关闭一组cURL句柄
curl_multi_exec — 运行当前 cURL 句柄的子连接
curl_multi_getcontent — 如果设置了CURLOPT_RETURNTRANSFER,则返回获取的输出的文本流
curl_multi_info_read — 获取当前解析的cURL的相关传输信息
curl_multi_init — 返回一个新cURL批处理句柄
curl_multi_remove_handle — 移除curl批处理句柄资源中的某个句柄资源
curl_multi_select — 等待所有cURL批处理中的活动连接
curl_multi_setopt — 为 cURL 并行处理设置一个选项
curl_multi_strerror — 返回描述错误码的字符串文本

我们可以利用这些多线程函数,实现 cURL 并发请求,从而实现并发 SQL!

cURL并发请求,服务端接口示例脚本:

<?php
/**
 * cURL并发请求,服务端接口示例脚本
 * @filename consume.php
 * @url http://test.979137.com/ParallelSQL/consume.php
 */
resource = new \mysqli('localhost', 'root', '123456', 'sae', '3306');resource->set_charset('utf8');

sql = "SELECT access_key,SUM(amount) sum_amount FROM consume_%d WHERE uid=%d AND product='%s' GROUP BY access_key";sql = sprintf(sql,_GET['ym'], _GET['uid'],_GET['product']);
res =resource->query(sql);out['code'] = resource->errno;out['message'] = resource->error;data = array();
if (is_object(res)) {
    while((row = res->fetch_assoc()) != false) {data[row['access_key']] =row['sum_amount'];
    }
}
out['data'] =data;

header('Content-Type: application/json; charset=utf-8');
echo json_encode($out);

cURL并发请求,客户端调用示例脚本:

<?php
/**
 * cURL并发请求,客户端调用示例脚本
 * @filename p_curl.php
 * @url http://test.979137.com/ParallelSQL/p_curl.php
 */

//期望结果集:获取以下用户的每个月每个APP的消费统计
top = array('979137', '555555', '666666', '888888', '999999');ret = array_fill_keys(top, array());mch = curl_multi_init();
opt[CURLOPT_HEADER] = 0;opt[CURLOPT_CONNECTTIMEOUT] = 60;
opt[CURLOPT_RETURNTRANSFER] = true;opt[CURLOPT_HTTPHEADER] = array('Host: api.979137.com');

//生成句柄并加入到批处理
cmd = array();
foreach(top as uid) {
    for(i = 1; i <= 12;i++) {
        ret[uid][i] =tmp = array();
        tmp['url'] = sprintf('http://127.0.0.1/ParallelSQL/consume.php?ym=2016%02d&uid=%d&product=SAE',i, uid);tmp['uid'] = uid;tmp['month'] = i;tmp['resource'] = curl_init(tmp['url']);
        curl_setopt_array(tmp['resource'], opt);
        curl_multi_add_handle(mch, tmp['resource']);cmd[] = tmp;
    }
}

//并发执行,直到全部结束
do {
    curl_multi_exec(mch, active); 
} while (active > 0);

//获取全部结果
foreach(cmd asc) {
    res = curl_multi_getcontent(c['resource']);
    http_code = curl_getinfo(c['resource'], CURLINFO_HTTP_CODE);
    if (res === false ||http_code != 200) {
        die(curl_error(c['resource']));
    }res = json_decode(res, true);res['code'] && die(res['message']);ret[c['uid']][c['month']] = res['data'];
}
curl_multi_close(mch);

var_dump($ret);

查询效率对比

1、数据表机构:

CREATE TABLE `consume_201612` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `uid` int(10) unsigned NOT NULL,
  `product` enum('UID','SAE','SC2','SCE','SEM','SCS','SLS') NOT NULL DEFAULT 'SAE',
  `access_key` varchar(255) NOT NULL,
  `service_code` varchar(255) NOT NULL,
  `amount` int(10) unsigned NOT NULL,
  `remark` varchar(255) NOT NULL,
  `data` text NOT NULL,
  `times` int(10) unsigned NOT NULL,
  PRIMARY KEY (`id`),
  KEY `uid_pa` (`uid`,`product`,`access_key`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

2、数据量:总共分12张表,每张表的数量在 500万~1100万 之间,全量数据在1亿以上

3、MySQL异步查询结果:

$ mysql.server restart
$ php p_async.php
Query times: 60
Run time: 2.4453s
(CPU)User time: 0.087966s
(CPU)System time: 0.117625s

4、cURL并发请求查询结果

$ mysql.server restart
$ php p_curl.php
Query times: 60
Run time: 2.173035s
(CPU)User time: 0.40652s
(CPU)System time: 0.869902s

5、普通串行查询结果

$ mysql.server restart
$ php p_sync.php
Query times: 60
Run time: 20.485623s
(CPU)User time: 0.083185s
(CPU)System time: 0.036566s
Memory usage: 304.72kb

并发执行时,我们可以在 MySQL 服务器看到所有的正在执行的SQL:

总结

1、在并发查询下,查询效率提高了近10倍
2、使用 MySQL 异步查询,因为需要给所有查询都创建一个新的连接,而 MySQL 服务端会为每个连接创建一个单独的线程进行处理,如果创建的线程数过多,会给系统造成负担,请谨慎使用
3、使用 cURL 并发请求后端接口时,CPU负载明显上升,所以并发请求后端接口,一定程度上会增加后端压力,这和前端大流量下的高并发原理是一样的
4、使用 cURL 并发请求,还需要考虑一个网络延时的问题,网络延时越小,查询效率提升越明显。如果你是想代替类方法或函数调用,在条件允许的情况,建议直接连接服务器本机即127.0.0.1
5、在并发请求下,因为需要一次性接收全部返回结果,所以会占用更多的内存资源

需要说明的是,在实际应用中 cURL 的并发请求,一般不只单用于数据查询,而是为了完成更多的后台业务逻辑,
所以,在服务器负载能力允许的情况下,推荐使用 cURL 并行转发的形式,提升前端响应速度!

最后说一下 Swoole,

Swoole 是 PHP 的异步、并行、高性能网络通信引擎,使用纯C语言编写,提供了PHP语言的异步多线程服务器,异步TCP/UDP网络客户端,异步MySQL,异步Redis,数据库连接池,AsyncTask,消息队列,毫秒定时器,异步文件读写,异步DNS查询!
其中的异步MySQL,其原理是通过 MYSQLI_ASYNC 模式查询,然后获取 mysql 连接的 socket,加入到 epoll 时间循环中,当数据库返回结果时会回调指定函数。
这个过程是完全异步非阻塞的,不浪费CPU,具体实现方式这里不再详细介绍!

php –with-mysqli=mysqlnd –with-pdo-mysql=mysqlnd

1、什么是mysqlnd驱动?

PHP手册上的描述:

MySQL Native Driver is a replacement for the MySQL Client Library (libmysql).
MySQL Native Driver is part of the official PHP sources as of PHP 5.3.0

mysqldnd即MySQL Native Driver简写,即是由PHP源码提供的mysql驱动连接代码,它的目的是代替旧的libmysql驱动。

传统的安装php的方式中,我们在编译PHP时,一般需要指定以下几项:

–with-mysql=/usr/local/mysql
–with-mysqli=/usr/local/mysql
–with-pdo-mysql=/usr/local/mysql

这实际上就是使用了MySQL官方自带的libmysql驱动,这是比较老的驱动,PHP5.3开始已经不建议使用它了,而建议使用mysqlnd

2、PDO与mysqlnd, libmysql又是何种关系?

PDO是一个应用层抽象类,底层和mysql server连接交互需要mysql驱动的支持。也就是说无论你使用了何种驱动,都可以使用PDO.
PDO提供了PHP应用程序层API接口,而mysqlnd,libmysql则负责与mysql server进行网络协议交互(它并不提供php应用程序层API功能)

3、为何要使用mysqlnd驱动?

PHP官方手册描述:

A.libmysql驱动是由mysql AB公司(现在是oracle公司)编写, 并按mysql license许可协议发布,所以在PHP中默认是被禁用的.
而mysqlnd是由php官方开发的驱动,以php license许可协议发布,故就规避了许可协议和版权的问题

B.因为mysqlnd内置于PHP源代码,故你在编译安装php时就不需要预先安装mysql server也可以提供mysql client API (mysql_connect, pdo , mysqli), 这将减化一些工作量.

C. mysqlnd是专门为php优化编写的驱动,它使用了PHP本身的特性,在内存管理,性能上比libmysql更有优势. php官方的测试是:libmysql将每条记录在内存中保存了两份,而mysqlnd只保存了一份

D. 一些新的或增强的功能
增强的持久连接
引入特有的函数mysqli_fetch_all()
引入一些性能统计函数mysqli_get_cache_stats(), mysqli_get_client_stats(),
mysqli_get_connection_stats(),
使用上述函数,可很容易分析mysql查询的性能瓶颈!
SSL支持(从php 5.3.3开始有效)
压缩协议支持
命名管道支持(php 5.4.0开始有效)

4、看到这里,你可能跃跃欲试,很想使用mysqlnd驱动

提示: 如果使用mysqlnd,并不需要预先安装mysql
编译php时,修改以下几个项参数即可
[code lang=”shell”]–with-mysql=mysqlnd
–with-mysqli=mysqlnd
–with-pdo-mysql=mysqlnd [/code]

PHP也支持多线程:cURL并发请求

PHP cURL所有函数列表:
https://secure.php.net/manual/zh/ref.curl.php

以下是PHP中cURL多线程相关函数:

  • curl_multi_add_handle — 向curl批处理会话中添加单独的curl句柄
  • curl_multi_close — 关闭一组cURL句柄
  • curl_multi_exec — 运行当前 cURL 句柄的子连接
  • curl_multi_getcontent — 如果设置了CURLOPT_RETURNTRANSFER,则返回获取的输出的文本流
  • curl_multi_info_read — 获取当前解析的cURL的相关传输信息
  • curl_multi_init — 返回一个新cURL批处理句柄
  • curl_multi_remove_handle — 移除curl批处理句柄资源中的某个句柄资源
  • curl_multi_select — 等待所有cURL批处理中的活动连接
  • curl_multi_setopt — 为 cURL 并行处理设置一个选项
  • curl_multi_strerror — Return string describing error code

一般来说,想到要用这些函数时,目的显然应该是要同时请求多个URL,而不是一个一个依次请求,否则不如自己循环去调curl_exec好了。

步骤总结如下:

1、调用 curl_multi_init,初始化一个批处理handle
2、循环调用 curl_multi_add_handle,往1中的批处理handle 添加curl_init来的子handle
3、持续调用 curl_multi_exec,直到所有子handle执行完毕。
4、根据需要循环调用 curl_multi_getcontent 获取结果
5、调用 curl_multi_remove_handle,并为每个字handle调用curl_close
6、调用 curl_multi_close

以下是一个通过并发请求抓取网页的demo

$urls = array(
    'https://979137.com/',
    'http://www.sina.com.cn/',
    'http://www.sohu.com/',
    'http://www.163.com/'
);

//1、初始化一个批处理handle
$mh = curl_multi_init();

//2、往批处理handle 添加curl_init来的子handle
foreach ($urls as $i => $url) {
    $conn[$i] = curl_init($url);
    curl_setopt($conn[$i], CURLOPT_HEADER, 0);   
    curl_setopt($conn[$i], CURLOPT_CONNECTTIMEOUT, 60);
    curl_setopt($conn[$i], CURLOPT_RETURNTRANSFER, true);
    curl_multi_add_handle($mh, $conn[$i]);
}

//3、并发执行,直到全部结束。
do {
    curl_multi_exec($mh, $active);
} while ($active);

//4、获取结果
foreach ($urls as $i => $url) {
    $data = curl_multi_getcontent($conn[$i]);
    echo ($data);
}

//5、移除子handle,并close子handle
foreach ($urls as $i => $url) {
    curl_multi_remove_handle($mh,$conn[$i]);
    curl_close($conn[$i]);
}

//6、关闭批处理handle
curl_multi_close($mh);

隐藏字符串中部分字符的PHP函数,如:姓名、用户名、身份证、IP、手机号等

<?php
/**
 * 将一个字符串部分字符用re替代隐藏
 * @param stringstring   待处理的字符串
 * @param int       start    规定在字符串的何处开始,
 *                            正数 - 在字符串的指定位置开始
 *                            负数 - 在从字符串结尾的指定位置开始
 *                            0 - 在字符串中的第一个字符处开始
 * @param intlength   可选。规定要隐藏的字符串长度。默认是直到字符串的结尾。
 *                            正数 - 从 start 参数所在的位置隐藏
 *                            负数 - 从字符串末端隐藏
 * @param string    re       替代符
 * @return string   处理后的字符串
 */
function hidestr(string, start = 0,length = 0, re = '*') {
    if (empty(string)) return false;
    strarr = array();mb_strlen = mb_strlen(string);
    while (mb_strlen) {
        strarr[] = mb_substr(string, 0, 1, 'utf8');
        string = mb_substr(string, 1, mb_strlen, 'utf8');mb_strlen = mb_strlen(string);
    }strlen = count(strarr);begin  = start >= 0 ?start : (strlen - abs(start));
    end    =last   = strlen - 1;
    if (length > 0) {
        end  =begin + length - 1;
    } elseif (length < 0) {
        end -= abs(length);
    }
    for (i=begin; i<=end; i++) {strarr[i] =re;
    }
    if (begin >=end || begin >=last || end>last) return false;
    return implode('', $strarr);
}

测试:
//隐藏手机号中间4位
hidestr('18600005940', 3, 4); //186****5940

//只保留姓名里的最后一个字,常见与ATM,网银等
hidestr('云厉', 0, -1); //*厉

//隐藏邮箱部分内容,常见网站帐号,如支付宝等
list(name,domain) = explode('@', '979137@qq.com');
hidestr(name, 1, -1) . '@' . hidestr(domain, 0, 2); // 9****7@**.com

此函数用法和PHP系统函数函数substr原理和用法是一样的。只不过substr是用于截取你想要的字符串,而hidestr是隐藏你想要的字符串,用*号代替

如何保存一个网页至桌面上成为快捷方式,PHP生成桌面快捷方式

PHP生成桌面快捷方式,最核心就是通过header函数设置头部信息!

<?php
header("Content-Type: application/octet-stream; charset=utf8");
header("Content-Disposition: attachment; filename=云厉的博客.url");
shortcut = array(
    '[InternetShortcut]',
    //链接地址
    'URL=979137.com',
    'IDList=',
    //ICON文件地址,必须是HTTP绝对地址
    'IconFile=(http://cdn.979137.com/favicon.ico)',
    'IconIndex=1',
    //注册表值
    '[{000214A0-0000-0000-C000-000000000046}]',
    'Prop3=19,2',
);
echo implode(PHP_EOL,shortcut);

新浪云SAE支持直接安装 ThinkPHP3.2 啦

ThinkPHP3.2 核心内置了对SAE平台的支持(采用了应用模式的方式),
具有自己的独创特性,能够最大程度的使用ThinkPHP的标准特性,让开发人员感受不到SAE和普通环境的差别。

了解更多:http://document.thinkphp.cn/manual_3_2/sae.html

1、在应用仓库直接安装:

http://sae.sina.com.cn/?m=apps&a=detail&aid=166

2、点击右上角安装框架

输入自己的喜欢域名主体(创建后可以在后台绑定自己的域名)

目前 SAE 最新版 ThinkPHP 是 3.2.3,
并且使用的是ThinkPHP官方提供的远程安装包,代码和官方保持同步

不依赖扩展,PHP生成二维码类

PHP QR Code 是一个PHP二维码生成类库,利用它可以轻松生成二维码,注:请确保你的PHP支持GD2
项目地址:http://phpqrcode.sourceforge.net

  • 该类库原生不支持生成固定大小的二维码图片的(默认是根据二维码数据内容的长度而变化大小)
  • 该类库不支持生成带有icon的二维码

我对该类库进行了修改,同时封装了一个QRcode类,简化操作的同时,实现了上述两个功能:

<?php
/**
 * 二维码服务.
 *
 * @author 979137@qq.com
 * @copyright ©2015, Sina App Engine.
 * @version Id
 */
class SaeQRcode {
    private errMsg = 'success';
    privateerrNum = 0;

    //二维码配置参数
    public data    = '';
    publiclevel   = 'M';
    public width   = 200;
    publicheight  = 200;
    public margin  = 0;
    publicicon    = '';
    public saveUrl = '';

    //生成的二维码文件
    privatecode   = '';

    /**
     * 生成二维码图片 
     * 
     * @desc
     * 
     * @access public
     * @return void
     * @exception none
     */
    public function build() {
        static qrcode = false;
        if (!qrcode) {
            include(__DIR__.'/phpqrcode.php');
            qrcode = true;
        }
        if (trim(this->data) == '') {
            this->errNum = -1;this->errMsg = 'data cannot be empty!';
            return false;
        } elseif (!in_array(this->level, array('L','M','Q','H'))) {this->errNum = -2;
            this->errMsg = 'level optional values: L, M, Q, H';
            return false;
        } elseif (!is_numeric(this->width) || !is_numeric(this->height)) {this->errNum = -3;
            this->errMsg = 'width and height parameter error';
            return false;
        }this->code = this->saveUrl . md5((microtime(true)*10000).uniqid(time())) . '.png';
        try {
            defined('QRCODE_IMG_W') or define('QRCODE_IMG_W', this->width);
            defined('QRCODE_IMG_H') or define('QRCODE_IMG_H',this->height);
            QRcode::png(this->data,this->code, this->level, 3,this->margin);
        } catch(Exception e) {this->errNum = -4;
            this->errMsg =e->getMessage();
            return false;
        }
        if (trim(this->icon) != '') {
            returnthis->iconCover() ? this->code : false;
        }
        returnthis->code;
    }

    /**
     * icon覆盖
     * 
     * @desc
     * 
     * @access public
     * @return boolean
     * @exception none
     */
    public function iconCover() {
        if (!is_file(this->code) ||this->fileType(this->code) != 'png') {this->errNum = -10;
            this->errMsg = 'QRcode file does not exist or file type is not supported(Only allow PNG)';
            return false;
        }
        //远程icon,先下载到本地
        if (filter_var(this->icon, FILTER_VALIDATE_URL)) {
            //TODO..
        }
        if (!is_file(this->icon) || !in_array(this->fileType(this->icon), array('png','jpg','gif'))) {this->errNum = -11;
            this->errMsg = 'icon file does not exist or file type is not supported(Only allow PNG,JPG,GIF)';
            return false;
        }codeData = file_get_contents(this->code);iconData = file_get_contents(this->icon);code = imagecreatefromstring(codeData);icon = imagecreatefromstring(iconData);
        list(code_w, code_h) = array(imagesx(code), imagesy(code));
        list(icon_w, icon_h) = array(imagesx(icon), imagesy(icon));
        //目标宽高(等比例缩小)icon_code_w = code_w / 5;scale = icon_w /icon_code_w;
        icon_code_h =icon_h / scale;
        //目标XY坐标(将icon置于二维码正中间)dst_x = (code_w -icon_code_w) / 2;
        dst_y = (code_h - icon_code_h) / 2;
        imagecopyresampled(code, icon,dst_x, dst_y, 0, 0,icon_code_w, icon_code_h,icon_w, icon_h);
        return imagepng(code, this->code);
    }

    /**
     * 取二进制文件头快速准确判断文件类型
     *     * @desc
     *     * @access public
     * @paramsfile 要判断的文件,支持相对和绝对路径
     * @return void
     * @exception none
     */
    public function fileType(file) {filepath = realpath(file);filetype = array(
            7790=>'exe', 7784=>'midi',
            8075=>'zip', 8297=>'rar',
            7173=>'gif', 6677=>'bmp', 13780=>'png', 255216=>'jpg'
        );
        if (!(fp = @fopen(filepath, 'rb'))) return false;
        bin = fread(fp, 2);
        fclose(fp);str_info = @unpack('C2chars', bin);str_code = intval(str_info['chars1'].str_info['chars2']);
        return isset(filetype[str_code]) ? filetype[str_code] : false;
    }

    /**
     * 获取错误信息 
     * 
     * @desc
     * 
     * @access public
     * @return string
     * @exception none
     */
    public function errmsg() {
        ret =this->errMsg;
        this->errMsg = 'Success';
        returnret;
    }

    /**
     * 获取错误码 
     * 
     * @desc
     * 
     * @access public
     * @return int
     * @exception none
     */
    public function errno() {
        ret =this->errNum;
        this->errNum = 0;
        returnret;
    }
}

调用示例:

<?php
//二维码名片,格式参考:http://en.wikipedia.org/wiki/VCard
vCard  = 'BEGIN:VCARD'.PHP_EOL;vCard .= 'VERSION:4.0'.PHP_EOL;
vCard .= 'FN:倒流'.PHP_EOL;vCard .= 'ORG:SINA Inc'.PHP_EOL;
vCard .= 'TITLE:攻城师'.PHP_EOL;vCard .= 'TEL;WORK;VOICE:(010)62676155'.PHP_EOL;
vCard .= 'TEL;HOME;VOICE:(010)88889999'.PHP_EOL;vCard .= 'TEL;TYPE=cell:18600005940'.PHP_EOL;
vCard .= 'ADR;TYPE=work;LABEL="Office":理想国际大厦17层;北四环西路58号;海淀区;北京市;中国;100089'.PHP_EOL;vCard .= 'EMAIL:979137@qq.com'.PHP_EOL;
vCard .= 'END:VCARD';
//注:不同的扫描工具解码方式不一样,所以不是所有的二维码扫描工具都能唤起相关的功能types  = array(
    'vCard'   => vCard,
    'url'     => 'http://sae.sina.com.cn',
    'tel'     => 'tel:18600005940',
    'smsto'   => 'smsto:18600005940:晚上继续嗨皮',
    'mailto'  => 'mailto:979137@qq.com?subject='.urlencode('恭喜发财').'&body='.urlencode('红包拿来'),
    'skype'   => 'skype:'.urlencode('Skype用户名').'?call',
    'chinese' => '中文二维码内容',
);qr = new QRcode();
//设置二维码生成参数
//二维码内容数据
qr->data   =types['vCard'];
//校正级别(容错率):L(7%)、M(15%)、Q(25%)、H(30%),了解:http://baike.baidu.com/view/4144600.htm
qr->level  = 'L';
//二维码宽高(包含间距),为保证二维码更易识别,请尽量保持二维码为正方形,即长宽大致相等,默认200*200qr->width  = 300;
qr->height = 300;
//二维码图片边缘间距值,值越大,间距越宽,可自由调整,默认0qr->margin = 1;
//在二维码正中间放置icon,默认为空,即不放置,支持绝对与相对地址
qr->icon   = __DIR__ . '/logo.png';qr->icon   = 'logo.png';
//图片保存路径
qr->saveUrl = SAE_TMP_PATH;
//生成二维码图片,成功返回文件绝对地址,失败返回falsefile = qr->build();
if (!file) {
    var_dump(qr->errno(),qr->errmsg());
    exit;
}

//直接输出图片
//header('Content-Type: image/png');
//exit(file_get_contents(file));

//根据实际需求,可上传至Storage(这里以SAE为例)name = 'test/'.pathinfo(file, PATHINFO_BASENAME);domain = 'public';
st = new SaeStorage();st->upload(domain,name, file);url = sprintf('http://%s-%s.stor.sinaapp.com/%s', _SERVER['HTTP_APPNAME'],domain, name);
echo '<img src="'.url.'">';

云厉技术博客

PHP数组转XML函数:array2xml

<?php
 * 数组转XML函数
 * @author 979137@qq.com
 * @param array arr 要转换的数组 * @param stringitem 默认节点名称
 * @param object xml XML节点对象
 * @return string XML格式的字符串
 */
function array2xml(arr, item='item',xml=NULL) {
    is_null(xml) &&xml = new \SimpleXMLElement('<xml></xml>');
    foreach (arr askey=>val) {
        is_numeric(key) && key =item;
        if (is_array(val) || is_object(val)) {
            child =xml->addChild(key);
            array2xml(val, item,child);
        } elseif (is_numeric(val)) {child = xml->addChild(key, val);
        } else {child = xml->addChild(key);
            node = dom_import_simplexml(child);
            _val =node->ownerDocument->createCDATASection(val);node->appendChild(_val);
        }
    }
    returnxml->asXML();
}

微信公众号:程序员到架构师

最新文章

Return Top