分类 技术 下的文章

PHP多进程相关

参考文章

https://www.cnblogs.com/leezhxing/p/5223289.html

牛人博客

https://segmentfault.com/blog/tigerb?page=1

Manager::__construct

  • 加载环境配置变量 parse_ini_file
  • 读取配置 os / userPasswd / startNum / hangupLoopMicrotime / pipeDir
  • 初始化master对象(设置type,设置pid|posix_getpid,创建管道文件), daemon对象(设置type)
  • 进程间通信 匿名管道 命名管道(pcntl_fork|posix_mkfifo|fopen r+可以非阻塞打开管道文件|stream_set_blocking设置读写非阻塞|保证原子性)
  • 支持信号 reload|SIGUSR1/stop|SIGUSR2/terminate|SIGTERM/int|SIGINT
  • fork子进程(写时复制)
    -- 创建管道文件
    -- while(true)
    -- 执行业务逻辑闭包函数
    -- 根据标志位判断是否退出
    -- 非阻塞地 fopen(r+)|stream_set_blocking(0) 从管道中读取信号
    -- 父进程new4个work对象 $this->workers[$pid] type:master ?
  • 注册信号处理函数 pcntl_signal|pcntl_signal_dispatch 信号写入每个worker的管道
    -- reload 通过管道通知子进程设置退出位 (进程池)
    -- stop 通过管道通知子进程设置退出位 (进程池)
    -- int posix_kill子进程, 父进程退出
    -- terminate posix_kill子进程, 父进程退出
  • while(true)父进程
    -- pcntl_waitpid(WNOHANG) 非阻塞检测子进程退出
    -- pcntl_waitpid 有返回->是否收到信号->reload/stop->对应pid子进程从进程池移除再执行fork(如果是reload信号)

PHP调试技术手册

1内置api输出调试

1.1基本调试api

  • echo
  • printf
  • print_r
    /*
    %% - 返回百分比符号
    %b - 二进制数
    %c - 依照 ASCII 值的字符
    %d - 带符号十进制数
    %e - 可续计数法(比如 1.5e+3)
    %u - 无符号十进制数
    %f - 浮点数(local settings aware)
    %F - 浮点数(not local settings aware)
    %o - 八进制数
    %s - 字符串
    %x - 十六进制数(小写字母)
    %X - 十六进制数(大写字母)
    */
    
    $str = "Hello";
    $number = 123;
    $txt = sprintf("%s world. Day number %u", $str, $number);
    echo $txt;
    
  • var_dump
  • var_export
    • 所有的数据是可以作为组织好的变量输出的,都是能够作为直接赋值使用
    • 对资源型变量输出NULL
  • debug_zval_dump
    • 跟 var_dump 类似,唯一增加的一个值是 refcount,就是记录一个变量被引
      用了多少次
    • PHP COW机制(copy on write)
    foreach($arr as &$v){
        $count++;
        $v='bbbbbb';// 无论是否注释这行, 内存变化不会太大
    }
    
    foreach($arr as $v){
        $count++;
        $v='bbbbbb';// 不注释这行,内存使用上升,因为COW机制
    }    
    
    /*
    在复制一个对象的时候并不是真正的把原先的对象复制到内存的另外一个位置上,
    而是在新对象的内存映射表中设置一个指针,指向源对象的位置,
    并把那块内存的Copy-On-Write位设置为1.这样,在对新的对象执行读操作的时候,
    内存数据不发生任何变动,直接执行读操作;而在对新的对象执行写操作时,
    将真正的对象复制到新的内存地址中,并修改新对象的内存映射表指向这个新的位置,
    并在新的内存位置上执行写操作    
    */
    

1.2错误控制与日志记录调试

  • error_reporting

  • display_errors

  • log_errors 是否记录错误日志

  • error_log 指定错误日志位置

  • trigger_error(主要是能够触
    发的是 E_USER_ERROR, E_USER_WARNING, E_USER_NOTICE 三种级别的错误)

function e($num) {
    if (!is_int($num)) {
        trigger_error('num nust a int',E_USER_WARNING);
        return false;
    }
    echo 'ok';
}
e('test');
  • set_error_handler
    • 以下级别的错误不能由用户定义的函数来处理: E_ERROR 、 E_PARSE 、 E_CORE_ERROR 、 E_CORE_WARNING 、 E_COMPILE_ERROR 、 E_COMPILE_WARNING
