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 Class * By 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);
<?php /** * Socket 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_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'"); } }
<?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);
<?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'"); // } }
<?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);
<?php /** * EpollSocket 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_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'"); // } }