首页
关于
Search
1
给你10个市场数据调研报告的免费下载网站!以后竞品数据就从这里找!
136 阅读
2
php接口优化 使用curl_multi_init批量请求
130 阅读
3
2024年备考系统架构设计师
102 阅读
4
《从菜鸟到大师之路 ElasticSearch 篇》
101 阅读
5
PHP 文件I/O
89 阅读
php
thinkphp
laravel
工具
开源
mysql
数据结构
总结
思维逻辑
令人感动的创富故事
读书笔记
前端
vue
js
css
书籍
开源之旅
架构
消息队列
docker
教程
代码片段
redis
服务器
nginx
linux
科普
java
c
ElasticSearch
测试
php进阶
php基础
登录
Search
标签搜索
php函数
php语法
性能优化
安全
错误和异常处理
问题
vue
Composer
Session
缓存
框架
Swoole
api
并发
异步
正则表达式
php-fpm
mysql 索引
开发规范
协程
dafenqi
累计撰写
785
篇文章
累计收到
5
条评论
首页
栏目
php
thinkphp
laravel
工具
开源
mysql
数据结构
总结
思维逻辑
令人感动的创富故事
读书笔记
前端
vue
js
css
书籍
开源之旅
架构
消息队列
docker
教程
代码片段
副业
redis
服务器
nginx
linux
科普
java
c
ElasticSearch
测试
php进阶
php基础
页面
关于
搜索到
785
篇与
的结果
2023-08-07
php deamon 后台服务(守护进程)
php http server守护进程(daemon)
2023年08月07日
10 阅读
0 评论
0 点赞
2023-08-07
PHP Socket 深度探索
PHP Socket 深度探索Socket(套接字)一直是网络层的底层核心内容,也是 TCP/IP 以及 UDP 底层协议的实现通道。随着互联网信息时代的爆炸式发展,当代服务器的性能问题面临越来越大的挑战,著名的 C10K 问题(http://www.kegel.com/c10k.html)也随之出现。幸亏通过大牛们的不懈努力,区别于传统的 select/poll 的 epoll/kqueue 方式出现了,目前 linux2.6 以上的内核都普遍支持,这是 Socket 领域一项巨大的进步,不仅解决了 C10K 问题,也渐渐成为了当代互联网的底层核心技术。libevent 库就是其中一个比较出彩的项目(现在非常多的开源项目都有用到,包括 Memcached),感兴趣的朋友可以研究一下。由于网络上系统介绍这个部分的文章并不多,而涉及 PHP 的就更少了,所以石头君在这里希望通过《Socket深度探究4PHP》这个系列给对这个领域感兴趣的读者们一定的帮助,也希望大家能和我一起对这个问题进行更深入的探讨。首先,解释一下目前 Socket 领域比较易于混淆的概念有:阻塞/非阻塞、同步/异步、多路复用等。1、阻塞/非阻塞:这两个概念是针对 IO 过程中进程的状态来说的,阻塞 IO 是指调用结果返回之前,当前线程会被挂起;相反,非阻塞指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。2、同步/异步:这两个概念是针对调用如果返回结果来说的,所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回;相反,当一个异步过程调用发出后,调用者不能立刻得到结果,实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。3、多路复用(IO/Multiplexing):为了提高数据信息在网络通信线路中传输的效率,在一条物理通信线路上建立多条逻辑通信信道,同时传输若干路信号的技术就叫做多路复用技术。对于 Socket 来说,应该说能同时处理多个连接的模型都应该被称为多路复用,目前比较常用的有 select/poll/epoll/kqueue 这些 IO 模型(目前也有像 Apache 这种每个连接用单独的进程/线程来处理的 IO 模型,但是效率相对比较差,也很容易出问题,所以暂时不做介绍了)。在这些多路复用的模式中,异步阻塞/非阻塞模式的扩展性和性能最好。感觉概念很抽象对吧,“一切答案在于现场”,下面让我们从三种经典的 PHP Socket IO 模型实例来对以上的概念再做一次分析:1、使用 accept 阻塞的古老模型:属于同步阻塞 IO 模型,代码如下:socket_server.php<?php/**SocketServer ClassBy James.Huang <shagoo#gmail.com>**/set_time_limit(0);class SocketServer { private static $socket; function SocketServer($port) {global $errno, $errstr; if ($port < 1024) { die("Port must be a number which bigger than 1024/n"); } $socket = stream_socket_server("tcp://0.0.0.0:{$port}", $errno, $errstr); if (!$socket) die("$errstr ($errno)"); // stream_set_timeout($socket, -1); // 保证服务端 socket 不会超时,似乎没用:) while ($conn = stream_socket_accept($socket, -1)) { // 这样设置不超时才油用 static $id = 0; static $ct = 0; $ct_last = $ct; $ct_data = ''; $buffer = ''; $id++; // increase on each accept echo "Client $id come./n"; while (!preg_match('//r?/n/', $buffer)) { // 没有读到结束符,继续读// if (feof($conn)) break; // 防止 popen 和 fread 的 bug 导致的死循环 $buffer = fread($conn, 1024); echo 'R'; // 打印读的次数 $ct += strlen($buffer); $ct_data .= preg_replace('//r?/n/', '', $buffer); } $ct_size = ($ct - $ct_last) * 8; echo "[$id] " . __METHOD__ . " > " . $ct_data . "/n"; fwrite($conn, "Received $ct_size byte data./r/n"); fclose($conn); } fclose($socket);}}new SocketServer(2000);socket_client.php<?php/**Socket Test ClientBy James.Huang <shagoo#gmail.com>**/function debug ($msg){// echo $msg; error_log($msg, 3, '/tmp/socket.log');}if ($argv[1]) {$socket_client = stream_socket_client('tcp://0.0.0.0:2000', $errno, $errstr, 30);// stream_set_blocking($socket_client, 0);// stream_set_timeout($socket_client, 0, 100000);if (!$socket_client) {die("$errstr ($errno)");} else {$msg = trim($argv[1]); for ($i = 0; $i < 10; $i++) { $res = fwrite($socket_client, "$msg($i)"); usleep(100000); echo 'W'; // 打印写的次数// debug(fread($socket_client, 1024)); // 将产生死锁,因为 fread 在阻塞模式下未读到数据时将等待} fwrite($socket_client, "/r/n"); // 传输结束符 debug(fread($socket_client, 1024)); fclose($socket_client);}}else {// $phArr = array();// for ($i = 0; $i < 10; $i++) {// $phArr[$i] = popen("php ".__FILE__." '{$i}:test'", 'r');// }// foreach ($phArr as $ph) {// pclose($ph);// }for ($i = 0; $i < 10; $i++) {system("php ".__FILE__." '{$i}:test'");}}首先,解释一下以上的代码逻辑:客户端 socket_client.php 循环发送数据,最后发送结束符;服务端 socket_server.php 使用 accept 阻塞方式接收 socket 连接,然后循环接收数据,直到收到结束符,返回结果数据(接收到的字节数)。虽然逻辑很简单,但是其中有几种情况很值得分析一下:A> 默认情况下,运行 php socket_client.php test,客户端打出 10 个 W,服务端打出若干个 R 后面是接收到的数据,/tmp/socket.log 记录下服务端返回的接收结果数据。这种情况很容易理解,不再赘述。然后,使用 telnet 命令同时打开多个客户端,你会发现服务器一个时间只处理一个客户端,其他需要在后面“排队”;这就是阻塞 IO 的特点,这种模式的弱点很明显,效率极低。B> 只打开 socket_client.php 第 26 行的注释代码,再次运行 php socket_client.php test 客户端打出一个 W,服务端也打出一个 R,之后两个程序都卡住了。这是为什么呢,分析逻辑后你会发现,这是由于客户端在未发送结束符之前就向服务端要返回数据;而服务端由于未收到结束符,也在向客户端要结束符,造成死锁。而之所以只打出一个 W 和 R,是因为 fread 默认是阻塞的。要解决这个死锁,必须打开 socket_client.php 第 16 行的注释代码,给 socket 设置一个 0.1 秒的超时,再次运行你会发现隔 0.1 秒出现一个 W 和 R 之后正常结束,服务端返回的接收结果数据也正常记录了。可见 fread 缺省是阻塞的,我们在编程的时候要特别注意,如果没有设置超时,就很容易会出现死锁。C> 只打开 15 行注释,运行 php socket_client.php test,结果基本和情况 A 相同,唯一不同的是 /tmp/socket.log 没有记录下返回数据。这里可以看出客户端运行在阻塞和非阻塞模式的区别,当然在客户端不在乎接受结果的情况下,可以使用非阻塞模式来获得最大效率。D> 运行 php socket_client.php 是连续运行 10 次上面的逻辑,这个没什么问题;但是很奇怪的是如果你使用 35 - 41 行的代码,用 popen 同时开启 10 个进程来运行,就会造成服务器端的死循环,十分怪异!后来经调查发现只要是用 popen 打开的进程创建的连接会导致 fread 或者 socket_read 出错直接返回空字串,从而导致死循环,查阅 PHP 源代码后发现 PHP 的 popen 和 fread 函数已经完全不是 C 原生的了,里面都插入了大量的 php_stream_* 实现逻辑,初步估计是其中的某个 bug 导致的 Socket 连接中断所导致的,解决方法就是打开 socket_server.php 中 31 行的代码,如果连接中断则跳出循环,但是这样一来就会有很多数据丢失了,这个问题需要特别注意!2、使用 select/poll 的同步模型:属于同步非阻塞 IO 模型,代码如下:select_server.php<?php/**SelectSocketServer ClassBy James.Huang <shagoo#gmail.com>**/set_time_limit(0);class SelectSocketServer { private static $socket; private static $timeout = 60; private static $maxconns = 1024; private static $connections = array(); function SelectSocketServer($port) {global $errno, $errstr; if ($port < 1024) { die("Port must be a number which bigger than 1024/n"); } $socket = socket_create_listen($port); if (!$socket) die("Listen $port failed"); socket_set_nonblock($socket); // 非阻塞 while (true) { $readfds = array_merge(self::$connections, array($socket)); $writefds = array(); // 选择一个连接,获取读、写连接通道 if (socket_select($readfds, $writefds, $e = null, $t = self::$timeout)) { // 如果是当前服务端的监听连接 if (in_array($socket, $readfds)) { // 接受客户端连接 $newconn = socket_accept($socket); $i = (int) $newconn; $reject = ''; if (count(self::$connections) >= self::$maxconns) { $reject = "Server full, Try again later./n"; } // 将当前客户端连接放入 socket_select 选择 self::$connections[$i] = $newconn; // 输入的连接资源缓存容器 $writefds[$i] = $newconn; // 连接不正常 if ($reject) { socket_write($writefds[$i], $reject); unset($writefds[$i]); self::close($i); } else { echo "Client $i come./n"; } // remove the listening socket from the clients-with-data array $key = array_search($socket, $readfds); unset($readfds[$key]); } // 轮循读通道 foreach ($readfds as $rfd) { // 客户端连接 $i = (int) $rfd; // 从通道读取 $line = @socket_read($rfd, 2048, PHP_NORMAL_READ); if ($line === false) { // 读取不到内容,结束连接 echo "Connection closed on socket $i./n"; self::close($i); continue; } $tmp = substr($line, -1); if ($tmp != "/r" && $tmp != "/n") { // 等待更多数据 continue; } // 处理逻辑 $line = trim($line); if ($line == "quit") { echo "Client $i quit./n"; self::close($i); break; } if ($line) { echo "Client $i >>" . $line . "/n"; } } // 轮循写通道 foreach ($writefds as $wfd) { $i = (int) $wfd; $w = socket_write($wfd, "Welcome Client $i!/n"); } } }}function close ($i) {socket_shutdown(self::$connections[$i]); socket_close(self::$connections[$i]); unset(self::$connections[$i]);}}new SelectSocketServer(2000);select_client.php<?php/**SelectSocket Test ClientBy James.Huang <shagoo#gmail.com>**/function debug ($msg){// echo $msg; error_log($msg, 3, '/tmp/socket.log');}if ($argv[1]) {$socket_client = stream_socket_client('tcp://0.0.0.0:2000', $errno, $errstr, 30);// stream_set_timeout($socket_client, 0, 100000);if (!$socket_client) {die("$errstr ($errno)");} else {$msg = trim($argv[1]); for ($i = 0; $i < 10; $i++) { $res = fwrite($socket_client, "$msg($i)/n"); usleep(100000);// debug(fread($socket_client, 1024)); // 将产生死锁,因为 fread 在阻塞模式下未读到数据时将等待} fwrite($socket_client, "quit/n"); // add end token debug(fread($socket_client, 1024)); fclose($socket_client);}}else {$phArr = array(); for ($i = 0; $i < 10; $i++) {$phArr[$i] = popen("php ".__FILE__." '{$i}:test'", 'r');} foreach ($phArr as $ph) {pclose($ph);}// for ($i = 0; $i < 10; $i++) {// system("php ".__FILE__." '{$i}:test'");// }}以上代码的逻辑也很简单,select_server.php 实现了一个类似聊天室的功能,你可以使用 telnet 工具登录上去,和其他用户文字聊天,也可以键入“quit”命令离开;而 select_client.php 则模拟了一个登录用户连续发 10 条信息,然后退出。这里也分析两个问题:A> 这里如果我们执行 php select_client.php 程序将会同时打开 10 个连接,同时进行模拟登录用户操作;观察服务端打印的数据你会发现服务端确实是在同时处理这些连接,这就是多路复用实现的非阻塞 IO 模型,当然这个模型并没有真正的实现异步,因为最终服务端程序还是要去通道里面读取数据,得到结果后同步返回给客户端。如果这次你也使用 telnet 命令同时打开多个客户端,你会发现服务端可以同时处理这些连接,这就是非阻塞 IO,当然比古老的阻塞 IO 效率要高多了,但是这种模式还是有局限的,继续看下去你就会发现了~B> 我在 select_server.php 中设置了几个参数,大家可以调整试试:$timeout :表示的是 select 的超时时间,这个一般来说不要太短,否则会导致 CPU 负载过高。$maxconns :表示的是最大连接数,客户端超过这个数的话,服务器会拒绝接收。这里要提到的一点是,由于 select 是通过句柄来读写的,所以会受到系统默认参数 __FD_SETSIZE 的限制,一般默认值为 1024,修改的话需要重新编译内核;另外通过测试发现 select 模式的性能会随着连接数的增大而线性便差(详情见《Socket深度探究4PHP(二)》),这也就是 select 模式最大的问题所在,所以如果是超高并发服务器建议使用下一种模式。3、使用 epoll/kqueue 的异步模型:属于异步阻塞/非阻塞 IO 模型,代码如下:epoll_server.php<?php/**EpollSocketServer Class (use libevent)By James.Huang <shagoo#gmail.com>Defined constants:EV_TIMEOUT (integer)EV_READ (integer)EV_WRITE (integer)EV_SIGNAL (integer)EV_PERSIST (integer)EVLOOP_NONBLOCK (integer)EVLOOP_ONCE (integer)**/set_time_limit(0);class EpollSocketServer{ private static $socket; private static $connections; private static $buffers;function EpollSocketServer ($port) {global $errno, $errstr; if (!extension_loaded('libevent')) { die("Please install libevent extension firstly/n"); } if ($port < 1024) { die("Port must be a number which bigger than 1024/n"); } $socket_server = stream_socket_server("tcp://0.0.0.0:{$port}", $errno, $errstr); if (!$socket_server) die("$errstr ($errno)"); stream_set_blocking($socket_server, 0); // 非阻塞 $base = event_base_new(); $event = event_new(); event_set($event, $socket_server, EV_READ | EV_PERSIST, array(__CLASS__, 'ev_accept'), $base); event_base_set($event, $base); event_add($event); event_base_loop($base); self::$connections = array(); self::$buffers = array();}function ev_accept($socket, $flag, $base) {static $id = 0; $connection = stream_socket_accept($socket); stream_set_blocking($connection, 0); $id++; // increase on each accept $buffer = event_buffer_new($connection, array(__CLASS__, 'ev_read'), array(__CLASS__, 'ev_write'), array(__CLASS__, 'ev_error'), $id); event_buffer_base_set($buffer, $base); event_buffer_timeout_set($buffer, 30, 30); event_buffer_watermark_set($buffer, EV_READ, 0, 0xffffff); event_buffer_priority_set($buffer, 10); event_buffer_enable($buffer, EV_READ | EV_PERSIST); // we need to save both buffer and connection outside self::$connections[$id] = $connection; self::$buffers[$id] = $buffer;}function ev_error($buffer, $error, $id) {event_buffer_disable(self::$buffers[$id], EV_READ | EV_WRITE); event_buffer_free(self::$buffers[$id]); fclose(self::$connections[$id]); unset(self::$buffers[$id], self::$connections[$id]);}function ev_read($buffer, $id) {static $ct = 0; $ct_last = $ct; $ct_data = ''; while ($read = event_buffer_read($buffer, 1024)) { $ct += strlen($read); $ct_data .= $read; } $ct_size = ($ct - $ct_last) * 8; echo "[$id] " . __METHOD__ . " > " . $ct_data . "/n"; event_buffer_write($buffer, "Received $ct_size byte data./r/n");}function ev_write($buffer, $id) {echo "[$id] " . __METHOD__ . "/n";}}new EpollSocketServer(2000);epoll_client.php<?php/**EpollSocket Test ClientBy James.Huang <shagoo#gmail.com>**/function debug ($msg){// echo $msg; error_log($msg, 3, '/tmp/socket.log');}if ($argv[1]) { $socket_client = stream_socket_client('tcp://0.0.0.0:2000', $errno, $errstr, 30);// stream_set_blocking($socket_client, 0); if (!$socket_client) {die("$errstr ($errno)");} else {$msg = trim($argv[1]); for ($i = 0; $i < 10; $i++) { $res = fwrite($socket_client, "$msg($i)"); usleep(100000); debug(fread($socket_client, 1024)); } fclose($socket_client);}}else {$phArr = array(); for ($i = 0; $i < 10; $i++) {$phArr[$i] = popen("php ".__FILE__." '{$i}:test'", 'r');} foreach ($phArr as $ph) {pclose($ph);}// for ($i = 0; $i < 10; $i++) {// system("php ".__FILE__." '{$i}:test'");// }}先说一下,以上的例子是基于 PHP 的 libevent 扩展实现的,需要运行的话要先安装此扩展,参考:http://pecl.php.net/package/libevent。这个例子做的事情和前面介绍的第一个模型一样,epoll_server.php 实现的服务端也是接受客户端数据,然后返回结果(接收到的字节数)。但是,当你运行 php epoll_client.php 的时候你会发现服务端打印出来的结果和 accept 阻塞模型就大不一样了,当然运行效率也有极大的提升,这是为什么呢?接下来就介绍一下 epoll/kqueue 模型:在介绍 select 模式的时候我们提到了这种模式的局限,而 epoll 就是为了解决 poll 的这两个缺陷而生的。首先,epoll 模式基本没有限制(参考 cat /proc/sys/fs/file-max 默认就达到 300K,很令人兴奋吧,其实这也就是所谓基于 epoll 的 Erlang 服务端可以同时处理这么多并发连接的根本原因,不过现在 PHP 理论上也可以做到了,呵呵);另外,epoll 模式的性能也不会像 select 模式那样随着连接数的增大而变差,测试发现性能还是很稳定的(下篇会有详细介绍)。epoll 工作有两种模式 LT(level triggered) 和 ET(edge-triggered),前者是缺省模式,同时支持阻塞和非阻塞 IO 模式,虽然性能比后者差点,但是比较稳定,一般来说在实际运用中,我们都是用这种模式(ET 模式和 WinSock 都是纯异步非阻塞模型)。而另外一点要说的是 libevent 是在编译阶段选择系统的 I/O demultiplex 机制的,不支持在运行阶段根据配置再次选择,所以我们在这里也就不细讨论 libevent 的实现的细节了,如果朋友有兴趣进一步了解的话,请参考:http://monkey.org/~provos/libevent/。到这里,第一部分的内容结束了,相信大家已经了解了 Socket 编程的几个重点概念和一些实战技巧,在下一篇《Socket深度探究4PHP(二)》我将会对 select/poll/epoll/kqueue 几种模式做一下深入的介绍和对比,另外也会涉及到两种重要的 I/O 多路复用模式:Reactor 和 Proactor 模式。To be continued ...上一篇《Socket深度探究4PHP(一)》中,大家应该对 poll/select/epoll/kqueue 这几个 IO 模型有了一定的了解,为了让大家更深入的理解 Socket 的技术内幕,在这个篇幅,我会对这几种模式做一个比较详细的分析和对比;另外,大家可能也同说过 AIO 的概念,这里也会做一个简单的介绍;最后我们会对两种主流异步模式 Reactor 和 Proactor 模式进行对比和讨论。首先,然我们逐个介绍一下 2.6 内核(2.6.21.1)中的 poll/select/epoll/kqueue 这几个 IO 模型。POLL先说说 poll,poll 和 select 为大部分 Unix/Linux 程序员所熟悉,这俩个东西原理类似,性能上也不存在明显差异,但 select 对所监控的文件描述符数量有限制,所以这里选用 poll 做说明。poll 是一个系统调用,其内核入口函数为 sys_poll,sys_poll 几乎不做任何处理直接调用 do_sys_poll,do_sys_poll 的执行过程可以分为三个部分:1、将用户传入的 pollfd 数组拷贝到内核空间,因为拷贝操作和数组长度相关,时间上这是一个 O(n) 操作,这一步的代码在 do_sys_poll 中包括从函数开始到调用 do_poll 前的部分。2、查询每个文件描述符对应设备的状态,如果该设备尚未就绪,则在该设备的等待队列中加入一项并继续查询下一设备的状态。查询完所有设备后如果没有一个设备就绪,这时则需要挂起当前进程等待,直到设备就绪或者超时,挂起操作是通过调用 schedule_timeout 执行的。设备就绪后进程被通知继续运行,这时再次遍历所有设备,以查找就绪设备。这一步因为两次遍历所有设备,时间复杂度也是 O(n),这里面不包括等待时间。相关代码在 do_poll 函数中。3、将获得的数据传送到用户空间并执行释放内存和剥离等待队列等善后工作,向用户空间拷贝数据与剥离等待队列等操作的的时间复杂度同样是 O(n),具体代码包括 do_sys_poll 函数中调用 do_poll 后到结束的部分。但是,即便通过 select() 或者 poll() 函数复用事件通知具有突出的优点,不过其他具有类似功能的函数实现也可以达到同样的性能。然而,这些实现在跨平台方面没有实现标准化。你必须在使用这些特定函数实现同丧失可移植性之间进行权衡。我们现在就讨论一下两个替代方法:Solaris 系统下的 /dev/poll 和 FreeBSD 系统下的 kqueue:1、Solaris 系统下的 /dev/poll:在Solaris 7系统上,Sun引入了/dev/poll设备。在使用 /dev/poll的时候,你首先要打开/dev/poll作为一个普通文件。然后构造pollfd结构,方式同普通的poll()函数调用一样。这些 pollfd结构随后写入到打开的 /dev/poll 文件描述符。在打开句柄的生存周期内, /dev/poll会根据pollfd结构返回事件(注意,pollfd结构内的事件字段中的特定POLLREMOVE将从/dev/poll的列表中删除对应的fd)。通过调用特定的ioctl (DP_POLL) 和dvpoll,程序就可以从/dev/poll获得需要的信息。在使用dvpoll结构的情况下,发生的事件就可以被检测到了。2、FreeBSD 系统下的 kqueue:在FreeBSD 4.1中推出。FreeBSD的kqueue API设计为比其他对应函数提供更为广泛的事件通知能力。kqueue API提供了一套通用过滤器,可以模仿poll()语法(EVFILT_READ和EVFILT_WRITE)。不过,它还实现了文件系统变化(EVFILT_VNODE)、进程状态变更(EVFILT_PROC)和信号交付(EVFILT_SIGNAL)的有关通知。EPOLL接下来分析 epoll,与 poll/select 不同,epoll 不再是一个单独的系统调用,而是由 epoll_create/epoll_ctl/epoll_wait 三个系统调用组成,后面将会看到这样做的好处。先来看 sys_epoll_create(epoll_create对应的内核函数),这个函数主要是做一些准备工作,比如创建数据结构,初始化数据并最终返回一个文件描述符(表示新创建的虚拟 epoll 文件),这个操作可以认为是一个固定时间的操作。epoll 是做为一个虚拟文件系统来实现的,这样做至少有以下两个好处:1、可以在内核里维护一些信息,这些信息在多次 epoll_wait 间是保持的,比如所有受监控的文件描述符。2、epoll 本身也可以被 poll/epoll。具体 epoll 的虚拟文件系统的实现和性能分析无关,不再赘述。在 sys_epoll_create 中还能看到一个细节,就是 epoll_create 的参数 size 在现阶段是没有意义的,只要大于零就行。接着是 sys_epoll_ctl(epoll_ctl对应的内核函数),需要明确的是每次调用 sys_epoll_ctl 只处理一个文件描述符,这里主要描述当 op 为 EPOLL_CTL_ADD 时的执行过程,sys_epoll_ctl 做一些安全性检查后进入 ep_insert,ep_insert 里将 ep_poll_callback 做为回掉函数加入设备的等待队列(假定这时设备尚未就绪),由于每次 poll_ctl 只操作一个文件描述符,因此也可以认为这是一个 O(1) 操作。ep_poll_callback 函数很关键,它在所等待的设备就绪后被系统回掉,执行两个操作:1、将就绪设备加入就绪队列,这一步避免了像 poll 那样在设备就绪后再次轮询所有设备找就绪者,降低了时间复杂度,由 O(n) 到 O(1)。2、唤醒虚拟的 epoll 文件。最后是 sys_epoll_wait,这里实际执行操作的是 ep_poll 函数。该函数等待将进程自身插入虚拟 epoll 文件的等待队列,直到被唤醒(见上面 ep_poll_callback 函数描述),最后执行 ep_events_transfer 将结果拷贝到用户空间。由于只拷贝就绪设备信息,所以这里的拷贝是一个 O(1) 操作。还有一个让人关心的问题就是 epoll 对 EPOLLET 的处理,即边沿触发的处理,粗略看代码就是把一部分水平触发模式下内核做的工作交给用户来处理,直觉上不会对性能有太大影响,感兴趣的朋友欢迎讨论。POLL/EPOLL 对比:表面上 poll 的过程可以看作是由一次 epoll_create,若干次 epoll_ctl,一次 epoll_wait,一次 close 等系统调用构成,实际上 epoll 将 poll 分成若干部分实现的原因正是因为服务器软件中使用 poll 的特点(比如Web服务器):1、需要同时 poll 大量文件描述符;2、每次 poll 完成后就绪的文件描述符只占所有被 poll 的描述符的很少一部分。3、前后多次 poll 调用对文件描述符数组(ufds)的修改只是很小;传统的 poll 函数相当于每次调用都重起炉灶,从用户空间完整读入 ufds,完成后再次完全拷贝到用户空间,另外每次 poll 都需要对所有设备做至少做一次加入和删除等待队列操作,这些都是低效的原因。epoll 将以上情况都细化考虑,不需要每次都完整读入输出 ufds,只需使用 epoll_ctl 调整其中一小部分,不需要每次 epoll_wait 都执行一次加入删除等待队列操作,另外改进后的机制使的不必在某个设备就绪后搜索整个设备数组进行查找,这些都能提高效率。另外最明显的一点,从用户的使用来说,使用 epoll 不必每次都轮询所有返回结果已找出其中的就绪部分,O(n) 变 O(1),对性能也提高不少。此外这里还发现一点,是不是将 epoll_ctl 改成一次可以处理多个 fd(像 semctl 那样)会提高些许性能呢?特别是在假设系统调用比较耗时的基础上。不过关于系统调用的耗时问题还会在以后分析。POLL/EPOLL 测试数据对比:测试的环境:我写了三段代码来分别模拟服务器,活动的客户端,僵死的客户端,服务器运行于一个自编译的标准 2.6.11 内核系统上,硬件为 PIII933,两个客户端各自运行在另外的 PC 上,这两台PC比服务器的硬件性能要好,主要是保证能轻易让服务器满载,三台机器间使用一个100M交换机连接。服务器接受并poll所有连接,如果有request到达则回复一个response,然后继续poll。活动的客户端(Active Client)模拟若干并发的活动连接,这些连接不间断的发送请求接受回复。僵死的客户端(zombie)模拟一些只连接但不发送请求的客户端,其目的只是占用服务器的poll描述符资源。测试过程:保持10个并发活动连接,不断的调整僵并发连接数,记录在不同比例下使用 poll 与 epoll 的性能差别。僵死并发连接数根据比例分别是:0,10,20,40,80,160,320,640,1280,2560,5120,10240。下图中横轴表示僵死并发连接与活动并发连接之比,纵轴表示完成 40000 次请求回复所花费的时间,以秒为单位。红色线条表示 poll 数据,绿色表示 epoll 数据。可以看出,poll 在所监控的文件描述符数量增加时,其耗时呈线性增长,而 epoll 则维持了一个平稳的状态,几乎不受描述符个数影响。但是要注意的是在监控的所有客户端都是活动时,poll 的效率会略高于 epoll(主要在原点附近,即僵死并发连接为0时,图上不易看出来),究竟 epoll 实现比 poll 复杂,监控少量描述符并非它的长处。epoll 的优点综述1、支持一个进程打开大数目的socket描述符(FD):select 最不能忍受的是一个进程所打开的 FD 是有一定限制的,由 FD_SETSIZE 设置,在 Linux 中,这个值是 1024。对于那些需要支持的上万连接数目的网络服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的 Apache 方案),不过虽然 linux 上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。不过 epoll 则没有这个限制,它所支持的 FD 上限是最大可以打开文件的数目,这个数字一般远大于 1024,举个例子,在 1GB 内存的机器上大约是 10 万左右,具体数目可以 cat /proc/sys/fs/file-max 察看,一般来说这个数目和系统内存关系很大。2、IO 效率不随 FD 数目增加而线性下降:传统的 select/poll 另一个致命弱点就是当你拥有一个很大的 socket 集合,不过由于网络延时,任一时间只有部分的 socket 是"活跃"的,但是 select/poll 每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是 epoll 不存在这个问题,它只会对"活跃"的 socket 进行操作---这是因为在内核实现中 epoll 是根据每个 fd 上面的 callback 函数实现的。那么,只有"活跃"的 socket 才会主动的去调用 callback 函数,其他 idle 状态 socket 则不会,在这点上,epoll 实现了一个"伪"AIO,因为这时候推动力在 os 内核。在一些 benchmark 中,如果所有的 socket 基本上都是活跃的 --- 比如一个高速LAN环境,epoll 并不比 select/poll 有什么效率,相反,如果过多使用 epoll_ctl,效率相比还有稍微的下降。但是一旦使用 idle connections 模拟 WAN 环境,epoll 的效率就远在 select/poll 之上了。3、使用 mmap 加速内核与用户空间的消息传递:这点实际上涉及到 epoll 的具体实现了。无论是 select,poll 还是 epoll 都需要内核把 FD 消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll 是通过内核于用户空间 mmap 同一块内存实现的。而如果你想我一样从 2.5 内核就关注 epoll 的话,一定不会忘记手工 mmap 这一步的。4、内核微调:这一点其实不算 epoll 的优点了,而是整个 linux 平台的优点。也许你可以怀疑 linux 平台,但是你无法回避 linux 平台赋予你微调内核的能力。比如,内核 TCP/IP 协议栈使用内存池管理 sk_buff 结构,那么可以在运行时期动态调整这个内存 pool(skb_head_pool) 的大小 --- 通过 echo XXXX > /proc/sys/net/core/hot_list_length 完成。再比如 listen 函数的第 2 个参数(TCP 完成 3 次握手的数据包队列长度),也可以根据你平台内存大小动态调整。更甚至在一个数据包面数目巨大但同时每个数据包本身大小却很小的特殊系统上尝试最新的 NAPI 网卡驱动架构。AIO 和 Epollepoll 和 aio(这里的aio是指linux 2.6内核后提供的aio api)的区别:1、aio 是异步非阻塞的。其实是aio是用线程池实现了异步IO。2、epoll 在这方面的定义上有点复杂,首先 epoll 的 fd 集里面每一个 fd 都是非阻塞的,但是 epoll(包括 select/poll)在调用时阻塞等待 fd 可用,然后 epoll 只是一个异步通知机制,只是在 fd 可用时通知你,并没有做任何 IO 操作,所以不是传统的异步。在这方面,Windows 无疑是前行者,当然 Boost C++ 库已经实现了 linux 下 aio 的机制,有兴趣的朋友可以参考:http://stlchina.huhoo.net/twiki/bin/view.pl/Main/WebHomeReactor 和 Proactor一般地,I/O多路复用机制都依赖于一个事件多路分离器(Event Demultiplexer)。分离器对象可将来自事件源的I/O事件分离出来,并分发到对应的read/write事件处理器(Event Handler)。开发人员预先注册需要处理的事件及其事件处理器(或回调函数);事件分离器负责将请求事件传递给事件处理器。两个与事件分离器有关的模式是Reactor和Proactor。Reactor模式采用同步IO,而Proactor采用异步IO。在Reactor中,事件分离器负责等待文件描述符或socket为读写操作准备就绪,然后将就绪事件传递给对应的处理器,最后由处理器负责完成实际的读写工作。而在Proactor模式中,处理器--或者兼任处理器的事件分离器,只负责发起异步读写操作。IO操作本身由操作系统来完成。传递给操作系统的参数需要包括用户定义的数据缓冲区地址和数据大小,操作系统才能从中得到写出操作所需数据,或写入从socket读到的数据。事件分离器捕获IO操作完成事件,然后将事件传递给对应处理器。比如,在windows上,处理器发起一个异步IO操作,再由事件分离器等待IOCompletion事件。典型的异步模式实现,都建立在操作系统支持异步API的基础之上,我们将这种实现称为“系统级”异步或“真”异步,因为应用程序完全依赖操作系统执行真正的IO工作。举个例子,将有助于理解Reactor与Proactor二者的差异,以读操作为例(类操作类似)。在Reactor中实现读:注册读就绪事件和相应的事件处理器事件分离器等待事件事件到来,激活分离器,分离器调用事件对应的处理器。事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。与如下Proactor(真异步)中的读过程比较:处理器发起异步读操作(注意:操作系统必须支持异步IO)。在这种情况下,处理器无视IO就绪事件,它关注的是完成事件。事件分离器等待操作完成事件在分离器等待过程中,操作系统利用并行的内核线程执行实际的读操作,并将结果数据存入用户自定义缓冲区,最后通知事件分离器读操作完成。事件分离器呼唤处理器。事件处理器处理用户自定义缓冲区中的数据,然后启动一个新的异步操作,并将控制权返回事件分离器。对于不提供异步 IO API 的操作系统来说,这种办法可以隐藏 Socket API 的交互细节,从而对外暴露一个完整的异步接口。借此,我们就可以进一步构建完全可移植的,平台无关的,有通用对外接口的解决方案。上述方案已经由Terabit P/L公司实现为 TProactor (ACE compatible Proactor) :http://www.terabit.com.au/solutions.php。正是因为 linux 对 aio 支持的不完整,所以 ACE_Proactor 框架在 linux 上的表现很差,大部分在 windows 上执行正常的代码,在 linux 则运行异常,甚至不能编译通过。这个问题一直困扰着很大多数 ACE 的用户,现在好了,有一个 TProactor 帮助解决了在 Linux 不完整支持 AIO 的条件下,正常使用(至少是看起来正常)ACE_Proactor。TProactor 有两个版本:C++ 和 Java 的。C++ 版本采用 ACE 跨平台底层类开发,为所有平台提供了通用统一的主动式异步接口。Boost.Asio 库,也是采取了类似的这种方案来实现统一的 IO 异步接口。以下是一张 TProactor 架构设计图,有兴趣的朋友可以看看:到这里,第二部分的内容结束了,相信大家对 Socket 的底层技术原理有了一个更深层次的理解,在下一篇《Socket深度探究4PHP(三)》我将会深入 PHP 源代码,探究一下 PHP 在 Socket 这部分的一些技术内幕,然后介绍一下目前在这个领域比较活跃的项目(node.js)。To be continued ...看过前两篇文章《Socket深度探究4PHP(一)》和《Socket深度探究4PHP(二)》,大家应该对目前 Socket 技术的底层有了一定的了解。本文我们会对 PHP-5.3.6 的源码中的Socket 模块进行一定的分析,然后再简单介绍一下目前比较热门的一些相关技术,比如 Node.js 等。自 PHP4 之后,越来越多的模块都被作为扩展提取出来(可单独编译),都在 PHP 源码的 ext 目录下面,因此我们我需要先进入 ext/sockets/ 目录,做过 PHP 扩展的同学应该都很熟悉下面的一些文件了,这次我们主要分析的是 php_sockets.h 和 sockets.c 这两个 C 源码文件。ext/sockets/php_sockets.h这个头文件很简单,我们主要看一下下面列出的几个重点:32 行:ifdef PHP_WIN32include <winsock.h>elseif HAVE_SYS_SOCKET_Hinclude <sys/socket.h>endifendif以上就是 PHP 对于不同环境 Socket 底层调用的定义了,我们可以看到不管是 Unix 还是 Windows 环境,PHP均调用的是系统标准的 BSD Socket 库。然后我们看下面这个重要的结构体定义:82 行:typedef struct {PHP_SOCKET bsd_socket;int type;int error;int blocking;} php_socket;这个就是 php socket 的存储结构了,此结构体在以下的代码阅读中将会大量出现,里面的几个字段很容易理解:bsd_socket 就是标准的 socket 类型,type 表示 socket 类型(PF_UNIX/AF_UNIX),error 是错误代码,blocking 则表示是否阻塞。ext/sockets/sockets.c这个文件比较长,为了直接切入重点,我们会按照《Socket 深度探索 4 PHP (一) 》中 select_server.php 部分代码来按顺序分析一下在最经典的 select 模式中我们用到的主要方法:socket_create_listen859 行:PHP_FUNCTION(socket_create_listen)这个函数很简单,初始化 php_sock 并获取 socket 需要监听的端口,然后传入下面的 php_open_listen_sock 函数进行加工,最后调用 ZEND_REGISTER_RESOURCE 宏返回 php_sock。347行:static int php_open_listen_sock(php_socket **php_sock, int port, int backlog TSRMLS_DC)此函数基本上就是 socket 的标准初始化过程:socket(...) -> bind(...) -> listen(...)(详见 368 行至 391 行)。sock->bsd_socket = socket(PF_INET, SOCK_STREAM, 0);sock->blocking = 1;...sock->type = PF_INET;...if (bind(sock->bsd_socket, (struct sockaddr *)&la, sizeof(la)) != 0) {...}if (listen(sock->bsd_socket, backlog) != 0) {...}socket_set_nonblock906 行:PHP_FUNCTION(socket_set_nonblock)这个函数也很简单,从 ZEND_FETCH_RESOURCE 取出 runtime 中的 php_sock 然后调用 php_set_sock_blocking 函数来设置 sockfd 的阻塞或者非阻塞(此函数可以参考 main/network.c 第 1069 行,我们可以看到 PHP 是使用 fcntl 函数来设置的)。socket_select785 行:PHP_FUNCTION(socket_select)也是标准的 select 函数调用,过程如下:FD_ZERO(...) -> php_sock_array_to_fd_set(...) -> select(...) -> php_sock_array_from_fd_set(...),可能比较特殊的就是 php_sock_array_from_fd_set() 和 php_sock_array_from_fd_set() 两个函数,这是由于我们要先把 PHP 的 fd 数组转换成原生 fd 集合,才能调用原生的 select 函数,而最后系统还把 fd 集合重新转回到PHP 的 fd 数组(具体代码参考 799 行至 851 行)。socket_accept881 行:PHP_FUNCTION(socket_accept)此函数基本上也就是 socket 原生 accept 函数的包装,具体代码可参考 397 行:php_accept_connect 函数中的逻辑,最后调用 ZEND_REGISTER_RESOURCE 宏返回 new_sock,若失败程序会清理使用的 out_socket 资源。socket_write986 行:PHP_FUNCTION(socket_write)按照以上的思路看这个函数也非常简单,详见 986 行,唯一值得注意的是对于不同操作系统调用的函数有点不同,代码(见 1004 行)如下:ifndef PHP_WIN32retval = write(php_sock->bsd_socket, str, MIN(length, str_len));elseretval = send(php_sock->bsd_socket, str, min(length, str_len), 0);endifsocket_read1021 行:PHP_FUNCTION(socket_read)此函数是用于接受 socket 的数据,调用的原生函数是 recv(),不过这里需要注意的是 PHP 为我们提供两种获取方式:1、PHP_NORMAL_READ按行读取,具体代码见 419 行:php_read 函数的逻辑,我们注意到此函数在非阻塞模式下会立即返回,否则将会读取直至遇到 \n 或者 \r 字符。2、PHP_BINARY_READ代码见 1045 行:retval = recv(php_sock->bsd_socket, tmpbuf, length, 0); 相当原生和“环保”。最后,如果返回值为 -1 则会进行一些错误记录和系统清理工作。socket_close970 行:PHP_FUNCTION(socket_close)清理 socket 运行时所用的资源。socket_shutdown1968 行:PHP_FUNCTION(socket_shutdown)调用原生 shutdown 函数来关闭 socket。分析下来,PHP 的 socket 模块中绝大部分的代码还是使用的是系统标准的原生 socket 库,其中唯一有可能造成性能隐患的就是 select 中 PHP 的 fd 数组与原生 fd 集合转换,至于其他的一些简单的数据拷贝基本对效率不会有什么影响。总的来说,PHP 的 socket 模块应该效率还是比较高的,但是在使用的时候还是需要注意到一些资源的及时释放,因为毕竟是 Daemon 程序,需要不断运行的,而且 PHP 的数据结构是很占内存(是原生 C 的 4 倍左右)的。node.js最后,我们看看现在很流行的 Node.js(http://nodejs.org/),它采用了 JavaScript 的语言引擎,语法非常的简洁,对闭包的完美支持让它特别适合做异步 IO 的代码编写,下面是一个最简单的 HTTP Server,只用仅仅六行代码:var http = require('http');http.createServer(function (req, res) {res.writeHead(200, {'Content-Type': 'text/plain'});res.end('Hello World\n');}).listen(8000, "127.0.0.1");console.log('Server running at http://127.0.0.1:8000/');运行起来感受一下,有没有惊艳的感觉啊?事实上用它来写一些简单的服务确实很不错,有兴趣的朋友可以多研究研究(中文社区:http://cnodejs.org/),它有 8000 行 C++ 代码,2000 行 javascript 代码,使用 Google 的 V8 引擎(和 Mongodb 一样),相当的很小巧精悍。下面是我在使用过程总结出中几个要点,大家可以参考:1、使用 V8 引擎(和 Mongodb 一样),内置 JSON,代码简洁,使用方便。2、使用单线程非阻塞 I/O 中的 select 方式,比较稳定(但是对于超高并发有点力不从心)。3、一些第三方应用接口不是很稳定,比如 Mongodb 的接口,并发 200 出现卡死现象,Mysql 接口也比 fast-cgi 差很多。4、注意使用 try{...}catch{...} 来捕获错误;使用 process.on('uncaughtException', function(err){...}); 来处理未捕获的错误,否则出错会导致整个服务退出。当然,Node.js 还在不断的更新发展中,虽然目前我在公司的服务架构中还不敢使用它,我还是很希望它能够迅速成长起来,这样子我们开发服务中间件的时候,就会多出一个很棒的选项啦~
2023年08月07日
14 阅读
0 评论
0 点赞
2023-08-07
PHP语言规范
PHP语言规范PHP书写规范通用原则:1、语义化看到名字,就知道意思。2、通用前缀is表示是否、get表示读、set表示写。is后面优先跟形容词,而不是名词,比如是否多语言文字,应使用is_multilingual,而不是is_multilanguage。3、单数与复数参考js的函数命名规则:getElementById、getElementsByTagName、getElementsByName。例如:取我的多个好友的名字,应使用getFriendsName,而不是getFriendNames或者getFriendName取一个用户,是getUser取多个用户,是getUsers4、冗余后缀尽量不使用data、list、info后缀。比如,js的命名就很注意,使用getElementsByTagName而不是getElementsInfoByTagName。应该使用getFriends或者getFriendsUserId,而不是getFriendsList;应该使用getUser,而不使用getUserInfo或者getUserData。不过有时候很难避免,比如有2个函数,分别是取用户基本信息,和取用户详细信息。取用户基本信息:昵称、头像URI,函数名getUserBasic还是getUserBasicInfo?函数名以形容词结尾感觉不合适,待讨论。取用户详细信息:昵称、头像URI、签名、生日,函数名getUser没问题。5、含义模糊的类名、文件名、目录名每 当使用common、util、functions、class、object、basic作为文件名时要慎重,由于这些词太通用,发展下去里面东西可 能越来越多,变成垃圾箱。要给这些起一个准确的名字,比如要做字符串处理的类,可以叫StringLib.php,放在lib目录里。6、lib、plugin与addon的区别有些类、函数算做lib、plugin还是addon。待讨论。类名:大写字母开头,驼峰命名。一般使用名词,比如配置解析类ConfigParser,而不是ParseConfig。与Java、C++一致。例如:class UserModel类的文件名:与类名相同。这与php autoload有关,为了autoload,类名总要很长,待讨论。与Java一致。例如:class UserModel的文件名为UserModel.php非类文件名:全小写,下划线分隔,不得使用空格。比如get_user.php。目录名:全小写,下划线分隔,不得使用空格。比如model、www。函数名:小写字母开头,驼峰命名,例如:function addBlog()。与Java、C++一致。函数表示功能,即动作,所以动词优先,例如使用editBlog,而不用blogEdit。PHP内置函数由于历史原因,有多种风格,do_something,something_do,dosomething,比较新的函数用了doSomething,才与目前主流语言保持一致。比如:paser_str、json_encode、substr、fetchAll。历史原因可能无法改变,但我们能保证新的代码是严谨的,不要让自己成为历史原因。类中的函数:两个函数中间空一行。如果有时间的话,各个函数按英文字母排序,免得太混乱。例如:复制代码class BlogModel { public function addBlog(){ } public function updateBlog() { } } 复制代码文件注释:注释紧跟<?php下一行。注明作者。@version暂不需要写,因为svn提供了版本管理。格式按照PHPdoc的要求:http://manual.phpdoc.org/HTMLframesConverter/default/phpDocumentor/tutorial_tags.author.pkg.html复制代码<?php /**blog的各种业务:添加、更新@author sink*/class BlogModel { } ?> 复制代码API注释:一定要写输入参数,和输出格式。写清楚正确时输出什么,错误时输出什么。否则别人无法使用。函数注释:一定要写输出格式。写清楚正确时输出什么,错误时输出什么。如果输入参数比较复杂,包含数组,看参数无法一目了然,则要写输入参数的注释。文档注释与函数之间不能有空行。如果函数内部步骤比较复杂,需要写“行内注释”。例如:复制代码/**更新blog@param int $id blog_id@param array $data array( "content" => "", //内容 "tags" => "", //标签 "update_time" => "", //更新时间 )@return bool*/public function updateBlog($id,$data) {step1 //第一步:asdf step2 //第二步:qwer } 复制代码URI:根据rfc1034国际标准的规定,域名中禁止出现下划线“_”,域名不区分大小写。比如http://dl_dir.qq.com/是错误域名。http://example.com与http://EXAMPLE.COM相同。所以优先在URI中使用全小写,GET的name小写,但是GET的值除外。比如http://www.google.com/?hl=zh-CNhttp://www.google.com/?hl=zh-cnURI中非参数的专有名词的缩写是否使用小写,有争议无定论。比如http://fedoraproject.org/zh_CN/http://zh.wikipedia.org/zh-cn/http://code.google.com/intl/zh-CN/http://www.microsoft.com/en-us/语言文字代码是专有名词,ISO规定必须是减号,且建议地区使用大写。fedora的用法很奇怪,使用了自己制造的zh_CN,而不是zh-CN。而且不建议在URI中使用下划线。wiki用了小写,google用了大写,微软用了小写。优先在URI中使用减号“-”,而不是下划线,GET的name除外。比如http://example.com/1-2-2http://example.com/?user_id=123如果希望用户手动输入URI,则不要区分大小写,且优先使用小写,因为用户输入更方便。实际情况是:用户一般是手动输入域名,而不手动输入URI,因为URI很长。在这种情况下,URI小写是否有意义,如果使用 http://example.com/?userId=123,变量名就可以使用驼峰$userId = $_GET['userId'],就能够和Java、C++保持一致,这样数据库也要驼峰命名。待讨论。变量:全小写,下划线分隔,例如:$user_id。与Java、C++不一致。待讨论。类的成员变量、函数的形参、类实例化成一个对象,都遵守变量的命名规则。原因:URI、数据库有小写惯例,从$_GET、$_POST中获得参数入库,所以用小写。PHP内置变量$_GET、$_POST使用下划线开头,全大写。自定义的变量无论多么重要,都不要使用下划线开头,以免将来与内置变量冲突。比如:不要使用$_PUT、$_DELETE。常量:全大写,下划线分隔。例如:const MEMCACHE_TTL = 600;PHP短标签:使用,不使用短标签。因为与xml冲突,且不利于部署。类大括号换行:可以采用大括号单独占一行,也可以大括号与别的放在一行,有争议无定论,待讨论。class UserModel { } 支持换行者:http://www.php.net/manual/zh/language.oop5.basic.phphttp://pear.php.net/manual/en/standards.classdef.php函数大括号换行:有争议无定论,待讨论。function getUser() { } 支持换行者:http://www.php.net/manual/zh/language.oop5.basic.phphttp://pear.php.net/manual/en/standards.funcdef.phpif大括号换行:有争议无定论,待讨论。例如:if(!emptyempty($name)) { } 或者if(!emptyempty($name)){ } 支持换行者:http://www.possibility.com/Cpp/CppCodingStandard.html#brace支持同行者:http://www.php.net/manual/zh/language.oop5.basic.phphttp://pear.php.net/manual/en/standards.control.phpswitch大括号换行:复制代码switch (...) {case 1: ... break; default: } 复制代码支持换行者:http://www.possibility.com/Cpp/CppCodingStandard.html#switch数组小括号换行:有争议无定论,待讨论。复制代码$user = array("id" => "123", "name" => "user1", "email" => "a@example.com", ) 复制代码支持同行者:http://pear.php.net/manual/en/standards.arrays.php数组内部换行:2维及以上数组的数组内部换行。如复制代码$user = array('id' => '123', 'name' => 'user1', 'email' => 'a@example.com', ); 复制代码1维数组内部不换行?待讨论。如$users_id = array('23','12','24');数组最后的逗号:数组每一行最后要有逗号,这样方便以后添加。不过前端JSON最后不能有逗号,否则有的浏览器不支持,待讨论。比如复制代码$user = array('id' => '123', 'name' => 'user1', //正确 ); $user = array('id' => '123', 'name' => 'user1' //错误 ); 复制代码单引号与双引号:优先使用单引号,当需要转义时使用双引号。这与JSON不同,JSON全是双引号,待讨论。比如:echo 'name is:' . $name . '.' . "\n"; $user = array('id' => '123', ); 条件判断的大括号:必须有大括号,即使只有一行。正确:if(!emptyempty($name)) {doSomething(); } 错误:if(!emptyempty($name))doSomething(); 回车换行:使用换行LF(\n,0a,Unix风格)。不使用CR+LF(Windows风格)。参考:http://zh.wikipedia.org/zh-cn/%E6%8F%9B%E8%A1%8Ceclipse——》workspace——》New text file line delimiter——》Other:Unix编码:使用UTF-8 no BOM。不得使用Windows记事本进行保存,因为记事本是UTF-8 BOM CR+LF。eclipse——》workspace——》Text file encoding——》Other:UTF-8缩进:使用4个空格进行缩进,也可以采用tab进行缩进。有争议无定论,待讨论。支持4个空格者:http://www.oracle.com/technetwork/java/codeconventions-136091.html#262支持2个空格者:http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Spaces_vs._Tabs支持3、4或8个空格者:http://www.possibility.com/Cpp/CppCodingStandard.html#indent要保证缩进正确,如果使用4个空格,一定不要出现5个空格或者11个空格。eclipse——》General——》Editor——》Text Editors——》show whitespace charactersvim ~/.vimrcset expandtabset softtabstop=4set shiftwidth=4HTTP协议缓存:文章使用Last Modified表示最后修改时间,不禁止缓存。header('Last Modified:Sat, 30 Oct 2010 13:21:21 GMT');需要用户登录的页面,禁止缓存。header('Cache-Control:max-age=0'); header('Cache-Control:private');HTTP协议编码与mime:web输出一定要声明编码与mime。charset与分号之间要有一个空格。小写utf-8还是大写UTF-8,尚未找到文档,待调研。比如header('Content-Type:application/json; charset=UTF-8'); header('Content-Type:application/xml; charset=UTF-8'); header('Content-Type:application/xhtml+xml; charset=UTF-8'); header('Content-Type:text/plain; charset=UTF-8'); header('Content-Type:text/html; charset=UTF-8');专有名词大小写:在类、函数、文件名、目录名等各种地方,不特殊对待专有名词,不采用全大写。原因:专有名词难以界定,比如HTML、CSS、CRUD。而且全大写导致与驼峰冲突,比如页面助手类,全大写是HTMLHelper,不如HtmlHelper。支持不特殊处理:HTML是专有名词,但mime中就使用Content-Type:text/html,而不是text/HTML。例子:采用UserDb.php,而不是UserDB.php。
2023年08月07日
9 阅读
0 评论
0 点赞
2023-08-07
php 导出Excel方法
php 导出Excel方法<?php/**PHPExcel*Copyright (C) 2006 - 2014 PHPExcel*This library is free software; you can redistribute it and/ormodify it under the terms of the GNU Lesser General PublicLicense as published by the Free Software Foundation; eitherversion 2.1 of the License, or (at your option) any later version.*This library is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied warranty ofMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNULesser General Public License for more details.*You should have received a copy of the GNU Lesser General PublicLicense along with this library; if not, write to the Free SoftwareFoundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA*@category PHPExcel@package PHPExcel@copyright Copyright (c) 2006 - 2014 PHPExcel (http://www.codeplex.com/PHPExcel)@license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt LGPL@version 1.8.0, 2014-03-02*//* Error reporting /error_reporting(E_ALL);ini_set('display_errors', TRUE);ini_set('display_startup_errors', TRUE);define('EOL',(PHP_SAPI == 'cli') ? PHP_EOL : '');date_default_timezone_set('Asia/Shanghai');/* Include PHPExcel /require_once(dirname(__FILE__)."/../PHPExcel/PHPExcel.php");require_once(dirname(__FILE__)."/config.php");//require_once (dirname(__FILE__) . "/../include/common.inc.php");$diyid = isset($diyid) && is_numeric($diyid) ? $diyid : 0;$chk=$_GET['chk'];require_once ITVINC.'/diyform.cls.php';$diy = new diyform($diyid);/* if($dsql->IsTable($diy->table)){$sql = "SELECT * FROM $diy->table";$dsql->Execute('me',$sql);// while($arr = $dsql->GetArray('me',MYSQL_BOTH))//{// echo "id = {$arr[0]} ,name = {$arr[1]}";//}}*//*include_once ITVINC.'/datalistcp.class.php';if (isset($chk)) {$query = "SELECT * FROM {$diy->table} where ifcheck=$chk ORDER BY id DESC";}else {$query = "SELECT * FROM {$diy->table} ORDER BY id DESC";}$datalist = new DataListCP();$datalist->pageSize = 10;$datalist->SetParameter('diyid', $diyid);$datalist->SetSource($query);$fieldlist = $diy->getFieldList();*/// Create new PHPExcel object//echo date('H:i:s') , " Create new PHPExcel object" , EOL;$objPHPExcel = new PHPExcel();// Set document properties设置文档属性//echo date('H:i:s') , " Set document properties" , EOL;$objPHPExcel->getProperties()->setCreator("itv media inc")->setLastModifiedBy("itv media inc")->setTitle("Office 2007 XLSX Test Document")->setSubject("Office 2007 XLSX Test Document")->setDescription("Test document for Office 2007 XLSX, generated using PHP classes.")->setKeywords("office 2007 openxml php")->setCategory("Test result file");// Create a first sheet, representing sales data//写入单元格数据//echo date('H:i:s') , " Add some data" , EOL;$objPHPExcel->setActiveSheetIndex(0);if ($diyid==2||$diyid==4) {$objPHPExcel->getActiveSheet()->setCellValue('A1', 'id')->setCellValue('B1', '分组名称')->setCellValue('C1', '申请人姓名')->setCellValue('D1', '身份证号')->setCellValue('E1', '联系方式')->setCellValue('F1', '是否愿意担任小组联络人')->setCellValue('G1', '申请说明')->setCellValue('H1', '状态')->setCellValue('I1', 'time');}else {$objPHPExcel->getActiveSheet()->setCellValue('A1', 'id')->setCellValue('B1', '分组名称')->setCellValue('C1', '单位名称')->setCellValue('D1', '参加人数')->setCellValue('E1', '联系人')->setCellValue('F1', '联系电话')->setCellValue('G1', '申请说明')->setCellValue('H1', '状态')->setCellValue('I1', 'time');}if($dsql->IsTable($diy->table)){// $sql = "SELECT * FROM $diy->table";if (isset($chk) && $chk!=='') {$sql = "SELECT * FROM $diy->table where ifcheck='$chk'";}else {$sql = "SELECT * FROM $diy->table";}$dsql->Execute('me',$sql);$item=1;while($arr = $dsql->GetArray('me',MYSQL_BOTH)){$item++;//$arr[8]=date("Y-m-d H:i:s",$arr[8]);if($arr[1]=="1") {$arr[1]="已验证";}else {$arr[1]="未验证";}$objPHPExcel->getActiveSheet()->setCellValue("A$item", "$arr[0]")->setCellValue("B$item", "$diy->name")->setCellValue("C$item", "$arr[2]")->setCellValue("D$item", " $arr[3]")->setCellValue("E$item", " $arr[4]")->setCellValue("F$item", " $arr[5]")->setCellValue("G$item", "$arr[6]")->setCellValue("H$item", "$arr[1]")->setCellValue("I$item", date('Y-m-d H:i:s',$arr[7]));}}// Set column widths设置单元格宽度//echo date('H:i:s') , " Set column widths" , EOL;$objPHPExcel->getActiveSheet()->getColumnDimension('A')->setWidth(10);$objPHPExcel->getActiveSheet()->getColumnDimension('B')->setWidth(17);$objPHPExcel->getActiveSheet()->getColumnDimension('C')->setWidth(12);$objPHPExcel->getActiveSheet()->getColumnDimension('D')->setWidth(22);$objPHPExcel->getActiveSheet()->getColumnDimension('E')->setWidth(12);$objPHPExcel->getActiveSheet()->getColumnDimension('F')->setWidth(25);$objPHPExcel->getActiveSheet()->getColumnDimension('G')->setWidth(62);$objPHPExcel->getActiveSheet()->getColumnDimension('H')->setWidth(12);$objPHPExcel->getActiveSheet()->getColumnDimension('I')->setWidth(15);// Add conditional formatting//echo date('H:i:s') , " Add conditional formatting" , EOL;$objConditional1 = new PHPExcel_Style_Conditional();$objConditional1->setConditionType(PHPExcel_Style_Conditional::CONDITION_CELLIS)->setOperatorType(PHPExcel_Style_Conditional::OPERATOR_BETWEEN)->addCondition('200')->addCondition('400');$objConditional1->getStyle()->getFont()->getColor()->setARGB(PHPExcel_Style_Color::COLOR_YELLOW);$objConditional1->getStyle()->getFont()->setBold(true);$objConditional1->getStyle()->getNumberFormat()->setFormatCode(PHPExcel_Style_NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE);$objConditional2 = new PHPExcel_Style_Conditional();$objConditional2->setConditionType(PHPExcel_Style_Conditional::CONDITION_CELLIS)->setOperatorType(PHPExcel_Style_Conditional::OPERATOR_LESSTHAN)->addCondition('0');$objConditional2->getStyle()->getFont()->getColor()->setARGB(PHPExcel_Style_Color::COLOR_RED);$objConditional2->getStyle()->getFont()->setItalic(true);$objConditional2->getStyle()->getNumberFormat()->setFormatCode(PHPExcel_Style_NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE);$objConditional3 = new PHPExcel_Style_Conditional();$objConditional3->setConditionType(PHPExcel_Style_Conditional::CONDITION_CELLIS)->setOperatorType(PHPExcel_Style_Conditional::OPERATOR_GREATERTHANOREQUAL)->addCondition('0');$objConditional3->getStyle()->getFont()->getColor()->setARGB(PHPExcel_Style_Color::COLOR_GREEN);$objConditional3->getStyle()->getFont()->setItalic(true);$objConditional3->getStyle()->getNumberFormat()->setFormatCode(PHPExcel_Style_NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE);//$conditionalStyles = $objPHPExcel->getActiveSheet()->getStyle('B2')->getConditionalStyles(); 后来去array_push($conditionalStyles, $objConditional1);array_push($conditionalStyles, $objConditional2);array_push($conditionalStyles, $objConditional3);//$objPHPExcel->getActiveSheet()->getStyle('B2')->setConditionalStyles($conditionalStyles); 后来去// duplicate the conditional styles across a range of cells复制单元格格式//echo date('H:i:s') , " Dulicate the conditional formatting across a range of cells" , EOL;/*$objPHPExcel->getActiveSheet()->duplicateConditionalStyle($objPHPExcel->getActiveSheet()->getStyle('B2')->getConditionalStyles(), 后来去'B3:B7');*/// Set fonts设置字体//echo date('H:i:s'), " Set fonts" , EOL;$objPHPExcel->getActiveSheet()->getStyle('A1:I1')->getFont()->setBold(true);//$objPHPExcel->getActiveSheet()->getStyle('B1')->getFont()->setBold(true);//$objPHPExcel->getActiveSheet()->getStyle('A7:B7')->getFont()->setBold(true);//$objPHPExcel->getActiveSheet()->getStyle('B7')->getFont()->setBold(true);//set color设置背景色$objPHPExcel->getActiveSheet()->getStyle( 'A1:I1')->getFill()->setFillType(PHPExcel_Style_Fill::FILL_SOLID);$objPHPExcel->getActiveSheet()->getStyle( 'A1:I1')->getFill()->getStartColor()->setARGB('FFD2D2D2');// Set header and footer. When no different headers for odd/even are used, odd header is assumed.//echo date('H:i:s') , " Set header/footer" , EOL;$objPHPExcel->getActiveSheet()->getHeaderFooter()->setOddHeader('&L&BPersonal cash register&RPrinted on &D');$objPHPExcel->getActiveSheet()->getHeaderFooter()->setOddFooter('&L&B' . $objPHPExcel->getProperties()->getTitle() . '&RPage &P of &N');// Set page orientation and size设计页面方向和大小//echo date('H:i:s') , " Set page orientation and size" , EOL;$objPHPExcel->getActiveSheet()->getPageSetup()->setOrientation(PHPExcel_Worksheet_PageSetup::ORIENTATION_PORTRAIT);$objPHPExcel->getActiveSheet()->getPageSetup()->setPaperSize(PHPExcel_Worksheet_PageSetup::PAPERSIZE_A4);// Rename worksheet//echo date('H:i:s') , " Rename worksheet" , EOL;$objPHPExcel->getActiveSheet()->setTitle('工作表名字');// Set active sheet index to the first sheet, so Excel opens this as the first sheet$objPHPExcel->setActiveSheetIndex(0);// 保存文件// Save Excel 2007 file//echo date('H:i:s') , " Write to Excel2007 format" , EOL;$cttime=$diy->name.date('YmdHis').'.php';$cttimed=$diy->name.date('YmdHis');$callStartTime = microtime(true);$objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel2007');$objWriter->save(str_replace('.php', '.xlsx', $cttime));$callEndTime = microtime(true);$callTime = $callEndTime - $callStartTime;//echo date('H:i:s') , " File written to " , str_replace('.php', '.xlsx', pathinfo(__FILE__, PATHINFO_BASENAME)) , EOL;//echo 'Call time to write Workbook was ' , sprintf('%.4f',$callTime) , " seconds" , EOL;// Save Excel5 file//echo date('H:i:s') , " Write to Excel5 format" , EOL;$callStartTime = microtime(true);$objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel5');$objWriter->save(str_replace('.php', '.xls', $cttime));$callEndTime = microtime(true);$callTime = $callEndTime - $callStartTime;//直接输出到浏览器$objWriter = new PHPExcel_Writer_Excel5($objPHPExcel);header("Pragma: public");header("Expires: 0");header("Cache-Control:must-revalidate, post-check=0, pre-check=0");header("Content-Type:application/force-download");header("Content-Type:application/vnd.ms-execl");header("Content-Type:application/octet-stream");header("Content-Type:application/download");;header("Content-Disposition:attachment;filename=$cttimed.xls");header("Content-Transfer-Encoding:binary");$objWriter->save('php://output');//echo date('H:i:s') , " File written to " , str_replace('.php', '.xls', pathinfo(__FILE__, PATHINFO_BASENAME)) , EOL;//echo 'Call time to write Workbook was ' , sprintf('%.4f',$callTime) , " seconds" , EOL;// Echo memory usage//echo date('H:i:s') , ' Current memory usage: ' , (memory_get_usage(true) / 1024 / 1024) , " MB" , EOL;// Echo memory peak usage//echo date('H:i:s') , " Peak memory usage: " , (memory_get_peak_usage(true) / 1024 / 1024) , " MB" , EOL;// Echo done//echo date('H:i:s') , " Done writing file" , EOL;//echo 'File has been created in ' , getcwd() , EOL;
2023年08月07日
11 阅读
0 评论
0 点赞
2023-08-07
PHP 安全与性能
PHP 安全与性能Apache mod_php / php-fpm目录权限安全1.1. 用户权限web server 启动用户不能于运行用户为同一个用户web server 运行用户与php程序不能为同一个用户root 1082 0.0 0.1 11484 2236 ? Ss Mar01 0:00 nginx: master process /usr/sbin/nginxwww-data 13650 0.0 0.0 11624 1648 ? S 09:44 0:00 nginx: worker processwww-data 13651 0.0 0.0 11624 1132 ? S 09:44 0:00 nginx: worker processwww-data 13652 0.0 0.0 11624 1132 ? S 09:44 0:00 nginx: worker processwww-data 13653 0.0 0.0 11624 1132 ? S 09:44 0:00 nginx: worker process父进程root 启动 web server, 此时web server 父进程应该是 root,同时父进程监听80端口子进程父进程派生许多子进程,同时使用setuid,setgid将子进程权限切换为非root子进程用户可以通过httpd.conf设置User nobodyGroup nobodynginx.conf$ cat /etc/nginx/nginx.confuser www-data;fastcgi 进程root 13082 0.0 0.1 19880 2584 ? Ss 09:28 0:00 php-fpm: master process (/etc/php5/fpm/php-fpm.conf)www-data 13083 0.0 0.1 20168 3612 ? S 09:28 0:00 php-fpm: pool wwwwww-data 13084 0.0 0.1 20168 2808 ? S 09:28 0:00 php-fpm: pool wwwwww-data 13085 0.0 0.1 20168 2812 ? S 09:28 0:00 php-fpm: pool wwwwww-data 13086 0.0 0.1 20168 2812 ? S 09:28 0:00 php-fpm: pool wwwphp-fpm 于apache类似,都是root父进程,然后派生子进程,由于fastcgi 使用 9000 所有我们可以不使用root启动php-fpm现在我们开始讲解安全配置问题我们目的是避免用户通过漏洞提升权限,或者由于权限配置不当产生漏洞1.1.1. ApacheApache 案例Apache : rootApache 子进程 : nobodyHTDOCS 目录 : /var/www/var/www|--include|--image|--temp|--...很多人会将/var/www用户与组设置为 nobody:nogroup / nobody:nobody, 同时因为images会上传文件需要设置777, 很多书本于教程上面也是这样讲的, 这样配置会有什么问题呢?我们来分析一下:我们假设,一个用户上传一个文件到images目录,会有几种情况:上传一个.php文件,我们可以通过程序禁止上传.php文件我们上传一个.jpg文件,OK 通过了,通过某种手段将他重命名位.php扩展名的文件,然后通过http://www.example.com/images/your.php 运行它,your.php 可以做什么呢? 它可以查看所有文件,修改所有文件,创建其他php文件,去你可include目录下看config.php然后下载数据库。内部开发人员偷偷将一个程序植入到系统中,这个做code review 可以避免如何避免这样问题出现,有一个办法,我们新建一个用户www, webserver 进程是nobody,程序目录/var/www中的代码是www用户,nobody可能读取但不能修改。/var/www/images 目录所有者是nobody可以上传图片chown www /var/www/chown nobody /var/www/imagesfind /var/www/ -type d -exec chmod 555 {} \;find /var/www/ -type f -exec chmod 444 {} \;chmod 755 /var/www/images使所有可能目录允许运行.php文件,http://www.example.com/images/your.php 将被拒绝. include 也是同样处理方式,只允许使用include_once,require_one 包含,不允许http://www.example.com/include/your.php运行Order Deny,Allow Deny from all Order allow,deny Deny from all Order allow,deny Deny from all<Files *.php> Order allow,deny Deny from all </Files>1.1.2. Nginx / lighttpd + fastcgiNginx / lighttpd 案例分析nginx / lighttpd : rootweb server 子进程 : nobodyphp-fpm : rootphp-fpm 子进程 : nobodyHTDOCS 目录 : /var/www/var/www|--include|--image|--temp|--...fastcgi 遇到的问题与上面apache案例中遇到的问题类似,不同是的fastcgi把动态于静态完全分开了,这样更容易管理,我们可以这样入手nginx / lighttpd : rootweb server 子进程 : nobodyphp-fpm : rootphp-fpm 子进程 : wwwchown nobody /var/www/chown www /var/www/imagesfind /var/www/ -type d -exec chmod 555 {} \;find /var/www/ -type f -exec chmod 444 {} \;chmod 755 /var/www/images/var/www所有权限给nobody, images权限给www, 同时保证www用户可以读取/var/www下的程序文件location ~ ^/upload/.*.php${ deny all;}location ~ ^/static/images/.*.php${ deny all;}location ~ /include/.*.php$ {deny all;}location ~ .*.(sqlite|sq3)$ {deny all;}vim /etc/php5/fpm/pool.d/www.confuser = wwwgroup = www/etc/php5/fpm/pool.d/www.confchdir = /改为chdir = /var/wwwchroot可以彻底解决cd跳转问题,单配置比较繁琐chroot = /var/www这样当用户试图通过chdir跳转到/var/www以外的目录是,将被拒绝1.2. web server 版本信息Apache:ServerTokens ProductOnlyServerSignature OffNginx:server_tokens off;1.3. php_flag / php_admin_flag你在php.ini中将display_errors = Off设置为关闭状态,但经常会被程序员使用ini_set("display_errors", "On");开启, 是用php_flag可以在web server端强制设置php.ini参数php_flag register_globals offphp_flag magic_quotes_gpc offphp_admin_value(php_admin_flag) 与 php_value(php_flag) 有何不同?不同的地方是:php_admin_value(php_admin_flag) 命令只能用在apache的httpd.conf文件中, 而php_value(php_flag)则是用在.htacces在.htaccess中停用全局变量php_flag register_globals 0php_flag magic_quotes_gpc 0php_flag magic_quotes_runtime 01.4. 防止URL注入if ($request_uri ~* (.*)(insert|select|delete|update|count|concat|cost|union|drop|table|*|%|master|truncate|declare|'|;|and|or|(|)|exec)(.*)$ ) {return 403; }if ( $query_string ~ ".[;'<>].*" ){return 403;}php.ini2.1. Magic quotes限于5.2。x 版本magic_quotes_gpc = Onmagic_quotes_runtime = On测试程序 STR: <?phpif (get_magic_quotes_gpc()) {$str = $_POST['str']; echo '这里是get_magic_quotes_gpc()转义过后的:' ,$str, '<hr />';} else {$str = addslashes($_POST['str']); echo '现在通过addslashes传递过来的值是:' ,$_POST['str'], '<br>';}function stringFilter($str){if (ini_get('magic_quotes_gpc)') { return $str; } else { return addslashes($str); }}2.2. 危险PHP函数这些函数应该尽量避免使用它们exec, system, ini_alter, readlink, symlink, leak, proc_open, popepassthru, chroot, scandir, chgrp, chown, escapeshellcmd, escapeshellarg, shell_exec, proc_get_status, max_execution_time, opendir,readdir, chdir ,dir, unlink,delete,copy,rename对于后门植入主要是用下面几个方法eval, gzinflate, str_rot13, base64_decode针对目录与文件的函数disable_functions=chdir,chroot,dir,getcwd,opendir,readdir,scandir,fopen,unlink,delete,copy,mkdir,rmdir,rename,file,file_get_contents,fputs,fwrite,chgrp,chmod,chown针对 php.ini 操作的函数ini_set,2.2.1. chdir()函数安全演示$ cat chdir.php 运行结果current:/wwwchdir:/Line #0 : root:x:0:0:root:/root:/bin/bashLine #1 : daemon:x:1:1:daemon:/usr/sbin:/bin/shLine #2 : bin:x:2:2:bin:/bin:/bin/shLine #3 : sys:x:3:3:sys:/dev:/bin/shLine #4 : sync:x:4:65534:sync:/bin:/bin/syncLine #5 : games:x:5:60:games:/usr/games:/bin/sh2.3. 隐藏PHP版本信息expose_php Off2.4. session名字可以泄露你的服务器采用php技术session.name = PHPSESSID伪装成Tomcatsession.name = JSESSIONID2.5. 隐藏PHP出错信息display_errors = Off同时开启error_log日志error_log = php_errors.log2.6. open_basedir 防止操作web环境意外文件目录open_basedir = /www/:/tmp/测试脚本<?phpchdir('/etc');printf(file('/etc/fstab'));实际效果Warning: chdir(): open_basedir restriction in effect. File(/etc) is not within the allowed path(s): (/www/:/tmp/) in /www/index.php on line 2Warning: file(): open_basedir restriction in effect. File(/etc/fstab) is not within the allowed path(s): (/www/:/tmp/) in /www/index.php on line 2Warning: file(/etc/fstab): failed to open stream: Operation not permitted in /www/index.php on line 2开发于安全3.1. 彻底解决目录于文件的安全选择一个MVC开发框架,它们的目录结构一般是这样的:/www/www/htdocs/index.php htdocs目录下只有一个index.php文件,他是MVC/HMVC框架入口文件/www/htdocs/static 这里防止静态文件/www/app/ 这里放置php文件然后放行index.php文件,在URL上不允许请求任何其他php文件,并返回404错误3.2. Session / Cookie安全session.save_path 默认session 存储在/tmp, 并且一明文的方式将变量存储在以sess_为前缀的文件中$ cat session.php<?phpsession_start();if(isset($_SESSION['views'])) $_SESSION['views']=$_SESSION['views']+1;else $_SESSION['views']=1;echo "Views=". $_SESSION['views'];?>http://www.example.com/session.php 我们刷新几次再看看sess_文件中的变化$ cat /tmp/sess_d837a05b472390cd6089fc8895234d1aviews|i:3;经过侧记你可以看到session文件中存储的是明文数据,所以不要将敏感数据放到Session中,如果必须这样作。建议你加密存储的数据有一个办法比较好,就是封装一下session.不再采用$_SESSION方式调用Class Encrype{}Class Session extend Encrype {function set($key,$value,$salt){ $value = Encrype($value) $_SESSION[$key] = $value } function get($key){ return $_SESSION[$key] }}Class Cookie extend Encrype {function set($key,$value,$salt){ $value = Encrype($value) $_COOKIE[$key] = $value } function get($key){ return $_COOKIE[$key] }}Cookiecookie 也需要作同样的处理,上面代码仅供参考,未做过运行测试3.3. 注入安全3.3.1. 禁止输出调试信息error_reporting(0);3.3.2. 预防SQL注入攻击SQL 注入<?php$mysql_server_name="172.16.0.4"; $mysql_username="dbuser"; $mysql_password="dbpass"; $mysql_database="dbname"; $conn=mysql_connect($mysql_server_name, $mysql_username, $mysql_password); $strsql=""; if($_GET['id']){ $strsql="select * from `order` where id=".$_GET['id']; }else{ $strsql="select * from `order` limit 100"; } echo $strsql; $result=@mysql_db_query($mysql_database, $strsql, $conn); $row=mysql_fetch_row($result); echo '<font face="verdana">'; echo '<table border="1" cellpadding="1" cellspacing="2">'; echo "\n<tr>\n"; for ($i=0; $i<mysql_num_fields($result); $i++) { echo '<td bgcolor="#000F00"><b>'. mysql_field_name($result, $i); echo "</b></td>\n"; } echo "</tr>\n"; mysql_data_seek($result, 0); while ($row=mysql_fetch_row($result)) { echo "<tr>\n"; for ($i=0; $i<mysql_num_fields($result); $i++ ) { echo '<td bgcolor="#00FF00">'; echo "$row[$i]"; echo '</td>'; } echo "</tr>\n"; } echo "</table>\n"; echo "</font>"; mysql_free_result($result); mysql_close(); mysql_real_escape_string() / mysqli_real_escape_string() 可以转义 SQL 语句中使用的字符串中的特殊字符$username = mysqli_real_escape_string( $GET['username'] );mysql_query( “SELECT * FROM tbl_employee WHERE username = ’”.$username.“‘”);<?php// 转义用户名和密码,以便在 SQL 中使用$user = mysql_real_escape_string($user);$pass = mysql_real_escape_string($pass);$sql = "SELECT * FROM users WHERE user='" . $user . "' AND password='" . $pwd . "'"// 更多代码?>3.3.3. SHELL 命令注入SHELL 命令注入, 原理是PHP中``符号或者system,exec等等函数会执行系统命令。<?phpsystem("iconv -f ".$_GET['from']." -t ".$_GET['from']." ".$_GET['file'])<?php$c=urldecode($_GET['c']);if($c){$c;}示例:http://www.example.com/file.php?c=echo%20helloworld>test.txt!$_GET['c']||{$_GET['c']};执行效率如果是web应用程序,通常我们必须将执行时间控制在30秒以内, 10秒最佳. 否则用户是没有耐心等待你的网站打开.4.1. timeout下面的流程展示了从用户打开浏览器到页面展示出来的整个流程, 每个流程都可能出现 timeoutuser -> dns -> web server -> app server -> cache -> database严格限制运行时间外部引用域名必须写入hosts文件, 防止解析时间过长必须设置严格的超时策略, 方式程序长时间等待不退出, 占用系统资源<?php$ctx = stream_context_create(array( 'http' => array( 'timeout' => 1 //设置一个超时时间,单位为秒 )));file_get_contents("http://example.com/file.ext", false, $ctx);?><?php$ctx = stream_context_create(array( 'http' => array( 'method' => 'GET', 'header' => 'Accept-Encoding: gzip, deflate', 'timeout' => 1 )));$html = file_get_contents("http://www.163.com/", false, $ctx);echo strlen($html);?>4.1.1. mysqlshow variables like '%timeout%'4.2. 浏览器上传文件尺寸控制Nginxclient_max_body_size 8M设置不能过大,因为可以通过你的网站上传功能,持续上传实现攻击。
2023年08月07日
15 阅读
0 评论
0 点赞
1
...
126
127
128
...
157