set_error_handler('custom_error_handler');
function custom_error_handler($err_no,$err_str,$err_file,$err_line) {
    // 定义日志路径(区分ERROR,WARNING,NOTICE)
    $log_path = 'php_log_%s'.date('Ymd').'.log';
    // 错误信息模版(区分ERROR,WARNING,NOTICE)
    $log_template = date('Y-m-d H:i:s') . "Erro Type:%s,on $err_file->$err_line:$err_str";
    switch($err_no) {
        case E_USER_ERROR:
        $log_path = sprintf($log_path,'error');
        $log_str = sprintf($log_template,'E_USER_ERROR');
        break;
        case E_NOTICE:
        case E_USER_NOTICE:
        $log_path = sprintf($log_path,'notice');
        $log_str = sprintf($log_template,'E_NOTICE|E_USER_NOTICE');        
        break;
        case E_WARNING:
        case E_USER_WARINING:
        $log_path = sprintf($log_path,'warning');
        $log_str = sprintf($log_template,'E_WARNING|E_USER_WARNING');  
        break;
    }
    // file_put_contents 文件不存在则创建
    file_put_contents($log_path,$log_str,FILE_APPEND|LOCK_EX);
}
  • set_exception_handler : 处理出现未捕获的异
    常之后需要调用的处理方法

  • 日志记录

    • 日志记录除了PHP解析级别的错误,更多的是我们程序在执行过程中的一些错误,比如 文件资源打开错误(文件不存在,没有权限,文件格式不正确),远程服务资源访问失败(网络错误,协议不正确,用户/密码错误)等等, 任何你认为不会出错的地方都输出LOG
    • 简单日志记录类
    class MyLog 
    {
        const LOG_ERROR = 1;
        const LOG_WARNING = 2;
        const LOG_NOTICE = 3;
        private $str = '';
        private $log_path = 'php_log'.date('Ymd').'.log';
        
        public function log($msg,$level) 
        {
            switch ($level) {
                case self::LOG_ERROR:
                    $this->str .= date('Y-m-d H:i:s') . ' ERROR:' . $msg . "\n";
                break;
                case self::LOG_WARNING:
                    $this->str .= date('Y-m-d H:i:s') . ' WARNING:' . $msg . "\n";
                break;
                case self::LOG_NOTICE:
                    $this->str .= date('Y-m-d H:i:s') . ' NOTICE:' . $msg . "\n";
                break;
                default:
                    return;
            }
        }
        public function __destruct() 
        {
            if ($this->str) {
                file_put_contents($this->str,$this->log_path,FILE_APPEND|LOCK_EX);            
            }
        }
    }
    
    • 使用error_log作日志记录
    // 如果无法连接到数据库,发送通知到服务器日志
    if (! Ora_Logon ( $username ,  $password )) {
         error_log ( "Oracle database not available!" ,  0 );
    }
    
    // 如果用尽了 FOO,通过邮件通知管理员
    if (!( $foo  =  allocate_new_foo ())) {
         error_log ( "Big trouble, we're all out of FOOs!" ,  1 , "operator@example.com" );
    }
    
    // 调用 error_log() 的另一种方式:
    error_log ( "You messed up!" ,  3 ,  "/var/tmp/my-errors.log" );
    

2浏览器调试

2.1页面输出调试

...各种ide+调试

4PHP性能调试技术

4.1基本时间占用监测

4.2XDEBUG性能监测

  • 配置

    • xdebug.trace_output_dir trace 数据输出目录
    • xdebug.profiler_output_dir 输出监控结果文件目录
    • xdebug.profiler_output_name 如果设置本选项,那么每次结果都只会输出到一个文件
    • xdebug.auto_trace 是否打开 trace
    • xdebug.auto_profile 是否打开性能监测
  • 打开xdebug的性能监测后, 在config.php中指定目录

