[他山之石]PHP扩展之网络socket扩展导读

jackxiang 2012-9-8 22:54 | |
没想到在CSDN上看到一个兄弟写的文章成连载了,于是细看了下,发现对PHP的Socket的编码及扩展实现写得不错,建议看下其扩展写法,很有学习教育意义,再就是对Epoll模型的分析很到位,觉得写得都不错,特摘录下Socket这块的PHP扩展代码分析结合其PHP的Soket服务器写法,原文来自:
http://blog.csdn.net/shagoo/article/details/6647961
http://blog.csdn.net/shagoo/article/details/6396089
《Socket深度探究4PHP(一)》和《Socket深度探究4PHP(二)》 Socket 深度探究 4 PHP (三) 应该是写得很有体系的难得一见的不错的好文章。

我在此仅仅对PHPSocket网络扩展作摘录,如下:
select/poll 的同步模型:属于同步非阻塞 IO 模型,代码如下:

select_server.php
[php] view plaincopy
<?php  
/**
* SelectSocketServer Class
* By 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] view plaincopy
<?php  
/**
* SelectSocket Test Client
* By 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 模式最大的问题所在,所以如果是超高并发服务器建议使用下一种模式。


根据上面这一段来阅读PHP的扩展源代码是相当不错的:
PHP 源码的 ext 目录下面,因此我们我需要先进入 ext/sockets/ 目录,做过 PHP 扩展的同学应该都很熟悉下面的一些文件了,这次我们主要分析的是 php_sockets.h 和 sockets.c 这两个 C 源码文件。

ext/sockets/php_sockets.h

这个头文件很简单,我们主要看一下下面列出的几个重点:

32 行:
[cpp] view plaincopy
#ifdef PHP_WIN32  
#include <winsock.h>  
#else  
#if HAVE_SYS_SOCKET_H  
#include <sys/socket.h>  
#endif  
#endif  

以上就是 PHP 对于不同环境 Socket 底层调用的定义了,我们可以看到不管是 Unix 还是 Windows 环境,PHP均调用的是系统标准的 BSD Socket 库。然后我们看下面这个重要的结构体定义:

82 行:
[cpp] view plaincopy
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_listen

859 行: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 行)。
[cpp] view plaincopy
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_nonblock

906 行:PHP_FUNCTION(socket_set_nonblock)
这个函数也很简单,从 ZEND_FETCH_RESOURCE 取出 runtime 中的 php_sock 然后调用 php_set_sock_blocking 函数来设置 sockfd 的阻塞或者非阻塞(此函数可以参考 main/network.c 第 1069 行,我们可以看到 PHP 是使用 fcntl 函数来设置的)。

>socket_select

785 行: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_accept

881 行:PHP_FUNCTION(socket_accept)
此函数基本上也就是 socket 原生 accept 函数的包装,具体代码可参考 397 行:php_accept_connect 函数中的逻辑,最后调用 ZEND_REGISTER_RESOURCE 宏返回 new_sock,若失败程序会清理使用的 out_socket 资源。

>socket_write

986 行:PHP_FUNCTION(socket_write)
按照以上的思路看这个函数也非常简单,详见 986 行,唯一值得注意的是对于不同操作系统调用的函数有点不同,代码(见 1004 行)如下:
[cpp] view plaincopy
#ifndef PHP_WIN32  
    retval = write(php_sock->bsd_socket, str, MIN(length, str_len));  
#else  
    retval = send(php_sock->bsd_socket, str, min(length, str_len), 0);  
#endif  

>socket_read

1021 行: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_close

970 行:PHP_FUNCTION(socket_close)
清理 socket 运行时所用的资源。

>socket_shutdown

1968 行: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,只用仅仅六行代码:
[javascript] view plaincopy
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 还在不断的更新发展中,虽然目前我在公司的服务架构中还不敢使用它,我还是很希望它能够迅速成长起来,这样子我们开发服务中间件的时候,就会多出一个很棒的选项啦~

作者:jackxiang@向东博客 专注WEB应用 构架之美 --- 构架之美,在于尽态极妍 | 应用之美,在于药到病除
地址:https://jackxiang.com/post/5560/
版权所有。转载时必须以链接形式注明作者和原始出处及本声明!

评论列表
2014-4-13 02:11 | 小红帽 Email Homepage
呵呵,我去年刚好也是看到石头君的文章,照着博主的思路写了一个php socket 服务器框架workerman,采用的libevebt + 非阻塞 + 多路复用。压测了下,性能很强悍,不必c写的差多少,而且比想象中的稳定,在公司用了几个月了,没出现啥大问题。
分页: 1/1 第一页 1 最后页
发表评论

昵称

网址

电邮

打开HTML 打开UBB 打开表情 隐藏 记住我 [登入] [注册]