Deprecated
: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in
/www/wwwroot/testblog.58heshihu.com/var/Widget/Archive.php
on line
1057
首页
关于
Search
1
给你10个市场数据调研报告的免费下载网站!以后竞品数据就从这里找!
182 阅读
2
php接口优化 使用curl_multi_init批量请求
144 阅读
3
《从菜鸟到大师之路 ElasticSearch 篇》
107 阅读
4
2024年备考系统架构设计师
104 阅读
5
PHP 文件I/O
92 阅读
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
累计撰写
786
篇文章
累计收到
28
条评论
首页
栏目
php
thinkphp
laravel
工具
开源
mysql
数据结构
总结
思维逻辑
令人感动的创富故事
读书笔记
前端
vue
js
css
书籍
开源之旅
架构
消息队列
docker
教程
代码片段
副业
redis
服务器
nginx
linux
科普
java
c
ElasticSearch
测试
php进阶
php基础
页面
关于
搜索到
3
篇与
的结果
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-04
php 利用socket发送GET,POST请求
php 利用socket发送GET,POST请求作为php程序员一定会接触http协议,也只有深入了解http协议,编程水平才会更进一步。最近我一直在学习php的关于http的编程,许多东西恍然大悟,受益匪浅。希望分享给大家。本文需要有一定http基础的开发者阅读。今天给大家带来的是如何利用socket发送GET,POST请求。我借用燕十八老师封装好的一个Http类给进行说明。在日常编程中相信很多人和我一样大部分时间是利用浏览器向服务器提出GET,POST请求,那么可否利用其它方式提出GET,POST请求呢?答案必然是肯定的。了解过HTTP协议的人知道,浏览器提交请求的实质是向服务器发送一个请求信息,这个请求信息有请求行,请求头,请求体(非必须)构成。服务器根据请求信息返回一个响应信息。连接断开。HTTP请求的格式如下所示:1 <request-line> 2 <headers> 3 <blank line> 4 [<request-body>]HTTP响应的格式与请求的格式十分相似:<status-line> <headers> <blank line> [<response-body>]我们可以利用HTTP发送请求的原理,可以重新考虑利用socket发送HTTP请求。Socket的英文原义是“孔”或“插座”。通常也称作“套接字”,用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信。在Internet上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。如此看来,其实利用socket操作远程文件和读写本地的文件一样容易,把本地文件看成通过硬件传输,远程文件通过网线传输就行了。因而可以将发送请求的考虑成 建立连接->打开socket接口(fsockopen())->写入请求(fwrite())->读出响应(fread()->关闭文件(fclose())。话不多说,直接上代码: <?php interface Proto { // 连接url function conn($url); //发送get查询 function get(); // 发送post查询 function post(); // 关闭连接 function close(); } class Http implements Proto { const CRLF = "\r\n"; protected $errno = -1; protected $errstr = ''; protected $response = ''; protected $url = null; protected $version = 'HTTP/1.1'; protected $fh = null; protected $line = array(); protected $header = array(); protected $body = array(); public function __construct($url) { $this->conn($url); $this->setHeader('Host: ' . $this->url['host']); } // 此方法负责写请求行 protected function setLine($method) { $this->line[0] = $method . ' ' . $this->url['path'] . '?' .$this->url['query'] . ' '. $this->version; } // 此方法负责写头信息 public function setHeader($headerline) { $this->header[] = $headerline; } // 此方法负责写主体信息 protected function setBody($body) { $this->body[] = http_build_query($body); } // 连接url public function conn($url) { $this->url = parse_url($url); // 判断端口 if(!isset($this->url['port'])) { $this->url['port'] = 80; } // 判断query if(!isset($this->url['query'])) { $this->url['query'] = ''; } $this->fh = fsockopen($this->url['host'],$this->url['port'],$this->errno,$this->errstr,3); } //构造get请求的数据 public function get() { $this->setLine('GET'); $this->request(); return $this->response; } // 构造post查询的数据 public function post($body = array()) { $this->setLine('POST'); // 设计content-type $this->setHeader('Content-type: application/x-www-form-urlencoded'); // 设计主体信息,比GET不一样的地方 $this->setBody($body); // 计算content-length $this->setHeader('Content-length: ' . strlen($this->body[0])); $this->request(); return $this->response; } // 真正请求 public function request() { // 把请求行,头信息,实体信息 放在一个数组里,便于拼接 $req = array_merge($this->line,$this->header,array(''),$this->body,array('')); //print_r($req); $req = implode(self::CRLF,$req); //echo $req; exit; fwrite($this->fh,$req); while(!feof($this->fh)) { $this->response .= fread($this->fh,1024); } $this->close(); // 关闭连接 } // 关闭连接 public function close() { fclose($this->fh); } }利用此类发送一个简单的GET请求:<?php //记得引用Http类 $url="http://home.cnblogs.com/u/DeanChopper/"; $http=new Http($url); $response=$http->get(); print_r($response);返回值为信息,可以对响应信息进行进一步处理,得到自己想得到的内容。
2023年08月04日
14 阅读
0 评论
0 点赞
2023-08-04
php与socket
php与socket一直以来很少看到有多少人使用php的socket模块来做一些事情,大概大家都把它定位在脚本语言的范畴内吧,但是其实php的socket模块可以做很多事情,包括做ftplist,http post提交,smtp提交,组包并进行特殊报文的交互(如smpp协议),whois查询。这些都是比较常见的查询。特别是php的socket扩展库可以做的事情简直不会比c差多少。预备知识:php的socket连接函数1、集成于内核的socket这个系列的函数仅仅只能做主动连接无法实现端口监听相关的功能。而且在4.3.0之前所有socket连接只能工作在阻塞模式下。此系列函数包括fsockopen,pfsockopen这两个函数的具体信息可以查询php.net的用户手册他们均会返回一个资源编号对于这个资源可以使用几乎所有对文件操作的函数对其进行操作如fgets(),fwrite(), fclose()等单注意的是所有函数遵循这些函数面对网络信息流时的规律,例如:fread() 从文件指针 handle 读取最多 length 个字节。 该函数在读取完 length 个字节数,或到达 EOF 的时候,或(对于网络流)当一个包可用时就会停止读取文件,视乎先碰到哪种情况。可以看出对于网络流就必须注意取到的是一个完整的包就停止。2、php扩展模块带有的socket功能。php4.x 有这么一个模块extension=php_sockets.dll,RHT9上安装后也有一个extension=php_sockets.so的(这个依稀记得是有的需要确认一下,好久没有玩linux了)当打开这个此模块以后就意味着php拥有了强大的socket功能,包括listen端口,阻塞及非阻塞模式的切换,multi-client 交互式处理等这个系列的函数列表参看http://www.php.net/manual/en/ref.sockets.php看过这个列表觉得是不是非常丰富呢?不过非常遗憾这个模块还非常年轻还有很多地方不成熟,相关的参考文档也非常少:(我也正在研究中,因此暂时不具体讨论它,仅给大家一个参考文章http://www.zend.com/pecl/tutorials/sockets.php下面举例说明:例子1简单应用——whois查询看一段代码CODE:<?php$server="whois.verisign-grs.com";//TLD .com whois server$data = "";$domain = "abc.com";//serch domain$fp = fsockopen($server,43);if ($fp) {fputs($fp,$domain."\r\n");while (!feof($fp)) {$data .= fgets($fp,1000);}}fclose($fp);echo ln2br($data);}[Copy to clipboard]这个应用因该非常常见了:),不用多废话了。下面看看对于ftplist的应用CODE:<?php$server="192.168.1.3";//服务器ip端口用默认的21举例而已所以不要那么复杂$data = "";$fp = fsockopen($server,21);//打开服务器$data .= fread($fp,1024);//读取状态注意用的fread那么是一个可用报文结束为一段读取。fwrite($fp,"USER hack\r\n");//登陆信息$data .= fgets($fp,1024);//读取是否有此用户,是否等待密码fwrite($fp,"PASS 123456\r\n");//密码$data .= fgets($fp,1024);//是否验证成功fwrite($fp,"REST 100\r\n");//重置数据流$data .= fgets($fp,1024);fwrite($fp,"PWD\r\n");//当前目录$data .= fgets($fp,1024);fwrite($fp,"TYPE A\r\n");//切换传输模式为A——ASCII模式$data .= fgets($fp,1024);fwrite($fp,"CWD ./123456\r\n");//更换目录为123456$data .= fgets($fp,1024);fwrite($fp,"PASV\r\n");//切换为被动模式$tstring = fgets($fp,1024);$data .=$tstring;$ports=ftp_pasvs($tstring);//获取服务器分配的端口及ipfwrite($fp,"LIST -al\r\n");//列表$sock_data=ftp_data_connection($ports);//连接被动模式下的端口while (!feof($sock_data)) {//循环获取数据$data .= fgets($sock_data, 1024);}echo nl2br($data);function ftp_pasvs($string)//用于获取被动模式下的相关连接信息{$ip_port = preg_replace("/^(.+()([0-9]{1,3},[0-9]{1,3},[0-9]{1,3},[0-9]{1,3},[0-9]+,[0-9]+)()?.*\r\n)$/i","\\2",$string);return $ip_port;}function ftp_data_connection($ip_port)//连接服务器数据端口{$ip_port=trim($ip_port);if (!preg_match("/^[0-9]{1,3},[0-9]{1,3},[0-9]{1,3},[0-9]{1,3},[0-9]+,[0-9]+$/i", $ip_port)) {return "false";}$DATA = explode(",", $ip_port);$ipaddr = $DATA[0].".".$DATA[1].".".$DATA[2].".".$DATA[3];$port = $DATA[4]*256 + $DATA[5];//echo $port."|".$ipaddr;//exit;$data_connection = @fsockopen($ipaddr, $port);if (!$data_connection) {return "false";}return $data_connection;}?>[Copy to clipboard]以上这段代码作了一下简单的注释应该比较清晰,现说明几点问题。1、为什么采用fread:因为此函数是以网络包为截断的,使用这样的截断来获得信息可以很好的保持兼容性,因为对于ftpclient来讲服务器是一个未知的环境(wu-ftp,ser-u,g6ftp...),在这样的环境下有利于读取ftp的特征字符串以便以后使用。2、为什么不用主动模式连接服务器传输数据:主动模式必须客户端指定端口然后进行传输,而php自带的函数并不具备此特性。除非使用扩展库,这样程序兼容性就会。。同时由服务器指定端口避免了客户端寻找合适端口造成的额外负担,尽管这样的负担微乎其微。与这个代码类似的应用还有estmp发送邮件,管与此应用我不多说,有兴趣的朋友可以到我的blog上看看有相关代码(www.freeplug.org)冰血老弟不介意我做点小广告吧哈哈。POST,GET提交这个话题很老了:)我想不需要我在这里提起了文章很多。在看下面一个例子前先提及一组函数pack,unpack。任何一款拥有socket操作能力的语言都有一个专门用于组包的函数,php也不例外当然这组函数的用途不仅仅是组包。下面简单的介绍一下:应用一:输入16进制或者2进制流。CODE:$src="3B06";$binvar = pack('H*',$src);echo $binvar;[Copy to clipboard]看看这个程序,相当于下面的程序CODE:echo chr(0x3B).chr(0x06);[Copy to clipboard]在数据量很小的时候后面的做法,更为简便。但是大量数据的时候,前一种做法则更为实际工整些,代码量也很少。应用二:网络数据流拆包CODE:$elength = str_len($bin);extract(unpack("NLEN/Hcontent", $bin));[Copy to clipboard]将$bin拆成一维数组并解开到变量$LEN和$content内。下面看例子了这个是利用php实现中国移动cmpp登陆消息的一个缩写例子CODE:<?phpdefine("LOGINID",0x00000001);//登陆消息class SMS{var $host;var $mtport;var $moport;var $connectout;var $sms_sock;var $loginuser;var $loginpass;var $error_stop=0;function SMS($host,$user,$pass,$mtport,$moport,$timeout=30,$if_conn=0){$this->host=$host;$this->mtport=$mtport;$this->moport=$moport;$this->loginuser=$user;$this->loginpass=$pass;$if_conn && $this->login();}function login(){$fp=@fsockopen($this->host,$this->mtport,$errno,$errstr,$this->connectout);if(!$fp){$this->sms_sock='';$this->halt("error in login num=$errno, msg=$errstr");return false;}else{//$this->sms_sock=$fp;$data=pack("Na10a32",LOGINID,$this->loginuser,md5($this->loginpass));//这个地方就是组包了$data=pack("N",strlen($data)+4).$data;//$data是实际内容前面这个表示整个报文长度if(fputs($fp,$data) !== false){//print_r(unpack("N",fread($fp,4)));//此处用于调试时检测用//print_r(unpack("N",fread($fp,4)));//print_r(unpack("c",fread($fp,4)));//print_r(unpack("N",fread($fp,4)));@fread($fp,12);$results=@fread($fp,4);if($results){$rs=@unpack("Ncounts",$results);//返回socket结果。$this->sms_sock=$fp;}else{$this->sms_sock='';$this->halt("error in login: loginid=$this->loginuser loginpass=$this->loginpass");return false;}}else{$this->sms_sock='';$this->halt("error in submit logininfo: loginid=$this->loginuserloginpass=$this->loginpass");return false;}}return true;}function halt($msg){echo $msg;flush();$this->error_stop && exit;}}[Copy to clipboard]此例应用:主要用于底层的不依赖于http一类协议的通讯使用。在phpclass这个站点上有个smpp模块更为详细的演示了此类应用有兴趣的朋友可以看看。末了,不多说。由于时间仓促,有些得不对的地方,望各位用pm通知我,我会及时更正,有什么疑问也可以pm我。文章: PHP And Socket 书名: 《PHP Game Programming》作者: Matt Rutledget翻译: heiyeluren <heiyeluren_gmail_com>◇ Socket基础◇ 产生一个服务器◇ 产生一个客户端在这一章里你将了解到迷人而又让人容易糊涂的套接字(Sockets)。Sockets在PHP中是没有充分利用的功能。今天你将看到产生一个能使用客户端连接的服务器,并在客户端使用socket进行连接,服务器端将详细的处理信息发送给客户端。当你看到完整的socket过程,那么你将会在以后的程序开发中使用它。这个服务器是一个能让你连接的HTTP服务器,客户端是一个Web浏览器,这是一个单一的 客户端/服务器 的关系。◆ Socket 基础PHP使用Berkley的socket库来创建它的连接。你可以知道socket只不过是一个数据结构。你使用这个socket数据结构去开始一个客户端和服务器之间的会话。这个服务器是一直在监听准备产生一个新的会话。当一个客户端连接服务器,它就打开服务器正在进行监听的一个端口进行会话。这时,服务器端接受客户端的连接请求,那么就进行一次循环。现在这个客户端就能够发送信息到服务器,服务器也能发送信息给客户端。产生一个Socket,你需要三个变量:一个协议、一个socket类型和一个公共协议类型。产生一个socket有三种协议供选择,继续看下面的内容来获取详细的协议内容。定义一个公共的协议类型是进行连接一个必不可少的元素。下面的表我们看看有那些公共的协议类型。表一:协议名字/常量 描述AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用在IPv4的地址AF_INET6 与上面类似,不过是来用在IPv6的地址AF_UNIX 本地协议,使用在Unix和Linux系统上,它很少使用,一般都是当客户端和服务器在同一台及其上的时候使用表二:Socket类型名字/常量 描述SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。SOCK_SEQPACKET 这个协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。SOCK_RAW 这个socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议)SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序表三:公共协议名字/常量 描述ICMP 互联网控制消息协议,主要使用在网关和主机上,用来检查网络状况和报告错误信息UDP 用户数据报文协议,它是一个无连接,不可靠的传输协议TCP 传输控制协议,这是一个使用最多的可靠的公共协议,它能保证数据包能够到达接受者那儿,如果在传输过程中发生错误,那么它将重新发送出错数据包。现在你知道了产生一个socket的三个元素,那么我们就在php中使用socket_create()函数来产生一个socket。这个socket_create()函数需要三个参数:一个协议、一个socket类型、一个公共协议。socket_create()函数运行成功返回一个包含socket的资源类型,如果没有成功则返回false。Resourece socket_create(int protocol, int socketType, int commonProtocol);现在你产生一个socket,然后呢?php提供了几个操纵socket的函数。你能够绑定socket到一个IP,监听一个socket的通信,接受一个socket;现在我们来看一个例子,了解函数是如何产生、接受和监听一个socket。<?php$commonProtocol = getprotobyname(“tcp”);$socket = socket_create(AF_INET, SOCK_STREAM, $commonProtocol);socket_bind($socket, ‘localhost’, 1337);socket_listen($socket);// More socket functionality to come?>上面这个例子产生一个你自己的服务器端。例子第一行,$commonProtocol = getprotobyname(“tcp”);使用公共协议名字来获取一个协议类型。在这里使用的是TCP公共协议,如果你想使用UDP或者ICMP协议,那么你应该把getprotobyname()函数的参数改为“udp”或“icmp”。还有一个可选的办法是不使用getprotobyname()函数而是指定SOL_TCP或SOL_UDP在socket_create()函数中。$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);例子的第二行是产生一个socket并且返回一个socket资源的实例。在你有了一个socket资源的实例以后,你就必须把socket绑定到一个IP地址和某一个端口上。socket_bind($socket, ‘localhost’, 1337);在这里你绑定socket到本地计算机(127.0.0.1)和绑定socket到你的1337端口。然后你就需要监听所有进来的socket连接。socket_listen($socket);在第四行以后,你就需要了解所有的socket函数和他们的使用。表四:Socket函数函数名 描述socket_accept() 接受一个Socket连接socket_bind() 把socket绑定在一个IP地址和端口上socket_clear_error() 清除socket的错误或者最后的错误代码socket_close() 关闭一个socket资源socket_connect() 开始一个socket连接socket_create_listen() 在指定端口打开一个socket监听socket_create_pair() 产生一对没有区别的socket到一个数组里socket_create() 产生一个socket,相当于产生一个socket的数据结构socket_get_option() 获取socket选项socket_getpeername() 获取远程类似主机的ip地址socket_getsockname() 获取本地socket的ip地址socket_iovec_add() 添加一个新的向量到一个分散/聚合的数组socket_iovec_alloc() 这个函数创建一个能够发送接收读写的iovec数据结构socket_iovec_delete() 删除一个已经分配的iovecsocket_iovec_fetch() 返回指定的iovec资源的数据socket_iovec_free() 释放一个iovec资源socket_iovec_set() 设置iovec的数据新值socket_last_error() 获取当前socket的最后错误代码socket_listen() 监听由指定socket的所有连接socket_read() 读取指定长度的数据socket_readv() 读取从分散/聚合数组过来的数据socket_recv() 从socket里结束数据到缓存socket_recvfrom() 接受数据从指定的socket,如果没有指定则默认当前socketsocket_recvmsg() 从iovec里接受消息socket_select() 多路选择socket_send() 这个函数发送数据到已连接的socketsocket_sendmsg() 发送消息到socketsocket_sendto() 发送消息到指定地址的socketsocket_set_block() 在socket里设置为块模式socket_set_nonblock() socket里设置为非块模式socket_set_option() 设置socket选项socket_shutdown() 这个函数允许你关闭读、写、或者指定的socketsocket_strerror() 返回指定错误号的详细错误socket_write() 写数据到socket缓存socket_writev() 写数据到分散/聚合数组(注: 函数介绍删减了部分原文内容,函数详细使用建议参考英文原文,或者参考PHP手册)以上所有的函数都是PHP中关于socket的,使用这些函数,你必须把你的socket打开,如果你没有打开,请编辑你的php.ini文件,去掉下面这行前面的注释:extension=php_sockets.dll如果你无法去掉注释,那么请使用下面的代码来加载扩展库:<?phpif(!extension_loaded(‘sockets’)){if(strtoupper(substr(PHP_OS, 3)) == “WIN”){dl(‘php_sockets.dll’);}else{dl(‘sockets.so’);}}?>如果你不知道你的socket是否打开,那么你可以使用phpinfo()函数来确定socket是否打开。你通过查看phpinfo信息了解socket是否打开。如下图:查看phpinfo()关于socket的信息◆ 产生一个服务器现在我们把第一个例子进行完善。你需要监听一个指定的socket并且处理用户的连接。<?php$commonProtocol = getprotobyname("tcp");$socket = socket_create(AF_INET, SOCK_STREAM, $commonProtocol);socket_bind($socket, 'localhost', 1337);socket_listen($socket);// Accept any incoming connections to the server$connection = socket_accept($socket);if($connection){socket_write($connection, "You have connected to the socket...\n\r");}?>你应该使用你的命令提示符来运行这个例子。理由是因为这里将产生一个服务器,而不是一个Web页面。如果你尝试使用Web浏览器来运行这个脚本,那么很有可能它会超过30秒的限时。你可以使用下面的代码来设置一个无限的运行时间,但是还是建议使用命令提示符来运行。set_time_limit(0);在你的命令提示符中对这个脚本进行简单测试:Php.exe example01_server.php如果你没有在系统的环境变量中设置php解释器的路径,那么你将需要给php.exe指定详细的路径。当你运行这个服务器端的时候,你能够通过远程登陆(telnet)的方式连接到端口1337来测试这个服务器。如下图:上面的服务器端有三个问题:1. 它不能接受多个连接。2. 它只完成唯一的一个命令。3. 你不能通过Web浏览器连接这个服务器。这个第一个问题比较容易解决,你可以使用一个应用程序去每次都连接到服务器。但是后面的问题是你需要使用一个Web页面去连接这个服务器,这个比较困难。你可以让你的服务器接受连接,然后些数据到客户端(如果它一定要写的话),关闭连接并且等待下一个连接。在上一个代码的基础上再改进,产生下面的代码来做你的新服务器端:<?php// Set up our socket$commonProtocol = getprotobyname("tcp");$socket = socket_create(AF_INET, SOCK_STREAM, $commonProtocol);socket_bind($socket, 'localhost', 1337);socket_listen($socket);// Initialize the buffer$buffer = "NO DATA";while(true){// Accept any connections coming in on this socket$connection = socket_accept($socket);printf("Socket connected\r\n");// Check to see if there is anything in the bufferif($buffer != ""){printf("Something is in the buffer...sending data...\r\n");socket_write($connection, $buffer . "\r\n");printf("Wrote to socket\r\n");}else{printf("No Data in the buffer\r\n");}// Get the inputwhile($data = socket_read($connection, 1024, PHP_NORMAL_READ)){$buffer = $data;socket_write($connection, "Information Received\r\n");printf("Buffer: " . $buffer . "\r\n");}socket_close($connection);printf("Closed the socket\r\n\r\n");}?>这个服务器端要做什么呢?它初始化一个socket并且打开一个缓存收发数据。它等待连接,一旦产生一个连接,它将打印“Socket connected”在服务器端的屏幕上。这个服务器检查缓冲区,如果缓冲区里有数据,它将把数据发送到连接过来的计算机。然后它发送这个数据的接受信息,一旦它接受了信息,就把信息保存到数据里,并且让连接的计算机知道这些信息,最后关闭连接。当连接关闭后,服务器又开始处理下一次连接。(翻译的烂,附上原文)This is what the server does. It initializes the socket and the buffer that you use to receiveand send data. Then it waits for a connection. Once a connection is created it prints“Socket connected” to the screen the server is running on. The server then checks to see ifthere is anything in the buffer; if there is, it sends the data to the connected computer.After it sends the data it waits to receive information. Once it receives information it storesit in the data, lets the connected computer know that it has received the information, andthen closes the connection. After the connection is closed, the server starts the wholeprocess again.◆ 产生一个客户端处理第二个问题是很容易的。你需要产生一个php页连接一个socket,发送一些数据进它的缓存并处理它。然后你又个处理后的数据在还顿,你能够发送你的数据到服务器。在另外一台客户端连接,它将处理那些数据。To solve the second problem is very easy. You need to create a PHP page that connects toa socket, receive any data that is in the buffer, and process it. After you have processed thedata in the buffer you can send your data to the server. When another client connects, itwill process the data you sent and the client will send more data back to the server.下面的例子示范了使用socket:<?php// Create the socket and connect$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);$connection = socket_connect($socket,’localhost’, 1337);while($buffer = socket_read($socket, 1024, PHP_NORMAL_READ)){if($buffer == “NO DATA”){echo(“NO DATA”);break;}else{// Do something with the data in the bufferecho(“Buffer Data: “ . $buffer . “”);}}echo(“Writing to Socket”);// Write some test data to our socketif(!socket_write($socket, “SOME DATA\r\n”)){echo(“Write failed”);}// Read any response from the socketwhile($buffer = socket_read($socket, 1024, PHP_NORMAL_READ)){echo(“Data sent was: SOME DATA Response was:” . $buffer . “”);}echo(“Done Reading from Socket”);?>这个例子的代码演示了客户端连接到服务器。客户端读取数据。如果这是第一时间到达这个循环的首次连接,这个服务器将发送“NO DATA”返回给客户端。如果情况发生了,这个客户端在连接之上。客户端发送它的数据到服务器,数据发送给服务器,客户端等待响应。一旦接受到响应,那么它将把响应写到屏幕上。结合Socket的坦克大战(因为是描述游戏和socket结合,跟本文联系不大,所以不翻译,建议参考英文原文)[ 题外话 ]翻译文章的初衷是因为我个人对socket非常感兴趣,而且目前国内见php的文章比较少,除了php手册里面的部分内容,所以在我看了《PHP Game Programming》这本书里有关于socket的内容后毅然决定要翻译,我知道翻译出来的质量不行,还请见谅。另外,我在《Core PHP Programming》Third Edition中也发现里面的Socket内容讲的不错,如果有空,我想也许我会把它也给翻译一下。这是我第一次翻译文章,花了我近五个小时,文章可以说是错误百出,如果翻译的不合理请见谅,如果有兴趣提高这个内容可以给我发邮件。这个凌晨时分,竟然无法入眠,不知道是不是在其他角落,也有人同我一样。希望本文能够给向学习PHP Socket编程的朋友一点帮助,感谢你阅读这个错误百出的文章。翻译:heiyeluren <heiyeluren_at_gmail.com>时间:2005-8-14 04:46
2023年08月04日
18 阅读
0 评论
0 点赞