4.4 使用 Xhprof 进行性能分析

  • 安装xhprof

  • php-config 是一个简单的命令行脚本用于获取所安装的 PHP 配置的信息。

    • 在编译扩展时,如果安装有多个 PHP 版本,可以在配置时用 --with-php-config 选项来指定使用哪一个版本编译,该选项指定了相对应的 php-config 脚本的路径
    wget http://pecl.php.net/get/xhprof-0.9.3.tgz
    
    tar zxf xhprof-0.9.3.tgz
    
    cd xhprof-0.9.3/extension/
    
    phpize
    
    ./configure --with-php-config=/usr/local/php/bin/php-config
    
    make && make install
    
  • 配置

[xhprof]
extension=xhprof.so
;
; directory used by default implementation of the iXHProfRuns
; interface (namely, the XHProfRuns_Default class) for storing
; XHProf runs.
;
xhprof.output_dir=<directory_for_storing_xhprof_runs>

// 确认web进程对目录具有写权限
  • 分析代码
// start profiling
xhprof_enable();

// run program
function test1() {
    sleep(3);

    return;
}

function test2() {
    test1();
}

function test3() {
    test2();
}

echo "<h3>Xdebug profiler test</h3>";
test3();

// stop profiler
$data = xhprof_disable();

include_once "path/to/xhprof_lib/utils/xhprof_lib.php";
include_once "path/to/xhprof_lib/utils/xhprof_runs.php";
$xhprof_runs = new XHProfRuns_Default();
$run_id = $xhprof_runs->save_run($data,'test');
var_dump($run_id);

// 访问 http://119.29.149.18/www/xhprof_html/?run=570d00e3007a0&source=test

5 使用php扩展taint(防止xss,sql inject)

+ https://github.com/laruence/taint

6. SocketLog -- 微信调试、API调试和AJAX的调试的工具

+ https://github.com/luofei614/SocketLog

Swoole入门到实战打造高性能赛事直播平台

PHP7.2源码编译

安装步骤
1. 解压
2. ./configure --prefix=/usr/local/php
3. make && make install

编辑~/.bash_profile 增加
alias php7=/usr/local/php/bin/php
运行 source ~/.bash_profile

查看php.ini默认路径
php --ini # ./configure –with-config-file-path=/usr/local/php/etc (编译时指定)

客户端和服务端

tcp服务端

<?php

// 构建server对象
$serv = new swoole_server('127.0.0.1', 9501);

// 设置运行参数
$serv->set(array(
    'worker_num' => 4,
//    'daemonize' => true,
    'backlog' => 128,
));

// 注册事件回调

/**
 * $fd: 客户端唯一标识
 * $reactor_id: 处理线程id
 */
$serv->on('connect', function ($serv, $fd, $reactor_id) {
        echo "client connect: {$fd}";
});

$serv->on('receive', function ($serv, $fd, $reactor_id, $data) {
        $serv->send($fd, 'Swoole: '.$data);
});

$serv->on('close', function ($serv, $fd) {
        echo "Client: Close.\n";
});

// 启动
$serv->start();

tcp客户端

<?php

// 构建客户端对象
$client = new swoole_client(SWOOLE_SOCK_TCP);

if (!$client->connect('127.0.0.1', 9501)) {
        exit('链接失败');
}

fwrite(STDOUT, "请输入\n");

while ($msg = trim(fgets(STDIN))) {
        $client->send($msg);
        $result = $client->recv();
        echo $result."\n";
}

http服务端

<?php

// 构建http_server对象

$http = new swoole_http_server('127.0.0.1', 9501);

// 设置静态资源目录
$http->set(array(
        'enable_static_handler' => true,
        'document_root' => '/path/to/document_root'
));


$http->on('request', function ($request, $response) {
        $response->end("收到浏览器信息:".json_encode($request->get));
});

$http->start();

websocket服务端

<?php
class Ws
{
    public function __construct() {
        $this->server = new swoole_websocket_server("0.0.0.0", 9501);
        $server->on('open', array($this, 'open'));
        $server->on('message', array($this, 'message'));
        $server->on('close', array($this, 'close'));
        // 注册任务处理事件
        $server->on('task', array($this, 'task'));
        $server->on('finih', array($this, 'finih'));
        $server->set(array(
            'task_worker_num' => 4
        ));
        $this->server->start();
    }

    public function open(swoole_websocket_server $server, $request) {
        echo "server: handshake success with fd{$request->fd}\n";
    }

    public function message(swoole_websocket_server $server, $frame) {
        // 投递任务处理
        $this->server->task(array('name' => 'zhorz', 'td' => $frame->td));
        echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n";
        $server->push($frame->fd, "this is server");
    }

    public function close($ser, $fd) {
        echo "client {$fd} closed\n";
    }

    public function task(swoole_server $serv, int $task_id, int $src_worker_id, mixed $data) {
        // 处理耗时任务
        // $task_id和$src_worker_id组合起来全局唯一
    }

    public function finish(swoole_server $serv, int $task_id, string $data) {
        // 当worker进程投递的任务在task_worker中完成时,task进程会通过swoole_server->finish()方法将任务处理的结果发送给worker进程
    }
}

websocket客户端

// 协议标识
ws://example.com:80/some/path

// js例子
var ws = new WebSocket("wss://echo.websocket.org");

ws.onopen = function(evt) { 
  console.log("Connection open ..."); 
  ws.send("Hello WebSockets!");
};

ws.onmessage = function(evt) {
  console.log( "Received Message: " + evt.data);
  ws.close();
};

ws.onclose = function(evt) {
  console.log("Connection closed.");
};  

异步非阻塞IO场景

swoole毫秒定时器

int swoole_timer_tick(int $ms, callable $callback, mixed $user_param);
int swoole_timer_after(int $after_time_ms, mixed $callback_function, mixed $user_param);
bool swoole_timer_clear(int $timer_id)

异步文件IO读取文件

// 异步读取文件内容(不能用于大文件的读取, 最大可读取4M的文件)
swoole_async_readfile(__DIR__."/server.php", function($filename, $content) {
     echo "$filename: $content"; 
});

// 异步写文件(最大可写入4M)
swoole_async_writefile('test.log', $file_content, function($filename) {
    echo "wirte ok.\n";
}, $flags = 0);

// 异步读文件(支持大文件读取, 每次只读$size个字节,不会占用太多内存, 在读完后会自动回调$callback函数)
bool swoole_async_read(string $filename, mixed $callback, int $size = 8192, int $offset = 0);
// 回调函数接受2个参数:bool callback(string $filename, string $content);
// ($filename,文件名称; $content,读取到的分段内容,如果内容为空,表明文件已读完)
bool callback(string $filename, string $content);

// 异步写文件, 通过传入的offset参数来确定写入的位置
bool swoole_async_write(string $filename, string $content, int $offset = -1, mixed $callback = NULL);

异步mysql

$db = new swoole_mysql;
$server = array(
    'host' => '192.168.56.102',
    'port' => 3306,
    'user' => 'test',
    'password' => 'test',
    'database' => 'test',
    'charset' => 'utf8', //指定字符集
    'timeout' => 2,  // 可选:连接超时时间(非查询超时时间),默认为SW_MYSQL_CONNECT_TIMEOUT(1.0)
);

$db->connect($server, function ($db, $r) {
    if ($r === false) {
        var_dump($db->connect_errno, $db->connect_error);
        die;
    }
    $sql = 'show tables';
    $db->query($sql, function(swoole_mysql $db, $r) {
        if ($r === false)
        {
            var_dump($db->error, $db->errno);
        }
        elseif ($r === true )
        {
            var_dump($db->affected_rows, $db->insert_id);
        }
        var_dump($r);
        $db->close();
    });
});

异步redis

// 安装redis
// 安装hiredis库
// 重新编译swoole支持redis --enable-anysc-redis
// ./configure -> make clean -> make && make install -> 查看是否成功: php --ri swoole (查看扩展的版本信息)

php命令行查看信息:

  --rf <name>      Show information about function <name>.
  --rc <name>      Show information about class <name>.
  --re <name>      Show information about extension <name>.
  --rz <name>      Show information about Zend extension <name>.
  --ri <name>      Show configuration for extension <name>.

swoole process

linux下检测进程是否存在

使用"kill -0 pid",kill -0不会向进程发送任何信号,但是会进行错误检查。如果进程存在,命令返回0,如果不存在返回1。
对于普通用户来说只能用于检查自己的进程,因为向其它用户的进程发送信号会因为没有权限而出错,返回值也是1

wait如何处理多个子进程

https://blog.csdn.net/sl1248/article/details/50936823
调用一次wait只能回收一个子进程
1. 父进程while循环
2. 父进程注册SIGCHLD信号处理

swoole_table内存共享(解决多进程/多线程数据共享和同步加锁问)

$table = new swoole_table(1024); // $size行数
$table->column('data',Table::TYPE_STRING, 1); // 增加一列
$table->create();// 申请内存创建表
$table->set('zhorz', array('data' => 'A')); // 设置行数据
$table->get('zhorz', 'data'); // 获取某行某字段值
$table->del('zhorz');// 删除行数据

swoole协程Swoole\Coroutine*

// 创建协程
go(function () {
    co::sleep(0.5);
    echo "hello";
});
go("test");
go([$object, "method"]);
// 通道操作
$c = new chan(1);
$c->push($data);
$c->pop();
// 协程客户端
$redis = new Co\Redis;
$mysql = new Co\MySQL;
$http = new Co\Http\Client;
$tcp = new Co\Client;
$http2 = new Co\Http2\Client;
// 协程的让出和恢复
use Swoole\Coroutine as co;
$id = go(function(){
    $id = co::getUid();
    echo "start coro $id\n";
    co::suspend();
    echo "resume coro $id @1\n";
    co::suspend();
    echo "resume coro $id @2\n";
});
echo "start to resume $id @1\n";
co::resume($id);
echo "start to resume $id @2\n";
co::resume($id);
echo "main\n";

协程之channel

// 协程间同步
use Swoole\Coroutine as co;
$chan = new co\Channel(1);
co::create(function () use ($chan) {
    for($i = 0; $i < 100000; $i++) {
        co::sleep(1.0);
        $chan->push(['rand' => rand(1000, 9999), 'index' => $i]);
        echo "$i\n";
    }
});
co::create(function () use ($chan) {
    while(1) {
        $data = $chan->pop();
        var_dump($data);
    }
});
swoole_event::wait();

协程之select

$c1 = new chan();
$c2 = new chan();
function fibonacci($c1, $c2)
{
    go(function () use ($c1, $c2) {
        $a = 0;
        $b = 1;
        while(1) {
            $read_list = [$c2];
            $write_list = [$c1];
            $result = chan::select($read_list, $write_list, 2);
            // 当$read或$write数组中有部分channel对象处于可读或可写状态,select会立即返回
            if ($write_list) {
                $t = $a + $b;
                $a = $b;
                $b = $t;
                $c1->push($a);
            }
            if ($read_list) {
                $ret = $c2->pop();
                if ($ret === 1) {
                    return 1;
                }
            }
        }
    });
}
$num = 10;
go(function () use ($c1, $c2, $num) {
    for ($i = 0; $i < $num; $i ++) {
        $ret = $c1->pop();
        echo "fibonacci @$i $ret\n";
    }
    $c2->push(1);
});    
fibonacci($c1, $c2);

协程并发请求的列子

$serv = new \swoole_http_server("127.0.0.1", 9503, SWOOLE_BASE);

$serv->on('request', function ($req, $resp) {
    $chan = new chan(2);
    go(function () use ($chan) {
        $cli = new Swoole\Coroutine\Http\Client('www.qq.com', 80);
            $cli->set(['timeout' => 10]);
            $cli->setHeaders([
            'Host' => "www.qq.com",
            "User-Agent" => 'Chrome/49.0.2587.3',
            'Accept' => 'text/html,application/xhtml+xml,application/xml',
            'Accept-Encoding' => 'gzip',
        ]);
        $ret = $cli->get('/');
        $chan->push(['www.qq.com' => $cli->body]);
    });

    go(function () use ($chan) {
        $cli = new Swoole\Coroutine\Http\Client('www.163.com', 80);
        $cli->set(['timeout' => 10]);
        $cli->setHeaders([
            'Host' => "www.163.com",
            "User-Agent" => 'Chrome/49.0.2587.3',
            'Accept' => 'text/html,application/xhtml+xml,application/xml',
            'Accept-Encoding' => 'gzip',
        ]);
        $ret = $cli->get('/');
        $chan->push(['www.163.com' => $cli->body]);
    });

    $result = [];
    for ($i = 0; $i < 2; $i++)
    {
        $result += $chan->pop();
    }
    $resp->end(json_encode($result));
});
$serv->start();

系统服务监控和优化

# 调用linux命令监听
netstat -anp 2>/dev/null | grep 8811 | grep LISTEN | wc -l

# 搜索文件关键词
find . -type f -name '*.php' | xargs grep 'echo 111'

# 平滑重启服务
swoole_set_process_name -> 设置进程名称
pidof 进程名称 -> 获取进程id
kill -USR1 `pidof 进程名称` -> 平滑重启??

# nginx负载均衡说明
http://tengine.taobao.org/nginx_docs/cn/docs/http/ngx_http_upstream_module.html

PHP中socket的使用

SOCKET编程

简单的TCP/IP服务器

class SocketServer 
{
    private $ip = '';
    private $port = '';
    private $err_msg = '';
    public function __construct($ip,$port)
    {
        $this->ip = $ip;
        $this->port = $port;
        $this->create_socket();
    }

    protected function create_socket()
    {
        // 三个返回socket资源的方法 : socket_create socket_accept socket_create_listen
        
        $sock = socket_create(AF_INET,SOCK_STREAM,getprotobyname('tcp'));
        if ($sock === false) {
            $this->fail();
        }

        // 绑定ip,port
        $retval = socket_bind($sock,$this->ip,$this->port);
        if ($retval === false) {
            $this->fail();
        }

        // 监听端口
        $retval = socket_listen($sock);
        if ($retval === false) {
            $this->fail();
        }     

        while (true) {
            // socket_accept会阻塞直至有连接或失败才返回
            if (($msg_sock = socket_accept($sock)) === false) {
                break;
            }
            if (!$msg_sock) {
                echo "有客户端链接\n";
            }
            $msg = "\nwelcome\n";
            socket_write($msg_sock,$msg,strlen($msg));

            // 循环判断socket是否有新的数据,没数据返回""
            while (true) {
                if (($msg = socket_read($msg_sock,1024,PHP_NORMAL_READ)) === false) {
                    break 2; // 跳出两层循环
                }
                // trim 去取 \n \r \0 空格..
                if (!$msg = trim($msg)) {
                    continue; // 空数据
                }
                if ($msg == 'quit') {
                    break;
                } elseif ($msg == 'shutdown') {
                    socket_close($msg_sock); 
                    break 2; // 跳出两层循环                   
                } else {
                    $retval = "\nYou just say " . $msg . "\n";
                    socket_write($msg_sock,$retval,strlen($retval));   
                    echo 'server receive msg : ' . $msg . "\n";           
                }
            }
            socket_close($msg_sock);
        }
        socket_close($sock);
    }

    protected function fail()
    {
        $err_no = socket_last_error();
        $this->err_msg = socket_strerror($err_no);
        echo "\n" . $this->err_msg ."\n";
        exit;
    }
}

set_time_limit(0);
$ip = "0.0.0.0";
$port = 8000;
$socket = new SocketServer($ip,$port);

简单的TCP/IP客户端

class SocketClient 
{
    private $ip = '';
    private $port = '';
    public function __construct($ip,$port,$uri)
    {
        $this->ip = $ip;
        $this->port = $port;
        $this->uri = $uri;
        $this->connect();
    }   

    protected function connect()
    {
        $sock = socket_create(AF_INET,SOCK_STREAM,getprotobyname('tcp'));
        $retval = socket_connect($sock,$this->ip,$this->port);
        if ($retval === false) {
            $this->fail();
        }
        // 写
        $in  =  "HEAD {$this->uri} HTTP/1.1\r\n";
        $in  .=  "Host: {$this->ip}\r\n";
        $in  .=  "Connection: Close\r\n\r\n"; // 注意两个\r\n表示结束
        socket_write($sock,$in,strlen($in));
        // 读
        $out = '';
        while ($retval = socket_read($sock,1024)) {
            $out .= $retval;
        }
        echo $out;
        socket_close($sock);
    } 

    protected function fail()
    {
        $err_no = socket_last_error();
        $this->err_msg = socket_strerror($err_no);
        echo "\n" . $this->err_msg ."\n";
        exit;
    }
}

$ip = "119.29.149.18";
$port = 80;
$uri = "/contract/";
$socket = new SocketClient($ip,$port,$uri);